Passed
Push — master ( 65b335...62b6dd )
by Matěj
02:43 queued 11s
created

ssg.build_yaml.Value.to_xml_element()   A

Complexity

Conditions 5

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 26.0575

Importance

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