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