|
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.