Passed
Push — master ( 5534fc...ed6ab7 )
by Matěj
02:32 queued 14s
created

ssg.build_yaml.Rule.validate_references()   B

Complexity

Conditions 6

Size

Total Lines 11
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 10
nop 2
dl 0
loc 11
ccs 0
cts 8
cp 0
crap 42
rs 8.6666
c 0
b 0
f 0
1
from __future__ import absolute_import
0 ignored issues
show
Coding Style introduced by
This module should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
2
from __future__ import print_function
3
4
import os
5
import os.path
6
import datetime
7
import sys
8
9
from .constants import XCCDF_PLATFORM_TO_CPE
10
from .rules import get_rule_dir_id, get_rule_dir_yaml, is_rule_dir
11
12
from .checks import is_cce_valid
13
from .yaml import open_and_expand, open_and_macro_expand
14
from .utils import required_key
15
16
from .xml import ElementTree as ET
17
from .shims import unicode_func
18
19
20
def add_sub_element(parent, tag, data):
21
    """
22
    Creates a new child element under parent with tag tag, and sets
23
    data as the content under the tag. In particular, data is a string
24
    to be parsed as an XML tree, allowing sub-elements of children to be
25
    added.
26
27
    If data should not be parsed as an XML tree, either escape the contents
28
    before passing into this function, or use ElementTree.SubElement().
29
30
    Returns the newly created subelement of type tag.
31
    """
32
    # This is used because our YAML data contain XML and XHTML elements
33
    # ET.SubElement() escapes the < > characters by &lt; and &gt;
34
    # and therefore it does not add child elements
35
    # we need to do a hack instead
36
    # TODO: Remove this function after we move to Markdown everywhere in SSG
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
37
    ustr = unicode_func("<{0}>{1}</{0}>").format(tag, data)
38
39
    try:
40
        element = ET.fromstring(ustr.encode("utf-8"))
41
    except Exception:
42
        msg = ("Error adding subelement to an element '{0}' from string: '{1}'"
43
               .format(parent.tag, ustr))
44
        raise RuntimeError(msg)
45
46
    parent.append(element)
47
    return element
48
49
50
def add_warning_elements(element, warnings):
0 ignored issues
show
Coding Style introduced by
This function should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
51
    # The use of [{dict}, {dict}] in warnings is to handle the following
52
    # scenario where multiple warnings have the same category which is
53
    # valid in SCAP and our content:
54
    #
55
    # warnings:
56
    #     - general: Some general warning
57
    #     - general: Some other general warning
58
    #     - general: |-
59
    #         Some really long multiline general warning
60
    #
61
    # Each of the {dict} should have only one key/value pair.
62
    for warning_dict in warnings:
63
        warning = add_sub_element(element, "warning", list(warning_dict.values())[0])
64
        warning.set("category", list(warning_dict.keys())[0])
65
66
67
class Profile(object):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
68
    """Represents XCCDF profile
69
    """
70
71
    def __init__(self, id_):
72
        self.id_ = id_
73
        self.title = ""
74
        self.description = ""
75
        self.extends = None
76
        self.selections = []
77
78
    @staticmethod
79
    def from_yaml(yaml_file, env_yaml=None):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
80
        yaml_contents = open_and_expand(yaml_file, env_yaml)
81
        if yaml_contents is None:
82
            return None
83
84
        basename, _ = os.path.splitext(os.path.basename(yaml_file))
85
86
        profile = Profile(basename)
87
        profile.title = required_key(yaml_contents, "title")
88
        del yaml_contents["title"]
89
        profile.description = required_key(yaml_contents, "description")
90
        del yaml_contents["description"]
91
        profile.extends = yaml_contents.pop("extends", None)
92
        profile.selections = required_key(yaml_contents, "selections")
93
        del yaml_contents["selections"]
94
95
        if yaml_contents:
96
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
97
                               % (yaml_file, yaml_contents))
98
99
        return profile
100
101
    def to_xml_element(self):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
102
        element = ET.Element('Profile')
103
        element.set("id", self.id_)
104
        if self.extends:
105
            element.set("extends", self.extends)
106
        title = add_sub_element(element, "title", self.title)
107
        title.set("override", "true")
108
        desc = add_sub_element(element, "description", self.description)
109
        desc.set("override", "true")
110
111
        for selection in self.selections:
112
            if selection.startswith("!"):
113
                unselect = ET.Element("select")
114
                unselect.set("idref", selection[1:])
115
                unselect.set("selected", "false")
116
                element.append(unselect)
117
            elif "=" in selection:
118
                refine_value = ET.Element("refine-value")
119
                value_id, selector = selection.split("=", 1)
120
                refine_value.set("idref", value_id)
121
                refine_value.set("selector", selector)
122
                element.append(refine_value)
123
            else:
124
                select = ET.Element("select")
125
                select.set("idref", selection)
126
                select.set("selected", "true")
127
                element.append(select)
128
129
        return element
130
131
132
class Value(object):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (8/7)
Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
133
    """Represents XCCDF Value
134
    """
135
136
    def __init__(self, id_):
137
        self.id_ = id_
138
        self.title = ""
139
        self.description = ""
140
        self.type_ = "string"
141
        self.operator = "equals"
142
        self.interactive = False
143
        self.options = {}
144
        self.warnings = []
145
146
    @staticmethod
147
    def from_yaml(yaml_file, env_yaml=None):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
148
        yaml_contents = open_and_expand(yaml_file, env_yaml)
149
        if yaml_contents is None:
150
            return None
151
152
        value_id, _ = os.path.splitext(os.path.basename(yaml_file))
153
        value = Value(value_id)
154
        value.title = required_key(yaml_contents, "title")
155
        del yaml_contents["title"]
156
        value.description = required_key(yaml_contents, "description")
157
        del yaml_contents["description"]
158
        value.type_ = required_key(yaml_contents, "type")
159
        del yaml_contents["type"]
160
        value.operator = yaml_contents.pop("operator", "equals")
161
        possible_operators = ["equals", "not equal", "greater than",
162
                              "less than", "greater than or equal",
163
                              "less than or equal", "pattern match"]
164
165
        if value.operator not in possible_operators:
166
            raise ValueError(
167
                "Found an invalid operator value '%s' in '%s'. "
168
                "Expected one of: %s"
169
                % (value.operator, yaml_file, ", ".join(possible_operators))
170
            )
171
172
        value.interactive = \
173
            yaml_contents.pop("interactive", "false").lower() == "true"
174
175
        value.options = required_key(yaml_contents, "options")
176
        del yaml_contents["options"]
177
        value.warnings = yaml_contents.pop("warnings", [])
178
179
        for warning_list in value.warnings:
180
            if len(warning_list) != 1:
181
                raise ValueError("Only one key/value pair should exist for each dictionary")
182
183
        if yaml_contents:
184
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
185
                               % (yaml_file, yaml_contents))
186
187
        return value
188
189
    def to_xml_element(self):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
190
        value = ET.Element('Value')
191
        value.set('id', self.id_)
192
        value.set('type', self.type_)
193
        if self.operator != "equals":  # equals is the default
194
            value.set('operator', self.operator)
195
        if self.interactive:  # False is the default
196
            value.set('interactive', 'true')
197
        title = ET.SubElement(value, 'title')
198
        title.text = self.title
199
        add_sub_element(value, 'description', self.description)
200
        add_warning_elements(value, self.warnings)
201
202
        for selector, option in self.options.items():
203
            # do not confuse Value with big V with value with small v
204
            # value is child element of Value
205
            value_small = ET.SubElement(value, 'value')
206
            # by XCCDF spec, default value is value without selector
207
            if selector != "default":
208
                value_small.set('selector', str(selector))
209
            value_small.text = str(option)
210
211
        return value
212
213
    def to_file(self, file_name):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
214
        root = self.to_xml_element()
215
        tree = ET.ElementTree(root)
216
        tree.write(file_name)
217
218
219
class Benchmark(object):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (15/7)
Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
220
    """Represents XCCDF Benchmark
221
    """
222
    def __init__(self, id_):
223
        self.id_ = id_
224
        self.title = ""
225
        self.status = ""
226
        self.description = ""
227
        self.notice_id = ""
228
        self.notice_description = ""
229
        self.front_matter = ""
230
        self.rear_matter = ""
231
        self.cpes = []
232
        self.version = "0.1"
233
        self.profiles = []
234
        self.values = {}
235
        self.bash_remediation_fns_group = None
236
        self.groups = {}
237
        self.rules = {}
238
239
        # This is required for OCIL clauses
240
        conditional_clause = Value("conditional_clause")
241
        conditional_clause.title = "A conditional clause for check statements."
242
        conditional_clause.description = conditional_clause.title
243
        conditional_clause.type_ = "string"
244
        conditional_clause.options = {"": "This is a placeholder"}
245
246
        self.add_value(conditional_clause)
247
248
    @staticmethod
249
    def from_yaml(yaml_file, id_, product_yaml=None):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
250
        yaml_contents = open_and_macro_expand(yaml_file, product_yaml)
251
        if yaml_contents is None:
252
            return None
253
254
        benchmark = Benchmark(id_)
255
        benchmark.title = required_key(yaml_contents, "title")
256
        del yaml_contents["title"]
257
        benchmark.status = required_key(yaml_contents, "status")
258
        del yaml_contents["status"]
259
        benchmark.description = required_key(yaml_contents, "description")
260
        del yaml_contents["description"]
261
        notice_contents = required_key(yaml_contents, "notice")
262
        benchmark.notice_id = required_key(notice_contents, "id")
263
        del notice_contents["id"]
264
        benchmark.notice_description = required_key(notice_contents,
265
                                                    "description")
266
        del notice_contents["description"]
267
        if not notice_contents:
268
            del yaml_contents["notice"]
269
270
        benchmark.front_matter = required_key(yaml_contents,
271
                                              "front-matter")
272
        del yaml_contents["front-matter"]
273
        benchmark.rear_matter = required_key(yaml_contents,
274
                                             "rear-matter")
275
        del yaml_contents["rear-matter"]
276
        benchmark.version = str(required_key(yaml_contents, "version"))
277
        del yaml_contents["version"]
278
279
        if yaml_contents:
280
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
281
                               % (yaml_file, yaml_contents))
282
283
        benchmark.cpes = product_yaml.get("cpes", [])
284
285
        return benchmark
286
287
    def add_profiles_from_dir(self, action, dir_, env_yaml):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
288
        for dir_item in os.listdir(dir_):
289
            dir_item_path = os.path.join(dir_, dir_item)
290
            if not os.path.isfile(dir_item_path):
291
                continue
292
293
            _, ext = os.path.splitext(os.path.basename(dir_item_path))
294
            if ext != '.profile':
295
                sys.stderr.write(
296
                    "Encountered file '%s' while looking for profiles, "
297
                    "extension '%s' is unknown. Skipping..\n"
298
                    % (dir_item, ext)
299
                )
300
                continue
301
302
            self.profiles.append(Profile.from_yaml(dir_item_path, env_yaml))
303
            if action == "list-inputs":
304
                print(dir_item_path)
305
306
    def add_bash_remediation_fns_from_file(self, action, file_):
0 ignored issues
show
Coding Style Naming introduced by
The name add_bash_remediation_fns_from_file does not conform to the method naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
307
        if action == "list-inputs":
308
            print(file_)
309
        else:
310
            tree = ET.parse(file_)
311
            self.bash_remediation_fns_group = tree.getroot()
312
313
    def to_xml_element(self):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
314
        root = ET.Element('Benchmark')
315
        root.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
316
        root.set('xmlns:xhtml', 'http://www.w3.org/1999/xhtml')
317
        root.set('xmlns:dc', 'http://purl.org/dc/elements/1.1/')
318
        root.set('id', 'product-name')
319
        root.set('xsi:schemaLocation',
320
                 'http://checklists.nist.gov/xccdf/1.1 xccdf-1.1.4.xsd')
321
        root.set('style', 'SCAP_1.1')
322
        root.set('resolved', 'false')
323
        root.set('xml:lang', 'en-US')
324
        status = ET.SubElement(root, 'status')
325
        status.set('date', datetime.date.today().strftime("%Y-%m-%d"))
326
        status.text = self.status
327
        add_sub_element(root, "title", self.title)
328
        add_sub_element(root, "description", self.description)
329
        notice = add_sub_element(root, "notice", self.notice_description)
330
        notice.set('id', self.notice_id)
331
        add_sub_element(root, "front-matter", self.front_matter)
332
        add_sub_element(root, "rear-matter", self.rear_matter)
333
334
        for idref in self.cpes:
335
            plat = ET.SubElement(root, "platform")
336
            plat.set("idref", idref)
337
338
        version = ET.SubElement(root, 'version')
339
        version.text = self.version
340
        ET.SubElement(root, "metadata")
341
342
        for profile in self.profiles:
343
            if profile is not None:
344
                root.append(profile.to_xml_element())
345
346
        for value in self.values.values():
347
            root.append(value.to_xml_element())
348
        if self.bash_remediation_fns_group is not None:
349
            root.append(self.bash_remediation_fns_group)
350
        for group in self.groups.values():
351
            root.append(group.to_xml_element())
352
        for rule in self.rules.values():
353
            root.append(rule.to_xml_element())
354
355
        return root
356
357
    def to_file(self, file_name):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
358
        root = self.to_xml_element()
359
        tree = ET.ElementTree(root)
360
        tree.write(file_name)
361
362
    def add_value(self, value):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
363
        if value is None:
364
            return
365
        self.values[value.id_] = value
366
367
    def add_group(self, group):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
368
        if group is None:
369
            return
370
        self.groups[group.id_] = group
371
372
    def add_rule(self, rule):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
373
        if rule is None:
374
            return
375
        self.rules[rule.id_] = rule
376
377
    def to_xccdf(self):
378
        """We can easily extend this script to generate a valid XCCDF instead
379
        of SSG SHORTHAND.
380
        """
381
        raise NotImplementedError
382
383
    def __str__(self):
384
        return self.id_
385
386
387
class Group(object):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (9/7)
Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
388
    """Represents XCCDF Group
389
    """
390
    def __init__(self, id_):
391
        self.id_ = id_
392
        self.prodtype = "all"
393
        self.title = ""
394
        self.description = ""
395
        self.warnings = []
396
        self.values = {}
397
        self.groups = {}
398
        self.rules = {}
399
        self.platform = None
400
401
    @staticmethod
402
    def from_yaml(yaml_file, env_yaml=None):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
403
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
404
        if yaml_contents is None:
405
            return None
406
407
        group_id = os.path.basename(os.path.dirname(yaml_file))
408
        group = Group(group_id)
409
        group.prodtype = yaml_contents.pop("prodtype", "all")
410
        group.title = required_key(yaml_contents, "title")
411
        del yaml_contents["title"]
412
        group.description = required_key(yaml_contents, "description")
413
        del yaml_contents["description"]
414
        group.warnings = yaml_contents.pop("warnings", [])
415
        group.platform = yaml_contents.pop("platform", None)
416
417
        for warning_list in group.warnings:
418
            if len(warning_list) != 1:
419
                raise ValueError("Only one key/value pair should exist for each dictionary")
420
421
        if yaml_contents:
422
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
423
                               % (yaml_file, yaml_contents))
424
425
        return group
426
427
    def to_xml_element(self):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
428
        group = ET.Element('Group')
429
        group.set('id', self.id_)
430
        if self.prodtype != "all":
431
            group.set("prodtype", self.prodtype)
432
        title = ET.SubElement(group, 'title')
433
        title.text = self.title
434
        add_sub_element(group, 'description', self.description)
435
        add_warning_elements(group, self.warnings)
436
437
        if self.platform:
438
            platform_el = ET.SubElement(group, "platform")
439
            try:
440
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
441
            except KeyError:
442
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (103/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
443
            platform_el.set("idref", platform_cpe)
444
445
        for _value in self.values.values():
446
            group.append(_value.to_xml_element())
447
        for _group in self.groups.values():
448
            group.append(_group.to_xml_element())
449
        for _rule in self.rules.values():
450
            group.append(_rule.to_xml_element())
451
452
        return group
453
454
    def to_file(self, file_name):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
455
        root = self.to_xml_element()
456
        tree = ET.ElementTree(root)
457
        tree.write(file_name)
458
459
    def add_value(self, value):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
460
        if value is None:
461
            return
462
        self.values[value.id_] = value
463
464
    def add_group(self, group):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
465
        if group is None:
466
            return
467
        if self.platform and not group.platform:
468
            group.platform = self.platform
469
        self.groups[group.id_] = group
470
471
    def add_rule(self, rule):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
472
        if rule is None:
473
            return
474
        if self.platform and not rule.platform:
475
            rule.platform = self.platform
476
        self.rules[rule.id_] = rule
477
478
    def __str__(self):
479
        return self.id_
480
481
482
class Rule(object):
0 ignored issues
show
best-practice introduced by
Too many instance attributes (13/7)
Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
483
    """Represents XCCDF Rule
484
    """
485
    def __init__(self, id_):
486
        self.id_ = id_
487
        self.prodtype = "all"
488
        self.title = ""
489
        self.description = ""
490
        self.rationale = ""
491
        self.severity = "unknown"
492
        self.references = {}
493
        self.identifiers = {}
494
        self.ocil_clause = None
495
        self.ocil = None
496
        self.external_oval = None
497
        self.warnings = []
498
        self.platform = None
499
500
    @staticmethod
501
    def from_yaml(yaml_file, env_yaml=None):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
502
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
503
        if yaml_contents is None:
504
            return None
505
506
        rule_id, ext = os.path.splitext(os.path.basename(yaml_file))
507
        if rule_id == "rule" and ext == ".yml":
508
            rule_id = get_rule_dir_id(yaml_file)
509
510
        rule = Rule(rule_id)
511
        rule.prodtype = yaml_contents.pop("prodtype", "all")
512
        rule.title = required_key(yaml_contents, "title")
513
        del yaml_contents["title"]
514
        rule.description = required_key(yaml_contents, "description")
515
        del yaml_contents["description"]
516
        rule.rationale = required_key(yaml_contents, "rationale")
517
        del yaml_contents["rationale"]
518
        rule.severity = required_key(yaml_contents, "severity")
519
        del yaml_contents["severity"]
520
        rule.references = yaml_contents.pop("references", {})
521
        rule.identifiers = yaml_contents.pop("identifiers", {})
522
        rule.ocil_clause = yaml_contents.pop("ocil_clause", None)
523
        rule.ocil = yaml_contents.pop("ocil", None)
524
        rule.external_oval = yaml_contents.pop("oval_external_content", None)
525
        rule.warnings = yaml_contents.pop("warnings", [])
526
        rule.platform = yaml_contents.pop("platform", None)
527
528
        for warning_list in rule.warnings:
529
            if len(warning_list) != 1:
530
                raise ValueError("Only one key/value pair should exist for each dictionary")
531
532
        if yaml_contents:
533
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
534
                               % (yaml_file, yaml_contents))
535
536
        rule.validate_identifiers(yaml_file)
537
        rule.validate_references(yaml_file)
538
        return rule
539
540
    def validate_identifiers(self, yaml_file):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
541
        if self.identifiers is None:
542
            raise ValueError("Empty identifier section in file %s" % yaml_file)
543
544
        # Validate all identifiers are non-empty:
545
        for ident_type, ident_val in self.identifiers.items():
546
            if not isinstance(ident_type, str) or not isinstance(ident_val, str):
547
                raise ValueError("Identifiers and values must be strings: %s in file %s"
548
                                 % (ident_type, yaml_file))
549
            if ident_val.strip() == "":
550
                raise ValueError("Identifiers must not be empty: %s in file %s"
551
                                 % (ident_type, yaml_file))
552
            if ident_type[0:3] == 'cce':
553
                if not is_cce_valid("CCE-" + ident_val):
554
                    raise ValueError("CCE Identifiers must be valid: value %s for cce %s"
555
                                     " in file %s" % (ident_val, ident_type, yaml_file))
556
557
    def validate_references(self, yaml_file):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
558
        if self.references is None:
559
            raise ValueError("Empty references section in file %s" % yaml_file)
560
561
        for ref_type, ref_val in self.references.items():
562
            if not isinstance(ref_type, str) or not isinstance(ref_val, str):
563
                raise ValueError("References and values must be strings: %s in file %s"
564
                                 % (ref_type, yaml_file))
565
            if ref_val.strip() == "":
566
                raise ValueError("References must not be empty: %s in file %s"
567
                                 % (ref_type, yaml_file))
568
569
    def to_xml_element(self):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (19/15).
Loading history...
570
        rule = ET.Element('Rule')
571
        rule.set('id', self.id_)
572
        if self.prodtype != "all":
573
            rule.set("prodtype", self.prodtype)
574
        rule.set('severity', self.severity)
575
        add_sub_element(rule, 'title', self.title)
576
        add_sub_element(rule, 'description', self.description)
577
        add_sub_element(rule, 'rationale', self.rationale)
578
579
        main_ident = ET.Element('ident')
580
        for ident_type, ident_val in self.identifiers.items():
581
            if '@' in ident_type:
582
                # the ident is applicable only on some product
583
                # format : 'policy@product', eg. 'stigid@product'
584
                # for them, we create a separate <ref> element
585
                policy, product = ident_type.split('@')
586
                ident = ET.SubElement(rule, 'ident')
587
                ident.set(policy, ident_val)
588
                ident.set('prodtype', product)
589
            else:
590
                main_ident.set(ident_type, ident_val)
591
592
        if main_ident.attrib:
593
            rule.append(main_ident)
594
595
        main_ref = ET.Element('ref')
596
        for ref_type, ref_val in self.references.items():
597
            if '@' in ref_type:
598
                # the reference is applicable only on some product
599
                # format : 'policy@product', eg. 'stigid@product'
600
                # for them, we create a separate <ref> element
601
                policy, product = ref_type.split('@')
602
                ref = ET.SubElement(rule, 'ref')
603
                ref.set(policy, ref_val)
604
                ref.set('prodtype', product)
605
            else:
606
                main_ref.set(ref_type, ref_val)
607
608
        if main_ref.attrib:
609
            rule.append(main_ref)
610
611
        if self.external_oval:
612
            check = ET.SubElement(rule, 'check')
613
            check.set("system", "http://oval.mitre.org/XMLSchema/oval-definitions-5")
614
            external_content = ET.SubElement(check, "check-content-ref")
615
            external_content.set("href", self.external_oval)
616
        else:
617
            # TODO: This is pretty much a hack, oval ID will be the same as rule ID
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
618
            #       and we don't want the developers to have to keep them in sync.
619
            #       Therefore let's just add an OVAL ref of that ID.
620
            oval_ref = ET.SubElement(rule, "oval")
621
            oval_ref.set("id", self.id_)
622
623
        if self.ocil or self.ocil_clause:
624
            ocil = add_sub_element(rule, 'ocil', self.ocil if self.ocil else "")
625
            if self.ocil_clause:
626
                ocil.set("clause", self.ocil_clause)
627
628
        add_warning_elements(rule, self.warnings)
629
630
        if self.platform:
631
            platform_el = ET.SubElement(rule, "platform")
632
            try:
633
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
634
            except KeyError:
635
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (103/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
636
            platform_el.set("idref", platform_cpe)
637
638
        return rule
639
640
    def to_file(self, file_name):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
641
        root = self.to_xml_element()
642
        tree = ET.ElementTree(root)
643
        tree.write(file_name)
644
645
646
def load_benchmark_or_group(group_file, benchmark_file, guide_directory, action,
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
647
                            profiles_dir, env_yaml, bash_remediation_fns):
648
    """
649
    Loads a given benchmark or group from the specified benchmark_file or
650
    group_file, in the context of guide_directory, action, profiles_dir,
651
    env_yaml, and bash_remediation_fns.
652
653
    Returns the loaded group or benchmark.
654
    """
655
    group = None
656
    if group_file and benchmark_file:
657
        raise ValueError("A .benchmark file and a .group file were found in "
658
                         "the same directory '%s'" % (guide_directory))
659
660
    # we treat benchmark as a special form of group in the following code
661
    if benchmark_file:
662
        group = Benchmark.from_yaml(
663
            benchmark_file, 'product-name', env_yaml
664
        )
665
        if profiles_dir:
666
            group.add_profiles_from_dir(action, profiles_dir, env_yaml)
667
        group.add_bash_remediation_fns_from_file(action, bash_remediation_fns)
668
        if action == "list-inputs":
669
            print(benchmark_file)
670
671
    if group_file:
672
        group = Group.from_yaml(group_file, env_yaml)
673
        if action == "list-inputs":
674
            print(group_file)
675
676
    return group
677
678
679
def add_from_directory(action, parent_group, guide_directory, profiles_dir,
0 ignored issues
show
best-practice introduced by
Too many arguments (7/5)
Loading history...
Comprehensibility introduced by
This function exceeds the maximum number of variables (22/15).
Loading history...
680
                       bash_remediation_fns, output_file, env_yaml):
681
    """
682
    Process Variables, Benchmarks, and Rules in a given subdirectory,
683
    recursing as necessary.
684
685
    Behavior is dependent upon the value of action.
686
    """
687
    benchmark_file = None
688
    group_file = None
689
    rules = []
690
    values = []
691
    subdirectories = []
692
    for dir_item in os.listdir(guide_directory):
693
        dir_item_path = os.path.join(guide_directory, dir_item)
694
        _, extension = os.path.splitext(dir_item)
695
696
        if extension == '.var':
697
            values.append(dir_item_path)
698
        elif dir_item == "benchmark.yml":
699
            if benchmark_file:
700
                raise ValueError("Multiple benchmarks in one directory")
701
            benchmark_file = dir_item_path
702
        elif dir_item == "group.yml":
703
            if group_file:
704
                raise ValueError("Multiple groups in one directory")
705
            group_file = dir_item_path
706
        elif extension == '.rule':
707
            rules.append(dir_item_path)
708
        elif is_rule_dir(dir_item_path):
709
            rules.append(get_rule_dir_yaml(dir_item_path))
710
        elif dir_item != "tests":
711
            if os.path.isdir(dir_item_path):
712
                subdirectories.append(dir_item_path)
713
            else:
714
                sys.stderr.write(
715
                    "Encountered file '%s' while recursing, extension '%s' "
716
                    "is unknown. Skipping..\n"
717
                    % (dir_item, extension)
718
                )
719
720
    group = load_benchmark_or_group(group_file, benchmark_file, guide_directory, action,
721
                                    profiles_dir, env_yaml, bash_remediation_fns)
722
723
    if group is not None:
724
        if parent_group:
725
            parent_group.add_group(group)
726
        for value_yaml in values:
727
            if action == "list-inputs":
728
                print(value_yaml)
729
            else:
730
                value = Value.from_yaml(value_yaml, env_yaml)
731
                group.add_value(value)
732
733
        for subdir in subdirectories:
734
            add_from_directory(action, group, subdir, profiles_dir,
735
                               bash_remediation_fns, output_file,
736
                               env_yaml)
737
738
        for rule_yaml in rules:
739
            if action == "list-inputs":
740
                print(rule_yaml)
741
            else:
742
                rule = Rule.from_yaml(rule_yaml, env_yaml)
743
                group.add_rule(rule)
744
745
        if not parent_group:
746
            # We are on the top level!
747
            # Lets dump the XCCDF group or benchmark to a file
748
            if action == "build":
749
                group.to_file(output_file)
750