Passed
Branch master (53139b)
by Matěj
04:46 queued 02:30
created

ssg.build_yaml.Benchmark.from_yaml()   B

Complexity

Conditions 5

Size

Total Lines 39
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 34
nop 3
dl 0
loc 39
ccs 0
cts 30
cp 0
crap 30
rs 8.5973
c 0
b 0
f 0
1
from __future__ import absolute_import
2
from __future__ import print_function
3
4
import os
5
import os.path
6
import datetime
7
import sys
8
9
from .constants import XCCDF_PLATFORM_TO_CPE
10
from .constants import PRODUCT_TO_CPE_MAPPING
11
from .rules import get_rule_dir_id, get_rule_dir_yaml, is_rule_dir
12
13
from .checks import is_cce_valid
14
from .yaml import open_and_expand, open_and_macro_expand
15
from .utils import required_key
16
17
from .xml import ElementTree as ET
18
from .shims import unicode_func
19
20
21
def add_sub_element(parent, tag, data):
22
    """
23
    Creates a new child element under parent with tag tag, and sets
24
    data as the content under the tag. In particular, data is a string
25
    to be parsed as an XML tree, allowing sub-elements of children to be
26
    added.
27
28
    If data should not be parsed as an XML tree, either escape the contents
29
    before passing into this function, or use ElementTree.SubElement().
30
31
    Returns the newly created subelement of type tag.
32
    """
33
    # This is used because our YAML data contain XML and XHTML elements
34
    # ET.SubElement() escapes the < > characters by &lt; and &gt;
35
    # and therefore it does not add child elements
36
    # we need to do a hack instead
37
    # TODO: Remove this function after we move to Markdown everywhere in SSG
38
    ustr = unicode_func("<{0}>{1}</{0}>").format(tag, data)
39
40
    try:
41
        element = ET.fromstring(ustr.encode("utf-8"))
42
    except Exception:
43
        msg = ("Error adding subelement to an element '{0}' from string: '{1}'"
44
               .format(parent.tag, ustr))
45
        raise RuntimeError(msg)
46
47
    parent.append(element)
48
    return element
49
50
51
def add_warning_elements(element, warnings):
52
    # The use of [{dict}, {dict}] in warnings is to handle the following
53
    # scenario where multiple warnings have the same category which is
54
    # valid in SCAP and our content:
55
    #
56
    # warnings:
57
    #     - general: Some general warning
58
    #     - general: Some other general warning
59
    #     - general: |-
60
    #         Some really long multiline general warning
61
    #
62
    # Each of the {dict} should have only one key/value pair.
63
    for warning_dict in warnings:
64
        warning = add_sub_element(element, "warning", list(warning_dict.values())[0])
65
        warning.set("category", list(warning_dict.keys())[0])
66
67
68
class Profile(object):
69
    """Represents XCCDF profile
70
    """
71
72
    def __init__(self, id_):
73
        self.id_ = id_
74
        self.title = ""
75
        self.description = ""
76
        self.extends = None
77
        self.selections = []
78
79
    @staticmethod
80
    def from_yaml(yaml_file, env_yaml=None):
81
        yaml_contents = open_and_expand(yaml_file, env_yaml)
82
        if yaml_contents is None:
83
            return None
84
85
        basename, _ = os.path.splitext(os.path.basename(yaml_file))
86
87
        profile = Profile(basename)
88
        profile.title = required_key(yaml_contents, "title")
89
        del yaml_contents["title"]
90
        profile.description = required_key(yaml_contents, "description")
91
        del yaml_contents["description"]
92
        profile.extends = yaml_contents.pop("extends", None)
93
        profile.selections = required_key(yaml_contents, "selections")
94
        del yaml_contents["selections"]
95
96
        if yaml_contents:
97
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
98
                               % (yaml_file, yaml_contents))
99
100
        return profile
101
102
    def to_xml_element(self):
103
        element = ET.Element('Profile')
104
        element.set("id", self.id_)
105
        if self.extends:
106
            element.set("extends", self.extends)
107
        title = add_sub_element(element, "title", self.title)
108
        title.set("override", "true")
109
        desc = add_sub_element(element, "description", self.description)
110
        desc.set("override", "true")
111
112
        for selection in self.selections:
113
            if selection.startswith("!"):
114
                unselect = ET.Element("select")
115
                unselect.set("idref", selection[1:])
116
                unselect.set("selected", "false")
117
                element.append(unselect)
118
            elif "=" in selection:
119
                refine_value = ET.Element("refine-value")
120
                value_id, selector = selection.split("=", 1)
121
                refine_value.set("idref", value_id)
122
                refine_value.set("selector", selector)
123
                element.append(refine_value)
124
            else:
125
                select = ET.Element("select")
126
                select.set("idref", selection)
127
                select.set("selected", "true")
128
                element.append(select)
129
130
        return element
131
132
133
class Value(object):
134
    """Represents XCCDF Value
135
    """
136
137
    def __init__(self, id_):
138
        self.id_ = id_
139
        self.title = ""
140
        self.description = ""
141
        self.type_ = "string"
142
        self.operator = "equals"
143
        self.interactive = False
144
        self.options = {}
145
        self.warnings = []
146
147
    @staticmethod
148
    def from_yaml(yaml_file, env_yaml=None):
149
        yaml_contents = open_and_expand(yaml_file, env_yaml)
150
        if yaml_contents is None:
151
            return None
152
153
        value_id, _ = os.path.splitext(os.path.basename(yaml_file))
154
        value = Value(value_id)
155
        value.title = required_key(yaml_contents, "title")
156
        del yaml_contents["title"]
157
        value.description = required_key(yaml_contents, "description")
158
        del yaml_contents["description"]
159
        value.type_ = required_key(yaml_contents, "type")
160
        del yaml_contents["type"]
161
        value.operator = yaml_contents.pop("operator", "equals")
162
        possible_operators = ["equals", "not equal", "greater than",
163
                              "less than", "greater than or equal",
164
                              "less than or equal", "pattern match"]
165
166
        if value.operator not in possible_operators:
167
            raise ValueError(
168
                "Found an invalid operator value '%s' in '%s'. "
169
                "Expected one of: %s"
170
                % (value.operator, yaml_file, ", ".join(possible_operators))
171
            )
172
173
        value.interactive = \
174
            yaml_contents.pop("interactive", "false").lower() == "true"
175
176
        value.options = required_key(yaml_contents, "options")
177
        del yaml_contents["options"]
178
        value.warnings = yaml_contents.pop("warnings", [])
179
180
        for warning_list in value.warnings:
181
            if len(warning_list) != 1:
182
                raise ValueError("Only one key/value pair should exist for each dictionary")
183
184
        if yaml_contents:
185
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
186
                               % (yaml_file, yaml_contents))
187
188
        return value
189
190
    def to_xml_element(self):
191
        value = ET.Element('Value')
192
        value.set('id', self.id_)
193
        value.set('type', self.type_)
194
        if self.operator != "equals":  # equals is the default
195
            value.set('operator', self.operator)
196
        if self.interactive:  # False is the default
197
            value.set('interactive', 'true')
198
        title = ET.SubElement(value, 'title')
199
        title.text = self.title
200
        add_sub_element(value, 'description', self.description)
201
        add_warning_elements(value, self.warnings)
202
203
        for selector, option in self.options.items():
204
            # do not confuse Value with big V with value with small v
205
            # value is child element of Value
206
            value_small = ET.SubElement(value, 'value')
207
            # by XCCDF spec, default value is value without selector
208
            if selector != "default":
209
                value_small.set('selector', str(selector))
210
            value_small.text = str(option)
211
212
        return value
213
214
    def to_file(self, file_name):
215
        root = self.to_xml_element()
216
        tree = ET.ElementTree(root)
217
        tree.write(file_name)
218
219
220
class Benchmark(object):
221
    """Represents XCCDF Benchmark
222
    """
223
    def __init__(self, id_):
224
        self.id_ = id_
225
        self.title = ""
226
        self.status = ""
227
        self.description = ""
228
        self.notice_id = ""
229
        self.notice_description = ""
230
        self.front_matter = ""
231
        self.rear_matter = ""
232
        self.cpes = []
233
        self.version = "0.1"
234
        self.profiles = []
235
        self.values = {}
236
        self.bash_remediation_fns_group = None
237
        self.groups = {}
238
        self.rules = {}
239
240
        # This is required for OCIL clauses
241
        conditional_clause = Value("conditional_clause")
242
        conditional_clause.title = "A conditional clause for check statements."
243
        conditional_clause.description = conditional_clause.title
244
        conditional_clause.type_ = "string"
245
        conditional_clause.options = {"": "This is a placeholder"}
246
247
        self.add_value(conditional_clause)
248
249
    @staticmethod
250
    def from_yaml(yaml_file, id_, product_yaml=None):
251
        yaml_contents = open_and_macro_expand(yaml_file, product_yaml)
252
        if yaml_contents is None:
253
            return None
254
255
        benchmark = Benchmark(id_)
256
        benchmark.title = required_key(yaml_contents, "title")
257
        del yaml_contents["title"]
258
        benchmark.status = required_key(yaml_contents, "status")
259
        del yaml_contents["status"]
260
        benchmark.description = required_key(yaml_contents, "description")
261
        del yaml_contents["description"]
262
        notice_contents = required_key(yaml_contents, "notice")
263
        benchmark.notice_id = required_key(notice_contents, "id")
264
        del notice_contents["id"]
265
        benchmark.notice_description = required_key(notice_contents,
266
                                                    "description")
267
        del notice_contents["description"]
268
        if not notice_contents:
269
            del yaml_contents["notice"]
270
271
        benchmark.front_matter = required_key(yaml_contents,
272
                                              "front-matter")
273
        del yaml_contents["front-matter"]
274
        benchmark.rear_matter = required_key(yaml_contents,
275
                                             "rear-matter")
276
        del yaml_contents["rear-matter"]
277
        benchmark.version = str(required_key(yaml_contents, "version"))
278
        del yaml_contents["version"]
279
280
        if yaml_contents:
281
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
282
                               % (yaml_file, yaml_contents))
283
284
        if product_yaml:
285
            benchmark.cpes = PRODUCT_TO_CPE_MAPPING[product_yaml["product"]]
286
287
        return benchmark
288
289
    def add_profiles_from_dir(self, action, dir_, env_yaml):
290
        for dir_item in os.listdir(dir_):
291
            dir_item_path = os.path.join(dir_, dir_item)
292
            if not os.path.isfile(dir_item_path):
293
                continue
294
295
            _, ext = os.path.splitext(os.path.basename(dir_item_path))
296
            if ext != '.profile':
297
                sys.stderr.write(
298
                    "Encountered file '%s' while looking for profiles, "
299
                    "extension '%s' is unknown. Skipping..\n"
300
                    % (dir_item, ext)
301
                )
302
                continue
303
304
            self.profiles.append(Profile.from_yaml(dir_item_path, env_yaml))
305
            if action == "list-inputs":
306
                print(dir_item_path)
307
308
    def add_bash_remediation_fns_from_file(self, action, file_):
309
        if action == "list-inputs":
310
            print(file_)
311
        else:
312
            tree = ET.parse(file_)
313
            self.bash_remediation_fns_group = tree.getroot()
314
315
    def to_xml_element(self):
316
        root = ET.Element('Benchmark')
317
        root.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
318
        root.set('xmlns:xhtml', 'http://www.w3.org/1999/xhtml')
319
        root.set('xmlns:dc', 'http://purl.org/dc/elements/1.1/')
320
        root.set('id', 'product-name')
321
        root.set('xsi:schemaLocation',
322
                 'http://checklists.nist.gov/xccdf/1.1 xccdf-1.1.4.xsd')
323
        root.set('style', 'SCAP_1.1')
324
        root.set('resolved', 'false')
325
        root.set('xml:lang', 'en-US')
326
        status = ET.SubElement(root, 'status')
327
        status.set('date', datetime.date.today().strftime("%Y-%m-%d"))
328
        status.text = self.status
329
        add_sub_element(root, "title", self.title)
330
        add_sub_element(root, "description", self.description)
331
        notice = add_sub_element(root, "notice", self.notice_description)
332
        notice.set('id', self.notice_id)
333
        add_sub_element(root, "front-matter", self.front_matter)
334
        add_sub_element(root, "rear-matter", self.rear_matter)
335
336
        for idref in self.cpes:
337
            plat = ET.SubElement(root, "platform")
338
            plat.set("idref", idref)
339
340
        version = ET.SubElement(root, 'version')
341
        version.text = self.version
342
        ET.SubElement(root, "metadata")
343
344
        for profile in self.profiles:
345
            if profile is not None:
346
                root.append(profile.to_xml_element())
347
348
        for value in self.values.values():
349
            root.append(value.to_xml_element())
350
        if self.bash_remediation_fns_group is not None:
351
            root.append(self.bash_remediation_fns_group)
352
        for group in self.groups.values():
353
            root.append(group.to_xml_element())
354
        for rule in self.rules.values():
355
            root.append(rule.to_xml_element())
356
357
        return root
358
359
    def to_file(self, file_name):
360
        root = self.to_xml_element()
361
        tree = ET.ElementTree(root)
362
        tree.write(file_name)
363
364
    def add_value(self, value):
365
        if value is None:
366
            return
367
        self.values[value.id_] = value
368
369
    def add_group(self, group):
370
        if group is None:
371
            return
372
        self.groups[group.id_] = group
373
374
    def add_rule(self, rule):
375
        if rule is None:
376
            return
377
        self.rules[rule.id_] = rule
378
379
    def to_xccdf(self):
380
        """We can easily extend this script to generate a valid XCCDF instead
381
        of SSG SHORTHAND.
382
        """
383
        raise NotImplementedError
384
385
    def __str__(self):
386
        return self.id_
387
388
389
class Group(object):
390
    """Represents XCCDF Group
391
    """
392
    def __init__(self, id_):
393
        self.id_ = id_
394
        self.prodtype = "all"
395
        self.title = ""
396
        self.description = ""
397
        self.warnings = []
398
        self.values = {}
399
        self.groups = {}
400
        self.rules = {}
401
        self.platform = None
402
403
    @staticmethod
404
    def from_yaml(yaml_file, env_yaml=None):
405
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
406
        if yaml_contents is None:
407
            return None
408
409
        group_id = os.path.basename(os.path.dirname(yaml_file))
410
        group = Group(group_id)
411
        group.prodtype = yaml_contents.pop("prodtype", "all")
412
        group.title = required_key(yaml_contents, "title")
413
        del yaml_contents["title"]
414
        group.description = required_key(yaml_contents, "description")
415
        del yaml_contents["description"]
416
        group.warnings = yaml_contents.pop("warnings", [])
417
        group.platform = yaml_contents.pop("platform", None)
418
419
        for warning_list in group.warnings:
420
            if len(warning_list) != 1:
421
                raise ValueError("Only one key/value pair should exist for each dictionary")
422
423
        if yaml_contents:
424
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
425
                               % (yaml_file, yaml_contents))
426
427
        return group
428
429
    def to_xml_element(self):
430
        group = ET.Element('Group')
431
        group.set('id', self.id_)
432
        if self.prodtype != "all":
433
            group.set("prodtype", self.prodtype)
434
        title = ET.SubElement(group, 'title')
435
        title.text = self.title
436
        add_sub_element(group, 'description', self.description)
437
        add_warning_elements(group, self.warnings)
438
439
        if self.platform:
440
            platform_el = ET.SubElement(group, "platform")
441
            try:
442
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
443
            except KeyError:
444
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
445
            platform_el.set("idref", platform_cpe)
446
447
        for _value in self.values.values():
448
            group.append(_value.to_xml_element())
449
        for _group in self.groups.values():
450
            group.append(_group.to_xml_element())
451
        for _rule in self.rules.values():
452
            group.append(_rule.to_xml_element())
453
454
        return group
455
456
    def to_file(self, file_name):
457
        root = self.to_xml_element()
458
        tree = ET.ElementTree(root)
459
        tree.write(file_name)
460
461
    def add_value(self, value):
462
        if value is None:
463
            return
464
        self.values[value.id_] = value
465
466
    def add_group(self, group):
467
        if group is None:
468
            return
469
        if self.platform and not group.platform:
470
            group.platform = self.platform
471
        self.groups[group.id_] = group
472
473
    def add_rule(self, rule):
474
        if rule is None:
475
            return
476
        if self.platform and not rule.platform:
477
            rule.platform = self.platform
478
        self.rules[rule.id_] = rule
479
480
    def __str__(self):
481
        return self.id_
482
483
484
class Rule(object):
485
    """Represents XCCDF Rule
486
    """
487
    def __init__(self, id_):
488
        self.id_ = id_
489
        self.prodtype = "all"
490
        self.title = ""
491
        self.description = ""
492
        self.rationale = ""
493
        self.severity = "unknown"
494
        self.references = {}
495
        self.identifiers = {}
496
        self.ocil_clause = None
497
        self.ocil = None
498
        self.external_oval = None
499
        self.warnings = []
500
        self.platform = None
501
502
    @staticmethod
503
    def from_yaml(yaml_file, env_yaml=None):
504
        yaml_contents = open_and_macro_expand(yaml_file, env_yaml)
505
        if yaml_contents is None:
506
            return None
507
508
        rule_id, ext = os.path.splitext(os.path.basename(yaml_file))
509
        if rule_id == "rule" and ext == ".yml":
510
            rule_id = get_rule_dir_id(yaml_file)
511
512
        rule = Rule(rule_id)
513
        rule.prodtype = yaml_contents.pop("prodtype", "all")
514
        rule.title = required_key(yaml_contents, "title")
515
        del yaml_contents["title"]
516
        rule.description = required_key(yaml_contents, "description")
517
        del yaml_contents["description"]
518
        rule.rationale = required_key(yaml_contents, "rationale")
519
        del yaml_contents["rationale"]
520
        rule.severity = required_key(yaml_contents, "severity")
521
        del yaml_contents["severity"]
522
        rule.references = yaml_contents.pop("references", {})
523
        rule.identifiers = yaml_contents.pop("identifiers", {})
524
        rule.ocil_clause = yaml_contents.pop("ocil_clause", None)
525
        rule.ocil = yaml_contents.pop("ocil", None)
526
        rule.external_oval = yaml_contents.pop("oval_external_content", None)
527
        rule.warnings = yaml_contents.pop("warnings", [])
528
        rule.platform = yaml_contents.pop("platform", None)
529
530
        for warning_list in rule.warnings:
531
            if len(warning_list) != 1:
532
                raise ValueError("Only one key/value pair should exist for each dictionary")
533
534
        if yaml_contents:
535
            raise RuntimeError("Unparsed YAML data in '%s'.\n\n%s"
536
                               % (yaml_file, yaml_contents))
537
538
        rule.validate_identifiers(yaml_file)
539
        rule.validate_references(yaml_file)
540
        return rule
541
542
    def validate_identifiers(self, yaml_file):
543
        if self.identifiers is None:
544
            raise ValueError("Empty identifier section in file %s" % yaml_file)
545
546
        # Validate all identifiers are non-empty:
547
        for ident_type, ident_val in self.identifiers.items():
548
            if not isinstance(ident_type, str) or not isinstance(ident_val, str):
549
                raise ValueError("Identifiers and values must be strings: %s in file %s"
550
                                 % (ident_type, yaml_file))
551
            if ident_val.strip() == "":
552
                raise ValueError("Identifiers must not be empty: %s in file %s"
553
                                 % (ident_type, yaml_file))
554
            if ident_type[0:3] == 'cce':
555
                if not is_cce_valid("CCE-" + ident_val):
556
                    raise ValueError("CCE Identifiers must be valid: value %s for cce %s"
557
                                     " in file %s" % (ident_val, ident_type, yaml_file))
558
559
    def validate_references(self, yaml_file):
560
        if self.references is None:
561
            raise ValueError("Empty references section in file %s" % yaml_file)
562
563
        for ref_type, ref_val in self.references.items():
564
            if not isinstance(ref_type, str) or not isinstance(ref_val, str):
565
                raise ValueError("References and values must be strings: %s in file %s"
566
                                 % (ref_type, yaml_file))
567
            if ref_val.strip() == "":
568
                raise ValueError("References must not be empty: %s in file %s"
569
                                 % (ref_type, yaml_file))
570
571
    def to_xml_element(self):
572
        rule = ET.Element('Rule')
573
        rule.set('id', self.id_)
574
        if self.prodtype != "all":
575
            rule.set("prodtype", self.prodtype)
576
        rule.set('severity', self.severity)
577
        add_sub_element(rule, 'title', self.title)
578
        add_sub_element(rule, 'description', self.description)
579
        add_sub_element(rule, 'rationale', self.rationale)
580
581
        main_ident = ET.Element('ident')
582
        for ident_type, ident_val in self.identifiers.items():
583
            if '@' in ident_type:
584
                # the ident is applicable only on some product
585
                # format : 'policy@product', eg. 'stigid@product'
586
                # for them, we create a separate <ref> element
587
                policy, product = ident_type.split('@')
588
                ident = ET.SubElement(rule, 'ident')
589
                ident.set(policy, ident_val)
590
                ident.set('prodtype', product)
591
            else:
592
                main_ident.set(ident_type, ident_val)
593
594
        if main_ident.attrib:
595
            rule.append(main_ident)
596
597
        main_ref = ET.Element('ref')
598
        for ref_type, ref_val in self.references.items():
599
            if '@' in ref_type:
600
                # the reference is applicable only on some product
601
                # format : 'policy@product', eg. 'stigid@product'
602
                # for them, we create a separate <ref> element
603
                policy, product = ref_type.split('@')
604
                ref = ET.SubElement(rule, 'ref')
605
                ref.set(policy, ref_val)
606
                ref.set('prodtype', product)
607
            else:
608
                main_ref.set(ref_type, ref_val)
609
610
        if main_ref.attrib:
611
            rule.append(main_ref)
612
613
        if self.external_oval:
614
            check = ET.SubElement(rule, 'check')
615
            check.set("system", "http://oval.mitre.org/XMLSchema/oval-definitions-5")
616
            external_content = ET.SubElement(check, "check-content-ref")
617
            external_content.set("href", self.external_oval)
618
        else:
619
            # TODO: This is pretty much a hack, oval ID will be the same as rule ID
620
            #       and we don't want the developers to have to keep them in sync.
621
            #       Therefore let's just add an OVAL ref of that ID.
622
            oval_ref = ET.SubElement(rule, "oval")
623
            oval_ref.set("id", self.id_)
624
625
        if self.ocil or self.ocil_clause:
626
            ocil = add_sub_element(rule, 'ocil', self.ocil if self.ocil else "")
627
            if self.ocil_clause:
628
                ocil.set("clause", self.ocil_clause)
629
630
        add_warning_elements(rule, self.warnings)
631
632
        if self.platform:
633
            platform_el = ET.SubElement(rule, "platform")
634
            try:
635
                platform_cpe = XCCDF_PLATFORM_TO_CPE[self.platform]
636
            except KeyError:
637
                raise ValueError("Unsupported platform '%s' in rule '%s'." % (self.platform, self.id_))
638
            platform_el.set("idref", platform_cpe)
639
640
        return rule
641
642
    def to_file(self, file_name):
643
        root = self.to_xml_element()
644
        tree = ET.ElementTree(root)
645
        tree.write(file_name)
646
647
648
def load_benchmark_or_group(group_file, benchmark_file, guide_directory, action,
649
                            profiles_dir, env_yaml, bash_remediation_fns):
650
    """
651
    Loads a given benchmark or group from the specified benchmark_file or
652
    group_file, in the context of guide_directory, action, profiles_dir,
653
    env_yaml, and bash_remediation_fns.
654
655
    Returns the loaded group or benchmark.
656
    """
657
    group = None
658
    if group_file and benchmark_file:
659
        raise ValueError("A .benchmark file and a .group file were found in "
660
                         "the same directory '%s'" % (guide_directory))
661
662
    # we treat benchmark as a special form of group in the following code
663
    if benchmark_file:
664
        group = Benchmark.from_yaml(
665
            benchmark_file, 'product-name', env_yaml
666
        )
667
        if profiles_dir:
668
            group.add_profiles_from_dir(action, profiles_dir, env_yaml)
669
        group.add_bash_remediation_fns_from_file(action, bash_remediation_fns)
670
        if action == "list-inputs":
671
            print(benchmark_file)
672
673
    if group_file:
674
        group = Group.from_yaml(group_file, env_yaml)
675
        if action == "list-inputs":
676
            print(group_file)
677
678
    return group
679
680
681
def add_from_directory(action, parent_group, guide_directory, profiles_dir,
682
                       bash_remediation_fns, output_file, env_yaml):
683
    """
684
    Process Variables, Benchmarks, and Rules in a given subdirectory,
685
    recursing as necessary.
686
687
    Behavior is dependent upon the value of action.
688
    """
689
    benchmark_file = None
690
    group_file = None
691
    rules = []
692
    values = []
693
    subdirectories = []
694
    for dir_item in os.listdir(guide_directory):
695
        dir_item_path = os.path.join(guide_directory, dir_item)
696
        _, extension = os.path.splitext(dir_item)
697
698
        if extension == '.var':
699
            values.append(dir_item_path)
700
        elif dir_item == "benchmark.yml":
701
            if benchmark_file:
702
                raise ValueError("Multiple benchmarks in one directory")
703
            benchmark_file = dir_item_path
704
        elif dir_item == "group.yml":
705
            if group_file:
706
                raise ValueError("Multiple groups in one directory")
707
            group_file = dir_item_path
708
        elif extension == '.rule':
709
            rules.append(dir_item_path)
710
        elif is_rule_dir(dir_item_path):
711
            rules.append(get_rule_dir_yaml(dir_item_path))
712
        elif dir_item != "tests":
713
            if os.path.isdir(dir_item_path):
714
                subdirectories.append(dir_item_path)
715
            else:
716
                sys.stderr.write(
717
                    "Encountered file '%s' while recursing, extension '%s' "
718
                    "is unknown. Skipping..\n"
719
                    % (dir_item, extension)
720
                )
721
722
    group = load_benchmark_or_group(group_file, benchmark_file, guide_directory, action,
723
                                    profiles_dir, env_yaml, bash_remediation_fns)
724
725
    if group is not None:
726
        if parent_group:
727
            parent_group.add_group(group)
728
        for value_yaml in values:
729
            if action == "list-inputs":
730
                print(value_yaml)
731
            else:
732
                value = Value.from_yaml(value_yaml, env_yaml)
733
                group.add_value(value)
734
735
        for subdir in subdirectories:
736
            add_from_directory(action, group, subdir, profiles_dir,
737
                               bash_remediation_fns, output_file,
738
                               env_yaml)
739
740
        for rule_yaml in rules:
741
            if action == "list-inputs":
742
                print(rule_yaml)
743
            else:
744
                rule = Rule.from_yaml(rule_yaml, env_yaml)
745
                group.add_rule(rule)
746
747
        if not parent_group:
748
            # We are on the top level!
749
            # Lets dump the XCCDF group or benchmark to a file
750
            if action == "build":
751
                group.to_file(output_file)
752