1
|
|
|
#!/usr/bin/python3 |
2
|
|
|
|
3
|
|
|
from __future__ import print_function |
4
|
|
|
|
5
|
|
|
import fnmatch |
6
|
|
|
import sys |
7
|
|
|
import os |
8
|
|
|
import ssg |
9
|
|
|
import argparse |
10
|
|
|
|
11
|
|
|
import ssg.build_cpe |
12
|
|
|
import ssg.id_translate |
13
|
|
|
import ssg.products |
14
|
|
|
import ssg.xml |
15
|
|
|
import ssg.yaml |
16
|
|
|
from ssg.constants import XCCDF12_NS |
17
|
|
|
|
18
|
|
|
# This script requires two arguments: an OVAL file and a CPE dictionary file. |
19
|
|
|
# It is designed to extract any inventory definitions and the tests, states, |
20
|
|
|
# objects and variables it references and then write them into a standalone |
21
|
|
|
# OVAL CPE file, along with a synchronized CPE dictionary file. |
22
|
|
|
|
23
|
|
|
oval_ns = "http://oval.mitre.org/XMLSchema/oval-definitions-5" |
24
|
|
|
cpe_ns = "http://cpe.mitre.org/dictionary/2.0" |
25
|
|
|
|
26
|
|
|
|
27
|
|
|
def parse_args(): |
28
|
|
|
p = argparse.ArgumentParser(description="This script takes as input an " |
29
|
|
|
"OVAL file and a CPE dictionary file and extracts any inventory " |
30
|
|
|
"definitions and the tests, states objects and variables it " |
31
|
|
|
"references, and then write them into a standalone OVAL CPE file, " |
32
|
|
|
"along with a synchronized CPE dictionary file.") |
33
|
|
|
p.add_argument( |
34
|
|
|
"--product-yaml", |
35
|
|
|
help="YAML file with information about the product we are building. " |
36
|
|
|
"e.g.: ~/scap-security-guide/rhel7/product.yml " |
37
|
|
|
"needed for autodetection of profile root" |
38
|
|
|
) |
39
|
|
|
p.add_argument("idname", help="Identifier prefix") |
40
|
|
|
p.add_argument("cpeoutdir", help="Artifact output directory") |
41
|
|
|
p.add_argument("shorthandfile", help="shorthand xml to generate " |
42
|
|
|
"the CPE dictionary from") |
43
|
|
|
p.add_argument("ovalfile", help="OVAL file to process") |
44
|
|
|
p.add_argument("--cpe-items-dir", help="the directory where compiled CPE items are stored") |
45
|
|
|
|
46
|
|
|
return p.parse_args() |
47
|
|
|
|
48
|
|
|
|
49
|
|
|
def main(): |
50
|
|
|
args = parse_args() |
51
|
|
|
|
52
|
|
|
# parse oval file |
53
|
|
|
ovaltree = ssg.xml.parse_file(args.ovalfile) |
54
|
|
|
|
55
|
|
|
# extract inventory definitions |
56
|
|
|
# making (dubious) assumption that all inventory defs are CPE |
57
|
|
|
defs = ovaltree.find("./{%s}definitions" % oval_ns) |
58
|
|
|
inventory_defs = [] |
59
|
|
|
for el in defs.findall(".//{%s}definition" % oval_ns): |
60
|
|
|
if el.get("class") != "inventory": |
61
|
|
|
continue |
62
|
|
|
inventory_defs.append(el) |
63
|
|
|
|
64
|
|
|
# Keep the list of 'id' attributes from untranslated inventory def elements |
65
|
|
|
inventory_defs_id_attrs = [] |
66
|
|
|
|
67
|
|
|
defs.clear() |
68
|
|
|
[defs.append(inventory_def) for inventory_def in inventory_defs] |
69
|
|
|
# Fill in that list |
70
|
|
|
inventory_defs_id_attrs = \ |
71
|
|
|
[inventory_def.get("id") for inventory_def in inventory_defs] |
72
|
|
|
|
73
|
|
|
tests = ovaltree.find("./{%s}tests" % oval_ns) |
74
|
|
|
cpe_tests = ssg.build_cpe.extract_referred_nodes(defs, tests, "test_ref") |
75
|
|
|
tests.clear() |
76
|
|
|
[tests.append(cpe_test) for cpe_test in cpe_tests] |
77
|
|
|
|
78
|
|
|
states = ovaltree.find("./{%s}states" % oval_ns) |
79
|
|
|
cpe_states = ssg.build_cpe.extract_referred_nodes(tests, states, "state_ref") |
80
|
|
|
states.clear() |
81
|
|
|
[states.append(cpe_state) for cpe_state in cpe_states] |
82
|
|
|
|
83
|
|
|
objects = ovaltree.find("./{%s}objects" % oval_ns) |
84
|
|
|
cpe_objects = ssg.build_cpe.extract_referred_nodes(tests, objects, "object_ref") |
85
|
|
|
env_objects = ssg.build_cpe.extract_referred_nodes(objects, objects, "id") |
86
|
|
|
objects.clear() |
87
|
|
|
[objects.append(cpe_object) for cpe_object in cpe_objects] |
88
|
|
|
|
89
|
|
|
variables = ovaltree.find("./{%s}variables" % oval_ns) |
90
|
|
|
if variables is not None: |
91
|
|
|
ref_var_elems = set(ssg.build_cpe.extract_referred_nodes(objects, variables, "var_ref")) |
92
|
|
|
ref_var_elems.update(ssg.build_cpe.extract_referred_nodes(states, variables, "var_ref")) |
93
|
|
|
|
94
|
|
|
if ref_var_elems: |
95
|
|
|
variables.clear() |
96
|
|
|
has_external_vars = False |
97
|
|
|
for var in ref_var_elems: |
98
|
|
|
if (var.tag == "{%s}external_variable" % oval_ns): |
99
|
|
|
error_msg = "Error: External variable (%s) is referenced in a" |
100
|
|
|
"CPE OVAL check\n" % var.get('id') |
101
|
|
|
sys.stderr.write(error_msg) |
102
|
|
|
has_external_vars = True |
103
|
|
|
|
104
|
|
|
variables.append(var) |
105
|
|
|
|
106
|
|
|
# Make sure to inlude objects referenced by a local variable |
107
|
|
|
# But env_obj will return no objects if the local variable doesn't |
108
|
|
|
# reference any object via object_ref |
109
|
|
|
env_obj = ssg.build_cpe.extract_env_obj(env_objects, var) |
110
|
|
|
if env_obj: |
111
|
|
|
objects.append(env_obj) |
112
|
|
|
|
113
|
|
|
if has_external_vars: |
114
|
|
|
error_msg = "Error: External variables cannot be used by CPE OVAL checks.\n" |
115
|
|
|
sys.stderr.write(error_msg) |
116
|
|
|
sys.exit(1) |
117
|
|
|
else: |
118
|
|
|
ovaltree.remove(variables) |
119
|
|
|
|
120
|
|
|
# turn IDs into meaningless numbers |
121
|
|
|
translator = ssg.id_translate.IDTranslator(args.idname) |
122
|
|
|
ovaltree = translator.translate(ovaltree) |
123
|
|
|
|
124
|
|
|
product_yaml = ssg.products.load_product_yaml(args.product_yaml) |
125
|
|
|
product = product_yaml["product"] |
126
|
|
|
newovalfile = args.idname + "-" + product + "-" + os.path.basename(args.ovalfile) |
127
|
|
|
newovalfile = newovalfile.replace("cpe-oval-unlinked", "cpe-oval") |
128
|
|
|
ssg.xml.ElementTree.ElementTree(ovaltree).write(args.cpeoutdir + "/" + newovalfile) |
129
|
|
|
|
130
|
|
|
# Lets scrape the shorthand for the list of platforms referenced |
131
|
|
|
benchmark_cpe_names = set() |
132
|
|
|
shorthandtree = ssg.xml.parse_file(args.shorthandfile) |
133
|
|
|
for platform in shorthandtree.findall(".//{%s}platform" % XCCDF12_NS): |
134
|
|
|
cpe_name = platform.get("idref") |
135
|
|
|
# skip CPE AL platforms (they are handled later) |
136
|
|
|
# this is temporary solution until we get rid of old type of platforms in the benchmark |
137
|
|
|
if cpe_name.startswith("#"): |
138
|
|
|
continue |
139
|
|
|
benchmark_cpe_names.add(cpe_name) |
140
|
|
|
# add CPE names used by factref elements in CPEAL platforms |
141
|
|
|
for factref in shorthandtree.findall( |
142
|
|
|
".//ns1:fact-ref", {"ns1":ssg.constants.PREFIX_TO_NS["cpe-lang"]}): |
143
|
|
|
cpe_factref_name = factref.get("name") |
144
|
|
|
benchmark_cpe_names.add(cpe_factref_name) |
145
|
|
|
|
146
|
|
|
product_cpes = ssg.build_cpe.ProductCPEs() |
147
|
|
|
product_cpes.load_cpes_from_directory_tree(args.cpe_items_dir, product_yaml) |
148
|
|
|
cpe_list = ssg.build_cpe.CPEList() |
149
|
|
|
for cpe_name in benchmark_cpe_names: |
150
|
|
|
cpe_list.add(product_cpes.get_cpe(cpe_name)) |
151
|
|
|
|
152
|
|
|
cpedict_filename = "ssg-" + product + "-cpe-dictionary.xml" |
153
|
|
|
cpedict_path = os.path.join(args.cpeoutdir, cpedict_filename) |
154
|
|
|
cpe_list.to_file(cpedict_path, newovalfile) |
155
|
|
|
|
156
|
|
|
# replace and sync IDs, href filenames in input cpe dictionary file |
157
|
|
|
cpedicttree = ssg.xml.parse_file(cpedict_path) |
158
|
|
|
for check in cpedicttree.findall(".//{%s}check" % cpe_ns): |
159
|
|
|
checkhref = check.get("href") |
160
|
|
|
# If CPE OVAL references another OVAL file |
161
|
|
|
if checkhref == 'filename': |
162
|
|
|
# Sanity check -- Verify the referenced OVAL is truly defined |
163
|
|
|
# somewhere in the (sub)directory tree below CWD. In correct |
164
|
|
|
# scenario is should be located: |
165
|
|
|
# * either in input/oval/*.xml |
166
|
|
|
# * or copied by former run of "combine_ovals.py" script from |
167
|
|
|
# shared/ directory into build/ subdirectory |
168
|
|
|
refovalfilename = check.text |
169
|
|
|
refovalfilefound = False |
170
|
|
|
for dirpath, dirnames, filenames in os.walk(os.curdir, topdown=True): |
171
|
|
|
dirnames.sort() |
172
|
|
|
filenames.sort() |
173
|
|
|
# Case when referenced OVAL file exists |
174
|
|
|
for location in fnmatch.filter(filenames, refovalfilename + '.xml'): |
175
|
|
|
refovalfilefound = True |
176
|
|
|
break # break from the inner for loop |
177
|
|
|
|
178
|
|
|
if refovalfilefound: |
179
|
|
|
break # break from the outer for loop |
180
|
|
|
|
181
|
|
|
shared_dir = \ |
182
|
|
|
os.path.dirname(os.path.dirname(os.path.realpath(__file__))) |
183
|
|
|
if shared_dir is not None: |
184
|
|
|
for dirpath, dirnames, filenames in os.walk(shared_dir, topdown=True): |
185
|
|
|
dirnames.sort() |
186
|
|
|
filenames.sort() |
187
|
|
|
# Case when referenced OVAL file exists |
188
|
|
|
for location in fnmatch.filter(filenames, refovalfilename + '.xml'): |
189
|
|
|
refovalfilefound = True |
190
|
|
|
break # break from the inner for loop |
191
|
|
|
|
192
|
|
|
if refovalfilefound: |
193
|
|
|
break # break from the outer for loop |
194
|
|
|
|
195
|
|
|
# Referenced OVAL doesn't exist in the subdirtree below CWD: |
196
|
|
|
# * there's either typo in the refenced OVAL filename, or |
197
|
|
|
# * is has been forgotten to be placed into input/oval, or |
198
|
|
|
# * the <platform> tag of particular shared/ OVAL wasn't modified |
199
|
|
|
# to include the necessary referenced file. |
200
|
|
|
# Therefore display an error and exit with failure in such cases |
201
|
|
|
if not refovalfilefound: |
202
|
|
|
error_msg = "\n\tError: Can't locate \"%s\" OVAL file in the \ |
203
|
|
|
\n\tlist of OVAL checks for this product! Exiting..\n" % refovalfilename |
204
|
|
|
sys.stderr.write(error_msg) |
205
|
|
|
# sys.exit(1) |
206
|
|
|
check.set("href", os.path.basename(newovalfile)) |
207
|
|
|
|
208
|
|
|
# Sanity check to verify if inventory check OVAL id is present in the |
209
|
|
|
# list of known "id" attributes of inventory definitions. If not it |
210
|
|
|
# means provided ovalfile (sys.argv[1]) doesn't contain this OVAL |
211
|
|
|
# definition (it wasn't included due to <platform> tag restrictions) |
212
|
|
|
# Therefore display an error and exit with failure, since otherwise |
213
|
|
|
# we might end up creating invalid $(ID)-$(PROD)-cpe-oval.xml file |
214
|
|
|
if check.text not in inventory_defs_id_attrs: |
215
|
|
|
error_msg = "\n\tError: Can't locate \"%s\" definition in \"%s\". \ |
216
|
|
|
\n\tEnsure <platform> element is configured properly for \"%s\". \ |
217
|
|
|
\n\tExiting..\n" % (check.text, args.ovalfile, check.text) |
218
|
|
|
sys.stderr.write(error_msg) |
219
|
|
|
sys.exit(1) |
220
|
|
|
|
221
|
|
|
# Referenced OVAL checks passed both of the above sanity tests |
222
|
|
|
check.text = translator.generate_id("{" + oval_ns + "}definition", check.text) |
223
|
|
|
|
224
|
|
|
ssg.xml.ElementTree.ElementTree(cpedicttree).write(cpedict_path) |
225
|
|
|
|
226
|
|
|
sys.exit(0) |
227
|
|
|
|
228
|
|
|
|
229
|
|
|
if __name__ == "__main__": |
230
|
|
|
main() |
231
|
|
|
|