Passed
Push — master ( 4c49b2...31e49c )
by Matěj
04:23 queued 11s
created

split_remediation_content_and_metadata()   B

Complexity

Conditions 7

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7.0145

Importance

Changes 0
Metric Value
cc 7
eloc 15
nop 1
dl 0
loc 25
ccs 14
cts 15
cp 0.9333
crap 7.0145
rs 8
c 0
b 0
f 0
1 2
from __future__ import absolute_import
2 2
from __future__ import print_function
3
4 2
import sys
5 2
import os
6 2
import os.path
7 2
import re
8 2
import codecs
9 2
from collections import defaultdict, namedtuple
10
11
12 2
import ssg.yaml
13 2
from .jinja import process_file as jinja_process_file
14 2
from .xml import ElementTree
15 2
from .products import parse_name, map_name
16 2
from .constants import MULTI_PLATFORM_LIST
17
18 2
REMEDIATION_TO_EXT_MAP = {
19
    'anaconda': '.anaconda',
20
    'ansible': '.yml',
21
    'bash': '.sh',
22
    'puppet': '.pp'
23
}
24
25 2
FILE_GENERATED_HASH_COMMENT = '# THIS FILE IS GENERATED'
26
27 2
REMEDIATION_CONFIG_KEYS = ['complexity', 'disruption', 'platform', 'reboot',
28
                           'strategy']
29 2
REMEDIATION_ELM_KEYS = ['complexity', 'disruption', 'reboot', 'strategy']
30
31
32 2
def is_applicable_for_product(platform, product):
33
    """Based on the platform dict specifier of the remediation script to
34
    determine if this remediation script is applicable for this product.
35
    Return 'True' if so, 'False' otherwise"""
36
37
    # If the platform is None, platform must not exist in the config, so exit with False.
38 2
    if not platform:
39
        return False
40
41 2
    product, product_version = parse_name(product)
42
43
    # Define general platforms
44 2
    multi_platforms = ['multi_platform_all',
45
                       'multi_platform_' + product]
46
47
    # First test if platform isn't for 'multi_platform_all' or
48
    # 'multi_platform_' + product
49 2
    for _platform in multi_platforms:
50 2
        if _platform in platform and product in MULTI_PLATFORM_LIST:
51 2
            return True
52
53 2
    product_name = ""
54
    # Get official name for product
55 2
    if product_version is not None:
56 2
        product_name = map_name(product) + ' ' + product_version
57
    else:
58
        product_name = map_name(product)
59
60
    # Test if this is for the concrete product version
61 2
    for _name_part in platform.split(','):
62 2
        if product_name == _name_part.strip():
63 2
            return True
64
65
    # Remediation script isn't neither a multi platform one, nor isn't
66
    # applicable for this product => return False to indicate that
67 2
    return False
68
69
70 2
def get_available_functions(build_dir):
71
    """Parse the content of "$CMAKE_BINARY_DIR/bash-remediation-functions.xml"
72
    XML file to obtain the list of currently known SCAP Security Guide internal
73
    remediation functions"""
74
75
    # If location of /shared directory is known
76
    if build_dir is None or not os.path.isdir(build_dir):
77
        sys.stderr.write("Expected '%s' to be the build directory. It doesn't "
78
                         "exist or is not a directory." % (build_dir))
79
        sys.exit(1)
80
81
    # Construct the final path of XML file with remediation functions
82
    xmlfilepath = \
83
        os.path.join(build_dir, "bash-remediation-functions.xml")
84
85
    if not os.path.isfile(xmlfilepath):
86
        sys.stderr.write("Expected '%s' to contain the remediation functions. "
87
                         "The file was not found!\n" % (xmlfilepath))
88
        sys.exit(1)
89
90
    remediation_functions = []
91
    with codecs.open(xmlfilepath, "r", encoding="utf-8") as xmlfile:
92
        filestring = xmlfile.read()
93
        # This regex looks implementation dependent but we can rely on
94
        # ElementTree sorting XML attrs alphabetically. Hidden is guaranteed
95
        # to be the first attr and ID is guaranteed to be second.
96
        remediation_functions = re.findall(
97
            r'<Value hidden=\"true\" id=\"function_(\S+)\"',
98
            filestring, re.DOTALL
99
        )
100
101
    return remediation_functions
102
103
104 2
def get_fixgroup_for_type(fixcontent, remediation_type):
105
    """
106
    For a given remediation type, return a new subelement of that type.
107
108
    Exits if passed an unknown remediation type.
109
    """
110
    if remediation_type == 'anaconda':
111
        return ElementTree.SubElement(
112
            fixcontent, "fix-group", id="anaconda",
113
            system="urn:redhat:anaconda:pre",
114
            xmlns="http://checklists.nist.gov/xccdf/1.1")
115
116
    elif remediation_type == 'ansible':
117
        return ElementTree.SubElement(
118
            fixcontent, "fix-group", id="ansible",
119
            system="urn:xccdf:fix:script:ansible",
120
            xmlns="http://checklists.nist.gov/xccdf/1.1")
121
122
    elif remediation_type == 'bash':
123
        return ElementTree.SubElement(
124
            fixcontent, "fix-group", id="bash",
125
            system="urn:xccdf:fix:script:sh",
126
            xmlns="http://checklists.nist.gov/xccdf/1.1")
127
128
    elif remediation_type == 'puppet':
129
        return ElementTree.SubElement(
130
            fixcontent, "fix-group", id="puppet",
131
            system="urn:xccdf:fix:script:puppet",
132
            xmlns="http://checklists.nist.gov/xccdf/1.1")
133
134
    sys.stderr.write("ERROR: Unknown remediation type '%s'!\n"
135
                     % (remediation_type))
136
    sys.exit(1)
137
138
139 2
def is_supported_filename(remediation_type, filename):
140
    """
141
    Checks if filename has a supported extension for remediation_type.
142
143
    Exits when remediation_type is of an unknown type.
144
    """
145 2
    if remediation_type in REMEDIATION_TO_EXT_MAP:
146 2
        return filename.endswith(REMEDIATION_TO_EXT_MAP[remediation_type])
147
148
    sys.stderr.write("ERROR: Unknown remediation type '%s'!\n"
149
                     % (remediation_type))
150
    sys.exit(1)
151
152
153 2
def get_populate_replacement(remediation_type, text):
154
    """
155
    Return varname, fixtextcontribution
156
    """
157
158
    if remediation_type == 'bash':
159
        # Extract variable name
160
        varname = re.search(r'\npopulate (\S+)\n',
161
                            text, re.DOTALL).group(1)
162
        # Define fix text part to contribute to main fix text
163
        fixtextcontribution = '\n%s="' % varname
164
        return (varname, fixtextcontribution)
165
166
    sys.stderr.write("ERROR: Unknown remediation type '%s'!\n"
167
                     % (remediation_type))
168
    sys.exit(1)
169
170
171 2
def split_remediation_content_and_metadata(fix_file):
172 2
    remediation_contents = []
173 2
    config = defaultdict(lambda: None)
174
175
    # Assignment automatically escapes shell characters for XML
176 2
    for line in fix_file.splitlines():
177 2
        if line.startswith(FILE_GENERATED_HASH_COMMENT):
178
            continue
179
180 2
        if line.startswith('#') and line.count('=') == 1:
181 2
            (key, value) = line.strip('#').split('=')
182 2
            if key.strip() in REMEDIATION_CONFIG_KEYS:
183 2
                config[key.strip()] = value.strip()
184 2
                continue
185
186
        # If our parsed line wasn't a config item, add it to the
187
        # returned file contents. This includes when the line
188
        # begins with a '#' and contains an equals sign, but
189
        # the "key" isn't one of the known keys from
190
        # REMEDIATION_CONFIG_KEYS.
191 2
        remediation_contents.append(line)
192
193 2
    contents = "\n".join(remediation_contents)
194 2
    remediation = namedtuple('remediation', ['contents', 'config'])
195 2
    return remediation(contents=contents, config=config)
196
197
198 2
def parse_from_file_with_jinja(file_path, env_yaml):
199
    """
200
    Parses a remediation from a file. As remediations contain jinja macros,
201
    we need a env_yaml context to process these. In practice, no remediations
202
    use jinja in the configuration, so for extracting only the configuration,
203
    env_yaml can be an abritrary product.yml dictionary.
204
205
    If the logic of configuration parsing changes significantly, please also
206
    update ssg.fixes.parse_platform(...).
207
    """
208
209 2
    fix_file = jinja_process_file(file_path, env_yaml)
210 2
    return split_remediation_content_and_metadata(fix_file)
211
212
213 2
def parse_from_file_without_jinja(file_path):
214
    """
215
    Parses a remediation from a file. Doesn't process the Jinja macros.
216
    This function is useful in build phases in which all the Jinja macros
217
    are already resolved.
218
    """
219 2
    with open(file_path, "r") as f:
220 2
        f_str = f.read()
221 2
        return split_remediation_content_and_metadata(f_str)
222
223
224 2
class Remediation(object):
225 2
    def __init__(self, env_yaml, resolved_rules, product, file_path, fix_name, remediation_type):
226 2
        self.env_yaml = env_yaml
227 2
        self.resolved_rules = resolved_rules
228 2
        self.product = product
229 2
        self.file_path = file_path
230 2
        self.fix_name = fix_name
231 2
        self.remediation_type = remediation_type
232
233 2
    def process(self, fixes):
234
        """
235
        Process a fix, adding it to fixes iff the file is of a valid extension
236
        for the remediation type and the fix is valid for the current product.
237
238
        Note that platform is a required field in the contents of the fix.
239
        """
240
241 2
        if not is_supported_filename(self.remediation_type, self.file_path):
242
            return
243
244 2
        result = parse_from_file_with_jinja(self.file_path, self.env_yaml)
245
246 2
        if not result.config['platform']:
247
            raise RuntimeError(
248
                "The '%s' remediation script does not contain the "
249
                "platform identifier!" % (self.file_path))
250
251 2
        if is_applicable_for_product(result.config['platform'], self.product):
252 2
            fixes[self.fix_name] = result
253
254 2
        return result
255
256
257 2
class BashRemediation(Remediation):
258 2
    def __init__(self, env_yaml, resolved_rules, product, file_path, fix_name):
259 2
        super(BashRemediation, self).__init__(
260
            env_yaml, resolved_rules, product, file_path, fix_name, "bash")
261
262
263 2
class AnsibleRemediation(Remediation):
264 2
    def __init__(self, env_yaml, resolved_rules, product, file_path, fix_name):
265
        super(AnsibleRemediation, self).__init__(
266
            env_yaml, resolved_rules, product, file_path, fix_name, "ansible")
267
268 2
    def process(self, fixes):
269
        result = super(AnsibleRemediation, self).process(fixes)
270
271
        rule_path = os.path.join(
272
            self.resolved_rules, self.fix_name + ".yml")
273
        if not os.path.isfile(rule_path):
274
            msg = ("Ansible snippet for rule {rule_id} "
275
                   "doesn't have respective rule YML at {rule_fname}"
276
                   .format(rule_id=os.path.splitext(self.fix_name)[0], rule_fname=rule_path))
277
            print(msg, file=sys.stderr)
278
            return result
279
280
        import ssg.ansible
281
        from ssg import build_yaml
282
283
        remediation_obj = ssg.ansible.AnsibleRemediation(result.contents, result.config)
284
        remediation_obj.rule = build_yaml.Rule.from_yaml(rule_path)
285
        remediation_obj.update(self.product)
286
287
        updated_yaml_text = ssg.yaml.ordered_dump(
288
            remediation_obj.parsed, None, default_flow_style=False)
289
        result = result._replace(contents=updated_yaml_text)
290
291
        fixes[self.fix_name] = result
292
        return result
293
294
295 2
class AnacondaRemediation(Remediation):
296 2
    def __init__(self, env_yaml, resolved_rules, product, file_path, fix_name):
297
        super(AnacondaRemediation, self).__init__(
298
            env_yaml, resolved_rules, product, file_path, fix_name, "anaconda")
299
300
301 2
class PuppetRemediation(Remediation):
302 2
    def __init__(self, env_yaml, resolved_rules, product, file_path, fix_name):
303
        super(PuppetRemediation, self).__init__(
304
            env_yaml, resolved_rules, product, file_path, fix_name, "puppet")
305
306
307 2
REMEDIATION_TO_CLASS = {
308
    'anaconda': AnacondaRemediation,
309
    'ansible': AnsibleRemediation,
310
    'bash': BashRemediation,
311
    'puppet': PuppetRemediation,
312
}
313
314
315 2
def write_fixes_to_xml(remediation_type, build_dir, output_path, fixes):
316
    """
317
    Builds a fix-content XML tree from the contents of fixes
318
    and writes it to output_path.
319
    """
320
321
    fixcontent = ElementTree.Element("fix-content", system="urn:xccdf:fix:script:sh",
322
                                     xmlns="http://checklists.nist.gov/xccdf/1.1")
323
    fixgroup = get_fixgroup_for_type(fixcontent, remediation_type)
324
325
    remediation_functions = get_available_functions(build_dir)
326
327
    for fix_name in fixes:
328
        fix_contents, config = fixes[fix_name]
329
330
        fix_elm = ElementTree.SubElement(fixgroup, "fix")
331
        fix_elm.set("rule", fix_name)
332
333
        for key in REMEDIATION_ELM_KEYS:
334
            if config[key]:
335
                fix_elm.set(key, config[key])
336
337
        fix_elm.text = fix_contents + "\n"
338
339
        # Expand shell variables and remediation functions
340
        # into corresponding XCCDF <sub> elements
341
        expand_xccdf_subs(fix_elm, remediation_type, remediation_functions)
342
343
    tree = ElementTree.ElementTree(fixcontent)
344
    tree.write(output_path)
345
346
347 2
def write_fixes_to_dir(fixes, remediation_type, output_dir):
348
    """
349
    Writes fixes as files to output_dir, each fix as a separate file
350
    """
351
    try:
352
        extension = REMEDIATION_TO_EXT_MAP[remediation_type]
353
    except KeyError:
354
        raise ValueError("Unknown remediation type %s." % remediation_type)
355
356
    if not os.path.exists(output_dir):
357
        os.makedirs(output_dir)
358
    for fix_name, fix in fixes.items():
359
        fix_contents, config = fix
360
        fix_path = os.path.join(output_dir, fix_name + extension)
361
        with open(fix_path, "w") as f:
362
            for k, v in config.items():
363
                f.write("# %s = %s\n" % (k, v))
364
            f.write(fix_contents)
365
366
367 2
def expand_xccdf_subs(fix, remediation_type, remediation_functions):
368
    """For those remediation scripts utilizing some of the internal SCAP
369
    Security Guide remediation functions expand the selected shell variables
370
    and remediation functions calls with <xccdf:sub> element
371
372
    This routine translates any instance of the 'populate' function call in
373
    the form of:
374
375
            populate variable_name
376
377
    into
378
379
            variable_name="<sub idref="variable_name"/>"
380
381
    Also transforms any instance of the 'ansible-populate' function call in the
382
    form of:
383
            (ansible-populate variable_name)
384
    into
385
386
            <sub idref="variable_name"/>
387
388
    Also transforms any instance of some other known remediation function (e.g.
389
    'replace_or_append' etc.) from the form of:
390
391
            function_name "arg1" "arg2" ... "argN"
392
393
    into:
394
395
            <sub idref="function_function_name"/>
396
            function_name "arg1" "arg2" ... "argN"
397
    """
398
399
    if remediation_type == "ansible":
400
        fix_text = fix.text
401
402
        if "(ansible-populate " in fix_text:
403
            raise RuntimeError(
404
                "(ansible-populate VAR) has been deprecated. Please use "
405
                "(xccdf-var VAR) instead. Keep in mind that the latter will "
406
                "make an ansible variable out of XCCDF Value as opposed to "
407
                "substituting directly."
408
            )
409
410
        # If you change this string make sure it still matches the pattern
411
        # defined in OpenSCAP. Otherwise you break variable handling in
412
        # 'oscap xccdf generate fix' and the variables won't be customizable!
413
        # https://github.com/OpenSCAP/openscap/blob/1.2.17/src/XCCDF_POLICY/xccdf_policy_remediate.c#L588
414
        #   const char *pattern =
415
        #     "- name: XCCDF Value [^ ]+ # promote to variable\n  set_fact:\n"
416
        #     "    ([^:]+): (.+)\n  tags:\n    - always\n";
417
        # We use !!str typecast to prevent treating values as different types
418
        # eg. yes as a bool or 077 as an octal number
419
        fix_text = re.sub(
420
            r"- \(xccdf-var\s+(\S+)\)",
421
            r"- name: XCCDF Value \1 # promote to variable\n"
422
            r"  set_fact:\n"
423
            r"    \1: !!str (ansible-populate \1)\n"
424
            r"  tags:\n"
425
            r"    - always",
426
            fix_text
427
        )
428
429
        pattern = r'\(ansible-populate\s*(\S+)\)'
430
431
        # we will get list what looks like
432
        # [text, varname, text, varname, ..., text]
433
        parts = re.split(pattern, fix_text)
434
435
        fix.text = parts[0]  # add first "text"
436
        for index in range(1, len(parts), 2):
437
            varname = parts[index]
438
            text_between_vars = parts[index + 1]
439
440
            # we cannot combine elements and text easily
441
            # so text is in ".tail" of element
442
            xccdfvarsub = ElementTree.SubElement(fix, "sub", idref=varname)
443
            xccdfvarsub.tail = text_between_vars
444
        return
445
446
    elif remediation_type == "puppet":
447
        pattern = r'\(puppet-populate\s*(\S+)\)'
448
449
        # we will get list what looks like
450
        # [text, varname, text, varname, ..., text]
451
        parts = re.split(pattern, fix.text)
452
453
        fix.text = parts[0]  # add first "text"
454
        for index in range(1, len(parts), 2):
455
            varname = parts[index]
456
            text_between_vars = parts[index + 1]
457
458
            # we cannot combine elements and text easily
459
            # so text is in ".tail" of element
460
            xccdfvarsub = ElementTree.SubElement(fix, "sub", idref=varname)
461
            xccdfvarsub.tail = text_between_vars
462
        return
463
464
    elif remediation_type == "anaconda":
465
        pattern = r'\(anaconda-populate\s*(\S+)\)'
466
467
        # we will get list what looks like
468
        # [text, varname, text, varname, ..., text]
469
        parts = re.split(pattern, fix.text)
470
471
        fix.text = parts[0]  # add first "text"
472
        for index in range(1, len(parts), 2):
473
            varname = parts[index]
474
            text_between_vars = parts[index + 1]
475
476
            # we cannot combine elements and text easily
477
            # so text is in ".tail" of element
478
            xccdfvarsub = ElementTree.SubElement(fix, "sub", idref=varname)
479
            xccdfvarsub.tail = text_between_vars
480
        return
481
482
    elif remediation_type == "bash":
483
        # This remediation script doesn't utilize internal remediation functions
484
        # Skip it without any further processing
485
        if 'remediation_functions' not in fix.text:
486
            return
487
488
        # This remediation script utilizes some of internal remediation functions
489
        # Expand shell variables and remediation functions calls with <xccdf:sub>
490
        # elements
491
        pattern = r'\n+(\s*(?:' + r'|'.join(remediation_functions) + r')[^\n]*)\n'
492
        patcomp = re.compile(pattern, re.DOTALL)
493
        fixparts = re.split(patcomp, fix.text)
494
        if fixparts[0] is not None:
495
            # Split the portion of fix.text from fix start to first call of
496
            # remediation function, keeping only the third part:
497
            # * tail        to hold part of the fix.text after inclusion,
498
            #               but before first call of remediation function
499
            try:
500
                rfpattern = '(.*remediation_functions)(.*)'
501
                rfpatcomp = re.compile(rfpattern, re.DOTALL)
502
                _, _, tail, _ = re.split(rfpatcomp, fixparts[0], maxsplit=2)
503
            except ValueError:
504
                sys.stderr.write("Processing fix.text for: %s rule\n"
505
                                 % fix.get('rule'))
506
                sys.stderr.write("Unable to extract part of the fix.text "
507
                                 "after inclusion of remediation functions."
508
                                 " Aborting..\n")
509
                sys.exit(1)
510
            # If the 'tail' is not empty, make it new fix.text.
511
            # Otherwise use ''
512
            fix.text = tail if tail is not None else ''
0 ignored issues
show
introduced by
The variable tail does not seem to be defined for all execution paths.
Loading history...
513
            # Drop the first element of 'fixparts' since it has been processed
514
            fixparts.pop(0)
515
            # Perform sanity check on new 'fixparts' list content (to continue
516
            # successfully 'fixparts' has to contain even count of elements)
517
            if len(fixparts) % 2 != 0:
518
                sys.stderr.write("Error performing XCCDF expansion on "
519
                                 "remediation script: %s\n"
520
                                 % fix.get("rule"))
521
                sys.stderr.write("Invalid count of elements. Exiting!\n")
522
                sys.exit(1)
523
            # Process remaining 'fixparts' elements in pairs
524
            # First pair element is remediation function to be XCCDF expanded
525
            # Second pair element (if not empty) is the portion of the original
526
            # fix text to be used in newly added sublement's tail
527
            for idx in range(0, len(fixparts), 2):
528
                # We previously removed enclosing newlines when creating
529
                # fixparts list. Add them back and reuse the above 'pattern'
530
                fixparts[idx] = "\n%s\n" % fixparts[idx]
531
                # Sanity check (verify the first field truly contains call of
532
                # some of the remediation functions)
533
                if re.match(pattern, fixparts[idx], re.DOTALL) is not None:
534
                    # This chunk contains call of 'populate' function
535
                    if "populate" in fixparts[idx]:
536
                        varname, fixtextcontrib = get_populate_replacement(remediation_type,
537
                                                                           fixparts[idx])
538
                        # Define new XCCDF <sub> element for the variable
539
                        xccdfvarsub = ElementTree.Element("sub", idref=varname)
540
541
                        # If this is first sub element,
542
                        # the textcontribution needs to go to fix text
543
                        # otherwise, append to last subelement
544
                        nfixchildren = len(list(fix))
545
                        if nfixchildren == 0:
546
                            fix.text += fixtextcontrib
547
                        else:
548
                            previouselem = fix[nfixchildren-1]
549
                            previouselem.tail += fixtextcontrib
550
551
                        # If second pair element is not empty, append it as
552
                        # tail for the subelement (prefixed with closing '"')
553
                        if fixparts[idx + 1] is not None:
554
                            xccdfvarsub.tail = '"' + '\n' + fixparts[idx + 1]
555
                        # Otherwise append just enclosing '"'
556
                        else:
557
                            xccdfvarsub.tail = '"' + '\n'
558
                        # Append the new subelement to the fix element
559
                        fix.append(xccdfvarsub)
560
                    # This chunk contains call of other remediation function
561
                    else:
562
                        # Extract remediation function name
563
                        funcname = re.search(r'\n\s*(\S+)(| .*)\n',
564
                                             fixparts[idx],
565
                                             re.DOTALL).group(1)
566
                        # Define new XCCDF <sub> element for the function
567
                        xccdffuncsub = ElementTree.Element(
568
                            "sub", idref='function_%s' % funcname)
569
                        # Append original function call into tail of the
570
                        # subelement
571
                        xccdffuncsub.tail = fixparts[idx]
572
                        # If the second element of the pair is not empty,
573
                        # append it to the tail of the subelement too
574
                        if fixparts[idx + 1] is not None:
575
                            xccdffuncsub.tail += fixparts[idx + 1]
576
                        # Append the new subelement to the fix element
577
                        fix.append(xccdffuncsub)
578
                        # Ensure the newly added <xccdf:sub> element for the
579
                        # function will be always inserted at newline
580
                        # If xccdffuncsub is the first <xccdf:sub> element
581
                        # being added as child of <fix> and fix.text doesn't
582
                        # end up with newline character, append the newline
583
                        # to the fix.text
584
                        if list(fix).index(xccdffuncsub) == 0:
585
                            if re.search(r'.*\n$', fix.text) is None:
586
                                fix.text += '\n'
587
                        # If xccdffuncsub isn't the first child (first
588
                        # <xccdf:sub> being added), and tail of previous
589
                        # child doesn't end up with newline, append the newline
590
                        # to the tail of previous child
591
                        else:
592
                            previouselem = fix[list(fix).index(xccdffuncsub) - 1]
593
                            if re.search(r'.*\n$', previouselem.tail) is None:
594
                                previouselem.tail += '\n'
595
596
        # Perform a sanity check if all known remediation function calls have been
597
        # properly XCCDF substituted. Exit with failure if some wasn't
598
599
        # First concat output form of modified fix text (including text appended
600
        # to all children of the fix)
601
        modfix = [fix.text]
602
        for child in fix.getchildren():
603
            if child is not None and child.text is not None:
604
                modfix.append(child.text)
605
        modfixtext = "".join(modfix)
606
        for func in remediation_functions:
607
            # Then efine expected XCCDF sub element form for this function
608
            funcxccdfsub = "<sub idref=\"function_%s\"" % func
609
            # Finally perform the sanity check -- if function was properly XCCDF
610
            # substituted both the original function call and XCCDF <sub> element
611
            # for that function need to be present in the modified text of the fix
612
            # Otherwise something went wrong, thus exit with failure
613
            if func in modfixtext and funcxccdfsub not in modfixtext:
614
                sys.stderr.write("Error performing XCCDF <sub> substitution "
615
                                 "for function %s in %s fix. Exiting...\n"
616
                                 % (func, fix.get("rule")))
617
                sys.exit(1)
618
    else:
619
        sys.stderr.write("Unknown remediation type '%s'\n" % (remediation_type))
620
        sys.exit(1)
621