Passed
Push — master ( e9a66b...7d6652 )
by Jan
02:39 queued 12s
created

ssg.oval.find_testfile()   C

Complexity

Conditions 10

Size

Total Lines 35
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 10.1953

Importance

Changes 0
Metric Value
cc 10
eloc 24
nop 1
dl 0
loc 35
ccs 21
cts 24
cp 0.875
crap 10.1953
rs 5.9999
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like ssg.oval.find_testfile() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 .yaml 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
    oval_tree = ET.fromstring(header + body + footer)
122
123 2
    element_path = "./{%s}def-group/{%s}definition/{%s}metadata/{%s}affected/{%s}platform"
124 2
    element_ns_path = element_path % (ovalns, ovalns, ovalns, ovalns, ovalns)
125 2
    for node in oval_tree.findall(element_ns_path):
126 2
        platforms.append(node.text)
127
128 2
    return platforms
129
130
131 2
def parse_affected(oval_contents):
132
    """
133
    Returns the tuple (start_affected, end_affected, platform_indents) for
134
    the passed oval file contents. start_affected is the line number of
135
    starting tag of the <affected> element, end_affected is the line number of
136
    the closing tag of the </affected> element, and platform_indents is a
137
    string containing the indenting characters before the contents of the
138
    <affected> element.
139
    """
140
141
    start_affected = list(filter(lambda x: "<affected" in oval_contents[x],
142
                                 range(0, len(oval_contents))))
143
    if len(start_affected) != 1:
144
        raise ValueError("OVAL file does not contain a single <affected> "
145
                         "element; counted %d in:\n%s\n\n" %
146
                         (len(start_affected), "\n".join(oval_contents)))
147
148
    start_affected = start_affected[0]
149
150
    end_affected = list(filter(lambda x: "</affected" in oval_contents[x],
151
                               range(0, len(oval_contents))))
152
    if len(end_affected) != 1:
153
        raise ValueError("Malformed OVAL file does not contain a single "
154
                         "closing </affected>; counted %d in:\n%s\n\n" %
155
                         (len(start_affected), "\n".join(oval_contents)))
156
    end_affected = end_affected[0]
157
158
    if start_affected >= end_affected:
159
        raise ValueError("Malformed OVAL file: start affected tag begins "
160
                         "on the same line or after ending affected tag: "
161
                         "start:%d vs end:%d:\n%s\n\n" %
162
                         (start_affected, end_affected, oval_contents))
163
164
    # Validate that start_affected contains only a starting <affected> tag;
165
    # otherwise, using this information to update the <platform> subelements
166
    # would fail.
167
    start_line = oval_contents[start_affected]
168
    start_line = start_line.strip()
169
170
    if not start_line.startswith('<affected'):
171
        raise ValueError("Malformed OVAL file: line with starting affected "
172
                         "tag contains other elements: line:%s\n%s\n\n" %
173
                         (start_line, oval_contents))
174
    if '<' in start_line[1:]:
175
        raise ValueError("Malformed OVAL file: line with starting affected "
176
                         "tag contains other elements: line:%s\n%s\n\n" %
177
                         (start_line, oval_contents))
178
179
    # Validate that end_affected contains only an ending </affected> tag;
180
    # otherwise, using this information to update the <platform> subelements
181
    # would fail.
182
    end_line = oval_contents[end_affected]
183
    end_line = end_line.strip()
184
185
    if not end_line.startswith('</affected>'):
186
        raise ValueError("Malformed OVAL file: line with ending affected "
187
                         "tag contains other elements: line:%s\n%s\n\n" %
188
                         (end_line, oval_contents))
189
    if '<' in end_line[1:]:
190
        raise ValueError("Malformed OVAL file: line with ending affected "
191
                         "tag contains other elements: line:%s\n%s\n\n" %
192
                         (end_line, oval_contents))
193
194
    indents = ""
195
    if start_affected+1 == end_affected:
196
        # Since the affected element is present but empty, the indents should
197
        # be two more spaces than that of the starting <affected> element.
198
        start_index = oval_contents[start_affected].index('<')
199
        indents = oval_contents[start_affected][0:start_index]
200
        indents += "  "
201
    else:
202
        # Otherwise, grab the indents off the next line unmodified, as this is
203
        # likely a platform element tag. We don't validate here that this is
204
        # indeed the case, as other parts of the build infrastructure will
205
        # validate this for us.
206
        start_index = oval_contents[start_affected+1].index('<')
207
        indents = oval_contents[start_affected+1][0:start_index]
208
209
    return start_affected, end_affected, indents
210
211
212 2
def replace_external_vars(tree):
213
    """Replace external_variables with local_variables, so the definition can be
214
       tested independently of an XCCDF file"""
215
216
    # external_variable is a special case: we turn it into a local_variable so
217
    # we can test
218
    for node in tree.findall(".//{%s}external_variable" % ovalns):
219
        print("External_variable with id : " + node.get("id"))
220
        extvar_id = node.get("id")
221
        # for envkey, envval in os.environ.iteritems():
222
        #     print envkey + " = " + envval
223
        # sys.exit()
224
        if extvar_id not in os.environ.keys():
225
            print("External_variable specified, but no value provided via "
226
                  "environment variable", file=sys.stderr)
227
            sys.exit(2)
228
        # replace tag name: external -> local
229
        node.tag = "{%s}local_variable" % ovalns
230
        literal = ET.Element("oval:literal_component")
231
        literal.text = os.environ[extvar_id]
232
        node.append(literal)
233
        # TODO: assignment of external_variable via environment vars, for
234
        # testing
235
    return tree
236
237
238 2
def find_testfile_or_exit(testfile):
239
    """Find OVAL files in CWD or shared/oval and calls sys.exit if the file is not found"""
240
    _testfile = find_testfile(testfile)
241
    if _testfile is None:
242
        print("ERROR: %s does not exist! Please specify a valid OVAL file." % testfile,
243
              file=sys.stderr)
244
        sys.exit(1)
245
    else:
246
        return _testfile
247
248
249 2
def find_testfile(oval_id):
250
    """
251
    Find OVAL file by id in CWD, SHARED_OVAL, or LINUX_OS_GUIDE. Understands rule
252
    directories and defaults to returning shared.xml over {{{ product }}}.xml.
253
254
    Returns path to OVAL file or None if not found.
255
    """
256 2
    if os.path.isfile(os.path.abspath(oval_id)):
257
        return os.path.abspath(oval_id)
258
259 2
    if oval_id.endswith(".xml"):
260 2
        oval_id, _ = os.path.splitext(oval_id)
261 2
        oval_id = os.path.basename(oval_id)
262
263 2
    candidates = [oval_id, "%s.xml" % oval_id]
264
265 2
    found_file = None
266 2
    for path in ['.', SHARED_OVAL, LINUX_OS_GUIDE]:
267 2
        for root, dirs, _ in os.walk(path):
268 2
            dirs.sort()
269 2
            for candidate in candidates:
270 2
                search_file = os.path.join(root, candidate).strip()
271 2
                if os.path.isfile(search_file):
272
                    found_file = search_file
273
                    break
274
275 2
        for rule_dir in find_rule_dirs(path):
276 2
            rule_id = get_rule_dir_id(rule_dir)
277 2
            if rule_id == oval_id:
278 2
                ovals = get_rule_dir_ovals(rule_dir, product="shared")
279 2
                if ovals:
280 2
                    found_file = ovals[0]
281 2
                    break
282
283 2
    return found_file
284
285
286 2
def read_ovaldefgroup_file(testfile, yaml_env):
287
    """Read oval files"""
288
    body = process_file_with_macros(testfile, yaml_env)
289
    return body
290
291
292 2
def get_openscap_supported_oval_version():
293
    try:
294
        from openscap import oscap_get_version
295
        if [int(x) for x in str(oscap_get_version()).split(".")] >= [1, 2, 0]:
296
            return "5.11"
297
    except ImportError:
298
        pass
299
300
    return "5.10"
301
302
303 2
def parse_options():
304
    usage = "usage: %(prog)s [options] definition_file.xml"
305
    parser = argparse.ArgumentParser(usage=usage)
306
    # only some options are on by default
307
308
    oscap_oval_version = get_openscap_supported_oval_version()
309
310
    parser.add_argument("--oval_version",
311
                        default=oscap_oval_version,
312
                        dest="oval_version", action="store",
313
                        help="OVAL version to use. Example: 5.11, 5.10, ... "
314
                             "If not supplied the highest version supported by "
315
                             "openscap will be used: %s" % (oscap_oval_version))
316
    parser.add_argument("-q", "--quiet", "--silent", default=False,
317
                        action="store_true", dest="silent_mode",
318
                        help="Don't show any output when testing OVAL files")
319
    parser.add_argument("xmlfile", metavar="XMLFILE", help="OVAL XML file")
320
    args = parser.parse_args()
321
322
    return args
323
324
325 2
def main():
326
    global definitions
327
    global tests
328
    global objects
329
    global states
330
    global variables
331
    global silent_mode
332
333
    args = parse_options()
334
    silent_mode = args.silent_mode
335
    oval_version = args.oval_version
336
337
    testfile = args.xmlfile
338
    header = oval_generated_header("testoval.py", oval_version, "0.0.1")
339
    testfile = find_testfile_or_exit(testfile)
340
    yaml_env = dict()
341
    body = read_ovaldefgroup_file(testfile, yaml_env)
342
343
    defname = _add_elements(body, header, yaml_env)
344
    if defname is None:
345
        print("Error while evaluating oval: defname not set; missing "
346
              "definitions section?")
347
        sys.exit(1)
348
349
    ovaltree = ET.fromstring(header + footer)
350
351
    # append each major element type, if it has subelements
352
    for element in [definitions, tests, objects, states, variables]:
353
        if list(element):
354
            ovaltree.append(element)
355
356
    # re-map all the element ids from meaningful names to meaningless
357
    # numbers
358
    testtranslator = IDTranslator("scap-security-guide.testing")
359
    ovaltree = testtranslator.translate(ovaltree)
360
    (ovalfile, fname) = tempfile.mkstemp(prefix=defname, suffix=".xml")
361
    os.write(ovalfile, ET.tostring(ovaltree))
362
    os.close(ovalfile)
363
364
    cmd = ['oscap', 'oval', 'eval', '--results', fname + '-results', fname]
365
    if not silent_mode:
366
        print("Evaluating with OVAL tempfile: " + fname)
367
        print("OVAL Schema Version: %s" % oval_version)
368
        print("Writing results to: " + fname + "-results")
369
        print("Running command: %s\n" % " ".join(cmd))
370
371
    oscap_child = subprocess.Popen(cmd, stdout=subprocess.PIPE)
372
    cmd_out = oscap_child.communicate()[0]
373
374
    if isinstance(cmd_out, bytes):
375
        cmd_out = cmd_out.decode('utf-8')
376
377
    if not silent_mode:
378
        print(cmd_out, file=sys.stderr)
379
380
    if oscap_child.returncode != 0:
381
        if not silent_mode:
382
            print("Error launching 'oscap' command: return code %d" % oscap_child.returncode)
383
        sys.exit(2)
384
385
    if 'false' in cmd_out or 'error' in cmd_out:
386
        # at least one from the evaluated OVAL definitions evaluated to
387
        # 'false' result, exit with '1' to indicate OVAL scan FAIL result
388
        sys.exit(1)
389
390
    # perhaps delete tempfile?
391
    definitions = ET.Element("oval:definitions")
392
    tests = ET.Element("oval:tests")
393
    objects = ET.Element("oval:objects")
394
    states = ET.Element("oval:states")
395
    variables = ET.Element("oval:variables")
396
397
    # 'false' keyword wasn't found in oscap's command output
398
    # exit with '0' to indicate OVAL scan TRUE result
399
    sys.exit(0)
400