Passed
Pull Request — master (#5276)
by Matěj
02:20
created

ssg.oval.append()   A

Complexity

Conditions 3

Size

Total Lines 11
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 8.6667

Importance

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