Test Failed
Push — master ( 27f45a...5ab10d )
by Jan
03:21 queued 10s
created

ssg.oval.applicable_platforms()   B

Complexity

Conditions 5

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5.3906

Importance

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