Passed
Pull Request — rhel8-branch (#147)
by Jan
01:03
created

org_fedora_oscap.scap_content_handler   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 206
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 34
eloc 118
dl 0
loc 206
rs 9.68
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
F SCAPContentHandler.get_profiles() 0 60 14
A SCAPContentHandler.get_data_streams_checklists() 0 22 4
A SCAPContentHandler.__init__() 0 20 2
B SCAPContentHandler._get_scap_type() 0 10 6
B SCAPContentHandler._parse_profiles_from_xccdf() 0 31 7
A SCAPContentHandler.select_checklist() 0 13 1
1
#
2
# Copyright (C) 2021 Red Hat, Inc.
3
#
4
# This copyrighted material is made available to anyone wishing to use,
5
# modify, copy, or redistribute it subject to the terms and conditions of
6
# the GNU General Public License v.2, or (at your option) any later version.
7
# This program is distributed in the hope that it will be useful, but WITHOUT
8
# ANY WARRANTY expressed or implied, including the implied warranties of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
10
# Public License for more details.  You should have received a copy of the
11
# GNU General Public License along with this program; if not, write to the
12
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
13
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
14
# source code or documentation are not subject to the GNU General Public
15
# License and may only be used or replicated with the express permission of
16
# Red Hat, Inc.
17
#
18
# Red Hat Author(s): Jan Černý <[email protected]>
19
#
20
21
from collections import namedtuple
22
import os
23
import xml.etree.ElementTree as ET
24
25
from org_fedora_oscap.content_handling import parse_HTML_from_content
26
27
# namedtuple class (not a constant, pylint!) for info about a XCCDF profile
28
# pylint: disable-msg=C0103
29
ProfileInfo = namedtuple("ProfileInfo", ["id", "title", "description"])
30
31
ns = {
32
    "ds": "http://scap.nist.gov/schema/scap/source/1.2",
33
    "xccdf-1.1": "http://checklists.nist.gov/xccdf/1.1",
34
    "xccdf-1.2": "http://checklists.nist.gov/xccdf/1.2",
35
    "xlink": "http://www.w3.org/1999/xlink"
36
}
37
38
39
class SCAPContentHandlerError(Exception):
40
    """Exception class for errors related to SCAP content handling."""
41
    pass
42
43
44
class SCAPContentHandler:
45
    def __init__(self, file_path, tailoring_file_path=None):
46
        """
47
        Constructor for the SCAPContentHandler class.
48
49
        :param file_path: path to an SCAP file (only SCAP source data streams,
50
        XCCDF files and tailoring files are supported)
51
        :type file_path: str
52
        :param tailoring_file_path: path to the tailoring file, can be None if no tailoring exists
53
        :type tailoring_file_path: str
54
        """
55
        self.file_path = file_path
56
        tree = ET.parse(file_path)
57
        self.root = tree.getroot()
58
        if tailoring_file_path is not None:
59
            self.tailoring = ET.parse(tailoring_file_path).getroot()
60
        else:
61
            self.tailoring = None
62
        self.scap_type = self._get_scap_type(self.root)
63
        self._data_stream_id = None
64
        self._checklist_id = None
65
66
    def _get_scap_type(self, root):
67
        if root.tag == f"{{{ns['ds']}}}data-stream-collection":
68
            return "SCAP_SOURCE_DATA_STREAM"
69
        elif root.tag == f"{{{ns['xccdf-1.1']}}}Benchmark" or root.tag == f"{{{ns['xccdf-1.2']}}}Benchmark":
70
            return "XCCDF"
71
        elif root.tag == f"{{{ns['xccdf-1.1']}}}Tailoring" or root.tag == f"{{{ns['xccdf-1.2']}}}Tailoring":
72
            return "TAILORING"
73
        else:
74
            msg = f"Unsupported SCAP content type {root.tag}"
75
            raise SCAPContentHandlerError(msg)
76
77
    def get_data_streams_checklists(self):
78
        """
79
        Method to get data streams and their checklists found in the SCAP
80
        source data stream represented by the SCAPContentHandler.
81
82
        :return: a dictionary consisting of the IDs of the data streams as keys
83
                 and lists of their checklists' IDs as values
84
                 None if the file isn't a SCAP source data stream
85
        :rtype: dict(str -> list of strings)
86
        """
87
        if self.scap_type != "SCAP_SOURCE_DATA_STREAM":
88
            return None
89
        checklists = {}
90
        for data_stream in self.root.findall("ds:data-stream", ns):
91
            data_stream_id = data_stream.get("id")
92
            crefs = []
93
            for cref in data_stream.findall(
94
                    "ds:checklists/ds:component-ref", ns):
95
                cref_id = cref.get("id")
96
                crefs.append(cref_id)
97
            checklists[data_stream_id] = crefs
98
        return checklists
99
100
    def _parse_profiles_from_xccdf(self, benchmark):
101
        if benchmark is None:
102
            return []
103
        if benchmark.tag.startswith(f"{{{ns['xccdf-1.1']}}}"):
104
            xccdf_ns_prefix = "xccdf-1.1"
105
        elif benchmark.tag.startswith(f"{{{ns['xccdf-1.2']}}}"):
106
            xccdf_ns_prefix = "xccdf-1.2"
107
        else:
108
            raise SCAPContentHandlerError("Unsupported XML namespace")
109
        profiles = []
110
        for profile in benchmark.findall(f"{xccdf_ns_prefix}:Profile", ns):
111
            profile_id = profile.get("id")
112
            title = profile.find(f"{xccdf_ns_prefix}:title", ns)
113
            description = profile.find(f"{xccdf_ns_prefix}:description", ns)
114
            if description is None:
115
                description_text = ""
116
            else:
117
                description_text = parse_HTML_from_content(description.text)
118
            profile_info = ProfileInfo(
119
                profile_id, title.text, description_text)
120
            profiles.append(profile_info)
121
        # if there are no profiles we would like to prevent empty profile
122
        # selection list in the GUI so we create the default profile
123
        if len(profiles) == 0:
124
            default_profile = ProfileInfo(
125
                "default",
126
                "Default",
127
                "The implicit XCCDF profile. Usually, the default profile "
128
                "contains no rules.")
129
            profiles.append(default_profile)
130
        return profiles
131
132
    def select_checklist(self, data_stream_id, checklist_id):
133
        """
134
        Method to select a specific XCCDF Benchmark using
135
        :param data_stream_id: value of ds:data-stream/@id
136
        :type data_stream_id: str
137
        :param checklist_id: value of ds:component-ref/@id pointing to
138
            an xccdf:Benchmark
139
        :type checklist_id: str
140
        :return: None
141
142
        """
143
        self._data_stream_id = data_stream_id
144
        self._checklist_id = checklist_id
145
146
    def get_profiles(self):
147
        """
148
        Method to get a list of profiles defined in the currently selected
149
        checklist that is defined in the currently selected data stream.
150
151
        :return: list of profiles found in the checklist
152
        :rtype: list of ProfileInfo instances
153
154
        """
155
        if self.scap_type == "XCCDF":
156
            if (self._data_stream_id is not None or
157
                    self._checklist_id is not None):
158
                msg = "For XCCDF documents, the data_stream_id and " \
159
                    "checklist_id must be both None."
160
                raise SCAPContentHandlerError(msg)
161
            benchmark = self.root
162
        elif self.scap_type == "TAILORING":
163
            msg = "Tailoring files can't be processed separately"
164
            raise SCAPContentHandlerError(msg)
165
        elif self.scap_type == "SCAP_SOURCE_DATA_STREAM":
166
            if self._data_stream_id is None or self._checklist_id is None:
167
                msg = "For SCAP source data streams, data_stream_id and " \
168
                    "checklist_id must be both different than None"
169
                raise SCAPContentHandlerError(msg)
170
            cref_xpath = f"ds:data-stream[@id='{self._data_stream_id}']/" \
171
                f"ds:checklists/ds:component-ref[@id='{self._checklist_id}']"
172
            cref = self.root.find(cref_xpath, ns)
173
            if cref is None:
174
                msg = f"Can't find ds:component-ref " \
175
                    f"with id='{self._checklist_id}' " \
176
                    f"in ds:datastream with id='{self._data_stream_id}'"
177
                raise SCAPContentHandlerError(msg)
178
            cref_href = cref.get(f"{{{ns['xlink']}}}href")
179
            if cref_href is None:
180
                msg = f"The ds:component-ref with id='{self._checklist_id} '" \
181
                    f"in ds:datastream with id='{self._data_stream_id}' " \
182
                    f"doesn't have a xlink:href attribute."
183
                raise SCAPContentHandlerError(msg)
184
            if not cref_href.startswith("#"):
185
                msg = f"The component {cref_href} isn't local."
186
                raise SCAPContentHandlerError(msg)
187
            component_id = cref_href[1:]
188
            component = self.root.find(
189
                f"ds:component[@id='{component_id}']", ns)
190
            if component is None:
191
                msg = f"Can't find component {component_id}"
192
                raise SCAPContentHandlerError(msg)
193
            benchmark = component.find("xccdf-1.1:Benchmark", ns)
194
            if benchmark is None:
195
                benchmark = component.find("xccdf-1.2:Benchmark", ns)
196
            if benchmark is None:
197
                msg = f"The component {cref_href} doesn't contain " \
198
                    "an XCCDF Benchmark."
199
                raise SCAPContentHandlerError(msg)
200
        else:
201
            msg = f"Unsupported SCAP content type '{self.scap_type}'."
202
            raise SCAPContentHandlerError(msg)
203
        benchmark_profiles = self._parse_profiles_from_xccdf(benchmark)
204
        tailoring_profiles = self._parse_profiles_from_xccdf(self.tailoring)
205
        return benchmark_profiles + tailoring_profiles
206