Test Failed
Push — master ( ac2bbf...f7f56e )
by Jan
02:42 queued 12s
created

ssg.oval.replace_external_vars()   A

Complexity

Conditions 3

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 9.933

Importance

Changes 0
Metric Value
cc 3
eloc 13
nop 1
dl 0
loc 24
ccs 1
cts 12
cp 0.0833
crap 9.933
rs 9.75
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 re
7 2
import argparse
8 2
import tempfile
9 2
import subprocess
10
11 2
from .constants import oval_footer as footer
12 2
from .constants import oval_namespace as ovalns
13 2
from .rules import get_rule_dir_id, get_rule_dir_ovals, find_rule_dirs
14 2
from .xml import ElementTree as ET
15 2
from .xml import oval_generated_header
16 2
from .jinja import process_file_with_macros, add_python_functions
17 2
from .environment import open_environment
18 2
from .id_translate import IDTranslator
19 2
from .products import _get_implied_properties
20
21 2
SHARED_OVAL = re.sub(r'ssg/.*', 'shared', __file__) + '/checks/oval/'
22 2
LINUX_OS_GUIDE = re.sub(r'ssg/.*', 'linux_os', __file__) + '/guide/'
23
24
25 2
ASSUMED_OVAL_VERSION_STRING = "5.11"
26
27
28
# globals, to make recursion easier in case we encounter extend_definition
29 2
try:
30 2
    ET.register_namespace("oval", ovalns)
31
except AttributeError:
32
    # Legacy Python 2.6 fix, see e.g.
33
    # https://www.programcreek.com/python/example/57552/xml.etree.ElementTree._namespace_map
34
    from xml.etree import ElementTree as ET
35
    ET._namespace_map[ovalns] = "oval"
36
37 2
definitions = ET.Element("oval:definitions")
38 2
tests = ET.Element("oval:tests")
39 2
objects = ET.Element("oval:objects")
40 2
states = ET.Element("oval:states")
41 2
variables = ET.Element("oval:variables")
42 2
silent_mode = False
43
44
45
# append new child ONLY if it's not a duplicate
46 2
def append(element, newchild):
47
    global silent_mode
48
    newid = newchild.get("id")
49
    existing = element.find(".//*[@id='" + newid + "']")
50
    if existing is not None:
51
        if not silent_mode:
52
            sys.stderr.write("Notification: this ID is used more than once " +
53
                             "and should represent equivalent elements: " +
54
                             newid + "\n")
55
    else:
56
        element.append(newchild)
57
58
59 2
def _add_elements(body, header, yaml_env):
60
    """Add oval elements to the global Elements defined above"""
61
    global definitions
62
    global tests
63
    global objects
64
    global states
65
    global variables
66
67
    tree = ET.fromstring(header + body + footer)
68
    tree = replace_external_vars(tree)
69
    defname = None
70
    # parse new file(string) as an etree, so we can arrange elements
71
    # appropriately
72
    for childnode in tree.findall("./{%s}def-group/*" % ovalns):
73
        # print "childnode.tag is " + childnode.tag
74
        if childnode.tag is ET.Comment:
75
            continue
76
        if childnode.tag == ("{%s}definition" % ovalns):
77
            append(definitions, childnode)
78
            defname = childnode.get("id")
79
            # extend_definition is a special case:  must include a whole other
80
            # definition
81
            for defchild in childnode.findall(".//{%s}extend_definition"
82
                                              % ovalns):
83
                defid = defchild.get("definition_ref")
84
                extend_ref = find_testfile_or_exit(defid)
85
                includedbody = read_ovaldefgroup_file(extend_ref, yaml_env)
86
                # recursively add the elements in the other file
87
                _add_elements(includedbody, header, yaml_env)
88
        if childnode.tag.endswith("_test"):
89
            append(tests, childnode)
90
        if childnode.tag.endswith("_object"):
91
            append(objects, childnode)
92
        if childnode.tag.endswith("_state"):
93
            append(states, childnode)
94
        if childnode.tag.endswith("_variable"):
95
            append(variables, childnode)
96
    return defname
97
98
99 2
def applicable_platforms(oval_file, oval_version_string=None):
100
    """
101
    Returns the applicable platforms for a given oval file
102
    """
103
104 2
    platforms = []
105
106 2
    if not oval_version_string:
107 2
        oval_version_string = ASSUMED_OVAL_VERSION_STRING
108 2
    header = oval_generated_header("applicable_platforms", oval_version_string, "0.0.1")
109
110 2
    oval_version_list = [int(num) for num in oval_version_string.split(".")]
111 2
    subst_dict = dict(target_oval_version=oval_version_list)
112
113 2
    oval_filename_components = oval_file.split(os.path.sep)
114 2
    if len(oval_filename_components) > 3:
115 2
        subst_dict["rule_id"] = oval_filename_components[-3]
116
    else:
117
        msg = "Unable to get rule ID from OVAL path '{path}'".format(path=oval_file)
118
        print(msg, file=sys.stderr)
119
120 2
    subst_dict = _get_implied_properties(subst_dict)
121 2
    subst_dict['target_oval_version'] = [999, 999.999]
122
123 2
    body = process_file_with_macros(oval_file, subst_dict)
124
125 2
    try:
126 2
        oval_tree = ET.fromstring(header + body + footer)
127
    except Exception as e:
128
        msg = "Error while loading " + oval_file
129
        print(msg, file=sys.stderr)
130
        raise e
131
132 2
    element_path = "./{%s}def-group/{%s}definition/{%s}metadata/{%s}affected/{%s}platform"
133 2
    element_ns_path = element_path % (ovalns, ovalns, ovalns, ovalns, ovalns)
134 2
    for node in oval_tree.findall(element_ns_path):
135 2
        platforms.append(node.text)
136
137 2
    return platforms
138
139
140 2
def parse_affected(oval_contents):
141
    """
142
    Returns the tuple (start_affected, end_affected, platform_indents) for
143
    the passed oval file contents. start_affected is the line number of
144
    starting tag of the <affected> element, end_affected is the line number of
145
    the closing tag of the </affected> element, and platform_indents is a
146
    string containing the indenting characters before the contents of the
147
    <affected> element.
148
    """
149
150
    start_affected = list(filter(lambda x: "<affected" in oval_contents[x],
151
                                 range(0, len(oval_contents))))
152
    if len(start_affected) != 1:
153
        raise ValueError("OVAL file does not contain a single <affected> "
154
                         "element; counted %d in:\n%s\n\n" %
155
                         (len(start_affected), "\n".join(oval_contents)))
156
157
    start_affected = start_affected[0]
158
159
    end_affected = list(filter(lambda x: "</affected" in oval_contents[x],
160
                               range(0, len(oval_contents))))
161
    if len(end_affected) != 1:
162
        raise ValueError("Malformed OVAL file does not contain a single "
163
                         "closing </affected>; counted %d in:\n%s\n\n" %
164
                         (len(start_affected), "\n".join(oval_contents)))
165
    end_affected = end_affected[0]
166
167
    if start_affected >= end_affected:
168
        raise ValueError("Malformed OVAL file: start affected tag begins "
169
                         "on the same line or after ending affected tag: "
170
                         "start:%d vs end:%d:\n%s\n\n" %
171
                         (start_affected, end_affected, oval_contents))
172
173
    # Validate that start_affected contains only a starting <affected> tag;
174
    # otherwise, using this information to update the <platform> subelements
175
    # would fail.
176
    start_line = oval_contents[start_affected]
177
    start_line = start_line.strip()
178
179
    if not start_line.startswith('<affected'):
180
        raise ValueError("Malformed OVAL file: line with starting affected "
181
                         "tag contains other elements: line:%s\n%s\n\n" %
182
                         (start_line, oval_contents))
183
    if '<' in start_line[1:]:
184
        raise ValueError("Malformed OVAL file: line with starting affected "
185
                         "tag contains other elements: line:%s\n%s\n\n" %
186
                         (start_line, oval_contents))
187
188
    # Validate that end_affected contains only an ending </affected> tag;
189
    # otherwise, using this information to update the <platform> subelements
190
    # would fail.
191
    end_line = oval_contents[end_affected]
192
    end_line = end_line.strip()
193
194
    if not end_line.startswith('</affected>'):
195
        raise ValueError("Malformed OVAL file: line with ending affected "
196
                         "tag contains other elements: line:%s\n%s\n\n" %
197
                         (end_line, oval_contents))
198
    if '<' in end_line[1:]:
199
        raise ValueError("Malformed OVAL file: line with ending affected "
200
                         "tag contains other elements: line:%s\n%s\n\n" %
201
                         (end_line, oval_contents))
202
203
    indents = ""
204
    if start_affected+1 == end_affected:
205
        # Since the affected element is present but empty, the indents should
206
        # be two more spaces than that of the starting <affected> element.
207
        start_index = oval_contents[start_affected].index('<')
208
        indents = oval_contents[start_affected][0:start_index]
209
        indents += "  "
210
    else:
211
        # Otherwise, grab the indents off the next line unmodified, as this is
212
        # likely a platform element tag. We don't validate here that this is
213
        # indeed the case, as other parts of the build infrastructure will
214
        # validate this for us.
215
        start_index = oval_contents[start_affected+1].index('<')
216
        indents = oval_contents[start_affected+1][0:start_index]
217
218
    return start_affected, end_affected, indents
219
220
221 2
def replace_external_vars(tree):
222
    """Replace external_variables with local_variables, so the definition can be
223
       tested independently of an XCCDF file"""
224
225
    # external_variable is a special case: we turn it into a local_variable so
226
    # we can test
227
    for node in tree.findall(".//{%s}external_variable" % ovalns):
228
        print("External_variable with id : " + node.get("id"))
229
        extvar_id = node.get("id")
230
        # for envkey, envval in os.environ.iteritems():
231
        #     print envkey + " = " + envval
232
        # sys.exit()
233
        if extvar_id not in os.environ.keys():
234
            print("External_variable specified, but no value provided via "
235
                  "environment variable", file=sys.stderr)
236
            sys.exit(2)
237
        # replace tag name: external -> local
238
        node.tag = "{%s}local_variable" % ovalns
239
        literal = ET.Element("oval:literal_component")
240
        literal.text = os.environ[extvar_id]
241
        node.append(literal)
242
        # TODO: assignment of external_variable via environment vars, for
243
        # testing
244
    return tree
245
246
247 2
def find_testfile_or_exit(testfile):
248
    """Find OVAL files in CWD or shared/oval and calls sys.exit if the file is not found"""
249
    _testfile = find_testfile(testfile)
250
    if _testfile is None:
251
        print("ERROR: %s does not exist! Please specify a valid OVAL file." % testfile,
252
              file=sys.stderr)
253
        sys.exit(1)
254
    else:
255
        return _testfile
256
257
258 2
def find_testfile(oval_id):
259
    """
260
    Find OVAL file by id in CWD, SHARED_OVAL, or LINUX_OS_GUIDE. Understands rule
261
    directories and defaults to returning shared.xml over {{{ product }}}.xml.
262
263
    Returns path to OVAL file or None if not found.
264
    """
265 2
    if os.path.isfile(os.path.abspath(oval_id)):
266
        return os.path.abspath(oval_id)
267
268 2
    if oval_id.endswith(".xml"):
269 2
        oval_id, _ = os.path.splitext(oval_id)
270 2
        oval_id = os.path.basename(oval_id)
271
272 2
    candidates = [oval_id, "%s.xml" % oval_id]
273
274 2
    found_file = None
275 2
    for path in ['.', SHARED_OVAL, LINUX_OS_GUIDE]:
276 2
        for root, dirs, _ in os.walk(path):
277 2
            dirs.sort()
278 2
            for candidate in candidates:
279 2
                search_file = os.path.join(root, candidate).strip()
280 2
                if os.path.isfile(search_file):
281
                    found_file = search_file
282
                    break
283
284 2
        for rule_dir in find_rule_dirs(path):
285 2
            rule_id = get_rule_dir_id(rule_dir)
286 2
            if rule_id == oval_id:
287 2
                ovals = get_rule_dir_ovals(rule_dir, product="shared")
288 2
                if ovals:
289 2
                    found_file = ovals[0]
290 2
                    break
291
292 2
    return found_file
293
294
295 2
def read_ovaldefgroup_file(testfile, yaml_env):
296
    """Read oval files"""
297
    body = process_file_with_macros(testfile, yaml_env)
298
    return body
299
300
301 2
def get_openscap_supported_oval_version():
302
    try:
303
        from openscap import oscap_get_version
304
        if [int(x) for x in str(oscap_get_version()).split(".")] >= [1, 2, 0]:
305
            return "5.11"
306
    except ImportError:
307
        pass
308
309
    return "5.10"
310
311
312 2
def parse_options():
313
    usage = "usage: %(prog)s [options] definition_file.xml"
314
    parser = argparse.ArgumentParser(usage=usage)
315
    # only some options are on by default
316
317
    oscap_oval_version = get_openscap_supported_oval_version()
318
319
    parser.add_argument("--oval_version",
320
                        default=oscap_oval_version,
321
                        dest="oval_version", action="store",
322
                        help="OVAL version to use. Example: 5.11, 5.10, ... "
323
                             "If not supplied the highest version supported by "
324
                             "openscap will be used: %s" % (oscap_oval_version))
325
    parser.add_argument("-q", "--quiet", "--silent", default=False,
326
                        action="store_true", dest="silent_mode",
327
                        help="Don't show any output when testing OVAL files")
328
    parser.add_argument("xmlfile", metavar="XMLFILE", help="OVAL XML file")
329
    args = parser.parse_args()
330
331
    return args
332
333
334 2
def main():
335
    global definitions
336
    global tests
337
    global objects
338
    global states
339
    global variables
340
    global silent_mode
341
342
    args = parse_options()
343
    silent_mode = args.silent_mode
344
    oval_version = args.oval_version
345
346
    testfile = args.xmlfile
347
    header = oval_generated_header("testoval.py", oval_version, "0.0.1")
348
    testfile = find_testfile_or_exit(testfile)
349
    yaml_env = dict()
350
    body = read_ovaldefgroup_file(testfile, yaml_env)
351
352
    defname = _add_elements(body, header, yaml_env)
353
    if defname is None:
354
        print("Error while evaluating oval: defname not set; missing "
355
              "definitions section?")
356
        sys.exit(1)
357
358
    ovaltree = ET.fromstring(header + footer)
359
360
    # append each major element type, if it has subelements
361
    for element in [definitions, tests, objects, states, variables]:
362
        if list(element):
363
            ovaltree.append(element)
364
365
    # re-map all the element ids from meaningful names to meaningless
366
    # numbers
367
    testtranslator = IDTranslator("scap-security-guide.testing")
368
    ovaltree = testtranslator.translate(ovaltree)
369
    (ovalfile, fname) = tempfile.mkstemp(prefix=defname, suffix=".xml")
370
    os.write(ovalfile, ET.tostring(ovaltree))
371
    os.close(ovalfile)
372
373
    cmd = ['oscap', 'oval', 'eval', '--results', fname + '-results', fname]
374
    if not silent_mode:
375
        print("Evaluating with OVAL tempfile: " + fname)
376
        print("OVAL Schema Version: %s" % oval_version)
377
        print("Writing results to: " + fname + "-results")
378
        print("Running command: %s\n" % " ".join(cmd))
379
380
    oscap_child = subprocess.Popen(cmd, stdout=subprocess.PIPE)
381
    cmd_out = oscap_child.communicate()[0]
382
383
    if isinstance(cmd_out, bytes):
384
        cmd_out = cmd_out.decode('utf-8')
385
386
    if not silent_mode:
387
        print(cmd_out, file=sys.stderr)
388
389
    if oscap_child.returncode != 0:
390
        if not silent_mode:
391
            print("Error launching 'oscap' command: return code %d" % oscap_child.returncode)
392
        sys.exit(2)
393
394
    if 'false' in cmd_out or 'error' in cmd_out:
395
        # at least one from the evaluated OVAL definitions evaluated to
396
        # 'false' result, exit with '1' to indicate OVAL scan FAIL result
397
        sys.exit(1)
398
399
    # perhaps delete tempfile?
400
    definitions = ET.Element("oval:definitions")
401
    tests = ET.Element("oval:tests")
402
    objects = ET.Element("oval:objects")
403
    states = ET.Element("oval:states")
404
    variables = ET.Element("oval:variables")
405
406
    # 'false' keyword wasn't found in oscap's command output
407
    # exit with '0' to indicate OVAL scan TRUE result
408
    sys.exit(0)
409