|
1
|
|
|
import sys |
|
2
|
|
|
import os |
|
3
|
|
|
import re |
|
4
|
|
|
import argparse |
|
5
|
|
|
import tempfile |
|
6
|
|
|
import subprocess |
|
7
|
|
|
|
|
8
|
|
|
from ConfigParser import SafeConfigParser |
|
9
|
|
|
|
|
10
|
|
|
from ssg._constants import oval_footer as footer |
|
11
|
|
|
from ssg._constants import oval_namespace as ovalns |
|
12
|
|
|
from ssg._constants import timestamp |
|
13
|
|
|
from ssg._xml import ElementTree as ET |
|
14
|
|
|
from ssg._xml import oval_generated_header |
|
15
|
|
|
|
|
16
|
|
|
|
|
17
|
|
|
SHARED_OVAL = re.sub('shared.*', 'shared', __file__) + '/checks/oval/' |
|
18
|
|
|
|
|
19
|
|
|
|
|
20
|
|
|
try: |
|
21
|
|
|
from openscap import oscap_get_version |
|
22
|
|
|
if oscap_get_version() < 1.2: |
|
23
|
|
|
oval_version = "5.10" |
|
24
|
|
|
else: |
|
25
|
|
|
oval_version = "5.11" |
|
26
|
|
|
except ImportError: |
|
27
|
|
|
oval_version = "5.10" |
|
28
|
|
|
|
|
29
|
|
|
# globals, to make recursion easier in case we encounter extend_definition |
|
30
|
|
|
definitions = ET.Element("definitions") |
|
31
|
|
|
tests = ET.Element("tests") |
|
32
|
|
|
objects = ET.Element("objects") |
|
33
|
|
|
states = ET.Element("states") |
|
34
|
|
|
variables = ET.Element("variables") |
|
35
|
|
|
|
|
36
|
|
|
|
|
37
|
|
|
# append new child ONLY if it's not a duplicate |
|
38
|
|
|
def append(element, newchild): |
|
39
|
|
|
newid = newchild.get("id") |
|
40
|
|
|
existing = element.find(".//*[@id='" + newid + "']") |
|
41
|
|
|
if existing is not None: |
|
42
|
|
|
if not silent_mode: |
|
|
|
|
|
|
43
|
|
|
sys.stderr.write("Notification: this ID is used more than once " + |
|
44
|
|
|
"and should represent equivalent elements: " + |
|
45
|
|
|
newid + "\n") |
|
46
|
|
|
else: |
|
47
|
|
|
element.append(newchild) |
|
48
|
|
|
|
|
49
|
|
|
|
|
50
|
|
|
def add_oval_elements(body, header): |
|
51
|
|
|
"""Add oval elements to the global Elements defined above""" |
|
52
|
|
|
|
|
53
|
|
|
tree = ET.fromstring(header + body + footer) |
|
54
|
|
|
tree = replace_external_vars(tree) |
|
55
|
|
|
# parse new file(string) as an etree, so we can arrange elements |
|
56
|
|
|
# appropriately |
|
57
|
|
|
for childnode in tree.findall("./{%s}def-group/*" % ovalns): |
|
58
|
|
|
# print "childnode.tag is " + childnode.tag |
|
59
|
|
|
if childnode.tag is ET.Comment: |
|
60
|
|
|
continue |
|
61
|
|
|
if childnode.tag == ("{%s}definition" % ovalns): |
|
62
|
|
|
append(definitions, childnode) |
|
63
|
|
|
defname = childnode.get("id") |
|
64
|
|
|
# extend_definition is a special case: must include a whole other |
|
65
|
|
|
# definition |
|
66
|
|
|
for defchild in childnode.findall(".//{%s}extend_definition" |
|
67
|
|
|
% ovalns): |
|
68
|
|
|
defid = defchild.get("definition_ref") |
|
69
|
|
|
extend_ref = find_testfile(defid+".xml") |
|
70
|
|
|
includedbody = read_ovaldefgroup_file(extend_ref) |
|
71
|
|
|
# recursively add the elements in the other file |
|
72
|
|
|
add_oval_elements(includedbody, header) |
|
73
|
|
|
if childnode.tag.endswith("_test"): |
|
74
|
|
|
append(tests, childnode) |
|
75
|
|
|
if childnode.tag.endswith("_object"): |
|
76
|
|
|
append(objects, childnode) |
|
77
|
|
|
if childnode.tag.endswith("_state"): |
|
78
|
|
|
append(states, childnode) |
|
79
|
|
|
if childnode.tag.endswith("_variable"): |
|
80
|
|
|
append(variables, childnode) |
|
81
|
|
|
return defname |
|
|
|
|
|
|
82
|
|
|
|
|
83
|
|
|
|
|
84
|
|
|
def replace_external_vars(tree): |
|
85
|
|
|
"""Replace external_variables with local_variables, so the definition can be |
|
86
|
|
|
tested independently of an XCCDF file""" |
|
87
|
|
|
|
|
88
|
|
|
# external_variable is a special case: we turn it into a local_variable so |
|
89
|
|
|
# we can test |
|
90
|
|
|
for node in tree.findall(".//{%s}external_variable" % ovalns): |
|
91
|
|
|
print("External_variable with id : " + node.get("id")) |
|
92
|
|
|
extvar_id = node.get("id") |
|
93
|
|
|
# for envkey, envval in os.environ.iteritems(): |
|
94
|
|
|
# print envkey + " = " + envval |
|
95
|
|
|
# sys.exit() |
|
96
|
|
|
if extvar_id not in os.environ.keys(): |
|
97
|
|
|
print("External_variable specified, but no value provided via " |
|
98
|
|
|
"environment variable") |
|
99
|
|
|
sys.exit(2) |
|
100
|
|
|
# replace tag name: external -> local |
|
101
|
|
|
node.tag = "{%s}local_variable" % ovalns |
|
102
|
|
|
literal = ET.Element("literal_component") |
|
103
|
|
|
literal.text = os.environ[extvar_id] |
|
104
|
|
|
node.append(literal) |
|
105
|
|
|
# TODO: assignment of external_variable via environment vars, for |
|
106
|
|
|
# testing |
|
107
|
|
|
return tree |
|
108
|
|
|
|
|
109
|
|
|
|
|
110
|
|
|
def find_testfile(testfile): |
|
111
|
|
|
"""Find OVAL files in CWD or shared/oval""" |
|
112
|
|
|
for path in ['.', SHARED_OVAL]: |
|
113
|
|
|
for root, folder, files in os.walk(path): |
|
114
|
|
|
searchfile = root + '/' + testfile |
|
115
|
|
|
if not os.path.isfile(searchfile): |
|
116
|
|
|
searchfile = "" |
|
117
|
|
|
else: |
|
118
|
|
|
testfile = searchfile.strip() |
|
119
|
|
|
# Most likely found file, exit this loop |
|
120
|
|
|
break |
|
121
|
|
|
|
|
122
|
|
|
if not os.path.isfile(testfile): |
|
123
|
|
|
print("ERROR: %s does not exist! Please specify a valid OVAL file." |
|
124
|
|
|
% testfile) |
|
125
|
|
|
sys.exit(1) |
|
126
|
|
|
|
|
127
|
|
|
return testfile |
|
128
|
|
|
|
|
129
|
|
|
|
|
130
|
|
|
def read_ovaldefgroup_file(testfile): |
|
131
|
|
|
"""Read oval files""" |
|
132
|
|
|
with open(testfile, 'r') as test_file: |
|
133
|
|
|
body = test_file.read() |
|
134
|
|
|
return body |
|
135
|
|
|
|
|
136
|
|
|
|
|
137
|
|
|
def parse_options(): |
|
138
|
|
|
usage = "usage: %(prog)s [options] definition_file.xml" |
|
139
|
|
|
parser = argparse.ArgumentParser(usage=usage, version="%(prog)s ") |
|
140
|
|
|
# only some options are on by default |
|
141
|
|
|
|
|
142
|
|
|
parser.add_argument("--oval_version", default=oval_version, |
|
143
|
|
|
dest="oval_version", action="store", |
|
144
|
|
|
help="OVAL version to use. Example: 5.11, 5.10, ... \ |
|
145
|
|
|
[Default: %(default)s]") |
|
146
|
|
|
parser.add_argument("-q", "--quiet", "--silent", default=False, |
|
147
|
|
|
action="store_true", dest="silent_mode", |
|
148
|
|
|
help="Don't show any output when testing OVAL files") |
|
149
|
|
|
parser.add_argument("xmlfile", metavar="XMLFILE", help="OVAL XML file") |
|
150
|
|
|
args = parser.parse_args() |
|
151
|
|
|
|
|
152
|
|
|
return args |
|
153
|
|
|
|
|
154
|
|
|
|
|
155
|
|
|
def main(): |
|
156
|
|
|
global definitions |
|
157
|
|
|
global tests |
|
158
|
|
|
global objects |
|
159
|
|
|
global states |
|
160
|
|
|
global variables |
|
161
|
|
|
global silent_mode |
|
162
|
|
|
|
|
163
|
|
|
args = parse_options() |
|
164
|
|
|
silent_mode = args.silent_mode |
|
165
|
|
|
oval_version = args.oval_version |
|
166
|
|
|
|
|
167
|
|
|
testfile = args.xmlfile |
|
168
|
|
|
header = oval_generated_header("testoval.py", oval_version, "0.0.1") |
|
169
|
|
|
testfile = find_testfile(testfile) |
|
170
|
|
|
body = read_ovaldefgroup_file(testfile) |
|
171
|
|
|
defname = add_oval_elements(body, header) |
|
172
|
|
|
ovaltree = ET.fromstring(header + footer) |
|
173
|
|
|
|
|
174
|
|
|
# append each major element type, if it has subelements |
|
175
|
|
|
for element in [definitions, tests, objects, states, variables]: |
|
176
|
|
|
if element.getchildren(): |
|
177
|
|
|
ovaltree.append(element) |
|
178
|
|
|
# re-map all the element ids from meaningful names to meaningless |
|
179
|
|
|
# numbers |
|
180
|
|
|
testtranslator = idtranslate.IDTranslator("scap-security-guide.testing") |
|
|
|
|
|
|
181
|
|
|
ovaltree = testtranslator.translate(ovaltree) |
|
182
|
|
|
(ovalfile, fname) = tempfile.mkstemp(prefix=defname, suffix=".xml") |
|
183
|
|
|
os.write(ovalfile, ET.tostring(ovaltree)) |
|
184
|
|
|
os.close(ovalfile) |
|
185
|
|
|
if not silent_mode: |
|
186
|
|
|
print("Evaluating with OVAL tempfile: " + fname) |
|
187
|
|
|
print("OVAL Schema Version: %s" % oval_version) |
|
188
|
|
|
print("Writing results to: " + fname + "-results") |
|
189
|
|
|
cmd = "oscap oval eval --results " + fname + "-results " + fname |
|
190
|
|
|
oscap_child = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) |
|
191
|
|
|
cmd_out = oscap_child.communicate()[0] |
|
192
|
|
|
if not silent_mode: |
|
193
|
|
|
print cmd_out |
|
|
|
|
|
|
194
|
|
|
if oscap_child.returncode != 0: |
|
195
|
|
|
if not silent_mode: |
|
196
|
|
|
print("Error launching 'oscap' command: \n\t" + cmd) |
|
197
|
|
|
sys.exit(2) |
|
198
|
|
|
if 'false' in cmd_out: |
|
199
|
|
|
# at least one from the evaluated OVAL definitions evaluated to |
|
200
|
|
|
# 'false' result, exit with '1' to indicate OVAL scan FAIL result |
|
201
|
|
|
sys.exit(1) |
|
202
|
|
|
# perhaps delete tempfile? |
|
203
|
|
|
definitions = ET.Element("definitions") |
|
204
|
|
|
tests = ET.Element("tests") |
|
205
|
|
|
objects = ET.Element("objects") |
|
206
|
|
|
states = ET.Element("states") |
|
207
|
|
|
variables = ET.Element("variables") |
|
208
|
|
|
|
|
209
|
|
|
# 'false' keyword wasn't found in oscap's command output |
|
210
|
|
|
# exit with '0' to indicate OVAL scan TRUE result |
|
211
|
|
|
sys.exit(0) |
|
212
|
|
|
|