Passed
Push — master ( d4a97d...a9acad )
by Matěj
02:20 queued 12s
created

ssg.build_yaml   F

Complexity

Total Complexity 263

Size/Duplication

Total Lines 1315
Duplicated Lines 0 %

Test Coverage

Coverage 35.01%

Importance

Changes 0
Metric Value
eloc 956
dl 0
loc 1315
ccs 300
cts 857
cp 0.3501
rs 1.644
c 0
b 0
f 0
wmc 263

4 Functions

Rating   Name   Duplication   Size   Complexity  
A add_sub_element() 0 28 2
B reorder_according_to_ordering() 0 16 6
A add_warning_elements() 0 15 2
A add_nondata_subelements() 0 6 2

69 Methods

Rating   Name   Duplication   Size   Complexity  
A Profile.get_variable_selectors() 0 2 1
A Profile.__sub__() 0 11 1
B Profile.dump_yaml() 0 22 8
A Profile.get_rule_selectors() 0 2 1
A Profile.validate_refine_rules() 0 26 4
B Profile.to_xml_element() 0 36 7
A Profile.from_yaml() 0 24 4
B Profile._parse_selections() 0 21 6
A Profile.validate_rules() 0 21 4
A Profile.__init__() 0 9 1
B Profile.validate_variables() 0 31 5
A Group.add_value() 0 4 2
B Group.to_xml_element() 0 71 7
A Group.validate_prodtype() 0 8 3
A Group.add_rule() 0 7 4
A Benchmark.to_file() 0 4 1
A Benchmark.add_value() 0 4 2
B Group.from_yaml() 0 27 5
A Benchmark.__init__() 0 25 1
A Benchmark.add_rule() 0 4 2
A Value.to_file() 0 4 1
A Group.to_file() 0 4 1
A Group._pass_our_properties_on_to() 0 4 4
B Benchmark.from_yaml() 0 39 5
B Benchmark.add_profiles_from_dir() 0 27 7
A Benchmark.to_xccdf() 0 5 1
A ResolvableProfile._subtract_refinements() 0 11 4
A Benchmark.add_bash_remediation_fns_from_file() 0 7 2
A Group.__init__() 0 12 1
A Group.add_group() 0 7 4
A Benchmark.add_group() 0 4 2
B Value.from_yaml() 0 42 6
A Benchmark.__str__() 0 2 1
A Group.__str__() 0 2 1
A Value.to_xml_element() 0 23 5
B Benchmark.to_xml_element() 0 52 8
A Value.__init__() 0 9 1
A BuildLoader._get_new_loader() 0 3 1
A BuildLoader.export_group_to_file() 0 2 1
A BuildLoader.__init__() 0 6 3
A Rule.to_file() 0 4 1
F Rule.to_xml_element() 0 74 15
B BuildLoader._process_rules() 0 22 7
A DirectoryLoader._recurse_into_subdirs() 0 8 2
C Rule.validate_identifiers() 0 19 9
A Rule._set_attributes_from_dict() 0 10 4
A DirectoryLoader._process_values() 0 2 1
A Rule.make_refs_and_identifiers_product_specific() 0 31 4
B Rule.from_yaml() 0 33 8
A Rule.normalize() 0 10 2
A DirectoryLoader.__init__() 0 18 1
B Rule._make_items_product_specific() 0 23 8
B DirectoryLoader.load_benchmark_or_group() 0 27 6
A Rule.validate_prodtype() 0 8 3
C DirectoryLoader._collect_items_to_load() 0 27 11
A DirectoryLoader._get_new_loader() 0 2 1
C Rule.validate_references() 0 20 9
A Rule._verify_disa_cci_format() 0 11 4
A DirectoryLoader.process_directory_tree() 0 5 2
A BuildLoader._process_values() 0 5 2
A DirectoryLoader._load_group_process_and_recurse() 0 10 3
A DirectoryLoader._process_rules() 0 2 1
A Rule._verify_stigid_format() 0 6 3
A Rule._get_product_only_references() 0 11 5
A Rule.__init__() 0 18 1
A Rule.to_contents_dict() 0 10 2
A Rule.make_template_product_specific() 0 15 2
A ResolvableProfile.__init__() 0 3 1
B ResolvableProfile.resolve() 0 37 5

How to fix   Complexity   

Complexity

Complex classes like ssg.build_yaml often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1 2
from __future__ import absolute_import
2 2
from __future__ import print_function
3
4 2
import os
5 2
import os.path
6 2
from collections import defaultdict
7 2
from copy import deepcopy
8 2
import datetime
9 2
import re
10 2
import sys
11
12 2
import yaml
13
14 2
from .constants import XCCDF_PLATFORM_TO_CPE
15 2
from .constants import PRODUCT_TO_CPE_MAPPING
16 2
from .constants import XCCDF_REFINABLE_PROPERTIES
17 2
from .rules import get_rule_dir_id, get_rule_dir_yaml, is_rule_dir
18 2
from .rule_yaml import parse_prodtype
19
20 2
from .checks import is_cce_format_valid, is_cce_value_valid
21 2
from .yaml import DocumentationNotComplete, open_and_expand, open_and_macro_expand
22 2
from .utils import required_key, mkdir_p
23
24 2
from .xml import ElementTree as ET
25 2
from .shims import unicode_func
26
27
28 2
def add_sub_element(parent, tag, data):
29
    """
30
    Creates a new child element under parent with tag tag, and sets
31
    data as the content under the tag. In particular, data is a string
32
    to be parsed as an XML tree, allowing sub-elements of children to be
33
    added.
34
35
    If data should not be parsed as an XML tree, either escape the contents
36
    before passing into this function, or use ElementTree.SubElement().
37
38
    Returns the newly created subelement of type tag.
39
    """
40
    # This is used because our YAML data contain XML and XHTML elements
41
    # ET.SubElement() escapes the < > characters by &lt; and &gt;
42
    # and therefore it does not add child elements
43
    # we need to do a hack instead
44
    # TODO: Remove this function after we move to Markdown everywhere in SSG
45
    ustr = unicode_func("<{0}>{1}</{0}>").format(tag, data)
46
47
    try:
48
        element = ET.fromstring(ustr.encode("utf-8"))
49
    except Exception:
50
        msg = ("Error adding subelement to an element '{0}' from string: '{1}'"
51
               .format(parent.tag, ustr))
52
        raise RuntimeError(msg)
53
54
    parent.append(element)
55
    return element
56
57
58 2
def reorder_according_to_ordering(unordered, ordering, regex=None):
59 2
    ordered = []
60 2
    if regex is None:
61 2
        regex = "|".join(["({0})".format(item) for item in ordering])
62 2
    regex = re.compile(regex)
63
64 2
    items_to_order = list(filter(regex.match, unordered))
65 2
    unordered = set(unordered)
66
67 2
    for priority_type in ordering:
68 2
        for item in items_to_order:
69 2
            if priority_type in item and item in unordered:
70 2
                ordered.append(item)
71 2
                unordered.remove(item)
72 2
    ordered.extend(list(unordered))
73 2
    return ordered
74
75
76 2
def add_warning_elements(element, warnings):
77
    # The use of [{dict}, {dict}] in warnings is to handle the following
78
    # scenario where multiple warnings have the same category which is
79
    # valid in SCAP and our content:
80
    #
81
    # warnings:
82
    #     - general: Some general warning
83
    #     - general: Some other general warning
84
    #     - general: |-
85
    #         Some really long multiline general warning
86
    #
87
    # Each of the {dict} should have only one key/value pair.
88
    for warning_dict in warnings:
89
        warning = add_sub_element(element, "warning", list(warning_dict.values())[0])
90
        warning.set("category", list(warning_dict.keys())[0])
91
92
93 2
def add_nondata_subelements(element, subelement, attribute, attr_data):
94
    """Add multiple iterations of a sublement that contains an attribute but no data
95
       For example, <requires id="my_required_id"/>"""
96
    for data in attr_data:
97
        req = ET.SubElement(element, subelement)
98
        req.set(attribute, data)
99
100
101 2
class Profile(object):
102
    """Represents XCCDF profile
103
    """
104
105 2
    def __init__(self, id_):
106 2
        self.id_ = id_
107 2
        self.title = ""
108 2
        self.description = ""
109 2
        self.extends = None
110 2
        self.selected = []
111 2
        self.unselected = []
112 2
        self.variables = dict()
113 2
        self.refine_rules = defaultdict(list)
114
115 2
    @classmethod
116 2
    def from_yaml(cls, yaml_file, env_yaml=None):
117 2
        yaml_contents = open_and_expand(yaml_file, env_yaml)
118 2
        if yaml_contents is None:
119
            return None
120
121 2
        basename, _ = os.path.splitext(os.path.basename(yaml_file))
122
123 2
        profile = cls(basename)
124 2
        profile.title = required_key(yaml_contents, "title")
125 2
        del yaml_contents["title"]
126 2
        profile.description = required_key(yaml_contents, "description")
127 2
        del yaml_contents["description"]
128 2
        profile.extends = yaml_contents.pop("extends", None)
129 2
        selection_entries = required_key(yaml_contents, "selections")
130 2
        if selection_entries:
131 2
            profile._parse_selections(selection_entries)
132 2
        del yaml_contents["selections"]
133
134 2
        if yaml_contents:
135
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
136
                               % (yaml_file, yaml_contents))
137
138 2
        return profile
139
140 2
    def dump_yaml(self, file_name, documentation_complete=True):
141
        to_dump = {}
142
        to_dump["documentation_complete"] = documentation_complete
143
        to_dump["title"] = self.title
144
        to_dump["description"] = self.description
145
        if self.extends is not None:
146
            to_dump["extends"] = self.extends
147
148
        selections = []
149
        for item in self.selected:
150
            selections.append(item)
151
        for item in self.unselected:
152
            selections.append("!"+item)
153
        for varname in self.variables.keys():
154
            selections.append(varname+"="+self.variables.get(varname))
155
        for rule, refinements in self.refine_rules.items():
156
            for prop, val in refinements:
157
                selections.append("{rule}.{property}={value}"
158
                                  .format(rule=rule, property=prop, value=val))
159
        to_dump["selections"] = selections
160
        with open(file_name, "w+") as f:
161
            yaml.dump(to_dump, f, indent=4)
162
163 2
    def _parse_selections(self, entries):
164 2
        for item in entries:
165 2
            if "." in item:
166
                rule, refinement = item.split(".", 1)
167
                property_, value = refinement.split("=", 1)
168
                if property_ not in XCCDF_REFINABLE_PROPERTIES:
169
                    msg = ("Property '{property_}' cannot be refined. "
170
                           "Rule properties that can be refined are {refinables}. "
171
                           "Fix refinement '{rule_id}.{property_}={value}' in profile '{profile}'."
172
                           .format(property_=property_, refinables=XCCDF_REFINABLE_PROPERTIES,
173
                                   rule_id=rule, value=value, profile=self.id_)
174
                           )
175
                    raise ValueError(msg)
176
                self.refine_rules[rule].append((property_, value))
177 2
            elif "=" in item:
178 2
                varname, value = item.split("=", 1)
179 2
                self.variables[varname] = value
180 2
            elif item.startswith("!"):
181
                self.unselected.append(item[1:])
182
            else:
183 2
                self.selected.append(item)
184
185 2
    def to_xml_element(self):
186
        element = ET.Element('Profile')
187
        element.set("id", self.id_)
188
        if self.extends:
189
            element.set("extends", self.extends)
190
        title = add_sub_element(element, "title", self.title)
191
        title.set("override", "true")
192
        desc = add_sub_element(element, "description", self.description)
193
        desc.set("override", "true")
194
195
        for selection in self.selected:
196
            select = ET.Element("select")
197
            select.set("idref", selection)
198
            select.set("selected", "true")
199
            element.append(select)
200
201
        for selection in self.unselected:
202
            unselect = ET.Element("select")
203
            unselect.set("idref", selection)
204
            unselect.set("selected", "false")
205
            element.append(unselect)
206
207
        for value_id, selector in self.variables.items():
208
            refine_value = ET.Element("refine-value")
209
            refine_value.set("idref", value_id)
210
            refine_value.set("selector", selector)
211
            element.append(refine_value)
212
213
        for refined_rule, refinement_list in self.refine_rules.items():
214
            refine_rule = ET.Element("refine-rule")
215
            refine_rule.set("idref", refined_rule)
216
            for refinement in refinement_list:
217
                refine_rule.set(refinement[0], refinement[1])
218
            element.append(refine_rule)
219
220
        return element
221
222 2
    def get_rule_selectors(self):
223 2
        return list(self.selected + self.unselected)
224
225 2
    def get_variable_selectors(self):
226 2
        return self.variables
227
228 2
    def validate_refine_rules(self, rules):
229
        existing_rule_ids = [r.id_ for r in rules]
230
        for refine_rule, refinement_list in self.refine_rules.items():
231
            # Take first refinement to ilustrate where the error is
232
            # all refinements in list are invalid, so it doesn't really matter
233
            a_refinement = refinement_list[0]
234
235
            if refine_rule not in existing_rule_ids:
236
                msg = (
237
                    "You are trying to refine a rule that doesn't exist. "
238
                    "Rule '{rule_id}' was not found in the benchmark. "
239
                    "Please check all rule refinements for rule: '{rule_id}', for example: "
240
                    "- {rule_id}.{property_}={value}' in profile {profile_id}."
241
                    .format(rule_id=refine_rule, profile_id=self.id_,
242
                            property_=a_refinement[0], value=a_refinement[1])
243
                    )
244
                raise ValueError(msg)
245
246
            if refine_rule not in self.get_rule_selectors():
247
                msg = ("- {rule_id}.{property_}={value}' in profile '{profile_id}' is refining "
248
                       "a rule that is not selected by it. The refinement will not have any "
249
                       "noticeable effect. Either select the rule or remove the rule refinement."
250
                       .format(rule_id=refine_rule, property_=a_refinement[0],
251
                               value=a_refinement[1], profile_id=self.id_)
252
                       )
253
                raise ValueError(msg)
254
255 2
    def validate_variables(self, variables):
256
        variables_by_id = dict()
257
        for var in variables:
258
            variables_by_id[var.id_] = var
259
260
        for var_id, our_val in self.variables.items():
261
            if var_id not in variables_by_id:
262
                all_vars_list = [" - %s" % v for v in variables_by_id.keys()]
263
                msg = (
264
                    "Value '{var_id}' in profile '{profile_name}' is not known. "
265
                    "We know only variables:\n{var_names}"
266
                    .format(
267
                        var_id=var_id, profile_name=self.id_,
268
                        var_names="\n".join(sorted(all_vars_list)))
269
                )
270
                raise ValueError(msg)
271
272
            allowed_selectors = [str(s) for s in variables_by_id[var_id].options.keys()]
273
            if our_val not in allowed_selectors:
274
                msg = (
275
                    "Value '{var_id}' in profile '{profile_name}' "
276
                    "uses the selector '{our_val}'. "
277
                    "This is not possible, as only selectors {all_selectors} are available. "
278
                    "Either change the selector used in the profile, or "
279
                    "add the selector-value pair to the variable definition."
280
                    .format(
281
                        var_id=var_id, profile_name=self.id_, our_val=our_val,
282
                        all_selectors=allowed_selectors,
283
                    )
284
                )
285
                raise ValueError(msg)
286
287 2
    def validate_rules(self, rules, groups):
288
        existing_rule_ids = [r.id_ for r in rules]
289
        rule_selectors = self.get_rule_selectors()
290
        for id_ in rule_selectors:
291
            if id_ in groups:
292
                msg = (
293
                    "You have selected a group '{group_id}' instead of a "
294
                    "rule. Groups have no effect in the profile and are not "
295
                    "allowed to be selected. Please remove '{group_id}' "
296
                    "from profile '{profile_id}' before proceeding."
297
                    .format(group_id=id_, profile_id=self.id_)
298
                )
299
                raise ValueError(msg)
300
            if id_ not in existing_rule_ids:
301
                msg = (
302
                    "Rule '{rule_id}' was not found in the benchmark. Please "
303
                    "remove rule '{rule_id}' from profile '{profile_id}' "
304
                    "before proceeding."
305
                    .format(rule_id=id_, profile_id=self.id_)
306
                )
307
                raise ValueError(msg)
308
309 2
    def __sub__(self, other):
310
        profile = Profile(self.id_)
311
        profile.title = self.title
312
        profile.description = self.description
313
        profile.extends = self.extends
314
        profile.selected = list(set(self.selected) - set(other.selected))
315
        profile.selected.sort()
316
        profile.unselected = list(set(self.unselected) - set(other.unselected))
317
        profile.variables = dict ((k, v) for (k, v) in self.variables.items()
318
                             if k not in other.variables or v != other.variables[k])
319
        return profile
320
321
322 2
class ResolvableProfile(Profile):
323 2
    def __init__(self, * args, ** kwargs):
324
        super(ResolvableProfile, self).__init__(* args, ** kwargs)
325
        self.resolved = False
326
327 2
    def resolve(self, all_profiles):
328
        if self.resolved:
329
            return
330
331
        resolved_selections = set(self.selected)
332
        if self.extends:
333
            if self.extends not in all_profiles:
334
                msg = (
335
                    "Profile {name} extends profile {extended}, but"
336
                    "only profiles {known_profiles} are available for resolution."
337
                    .format(name=self.id_, extended=self.extends,
338
                            profiles=list(all_profiles.keys())))
339
                raise RuntimeError(msg)
340
            extended_profile = all_profiles[self.extends]
341
            extended_profile.resolve(all_profiles)
342
343
            extended_selects = set(extended_profile.selected)
344
            resolved_selections.update(extended_selects)
345
346
            updated_variables = dict(extended_profile.variables)
347
            updated_variables.update(self.variables)
348
            self.variables = updated_variables
349
350
            extended_refinements = deepcopy(extended_profile.refine_rules)
351
            updated_refinements = self._subtract_refinements(extended_refinements)
352
            updated_refinements.update(self.refine_rules)
353
            self.refine_rules = updated_refinements
354
355
        for uns in self.unselected:
356
            resolved_selections.discard(uns)
357
358
        self.unselected = []
359
        self.extends = None
360
361
        self.selected = sorted(resolved_selections)
362
363
        self.resolved = True
364
365 2
    def _subtract_refinements(self, extended_refinements):
366
        """
367
        Given a dict of rule refinements from the extended profile,
368
        "undo" every refinement prefixed with '!' in this profile.
369
        """
370
        for rule, refinements in list(self.refine_rules.items()):
371
            if rule.startswith("!"):
372
                for prop, val in refinements:
373
                    extended_refinements[rule[1:]].remove((prop, val))
374
                del self.refine_rules[rule]
375
        return extended_refinements
376
377
378 2
class Value(object):
379
    """Represents XCCDF Value
380
    """
381
382 2
    def __init__(self, id_):
383 2
        self.id_ = id_
384 2
        self.title = ""
385 2
        self.description = ""
386 2
        self.type_ = "string"
387 2
        self.operator = "equals"
388 2
        self.interactive = False
389 2
        self.options = {}
390 2
        self.warnings = []
391
392 2
    @staticmethod
393 2
    def from_yaml(yaml_file, env_yaml=None):
394 2
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
395 2
        if yaml_contents is None:
396
            return None
397
398 2
        value_id, _ = os.path.splitext(os.path.basename(yaml_file))
399 2
        value = Value(value_id)
400 2
        value.title = required_key(yaml_contents, "title")
401 2
        del yaml_contents["title"]
402 2
        value.description = required_key(yaml_contents, "description")
403 2
        del yaml_contents["description"]
404 2
        value.type_ = required_key(yaml_contents, "type")
405 2
        del yaml_contents["type"]
406 2
        value.operator = yaml_contents.pop("operator", "equals")
407 2
        possible_operators = ["equals", "not equal", "greater than",
408
                              "less than", "greater than or equal",
409
                              "less than or equal", "pattern match"]
410
411 2
        if value.operator not in possible_operators:
412
            raise ValueError(
413
                "Found an invalid operator value '%s' in '%s'. "
414
                "Expected one of: %s"
415
                % (value.operator, yaml_file, ", ".join(possible_operators))
416
            )
417
418 2
        value.interactive = \
419
            yaml_contents.pop("interactive", "false").lower() == "true"
420
421 2
        value.options = required_key(yaml_contents, "options")
422 2
        del yaml_contents["options"]
423 2
        value.warnings = yaml_contents.pop("warnings", [])
424
425 2
        for warning_list in value.warnings:
426
            if len(warning_list) != 1:
427
                raise ValueError("Only one key/value pair should exist for each dictionary")
428
429 2
        if yaml_contents:
430
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
431
                               % (yaml_file, yaml_contents))
432
433 2
        return value
434
435 2
    def to_xml_element(self):
436
        value = ET.Element('Value')
437
        value.set('id', self.id_)
438
        value.set('type', self.type_)
439
        if self.operator != "equals":  # equals is the default
440
            value.set('operator', self.operator)
441
        if self.interactive:  # False is the default
442
            value.set('interactive', 'true')
443
        title = ET.SubElement(value, 'title')
444
        title.text = self.title
445
        add_sub_element(value, 'description', self.description)
446
        add_warning_elements(value, self.warnings)
447
448
        for selector, option in self.options.items():
449
            # do not confuse Value with big V with value with small v
450
            # value is child element of Value
451
            value_small = ET.SubElement(value, 'value')
452
            # by XCCDF spec, default value is value without selector
453
            if selector != "default":
454
                value_small.set('selector', str(selector))
455
            value_small.text = str(option)
456
457
        return value
458
459 2
    def to_file(self, file_name):
460
        root = self.to_xml_element()
461
        tree = ET.ElementTree(root)
462
        tree.write(file_name)
463
464
465 2
class Benchmark(object):
466
    """Represents XCCDF Benchmark
467
    """
468 2
    def __init__(self, id_):
469
        self.id_ = id_
470
        self.title = ""
471
        self.status = ""
472
        self.description = ""
473
        self.notice_id = ""
474
        self.notice_description = ""
475
        self.front_matter = ""
476
        self.rear_matter = ""
477
        self.cpes = []
478
        self.version = "0.1"
479
        self.profiles = []
480
        self.values = {}
481
        self.bash_remediation_fns_group = None
482
        self.groups = {}
483
        self.rules = {}
484
485
        # This is required for OCIL clauses
486
        conditional_clause = Value("conditional_clause")
487
        conditional_clause.title = "A conditional clause for check statements."
488
        conditional_clause.description = conditional_clause.title
489
        conditional_clause.type_ = "string"
490
        conditional_clause.options = {"": "This is a placeholder"}
491
492
        self.add_value(conditional_clause)
493
494 2
    @classmethod
495 2
    def from_yaml(cls, yaml_file, id_, product_yaml=None):
496
        yaml_contents = open_and_macro_expand(yaml_file, product_yaml)
497
        if yaml_contents is None:
498
            return None
499
500
        benchmark = cls(id_)
501
        benchmark.title = required_key(yaml_contents, "title")
502
        del yaml_contents["title"]
503
        benchmark.status = required_key(yaml_contents, "status")
504
        del yaml_contents["status"]
505
        benchmark.description = required_key(yaml_contents, "description")
506
        del yaml_contents["description"]
507
        notice_contents = required_key(yaml_contents, "notice")
508
        benchmark.notice_id = required_key(notice_contents, "id")
509
        del notice_contents["id"]
510
        benchmark.notice_description = required_key(notice_contents,
511
                                                    "description")
512
        del notice_contents["description"]
513
        if not notice_contents:
514
            del yaml_contents["notice"]
515
516
        benchmark.front_matter = required_key(yaml_contents,
517
                                              "front-matter")
518
        del yaml_contents["front-matter"]
519
        benchmark.rear_matter = required_key(yaml_contents,
520
                                             "rear-matter")
521
        del yaml_contents["rear-matter"]
522
        benchmark.version = str(required_key(yaml_contents, "version"))
523
        del yaml_contents["version"]
524
525
        if yaml_contents:
526
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
527
                               % (yaml_file, yaml_contents))
528
529
        if product_yaml:
530
            benchmark.cpes = PRODUCT_TO_CPE_MAPPING[product_yaml["product"]]
531
532
        return benchmark
533
534 2
    def add_profiles_from_dir(self, dir_, env_yaml):
535
        for dir_item in os.listdir(dir_):
536
            dir_item_path = os.path.join(dir_, dir_item)
537
            if not os.path.isfile(dir_item_path):
538
                continue
539
540
            _, ext = os.path.splitext(os.path.basename(dir_item_path))
541
            if ext != '.profile':
542
                sys.stderr.write(
543
                    "Encountered file '%s' while looking for profiles, "
544
                    "extension '%s' is unknown. Skipping..\n"
545
                    % (dir_item, ext)
546
                )
547
                continue
548
549
            try:
550
                new_profile = Profile.from_yaml(dir_item_path, env_yaml)
551
            except DocumentationNotComplete:
552
                continue
553
            except Exception as exc:
554
                msg = ("Error building profile from '{fname}': '{error}'"
555
                       .format(fname=dir_item_path, error=str(exc)))
556
                raise RuntimeError(msg)
557
            if new_profile is None:
558
                continue
559
560
            self.profiles.append(new_profile)
561
562 2
    def add_bash_remediation_fns_from_file(self, file_):
563
        if not file_:
564
            # bash-remediation-functions.xml doens't exist
565
            return
566
567
        tree = ET.parse(file_)
568
        self.bash_remediation_fns_group = tree.getroot()
569
570 2
    def to_xml_element(self):
571
        root = ET.Element('Benchmark')
572
        root.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
573
        root.set('xmlns:xhtml', 'http://www.w3.org/1999/xhtml')
574
        root.set('xmlns:dc', 'http://purl.org/dc/elements/1.1/')
575
        root.set('id', 'product-name')
576
        root.set('xsi:schemaLocation',
577
                 'http://checklists.nist.gov/xccdf/1.1 xccdf-1.1.4.xsd')
578
        root.set('style', 'SCAP_1.1')
579
        root.set('resolved', 'false')
580
        root.set('xml:lang', 'en-US')
581
        status = ET.SubElement(root, 'status')
582
        status.set('date', datetime.date.today().strftime("%Y-%m-%d"))
583
        status.text = self.status
584
        add_sub_element(root, "title", self.title)
585
        add_sub_element(root, "description", self.description)
586
        notice = add_sub_element(root, "notice", self.notice_description)
587
        notice.set('id', self.notice_id)
588
        add_sub_element(root, "front-matter", self.front_matter)
589
        add_sub_element(root, "rear-matter", self.rear_matter)
590
591
        for idref in self.cpes:
592
            plat = ET.SubElement(root, "platform")
593
            plat.set("idref", idref)
594
595
        version = ET.SubElement(root, 'version')
596
        version.text = self.version
597
        ET.SubElement(root, "metadata")
598
599
        for profile in self.profiles:
600
            root.append(profile.to_xml_element())
601
602
        for value in self.values.values():
603
            root.append(value.to_xml_element())
604
        if self.bash_remediation_fns_group is not None:
605
            root.append(self.bash_remediation_fns_group)
606
607
        groups_in_bench = list(self.groups.keys())
608
        priority_order = ["system", "services"]
609
        groups_in_bench = reorder_according_to_ordering(groups_in_bench, priority_order)
610
611
        # Make system group the first, followed by services group
612
        for group_id in groups_in_bench:
613
            group = self.groups.get(group_id)
614
            # Products using application benchmark don't have system or services group
615
            if group is not None:
616
                root.append(group.to_xml_element())
617
618
        for rule in self.rules.values():
619
            root.append(rule.to_xml_element())
620
621
        return root
622
623 2
    def to_file(self, file_name):
624
        root = self.to_xml_element()
625
        tree = ET.ElementTree(root)
626
        tree.write(file_name)
627
628 2
    def add_value(self, value):
629
        if value is None:
630
            return
631
        self.values[value.id_] = value
632
633 2
    def add_group(self, group):
634
        if group is None:
635
            return
636
        self.groups[group.id_] = group
637
638 2
    def add_rule(self, rule):
639
        if rule is None:
640
            return
641
        self.rules[rule.id_] = rule
642
643 2
    def to_xccdf(self):
644
        """We can easily extend this script to generate a valid XCCDF instead
645
        of SSG SHORTHAND.
646
        """
647
        raise NotImplementedError
648
649 2
    def __str__(self):
650
        return self.id_
651
652
653 2
class Group(object):
654
    """Represents XCCDF Group
655
    """
656 2
    ATTRIBUTES_TO_PASS_ON = (
657
        "platform",
658
    )
659
660 2
    def __init__(self, id_):
661
        self.id_ = id_
662
        self.prodtype = "all"
663
        self.title = ""
664
        self.description = ""
665
        self.warnings = []
666
        self.requires = []
667
        self.conflicts = []
668
        self.values = {}
669
        self.groups = {}
670
        self.rules = {}
671
        self.platform = None
672
673 2
    @classmethod
674 2
    def from_yaml(cls, yaml_file, env_yaml=None):
675
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
676
        if yaml_contents is None:
677
            return None
678
679
        group_id = os.path.basename(os.path.dirname(yaml_file))
680
        group = cls(group_id)
681
        group.prodtype = yaml_contents.pop("prodtype", "all")
682
        group.title = required_key(yaml_contents, "title")
683
        del yaml_contents["title"]
684
        group.description = required_key(yaml_contents, "description")
685
        del yaml_contents["description"]
686
        group.warnings = yaml_contents.pop("warnings", [])
687
        group.conflicts = yaml_contents.pop("conflicts", [])
688
        group.requires = yaml_contents.pop("requires", [])
689
        group.platform = yaml_contents.pop("platform", None)
690
691
        for warning_list in group.warnings:
692
            if len(warning_list) != 1:
693
                raise ValueError("Only one key/value pair should exist for each dictionary")
694
695
        if yaml_contents:
696
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
697
                               % (yaml_file, yaml_contents))
698
        group.validate_prodtype(yaml_file)
699
        return group
700
701 2
    def validate_prodtype(self, yaml_file):
702
        for ptype in self.prodtype.split(","):
703
            if ptype.strip() != ptype:
704
                msg = (
705
                    "Comma-separated '{prodtype}' prodtype "
706
                    "in {yaml_file} contains whitespace."
707
                    .format(prodtype=self.prodtype, yaml_file=yaml_file))
708
                raise ValueError(msg)
709
710 2
    def to_xml_element(self):
711
        group = ET.Element('Group')
712
        group.set('id', self.id_)
713
        if self.prodtype != "all":
714
            group.set("prodtype", self.prodtype)
715
        title = ET.SubElement(group, 'title')
716
        title.text = self.title
717
        add_sub_element(group, 'description', self.description)
718
        add_warning_elements(group, self.warnings)
719
        add_nondata_subelements(group, "requires", "id", self.requires)
720
        add_nondata_subelements(group, "conflicts", "id", self.conflicts)
721
722
        if self.platform:
723
            platform_el = ET.SubElement(group, "platform")
724
            try:
725
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
726
            except KeyError:
727
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
728
            platform_el.set("idref", platform_cpe)
729
730
        for _value in self.values.values():
731
            group.append(_value.to_xml_element())
732
733
        # Rules that install or remove packages affect remediation
734
        # of other rules.
735
        # When packages installed/removed rules come first:
736
        # The Rules are ordered in more logical way, and
737
        # remediation order is natural, first the package is installed, then configured.
738
        rules_in_group = list(self.rules.keys())
739
        regex = r'(package_.*_(installed|removed))|(service_.*_(enabled|disabled))$'
740
        priority_order = ["installed", "removed", "enabled", "disabled"]
741
        rules_in_group = reorder_according_to_ordering(rules_in_group, priority_order, regex)
742
743
        # Add rules in priority order, first all packages installed, then removed,
744
        # followed by services enabled, then disabled
745
        for rule_id in rules_in_group:
746
            group.append(self.rules.get(rule_id).to_xml_element())
747
748
        # Add the sub groups after any current level group rules.
749
        # As package installed/removed and service enabled/disabled rules are usuallly in
750
        # top level group, this ensures groups that further configure a package or service
751
        # are after rules that install or remove it.
752
        groups_in_group = list(self.groups.keys())
753
        # The account group has to precede audit group because
754
        # the rule package_screen_installed is desired to be executed before the rule
755
        # audit_rules_privileged_commands, othervise the rule
756
        # does not catch newly installed screeen binary during remediation
757
        # and report fail
758
        # the software group should come before the
759
        # bootloader-grub2 group because of conflict between
760
        # rules rpm_verify_permissions and file_permissions_grub2_cfg
761
        # specific rules concerning permissions should
762
        # be applied after the general rpm_verify_permissions
763
        # The FIPS group should come before Crypto - if we want to set a different (stricter) Crypto Policy than FIPS.
764
        # the firewalld_activation must come before ruleset_modifications, othervise
765
        # remediations for ruleset_modifications won't work
766
        # rules from group disabling_ipv6 must precede rules from configuring_ipv6,
767
        # otherwise the remediation prints error although it is successful
768
        priority_order = [
769
            "accounts", "auditing",
770
            "software", "bootloader-grub2",
771
            "fips", "crypto",
772
            "firewalld_activation", "ruleset_modifications",
773
            "disabling_ipv6", "configuring_ipv6"
774
        ]
775
        groups_in_group = reorder_according_to_ordering(groups_in_group, priority_order)
776
        for group_id in groups_in_group:
777
            _group = self.groups[group_id]
778
            group.append(_group.to_xml_element())
779
780
        return group
781
782 2
    def to_file(self, file_name):
783
        root = self.to_xml_element()
784
        tree = ET.ElementTree(root)
785
        tree.write(file_name)
786
787 2
    def add_value(self, value):
788
        if value is None:
789
            return
790
        self.values[value.id_] = value
791
792 2
    def add_group(self, group):
793
        if group is None:
794
            return
795
        if self.platform and not group.platform:
796
            group.platform = self.platform
797
        self.groups[group.id_] = group
798
        self._pass_our_properties_on_to(group)
799
800 2
    def _pass_our_properties_on_to(self, obj):
801
        for attr in self.ATTRIBUTES_TO_PASS_ON:
802
            if hasattr(obj, attr) and getattr(obj, attr) is None:
803
                setattr(obj, attr, getattr(self, attr))
804
805 2
    def add_rule(self, rule):
806
        if rule is None:
807
            return
808
        if self.platform and not rule.platform:
809
            rule.platform = self.platform
810
        self.rules[rule.id_] = rule
811
        self._pass_our_properties_on_to(rule)
812
813 2
    def __str__(self):
814
        return self.id_
815
816
817 2
class Rule(object):
818
    """Represents XCCDF Rule
819
    """
820 2
    YAML_KEYS_DEFAULTS = {
821
        "prodtype": lambda: "all",
822
        "title": lambda: RuntimeError("Missing key 'title'"),
823
        "description": lambda: RuntimeError("Missing key 'description'"),
824
        "rationale": lambda: RuntimeError("Missing key 'rationale'"),
825
        "severity": lambda: RuntimeError("Missing key 'severity'"),
826
        "references": lambda: dict(),
827
        "identifiers": lambda: dict(),
828
        "ocil_clause": lambda: None,
829
        "ocil": lambda: None,
830
        "oval_external_content": lambda: None,
831
        "warnings": lambda: list(),
832
        "conflicts": lambda: list(),
833
        "requires": lambda: list(),
834
        "platform": lambda: None,
835
        "inherited_platforms": lambda: list(),
836
        "template": lambda: None,
837
    }
838
839 2
    def __init__(self, id_):
840 2
        self.id_ = id_
841 2
        self.prodtype = "all"
842 2
        self.title = ""
843 2
        self.description = ""
844 2
        self.rationale = ""
845 2
        self.severity = "unknown"
846 2
        self.references = {}
847 2
        self.identifiers = {}
848 2
        self.ocil_clause = None
849 2
        self.ocil = None
850 2
        self.oval_external_content = None
851 2
        self.warnings = []
852 2
        self.requires = []
853 2
        self.conflicts = []
854 2
        self.platform = None
855 2
        self.inherited_platforms = [] # platforms inherited from the group
856 2
        self.template = None
857
858 2
    @classmethod
859 2
    def from_yaml(cls, yaml_file, env_yaml=None):
860 2
        yaml_file = os.path.normpath(yaml_file)
861
862 2
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
863 2
        if yaml_contents is None:
864
            return None
865
866 2
        rule_id, ext = os.path.splitext(os.path.basename(yaml_file))
867 2
        if rule_id == "rule" and ext == ".yml":
868 2
            rule_id = get_rule_dir_id(yaml_file)
869
870 2
        rule = cls(rule_id)
871
872 2
        try:
873 2
            rule._set_attributes_from_dict(yaml_contents)
874
        except RuntimeError as exc:
875
            msg = ("Error processing '{fname}': {err}"
876
                   .format(fname=yaml_file, err=str(exc)))
877
            raise RuntimeError(msg)
878
879 2
        for warning_list in rule.warnings:
880
            if len(warning_list) != 1:
881
                raise ValueError("Only one key/value pair should exist for each dictionary")
882
883 2
        if yaml_contents:
884
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
885
                               % (yaml_file, yaml_contents))
886
887 2
        rule.validate_prodtype(yaml_file)
888 2
        rule.validate_identifiers(yaml_file)
889 2
        rule.validate_references(yaml_file)
890 2
        return rule
891
892 2
    def _verify_stigid_format(self, product):
893 2
        stig_id = self.references.get("stigid", None)
894 2
        if not stig_id:
895 2
            return
896 2
        if "," in stig_id:
897 2
            raise ValueError("Rules can not have multiple STIG IDs.")
898
899 2
    def _verify_disa_cci_format(self):
900 2
        cci_id = self.references.get("disa", None)
901 2
        if not cci_id:
902 2
            return
903
        cci_ex = re.compile(r'^CCI-[0-9]{6}$')
904
        for cci in cci_id.split(","):
905
            if not cci_ex.match(cci):
906
                raise ValueError("CCI '{}' is in the wrong format! "
907
                                 "Format should be similar to: "
908
                                 "CCI-XXXXXX".format(cci))
909
        self.references["disa"] = cci_id
910
911 2
    def normalize(self, product):
912 2
        try:
913 2
            self.make_refs_and_identifiers_product_specific(product)
914 2
            self.make_template_product_specific(product)
915 2
        except Exception as exc:
916 2
            msg = (
917
                "Error normalizing '{rule}': {msg}"
918
                .format(rule=self.id_, msg=str(exc))
919
            )
920 2
            raise RuntimeError(msg)
921
922 2
    def _get_product_only_references(self):
923 2
        PRODUCT_REFERENCES = ("stigid",)
924
925 2
        product_references = dict()
926
927 2
        for ref in PRODUCT_REFERENCES:
928 2
            start = "{0}@".format(ref)
929 2
            for gref, gval in self.references.items():
930 2
                if ref == gref or gref.startswith(start):
931 2
                    product_references[gref] = gval
932 2
        return product_references
933
934 2
    def make_template_product_specific(self, product):
935 2
        product_suffix = "@{0}".format(product)
936
937 2
        if not self.template:
938
            return
939
940 2
        not_specific_vars = self.template.get("vars", dict())
941 2
        specific_vars = self._make_items_product_specific(
942
            not_specific_vars, product_suffix, True)
943 2
        self.template["vars"] = specific_vars
944
945 2
        not_specific_backends = self.template.get("backends", dict())
946 2
        specific_backends = self._make_items_product_specific(
947
            not_specific_backends, product_suffix, True)
948 2
        self.template["backends"] = specific_backends
949
950 2
    def make_refs_and_identifiers_product_specific(self, product):
951 2
        product_suffix = "@{0}".format(product)
952
953 2
        product_references = self._get_product_only_references()
954 2
        general_references = self.references.copy()
955 2
        for todel in product_references:
956 2
            general_references.pop(todel)
957
958 2
        to_set = dict(
959
            identifiers=(self.identifiers, False),
960
            general_references=(general_references, True),
961
            product_references=(product_references, False),
962
        )
963 2
        for name, (dic, allow_overwrites) in to_set.items():
964 2
            try:
965 2
                new_items = self._make_items_product_specific(
966
                    dic, product_suffix, allow_overwrites)
967 2
            except ValueError as exc:
968 2
                msg = (
969
                    "Error processing {what} for rule '{rid}': {msg}"
970
                    .format(what=name, rid=self.id_, msg=str(exc))
971
                )
972 2
                raise ValueError(msg)
973 2
            dic.clear()
974 2
            dic.update(new_items)
975
976 2
        self.references = general_references
977 2
        self._verify_disa_cci_format()
978 2
        self.references.update(product_references)
979
980 2
        self._verify_stigid_format(product)
981
982 2
    def _make_items_product_specific(self, items_dict, product_suffix, allow_overwrites=False):
983 2
        new_items = dict()
984 2
        for full_label, value in items_dict.items():
985 2
            if "@" not in full_label and full_label not in new_items:
986 2
                new_items[full_label] = value
987 2
                continue
988
989 2
            if not full_label.endswith(product_suffix):
990 2
                continue
991
992 2
            label = full_label.split("@")[0]
993 2
            if label in items_dict and not allow_overwrites and value != items_dict[label]:
994 2
                msg = (
995
                    "There is a product-qualified '{item_q}' item, "
996
                    "but also an unqualified '{item_u}' item "
997
                    "and those two differ in value - "
998
                    "'{value_q}' vs '{value_u}' respectively."
999
                    .format(item_q=full_label, item_u=label,
1000
                            value_q=value, value_u=items_dict[label])
1001
                )
1002 2
                raise ValueError(msg)
1003 2
            new_items[label] = value
1004 2
        return new_items
1005
1006 2
    def _set_attributes_from_dict(self, yaml_contents):
1007 2
        for key, default_getter in self.YAML_KEYS_DEFAULTS.items():
1008 2
            if key not in yaml_contents:
1009 2
                value = default_getter()
1010 2
                if isinstance(value, Exception):
1011
                    raise value
1012
            else:
1013 2
                value = yaml_contents.pop(key)
1014
1015 2
            setattr(self, key, value)
1016
1017 2
    def to_contents_dict(self):
1018
        """
1019
        Returns a dictionary that is the same schema as the dict obtained when loading rule YAML.
1020
        """
1021
1022 2
        yaml_contents = dict()
1023 2
        for key in Rule.YAML_KEYS_DEFAULTS:
1024 2
            yaml_contents[key] = getattr(self, key)
1025
1026 2
        return yaml_contents
1027
1028 2
    def validate_identifiers(self, yaml_file):
1029 2
        if self.identifiers is None:
1030
            raise ValueError("Empty identifier section in file %s" % yaml_file)
1031
1032
        # Validate all identifiers are non-empty:
1033 2
        for ident_type, ident_val in self.identifiers.items():
1034 2
            if not isinstance(ident_type, str) or not isinstance(ident_val, str):
1035
                raise ValueError("Identifiers and values must be strings: %s in file %s"
1036
                                 % (ident_type, yaml_file))
1037 2
            if ident_val.strip() == "":
1038
                raise ValueError("Identifiers must not be empty: %s in file %s"
1039
                                 % (ident_type, yaml_file))
1040 2
            if ident_type[0:3] == 'cce':
1041 2
                if not is_cce_format_valid(ident_val):
1042
                    raise ValueError("CCE Identifier format must be valid: invalid format '%s' for CEE '%s'"
1043
                                     " in file '%s'" % (ident_val, ident_type, yaml_file))
1044 2
                if not is_cce_value_valid("CCE-" + ident_val):
1045
                    raise ValueError("CCE Identifier value is not a valid checksum: invalid value '%s' for CEE '%s'"
1046
                                     " in file '%s'" % (ident_val, ident_type, yaml_file))
1047
1048 2
    def validate_references(self, yaml_file):
1049 2
        if self.references is None:
1050
            raise ValueError("Empty references section in file %s" % yaml_file)
1051
1052 2
        for ref_type, ref_val in self.references.items():
1053 2
            if not isinstance(ref_type, str) or not isinstance(ref_val, str):
1054
                raise ValueError("References and values must be strings: %s in file %s"
1055
                                 % (ref_type, yaml_file))
1056 2
            if ref_val.strip() == "":
1057
                raise ValueError("References must not be empty: %s in file %s"
1058
                                 % (ref_type, yaml_file))
1059
1060 2
        for ref_type, ref_val in self.references.items():
1061 2
            for ref in ref_val.split(","):
1062 2
                if ref.strip() != ref:
1063
                    msg = (
1064
                        "Comma-separated '{ref_type}' reference "
1065
                        "in {yaml_file} contains whitespace."
1066
                        .format(ref_type=ref_type, yaml_file=yaml_file))
1067
                    raise ValueError(msg)
1068
1069 2
    def validate_prodtype(self, yaml_file):
1070 2
        for ptype in self.prodtype.split(","):
1071 2
            if ptype.strip() != ptype:
1072
                msg = (
1073
                    "Comma-separated '{prodtype}' prodtype "
1074
                    "in {yaml_file} contains whitespace."
1075
                    .format(prodtype=self.prodtype, yaml_file=yaml_file))
1076
                raise ValueError(msg)
1077
1078 2
    def to_xml_element(self):
1079
        rule = ET.Element('Rule')
1080
        rule.set('id', self.id_)
1081
        if self.prodtype != "all":
1082
            rule.set("prodtype", self.prodtype)
1083
        rule.set('severity', self.severity)
1084
        add_sub_element(rule, 'title', self.title)
1085
        add_sub_element(rule, 'description', self.description)
1086
        add_sub_element(rule, 'rationale', self.rationale)
1087
1088
        main_ident = ET.Element('ident')
1089
        for ident_type, ident_val in self.identifiers.items():
1090
            # This is not true if items were normalized
1091
            if '@' in ident_type:
1092
                # the ident is applicable only on some product
1093
                # format : 'policy@product', eg. 'stigid@product'
1094
                # for them, we create a separate <ref> element
1095
                policy, product = ident_type.split('@')
1096
                ident = ET.SubElement(rule, 'ident')
1097
                ident.set(policy, ident_val)
1098
                ident.set('prodtype', product)
1099
            else:
1100
                main_ident.set(ident_type, ident_val)
1101
1102
        if main_ident.attrib:
1103
            rule.append(main_ident)
1104
1105
        main_ref = ET.Element('ref')
1106
        for ref_type, ref_val in self.references.items():
1107
            # This is not true if items were normalized
1108
            if '@' in ref_type:
1109
                # the reference is applicable only on some product
1110
                # format : 'policy@product', eg. 'stigid@product'
1111
                # for them, we create a separate <ref> element
1112
                policy, product = ref_type.split('@')
1113
                ref = ET.SubElement(rule, 'ref')
1114
                ref.set(policy, ref_val)
1115
                ref.set('prodtype', product)
1116
            else:
1117
                main_ref.set(ref_type, ref_val)
1118
1119
        if main_ref.attrib:
1120
            rule.append(main_ref)
1121
1122
        if self.oval_external_content:
1123
            check = ET.SubElement(rule, 'check')
1124
            check.set("system", "http://oval.mitre.org/XMLSchema/oval-definitions-5")
1125
            external_content = ET.SubElement(check, "check-content-ref")
1126
            external_content.set("href", self.oval_external_content)
1127
        else:
1128
            # TODO: This is pretty much a hack, oval ID will be the same as rule ID
1129
            #       and we don't want the developers to have to keep them in sync.
1130
            #       Therefore let's just add an OVAL ref of that ID.
1131
            oval_ref = ET.SubElement(rule, "oval")
1132
            oval_ref.set("id", self.id_)
1133
1134
        if self.ocil or self.ocil_clause:
1135
            ocil = add_sub_element(rule, 'ocil', self.ocil if self.ocil else "")
1136
            if self.ocil_clause:
1137
                ocil.set("clause", self.ocil_clause)
1138
1139
        add_warning_elements(rule, self.warnings)
1140
        add_nondata_subelements(rule, "requires", "id", self.requires)
1141
        add_nondata_subelements(rule, "conflicts", "id", self.conflicts)
1142
1143
        if self.platform:
1144
            platform_el = ET.SubElement(rule, "platform")
1145
            try:
1146
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
1147
            except KeyError:
1148
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
1149
            platform_el.set("idref", platform_cpe)
1150
1151
        return rule
1152
1153 2
    def to_file(self, file_name):
1154
        root = self.to_xml_element()
1155
        tree = ET.ElementTree(root)
1156
        tree.write(file_name)
1157
1158
1159 2
class DirectoryLoader(object):
1160 2
    def __init__(self, profiles_dir, bash_remediation_fns, env_yaml):
1161
        self.benchmark_file = None
1162
        self.group_file = None
1163
        self.loaded_group = None
1164
        self.rule_files = []
1165
        self.value_files = []
1166
        self.subdirectories = []
1167
1168
        self.all_values = set()
1169
        self.all_rules = set()
1170
        self.all_groups = set()
1171
1172
        self.profiles_dir = profiles_dir
1173
        self.bash_remediation_fns = bash_remediation_fns
1174
        self.env_yaml = env_yaml
1175
        self.product = env_yaml["product"]
1176
1177
        self.parent_group = None
1178
1179 2
    def _collect_items_to_load(self, guide_directory):
1180
        for dir_item in os.listdir(guide_directory):
1181
            dir_item_path = os.path.join(guide_directory, dir_item)
1182
            _, extension = os.path.splitext(dir_item)
1183
1184
            if extension == '.var':
1185
                self.value_files.append(dir_item_path)
1186
            elif dir_item == "benchmark.yml":
1187
                if self.benchmark_file:
1188
                    raise ValueError("Multiple benchmarks in one directory")
1189
                self.benchmark_file = dir_item_path
1190
            elif dir_item == "group.yml":
1191
                if self.group_file:
1192
                    raise ValueError("Multiple groups in one directory")
1193
                self.group_file = dir_item_path
1194
            elif extension == '.rule':
1195
                self.rule_files.append(dir_item_path)
1196
            elif is_rule_dir(dir_item_path):
1197
                self.rule_files.append(get_rule_dir_yaml(dir_item_path))
1198
            elif dir_item != "tests":
1199
                if os.path.isdir(dir_item_path):
1200
                    self.subdirectories.append(dir_item_path)
1201
                else:
1202
                    sys.stderr.write(
1203
                        "Encountered file '%s' while recursing, extension '%s' "
1204
                        "is unknown. Skipping..\n"
1205
                        % (dir_item, extension)
1206
                    )
1207
1208 2
    def load_benchmark_or_group(self, guide_directory):
1209
        """
1210
        Loads a given benchmark or group from the specified benchmark_file or
1211
        group_file, in the context of guide_directory, profiles_dir,
1212
        env_yaml, and bash_remediation_fns.
1213
1214
        Returns the loaded group or benchmark.
1215
        """
1216
        group = None
1217
        if self.group_file and self.benchmark_file:
1218
            raise ValueError("A .benchmark file and a .group file were found in "
1219
                             "the same directory '%s'" % (guide_directory))
1220
1221
        # we treat benchmark as a special form of group in the following code
1222
        if self.benchmark_file:
1223
            group = Benchmark.from_yaml(
1224
                self.benchmark_file, 'product-name', self.env_yaml
1225
            )
1226
            if self.profiles_dir:
1227
                group.add_profiles_from_dir(self.profiles_dir, self.env_yaml)
1228
            group.add_bash_remediation_fns_from_file(self.bash_remediation_fns)
1229
1230
        if self.group_file:
1231
            group = Group.from_yaml(self.group_file, self.env_yaml)
1232
            self.all_groups.add(group.id_)
1233
1234
        return group
1235
1236 2
    def _load_group_process_and_recurse(self, guide_directory):
1237
        self.loaded_group = self.load_benchmark_or_group(guide_directory)
1238
1239
        if self.loaded_group:
1240
            if self.parent_group:
1241
                self.parent_group.add_group(self.loaded_group)
1242
1243
            self._process_values()
1244
            self._recurse_into_subdirs()
1245
            self._process_rules()
1246
1247 2
    def process_directory_tree(self, start_dir, extra_group_dirs=None):
1248
        self._collect_items_to_load(start_dir)
1249
        if extra_group_dirs is not None:
1250
            self.subdirectories += extra_group_dirs
1251
        self._load_group_process_and_recurse(start_dir)
1252
1253 2
    def _recurse_into_subdirs(self):
1254
        for subdir in self.subdirectories:
1255
            loader = self._get_new_loader()
1256
            loader.parent_group = self.loaded_group
1257
            loader.process_directory_tree(subdir)
1258
            self.all_values.update(loader.all_values)
1259
            self.all_rules.update(loader.all_rules)
1260
            self.all_groups.update(loader.all_groups)
1261
1262 2
    def _get_new_loader(self):
1263
        raise NotImplementedError()
1264
1265 2
    def _process_values(self):
1266
        raise NotImplementedError()
1267
1268 2
    def _process_rules(self):
1269
        raise NotImplementedError()
1270
1271
1272 2
class BuildLoader(DirectoryLoader):
1273 2
    def __init__(self, profiles_dir, bash_remediation_fns, env_yaml, resolved_rules_dir=None):
1274
        super(BuildLoader, self).__init__(profiles_dir, bash_remediation_fns, env_yaml)
1275
1276
        self.resolved_rules_dir = resolved_rules_dir
1277
        if resolved_rules_dir and not os.path.isdir(resolved_rules_dir):
1278
            os.mkdir(resolved_rules_dir)
1279
1280 2
    def _process_values(self):
1281
        for value_yaml in self.value_files:
1282
            value = Value.from_yaml(value_yaml, self.env_yaml)
1283
            self.all_values.add(value)
1284
            self.loaded_group.add_value(value)
1285
1286 2
    def _process_rules(self):
1287
        for rule_yaml in self.rule_files:
1288
            try:
1289
                rule = Rule.from_yaml(rule_yaml, self.env_yaml)
1290
            except DocumentationNotComplete:
1291
                # Happens on non-debug build when a rule is "documentation-incomplete"
1292
                continue
1293
            prodtypes = parse_prodtype(rule.prodtype)
1294
            if "all" not in prodtypes and self.product not in prodtypes:
1295
                continue
1296
            self.all_rules.add(rule)
1297
            self.loaded_group.add_rule(rule)
1298
1299
            rule.inherited_platforms.append(self.loaded_group.platform)
1300
1301
            if self.resolved_rules_dir:
1302
                output_for_rule = os.path.join(
1303
                    self.resolved_rules_dir, "{id_}.yml".format(id_=rule.id_))
1304
                mkdir_p(self.resolved_rules_dir)
1305
                with open(output_for_rule, "w") as f:
1306
                    rule.normalize(self.env_yaml["product"])
1307
                    yaml.dump(rule.to_contents_dict(), f)
1308
1309 2
    def _get_new_loader(self):
1310
        return BuildLoader(
1311
            self.profiles_dir, self.bash_remediation_fns, self.env_yaml, self.resolved_rules_dir)
1312
1313 2
    def export_group_to_file(self, filename):
1314
        return self.loaded_group.to_file(filename)
1315