Passed
Push — master ( 1b83a1...99b2eb )
by Jan
02:30 queued 15s
created

ssg.build_yaml.Benchmark.__init__()   A

Complexity

Conditions 1

Size

Total Lines 25
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.8696

Importance

Changes 0
Metric Value
cc 1
eloc 22
nop 2
dl 0
loc 25
ccs 1
cts 22
cp 0.0455
crap 1.8696
rs 9.352
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
import datetime
8 2
import re
9 2
import sys
10
11 2
import yaml
12
13 2
from .constants import XCCDF_PLATFORM_TO_CPE
14 2
from .constants import PRODUCT_TO_CPE_MAPPING
15 2
from .constants import STIG_PLATFORM_ID_MAP
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 Value(object):
323
    """Represents XCCDF Value
324
    """
325
326 2
    def __init__(self, id_):
327 2
        self.id_ = id_
328 2
        self.title = ""
329 2
        self.description = ""
330 2
        self.type_ = "string"
331 2
        self.operator = "equals"
332 2
        self.interactive = False
333 2
        self.options = {}
334 2
        self.warnings = []
335
336 2
    @staticmethod
337 2
    def from_yaml(yaml_file, env_yaml=None):
338 2
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
339 2
        if yaml_contents is None:
340
            return None
341
342 2
        value_id, _ = os.path.splitext(os.path.basename(yaml_file))
343 2
        value = Value(value_id)
344 2
        value.title = required_key(yaml_contents, "title")
345 2
        del yaml_contents["title"]
346 2
        value.description = required_key(yaml_contents, "description")
347 2
        del yaml_contents["description"]
348 2
        value.type_ = required_key(yaml_contents, "type")
349 2
        del yaml_contents["type"]
350 2
        value.operator = yaml_contents.pop("operator", "equals")
351 2
        possible_operators = ["equals", "not equal", "greater than",
352
                              "less than", "greater than or equal",
353
                              "less than or equal", "pattern match"]
354
355 2
        if value.operator not in possible_operators:
356
            raise ValueError(
357
                "Found an invalid operator value '%s' in '%s'. "
358
                "Expected one of: %s"
359
                % (value.operator, yaml_file, ", ".join(possible_operators))
360
            )
361
362 2
        value.interactive = \
363
            yaml_contents.pop("interactive", "false").lower() == "true"
364
365 2
        value.options = required_key(yaml_contents, "options")
366 2
        del yaml_contents["options"]
367 2
        value.warnings = yaml_contents.pop("warnings", [])
368
369 2
        for warning_list in value.warnings:
370
            if len(warning_list) != 1:
371
                raise ValueError("Only one key/value pair should exist for each dictionary")
372
373 2
        if yaml_contents:
374
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
375
                               % (yaml_file, yaml_contents))
376
377 2
        return value
378
379 2
    def to_xml_element(self):
380
        value = ET.Element('Value')
381
        value.set('id', self.id_)
382
        value.set('type', self.type_)
383
        if self.operator != "equals":  # equals is the default
384
            value.set('operator', self.operator)
385
        if self.interactive:  # False is the default
386
            value.set('interactive', 'true')
387
        title = ET.SubElement(value, 'title')
388
        title.text = self.title
389
        add_sub_element(value, 'description', self.description)
390
        add_warning_elements(value, self.warnings)
391
392
        for selector, option in self.options.items():
393
            # do not confuse Value with big V with value with small v
394
            # value is child element of Value
395
            value_small = ET.SubElement(value, 'value')
396
            # by XCCDF spec, default value is value without selector
397
            if selector != "default":
398
                value_small.set('selector', str(selector))
399
            value_small.text = str(option)
400
401
        return value
402
403 2
    def to_file(self, file_name):
404
        root = self.to_xml_element()
405
        tree = ET.ElementTree(root)
406
        tree.write(file_name)
407
408
409 2
class Benchmark(object):
410
    """Represents XCCDF Benchmark
411
    """
412 2
    def __init__(self, id_):
413
        self.id_ = id_
414
        self.title = ""
415
        self.status = ""
416
        self.description = ""
417
        self.notice_id = ""
418
        self.notice_description = ""
419
        self.front_matter = ""
420
        self.rear_matter = ""
421
        self.cpes = []
422
        self.version = "0.1"
423
        self.profiles = []
424
        self.values = {}
425
        self.bash_remediation_fns_group = None
426
        self.groups = {}
427
        self.rules = {}
428
429
        # This is required for OCIL clauses
430
        conditional_clause = Value("conditional_clause")
431
        conditional_clause.title = "A conditional clause for check statements."
432
        conditional_clause.description = conditional_clause.title
433
        conditional_clause.type_ = "string"
434
        conditional_clause.options = {"": "This is a placeholder"}
435
436
        self.add_value(conditional_clause)
437
438 2
    @classmethod
439 2
    def from_yaml(cls, yaml_file, id_, product_yaml=None):
440
        yaml_contents = open_and_macro_expand(yaml_file, product_yaml)
441
        if yaml_contents is None:
442
            return None
443
444
        benchmark = cls(id_)
445
        benchmark.title = required_key(yaml_contents, "title")
446
        del yaml_contents["title"]
447
        benchmark.status = required_key(yaml_contents, "status")
448
        del yaml_contents["status"]
449
        benchmark.description = required_key(yaml_contents, "description")
450
        del yaml_contents["description"]
451
        notice_contents = required_key(yaml_contents, "notice")
452
        benchmark.notice_id = required_key(notice_contents, "id")
453
        del notice_contents["id"]
454
        benchmark.notice_description = required_key(notice_contents,
455
                                                    "description")
456
        del notice_contents["description"]
457
        if not notice_contents:
458
            del yaml_contents["notice"]
459
460
        benchmark.front_matter = required_key(yaml_contents,
461
                                              "front-matter")
462
        del yaml_contents["front-matter"]
463
        benchmark.rear_matter = required_key(yaml_contents,
464
                                             "rear-matter")
465
        del yaml_contents["rear-matter"]
466
        benchmark.version = str(required_key(yaml_contents, "version"))
467
        del yaml_contents["version"]
468
469
        if yaml_contents:
470
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
471
                               % (yaml_file, yaml_contents))
472
473
        if product_yaml:
474
            benchmark.cpes = PRODUCT_TO_CPE_MAPPING[product_yaml["product"]]
475
476
        return benchmark
477
478 2
    def add_profiles_from_dir(self, action, dir_, env_yaml):
479
        for dir_item in os.listdir(dir_):
480
            dir_item_path = os.path.join(dir_, dir_item)
481
            if not os.path.isfile(dir_item_path):
482
                continue
483
484
            _, ext = os.path.splitext(os.path.basename(dir_item_path))
485
            if ext != '.profile':
486
                sys.stderr.write(
487
                    "Encountered file '%s' while looking for profiles, "
488
                    "extension '%s' is unknown. Skipping..\n"
489
                    % (dir_item, ext)
490
                )
491
                continue
492
493
            try:
494
                new_profile = Profile.from_yaml(dir_item_path, env_yaml)
495
            except DocumentationNotComplete:
496
                continue
497
            except Exception as exc:
498
                msg = ("Error building profile from '{fname}': '{error}'"
499
                       .format(fname=dir_item_path, error=str(exc)))
500
                raise RuntimeError(msg)
501
            if new_profile is None:
502
                continue
503
504
            self.profiles.append(new_profile)
505
            if action == "list-inputs":
506
                print(dir_item_path)
507
508 2
    def add_bash_remediation_fns_from_file(self, action, file_):
509
        if not file_:
510
            # bash-remediation-functions.xml doens't exist
511
            return
512
513
        if action == "list-inputs":
514
            print(file_)
515
        else:
516
            tree = ET.parse(file_)
517
            self.bash_remediation_fns_group = tree.getroot()
518
519 2
    def to_xml_element(self):
520
        root = ET.Element('Benchmark')
521
        root.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
522
        root.set('xmlns:xhtml', 'http://www.w3.org/1999/xhtml')
523
        root.set('xmlns:dc', 'http://purl.org/dc/elements/1.1/')
524
        root.set('id', 'product-name')
525
        root.set('xsi:schemaLocation',
526
                 'http://checklists.nist.gov/xccdf/1.1 xccdf-1.1.4.xsd')
527
        root.set('style', 'SCAP_1.1')
528
        root.set('resolved', 'false')
529
        root.set('xml:lang', 'en-US')
530
        status = ET.SubElement(root, 'status')
531
        status.set('date', datetime.date.today().strftime("%Y-%m-%d"))
532
        status.text = self.status
533
        add_sub_element(root, "title", self.title)
534
        add_sub_element(root, "description", self.description)
535
        notice = add_sub_element(root, "notice", self.notice_description)
536
        notice.set('id', self.notice_id)
537
        add_sub_element(root, "front-matter", self.front_matter)
538
        add_sub_element(root, "rear-matter", self.rear_matter)
539
540
        for idref in self.cpes:
541
            plat = ET.SubElement(root, "platform")
542
            plat.set("idref", idref)
543
544
        version = ET.SubElement(root, 'version')
545
        version.text = self.version
546
        ET.SubElement(root, "metadata")
547
548
        for profile in self.profiles:
549
            root.append(profile.to_xml_element())
550
551
        for value in self.values.values():
552
            root.append(value.to_xml_element())
553
        if self.bash_remediation_fns_group is not None:
554
            root.append(self.bash_remediation_fns_group)
555
556
        groups_in_bench = list(self.groups.keys())
557
        priority_order = ["system", "services"]
558
        groups_in_bench = reorder_according_to_ordering(groups_in_bench, priority_order)
559
560
        # Make system group the first, followed by services group
561
        for group_id in groups_in_bench:
562
            group = self.groups.get(group_id)
563
            # Products using application benchmark don't have system or services group
564
            if group is not None:
565
                root.append(group.to_xml_element())
566
567
        for rule in self.rules.values():
568
            root.append(rule.to_xml_element())
569
570
        return root
571
572 2
    def to_file(self, file_name):
573
        root = self.to_xml_element()
574
        tree = ET.ElementTree(root)
575
        tree.write(file_name)
576
577 2
    def add_value(self, value):
578
        if value is None:
579
            return
580
        self.values[value.id_] = value
581
582 2
    def add_group(self, group):
583
        if group is None:
584
            return
585
        self.groups[group.id_] = group
586
587 2
    def add_rule(self, rule):
588
        if rule is None:
589
            return
590
        self.rules[rule.id_] = rule
591
592 2
    def to_xccdf(self):
593
        """We can easily extend this script to generate a valid XCCDF instead
594
        of SSG SHORTHAND.
595
        """
596
        raise NotImplementedError
597
598 2
    def __str__(self):
599
        return self.id_
600
601
602 2
class Group(object):
603
    """Represents XCCDF Group
604
    """
605 2
    ATTRIBUTES_TO_PASS_ON = (
606
        "platform",
607
    )
608
609 2
    def __init__(self, id_):
610
        self.id_ = id_
611
        self.prodtype = "all"
612
        self.title = ""
613
        self.description = ""
614
        self.warnings = []
615
        self.requires = []
616
        self.conflicts = []
617
        self.values = {}
618
        self.groups = {}
619
        self.rules = {}
620
        self.platform = None
621
622 2
    @classmethod
623 2
    def from_yaml(cls, yaml_file, env_yaml=None):
624
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
625
        if yaml_contents is None:
626
            return None
627
628
        group_id = os.path.basename(os.path.dirname(yaml_file))
629
        group = cls(group_id)
630
        group.prodtype = yaml_contents.pop("prodtype", "all")
631
        group.title = required_key(yaml_contents, "title")
632
        del yaml_contents["title"]
633
        group.description = required_key(yaml_contents, "description")
634
        del yaml_contents["description"]
635
        group.warnings = yaml_contents.pop("warnings", [])
636
        group.conflicts = yaml_contents.pop("conflicts", [])
637
        group.requires = yaml_contents.pop("requires", [])
638
        group.platform = yaml_contents.pop("platform", None)
639
640
        for warning_list in group.warnings:
641
            if len(warning_list) != 1:
642
                raise ValueError("Only one key/value pair should exist for each dictionary")
643
644
        if yaml_contents:
645
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
646
                               % (yaml_file, yaml_contents))
647
        group.validate_prodtype(yaml_file)
648
        return group
649
650 2
    def validate_prodtype(self, yaml_file):
651
        for ptype in self.prodtype.split(","):
652
            if ptype.strip() != ptype:
653
                msg = (
654
                    "Comma-separated '{prodtype}' prodtype "
655
                    "in {yaml_file} contains whitespace."
656
                    .format(prodtype=self.prodtype, yaml_file=yaml_file))
657
                raise ValueError(msg)
658
659 2
    def to_xml_element(self):
660
        group = ET.Element('Group')
661
        group.set('id', self.id_)
662
        if self.prodtype != "all":
663
            group.set("prodtype", self.prodtype)
664
        title = ET.SubElement(group, 'title')
665
        title.text = self.title
666
        add_sub_element(group, 'description', self.description)
667
        add_warning_elements(group, self.warnings)
668
        add_nondata_subelements(group, "requires", "id", self.requires)
669
        add_nondata_subelements(group, "conflicts", "id", self.conflicts)
670
671
        if self.platform:
672
            platform_el = ET.SubElement(group, "platform")
673
            try:
674
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
675
            except KeyError:
676
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
677
            platform_el.set("idref", platform_cpe)
678
679
        for _value in self.values.values():
680
            group.append(_value.to_xml_element())
681
682
        # Rules that install or remove packages affect remediation
683
        # of other rules.
684
        # When packages installed/removed rules come first:
685
        # The Rules are ordered in more logical way, and
686
        # remediation order is natural, first the package is installed, then configured.
687
        rules_in_group = list(self.rules.keys())
688
        regex = r'(package_.*_(installed|removed))|(service_.*_(enabled|disabled))$'
689
        priority_order = ["installed", "removed", "enabled", "disabled"]
690
        rules_in_group = reorder_according_to_ordering(rules_in_group, priority_order, regex)
691
692
        # Add rules in priority order, first all packages installed, then removed,
693
        # followed by services enabled, then disabled
694
        for rule_id in rules_in_group:
695
            group.append(self.rules.get(rule_id).to_xml_element())
696
697
        # Add the sub groups after any current level group rules.
698
        # As package installed/removed and service enabled/disabled rules are usuallly in
699
        # top level group, this ensures groups that further configure a package or service
700
        # are after rules that install or remove it.
701
        groups_in_group = list(self.groups.keys())
702
        # The account group has to precede audit group because
703
        # the rule package_screen_installed is desired to be executed before the rule
704
        # audit_rules_privileged_commands, othervise the rule
705
        # does not catch newly installed screeen binary during remediation
706
        # and report fail
707
        # the software group should come before the
708
        # bootloader-grub2 group because of conflict between
709
        # rules rpm_verify_permissions and file_permissions_grub2_cfg
710
        # specific rules concerning permissions should
711
        # be applied after the general rpm_verify_permissions
712
        # The FIPS group should come before Crypto - if we want to set a different (stricter) Crypto Policy than FIPS.
713
        # the firewalld_activation must come before ruleset_modifications, othervise
714
        # remediations for ruleset_modifications won't work
715
        # rules from group disabling_ipv6 must precede rules from configuring_ipv6,
716
        # otherwise the remediation prints error although it is successful
717
        priority_order = [
718
            "accounts", "auditing",
719
            "software", "bootloader-grub2",
720
            "fips", "crypto",
721
            "firewalld_activation", "ruleset_modifications",
722
            "disabling_ipv6", "configuring_ipv6"
723
        ]
724
        groups_in_group = reorder_according_to_ordering(groups_in_group, priority_order)
725
        for group_id in groups_in_group:
726
            _group = self.groups[group_id]
727
            group.append(_group.to_xml_element())
728
729
        return group
730
731 2
    def to_file(self, file_name):
732
        root = self.to_xml_element()
733
        tree = ET.ElementTree(root)
734
        tree.write(file_name)
735
736 2
    def add_value(self, value):
737
        if value is None:
738
            return
739
        self.values[value.id_] = value
740
741 2
    def add_group(self, group):
742
        if group is None:
743
            return
744
        if self.platform and not group.platform:
745
            group.platform = self.platform
746
        self.groups[group.id_] = group
747
        self._pass_our_properties_on_to(group)
748
749 2
    def _pass_our_properties_on_to(self, obj):
750
        for attr in self.ATTRIBUTES_TO_PASS_ON:
751
            if hasattr(obj, attr) and getattr(obj, attr) is None:
752
                setattr(obj, attr, getattr(self, attr))
753
754 2
    def add_rule(self, rule):
755
        if rule is None:
756
            return
757
        if self.platform and not rule.platform:
758
            rule.platform = self.platform
759
        self.rules[rule.id_] = rule
760
        self._pass_our_properties_on_to(rule)
761
762 2
    def __str__(self):
763
        return self.id_
764
765
766 2
class Rule(object):
767
    """Represents XCCDF Rule
768
    """
769 2
    YAML_KEYS_DEFAULTS = {
770
        "prodtype": lambda: "all",
771
        "title": lambda: RuntimeError("Missing key 'title'"),
772
        "description": lambda: RuntimeError("Missing key 'description'"),
773
        "rationale": lambda: RuntimeError("Missing key 'rationale'"),
774
        "severity": lambda: RuntimeError("Missing key 'severity'"),
775
        "references": lambda: dict(),
776
        "identifiers": lambda: dict(),
777
        "ocil_clause": lambda: None,
778
        "ocil": lambda: None,
779
        "oval_external_content": lambda: None,
780
        "warnings": lambda: list(),
781
        "conflicts": lambda: list(),
782
        "requires": lambda: list(),
783
        "platform": lambda: None,
784
        "template": lambda: None,
785
    }
786
787 2
    def __init__(self, id_):
788 2
        self.id_ = id_
789 2
        self.prodtype = "all"
790 2
        self.title = ""
791 2
        self.description = ""
792 2
        self.rationale = ""
793 2
        self.severity = "unknown"
794 2
        self.references = {}
795 2
        self.identifiers = {}
796 2
        self.ocil_clause = None
797 2
        self.ocil = None
798 2
        self.oval_external_content = None
799 2
        self.warnings = []
800 2
        self.requires = []
801 2
        self.conflicts = []
802 2
        self.platform = None
803 2
        self.template = None
804
805 2
    @classmethod
806 2
    def from_yaml(cls, yaml_file, env_yaml=None):
807 2
        yaml_file = os.path.normpath(yaml_file)
808
809 2
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
810 2
        if yaml_contents is None:
811
            return None
812
813 2
        rule_id, ext = os.path.splitext(os.path.basename(yaml_file))
814 2
        if rule_id == "rule" and ext == ".yml":
815 2
            rule_id = get_rule_dir_id(yaml_file)
816
817 2
        rule = cls(rule_id)
818
819 2
        try:
820 2
            rule._set_attributes_from_dict(yaml_contents)
821
        except RuntimeError as exc:
822
            msg = ("Error processing '{fname}': {err}"
823
                   .format(fname=yaml_file, err=str(exc)))
824
            raise RuntimeError(msg)
825
826 2
        for warning_list in rule.warnings:
827
            if len(warning_list) != 1:
828
                raise ValueError("Only one key/value pair should exist for each dictionary")
829
830 2
        if yaml_contents:
831
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
832
                               % (yaml_file, yaml_contents))
833
834 2
        rule.validate_prodtype(yaml_file)
835 2
        rule.validate_identifiers(yaml_file)
836 2
        rule.validate_references(yaml_file)
837 2
        return rule
838
839 2
    def _make_stigid_product_specific(self, product):
840 2
        stig_id = self.references.get("stigid", None)
841 2
        if not stig_id:
842 2
            return
843 2
        if "," in stig_id:
844 2
            raise ValueError("Rules can not have multiple STIG IDs.")
845 2
        stig_platform_id = STIG_PLATFORM_ID_MAP.get(product, product.upper())
846 2
        product_specific_stig_id = "{platform_id}-{stig_id}".format(
847
            platform_id=stig_platform_id, stig_id=stig_id)
848 2
        self.references["stigid"] = product_specific_stig_id
849
850 2
    def normalize(self, product):
851 2
        try:
852 2
            self.make_refs_and_identifiers_product_specific(product)
853 2
            self.make_template_product_specific(product)
854 2
        except Exception as exc:
855 2
            msg = (
856
                "Error normalizing '{rule}': {msg}"
857
                .format(rule=self.id_, msg=str(exc))
858
            )
859 2
            raise RuntimeError(msg)
860
861 2
    def _get_product_only_references(self):
862 2
        PRODUCT_REFERENCES = ("stigid",)
863
864 2
        product_references = dict()
865
866 2
        for ref in PRODUCT_REFERENCES:
867 2
            start = "{0}@".format(ref)
868 2
            for gref, gval in self.references.items():
869 2
                if ref == gref or gref.startswith(start):
870 2
                    product_references[gref] = gval
871 2
        return product_references
872
873 2
    def make_template_product_specific(self, product):
874 2
        product_suffix = "@{0}".format(product)
875
876 2
        if not self.template:
877
            return
878
879 2
        not_specific_vars = self.template.get("vars", dict())
880 2
        specific_vars = self._make_items_product_specific(
881
            not_specific_vars, product_suffix, True)
882 2
        self.template["vars"] = specific_vars
883
884 2
        not_specific_backends = self.template.get("backends", dict())
885 2
        specific_backends = self._make_items_product_specific(
886
            not_specific_backends, product_suffix, True)
887 2
        self.template["backends"] = specific_backends
888
889 2
    def make_refs_and_identifiers_product_specific(self, product):
890 2
        product_suffix = "@{0}".format(product)
891
892 2
        product_references = self._get_product_only_references()
893 2
        general_references = self.references.copy()
894 2
        for todel in product_references:
895 2
            general_references.pop(todel)
896
897 2
        to_set = dict(
898
            identifiers=(self.identifiers, False),
899
            general_references=(general_references, True),
900
            product_references=(product_references, False),
901
        )
902 2
        for name, (dic, allow_overwrites) in to_set.items():
903 2
            try:
904 2
                new_items = self._make_items_product_specific(
905
                    dic, product_suffix, allow_overwrites)
906 2
            except ValueError as exc:
907 2
                msg = (
908
                    "Error processing {what} for rule '{rid}': {msg}"
909
                    .format(what=name, rid=self.id_, msg=str(exc))
910
                )
911 2
                raise ValueError(msg)
912 2
            dic.clear()
913 2
            dic.update(new_items)
914
915 2
        self.references = general_references
916 2
        self.references.update(product_references)
917
918 2
        self._make_stigid_product_specific(product)
919
920 2
    def _make_items_product_specific(self, items_dict, product_suffix, allow_overwrites=False):
921 2
        new_items = dict()
922 2
        for full_label, value in items_dict.items():
923 2
            if "@" not in full_label and full_label not in new_items:
924 2
                new_items[full_label] = value
925 2
                continue
926
927 2
            if not full_label.endswith(product_suffix):
928 2
                continue
929
930 2
            label = full_label.split("@")[0]
931 2
            if label in items_dict and not allow_overwrites and value != items_dict[label]:
932 2
                msg = (
933
                    "There is a product-qualified '{item_q}' item, "
934
                    "but also an unqualified '{item_u}' item "
935
                    "and those two differ in value - "
936
                    "'{value_q}' vs '{value_u}' respectively."
937
                    .format(item_q=full_label, item_u=label,
938
                            value_q=value, value_u=items_dict[label])
939
                )
940 2
                raise ValueError(msg)
941 2
            new_items[label] = value
942 2
        return new_items
943
944 2
    def _set_attributes_from_dict(self, yaml_contents):
945 2
        for key, default_getter in self.YAML_KEYS_DEFAULTS.items():
946 2
            if key not in yaml_contents:
947 2
                value = default_getter()
948 2
                if isinstance(value, Exception):
949
                    raise value
950
            else:
951 2
                value = yaml_contents.pop(key)
952
953 2
            setattr(self, key, value)
954
955 2
    def to_contents_dict(self):
956
        """
957
        Returns a dictionary that is the same schema as the dict obtained when loading rule YAML.
958
        """
959
960 2
        yaml_contents = dict()
961 2
        for key in Rule.YAML_KEYS_DEFAULTS:
962 2
            yaml_contents[key] = getattr(self, key)
963
964 2
        return yaml_contents
965
966 2
    def validate_identifiers(self, yaml_file):
967 2
        if self.identifiers is None:
968
            raise ValueError("Empty identifier section in file %s" % yaml_file)
969
970
        # Validate all identifiers are non-empty:
971 2
        for ident_type, ident_val in self.identifiers.items():
972 2
            if not isinstance(ident_type, str) or not isinstance(ident_val, str):
973
                raise ValueError("Identifiers and values must be strings: %s in file %s"
974
                                 % (ident_type, yaml_file))
975 2
            if ident_val.strip() == "":
976
                raise ValueError("Identifiers must not be empty: %s in file %s"
977
                                 % (ident_type, yaml_file))
978 2
            if ident_type[0:3] == 'cce':
979 2
                if not is_cce_format_valid("CCE-" + ident_val):
980
                    raise ValueError("CCE Identifier format must be valid: invalid format '%s' for CEE '%s'"
981
                                     " in file '%s'" % (ident_val, ident_type, yaml_file))
982 2
                if not is_cce_value_valid("CCE-" + ident_val):
983
                    raise ValueError("CCE Identifier value is not a valid checksum: invalid value '%s' for CEE '%s'"
984
                                     " in file '%s'" % (ident_val, ident_type, yaml_file))
985
986 2
    def validate_references(self, yaml_file):
987 2
        if self.references is None:
988
            raise ValueError("Empty references section in file %s" % yaml_file)
989
990 2
        for ref_type, ref_val in self.references.items():
991 2
            if not isinstance(ref_type, str) or not isinstance(ref_val, str):
992
                raise ValueError("References and values must be strings: %s in file %s"
993
                                 % (ref_type, yaml_file))
994 2
            if ref_val.strip() == "":
995
                raise ValueError("References must not be empty: %s in file %s"
996
                                 % (ref_type, yaml_file))
997
998 2
        for ref_type, ref_val in self.references.items():
999 2
            for ref in ref_val.split(","):
1000 2
                if ref.strip() != ref:
1001
                    msg = (
1002
                        "Comma-separated '{ref_type}' reference "
1003
                        "in {yaml_file} contains whitespace."
1004
                        .format(ref_type=ref_type, yaml_file=yaml_file))
1005
                    raise ValueError(msg)
1006
1007 2
    def validate_prodtype(self, yaml_file):
1008 2
        for ptype in self.prodtype.split(","):
1009 2
            if ptype.strip() != ptype:
1010
                msg = (
1011
                    "Comma-separated '{prodtype}' prodtype "
1012
                    "in {yaml_file} contains whitespace."
1013
                    .format(prodtype=self.prodtype, yaml_file=yaml_file))
1014
                raise ValueError(msg)
1015
1016 2
    def to_xml_element(self):
1017
        rule = ET.Element('Rule')
1018
        rule.set('id', self.id_)
1019
        if self.prodtype != "all":
1020
            rule.set("prodtype", self.prodtype)
1021
        rule.set('severity', self.severity)
1022
        add_sub_element(rule, 'title', self.title)
1023
        add_sub_element(rule, 'description', self.description)
1024
        add_sub_element(rule, 'rationale', self.rationale)
1025
1026
        main_ident = ET.Element('ident')
1027
        for ident_type, ident_val in self.identifiers.items():
1028
            # This is not true if items were normalized
1029
            if '@' in ident_type:
1030
                # the ident is applicable only on some product
1031
                # format : 'policy@product', eg. 'stigid@product'
1032
                # for them, we create a separate <ref> element
1033
                policy, product = ident_type.split('@')
1034
                ident = ET.SubElement(rule, 'ident')
1035
                ident.set(policy, ident_val)
1036
                ident.set('prodtype', product)
1037
            else:
1038
                main_ident.set(ident_type, ident_val)
1039
1040
        if main_ident.attrib:
1041
            rule.append(main_ident)
1042
1043
        main_ref = ET.Element('ref')
1044
        for ref_type, ref_val in self.references.items():
1045
            # This is not true if items were normalized
1046
            if '@' in ref_type:
1047
                # the reference is applicable only on some product
1048
                # format : 'policy@product', eg. 'stigid@product'
1049
                # for them, we create a separate <ref> element
1050
                policy, product = ref_type.split('@')
1051
                ref = ET.SubElement(rule, 'ref')
1052
                ref.set(policy, ref_val)
1053
                ref.set('prodtype', product)
1054
            else:
1055
                main_ref.set(ref_type, ref_val)
1056
1057
        if main_ref.attrib:
1058
            rule.append(main_ref)
1059
1060
        if self.oval_external_content:
1061
            check = ET.SubElement(rule, 'check')
1062
            check.set("system", "http://oval.mitre.org/XMLSchema/oval-definitions-5")
1063
            external_content = ET.SubElement(check, "check-content-ref")
1064
            external_content.set("href", self.oval_external_content)
1065
        else:
1066
            # TODO: This is pretty much a hack, oval ID will be the same as rule ID
1067
            #       and we don't want the developers to have to keep them in sync.
1068
            #       Therefore let's just add an OVAL ref of that ID.
1069
            oval_ref = ET.SubElement(rule, "oval")
1070
            oval_ref.set("id", self.id_)
1071
1072
        if self.ocil or self.ocil_clause:
1073
            ocil = add_sub_element(rule, 'ocil', self.ocil if self.ocil else "")
1074
            if self.ocil_clause:
1075
                ocil.set("clause", self.ocil_clause)
1076
1077
        add_warning_elements(rule, self.warnings)
1078
        add_nondata_subelements(rule, "requires", "id", self.requires)
1079
        add_nondata_subelements(rule, "conflicts", "id", self.conflicts)
1080
1081
        if self.platform:
1082
            platform_el = ET.SubElement(rule, "platform")
1083
            try:
1084
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
1085
            except KeyError:
1086
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
1087
            platform_el.set("idref", platform_cpe)
1088
1089
        return rule
1090
1091 2
    def to_file(self, file_name):
1092
        root = self.to_xml_element()
1093
        tree = ET.ElementTree(root)
1094
        tree.write(file_name)
1095
1096
1097 2
class DirectoryLoader(object):
1098 2
    def __init__(self, profiles_dir, bash_remediation_fns, env_yaml):
1099
        self.benchmark_file = None
1100
        self.group_file = None
1101
        self.loaded_group = None
1102
        self.rule_files = []
1103
        self.value_files = []
1104
        self.subdirectories = []
1105
1106
        self.all_values = set()
1107
        self.all_rules = set()
1108
        self.all_groups = set()
1109
1110
        self.profiles_dir = profiles_dir
1111
        self.bash_remediation_fns = bash_remediation_fns
1112
        self.env_yaml = env_yaml
1113
        self.product = env_yaml["product"]
1114
1115
        self.parent_group = None
1116
1117 2
    def _collect_items_to_load(self, guide_directory):
1118
        for dir_item in os.listdir(guide_directory):
1119
            dir_item_path = os.path.join(guide_directory, dir_item)
1120
            _, extension = os.path.splitext(dir_item)
1121
1122
            if extension == '.var':
1123
                self.value_files.append(dir_item_path)
1124
            elif dir_item == "benchmark.yml":
1125
                if self.benchmark_file:
1126
                    raise ValueError("Multiple benchmarks in one directory")
1127
                self.benchmark_file = dir_item_path
1128
            elif dir_item == "group.yml":
1129
                if self.group_file:
1130
                    raise ValueError("Multiple groups in one directory")
1131
                self.group_file = dir_item_path
1132
            elif extension == '.rule':
1133
                self.rule_files.append(dir_item_path)
1134
            elif is_rule_dir(dir_item_path):
1135
                self.rule_files.append(get_rule_dir_yaml(dir_item_path))
1136
            elif dir_item != "tests":
1137
                if os.path.isdir(dir_item_path):
1138
                    self.subdirectories.append(dir_item_path)
1139
                else:
1140
                    sys.stderr.write(
1141
                        "Encountered file '%s' while recursing, extension '%s' "
1142
                        "is unknown. Skipping..\n"
1143
                        % (dir_item, extension)
1144
                    )
1145
1146 2
    def load_benchmark_or_group(self, guide_directory):
1147
        """
1148
        Loads a given benchmark or group from the specified benchmark_file or
1149
        group_file, in the context of guide_directory, action, profiles_dir,
1150
        env_yaml, and bash_remediation_fns.
1151
1152
        Returns the loaded group or benchmark.
1153
        """
1154
        group = None
1155
        if self.group_file and self.benchmark_file:
1156
            raise ValueError("A .benchmark file and a .group file were found in "
1157
                             "the same directory '%s'" % (guide_directory))
1158
1159
        # we treat benchmark as a special form of group in the following code
1160
        if self.benchmark_file:
1161
            group = Benchmark.from_yaml(
1162
                self.benchmark_file, 'product-name', self.env_yaml
1163
            )
1164
            if self.profiles_dir:
1165
                group.add_profiles_from_dir(self.action, self.profiles_dir, self.env_yaml)
1166
            group.add_bash_remediation_fns_from_file(self.action, self.bash_remediation_fns)
1167
1168
        if self.group_file:
1169
            group = Group.from_yaml(self.group_file, self.env_yaml)
1170
            self.all_groups.add(group.id_)
1171
1172
        return group
1173
1174 2
    def _load_group_process_and_recurse(self, guide_directory):
1175
        self.loaded_group = self.load_benchmark_or_group(guide_directory)
1176
1177
        if self.loaded_group:
1178
            if self.parent_group:
1179
                self.parent_group.add_group(self.loaded_group)
1180
1181
            self._process_values()
1182
            self._recurse_into_subdirs()
1183
            self._process_rules()
1184
1185 2
    def process_directory_tree(self, start_dir, extra_group_dirs=None):
1186
        self._collect_items_to_load(start_dir)
1187
        if extra_group_dirs is not None:
1188
            self.subdirectories += extra_group_dirs
1189
        self._load_group_process_and_recurse(start_dir)
1190
1191 2
    def _recurse_into_subdirs(self):
1192
        for subdir in self.subdirectories:
1193
            loader = self._get_new_loader()
1194
            loader.parent_group = self.loaded_group
1195
            loader.process_directory_tree(subdir)
1196
            self.all_values.update(loader.all_values)
1197
            self.all_rules.update(loader.all_rules)
1198
            self.all_groups.update(loader.all_groups)
1199
1200 2
    def _get_new_loader(self):
1201
        raise NotImplementedError()
1202
1203 2
    def _process_values(self):
1204
        raise NotImplementedError()
1205
1206 2
    def _process_rules(self):
1207
        raise NotImplementedError()
1208
1209
1210 2
class BuildLoader(DirectoryLoader):
1211 2
    def __init__(self, profiles_dir, bash_remediation_fns, env_yaml, resolved_rules_dir=None):
1212
        super(BuildLoader, self).__init__(profiles_dir, bash_remediation_fns, env_yaml)
1213
1214
        self.action = "build"
1215
1216
        self.resolved_rules_dir = resolved_rules_dir
1217
        if resolved_rules_dir and not os.path.isdir(resolved_rules_dir):
1218
            os.mkdir(resolved_rules_dir)
1219
1220 2
    def _process_values(self):
1221
        for value_yaml in self.value_files:
1222
            value = Value.from_yaml(value_yaml, self.env_yaml)
1223
            self.all_values.add(value)
1224
            self.loaded_group.add_value(value)
1225
1226 2
    def _process_rules(self):
1227
        for rule_yaml in self.rule_files:
1228
            try:
1229
                rule = Rule.from_yaml(rule_yaml, self.env_yaml)
1230
            except DocumentationNotComplete:
1231
                # Happens on non-debug build when a rule is "documentation-incomplete"
1232
                continue
1233
            prodtypes = parse_prodtype(rule.prodtype)
1234
            if "all" not in prodtypes and self.product not in prodtypes:
1235
                continue
1236
            self.all_rules.add(rule)
1237
            self.loaded_group.add_rule(rule)
1238
            if self.resolved_rules_dir:
1239
                output_for_rule = os.path.join(
1240
                    self.resolved_rules_dir, "{id_}.yml".format(id_=rule.id_))
1241
                mkdir_p(self.resolved_rules_dir)
1242
                with open(output_for_rule, "w") as f:
1243
                    rule.normalize(self.env_yaml["product"])
1244
                    yaml.dump(rule.to_contents_dict(), f)
1245
1246 2
    def _get_new_loader(self):
1247
        return BuildLoader(
1248
            self.profiles_dir, self.bash_remediation_fns, self.env_yaml, self.resolved_rules_dir)
1249
1250 2
    def export_group_to_file(self, filename):
1251
        return self.loaded_group.to_file(filename)
1252
1253
1254 2
class ListInputsLoader(DirectoryLoader):
1255 2
    def __init__(self, profiles_dir, bash_remediation_fns, env_yaml):
1256
        super(ListInputsLoader, self).__init__(profiles_dir, bash_remediation_fns, env_yaml)
1257
1258
        self.action = "list-inputs"
1259
1260 2
    def _process_values(self):
1261
        for value_yaml in self.value_files:
1262
            print(value_yaml)
1263
1264 2
    def _process_rules(self):
1265
        for rule_yaml in self.rule_files:
1266
            print(rule_yaml)
1267
1268 2
    def _get_new_loader(self):
1269
        return ListInputsLoader(
1270
            self.profiles_dir, self.bash_remediation_fns, self.env_yaml)
1271
1272 2
    def load_benchmark_or_group(self, guide_directory):
1273
        result = super(ListInputsLoader, self).load_benchmark_or_group(guide_directory)
1274
1275
        if self.benchmark_file:
1276
            print(self.benchmark_file)
1277
1278
        if self.group_file:
1279
            print(self.group_file)
1280
1281
        return result
1282