|
1
|
|
|
#!/usr/bin/python3 |
|
2
|
|
|
|
|
3
|
|
|
from __future__ import print_function |
|
4
|
|
|
|
|
5
|
|
|
import sys |
|
6
|
|
|
import optparse |
|
7
|
|
|
import os.path |
|
8
|
|
|
|
|
9
|
|
|
""" |
|
10
|
|
|
This script can verify consistency of references (linkage) between XCCDF and |
|
11
|
|
|
OVAL, and also search based on other criteria such as existence of policy |
|
12
|
|
|
references in XCCDF. |
|
13
|
|
|
|
|
14
|
|
|
Purpose: |
|
15
|
|
|
This script can be used to perform various checks on the XCCDF |
|
16
|
|
|
and OVAL that is generated by the Makefile. This script limits its focus to |
|
17
|
|
|
the files in the src/output directory. This script is to be |
|
18
|
|
|
used as a development tool to aid in the creation of concise |
|
19
|
|
|
and structurally correct XCCDF and OVAL. |
|
20
|
|
|
|
|
21
|
|
|
Intent: |
|
22
|
|
|
Help XCCDF and OVAL developers spot potential mistakes in the |
|
23
|
|
|
XCCDF and OVAL content that is generated by the Makefile. |
|
24
|
|
|
|
|
25
|
|
|
Usage: |
|
26
|
|
|
./verify_references.py --all-checks ssg-rhel9-ds.xml |
|
27
|
|
|
|
|
28
|
|
|
You may find this informative as well: |
|
29
|
|
|
|
|
30
|
|
|
./verify_references.py -h |
|
31
|
|
|
""" |
|
32
|
|
|
|
|
33
|
|
|
import ssg.constants |
|
34
|
|
|
import ssg.xml |
|
35
|
|
|
|
|
36
|
|
|
xccdf_ns = ssg.constants.XCCDF12_NS |
|
37
|
|
|
oval_ns = ssg.constants.oval_namespace |
|
38
|
|
|
ocil_cs = ssg.constants.ocil_cs |
|
39
|
|
|
sce_cs = ssg.constants.SCE_SYSTEM |
|
40
|
|
|
|
|
41
|
|
|
# we use these strings to look for references within the XCCDF rules |
|
42
|
|
|
nist_ref_href = "http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-53r4.pdf" |
|
43
|
|
|
disa_ref_href = "https://public.cyber.mil/stigs/cci/" |
|
44
|
|
|
|
|
45
|
|
|
# default exit value - success |
|
46
|
|
|
exit_value = 0 |
|
47
|
|
|
|
|
48
|
|
|
|
|
49
|
|
|
def parse_options(): |
|
50
|
|
|
usage = "usage: %prog [options] xccdf_file" |
|
51
|
|
|
parser = optparse.OptionParser(usage=usage, version="%prog ") |
|
52
|
|
|
# only some options are on by default |
|
53
|
|
|
parser.add_option("-p", "--profile", default=False, |
|
54
|
|
|
action="store", dest="profile_name", |
|
55
|
|
|
help="act on Rules from this XCCDF Profile only") |
|
56
|
|
|
parser.add_option("--rules-with-invalid-checks", default=False, |
|
57
|
|
|
action="store_true", dest="rules_with_invalid_checks", |
|
58
|
|
|
help="print XCCDF Rules that reference an invalid/nonexistent check") |
|
59
|
|
|
parser.add_option("--rules-without-checks", default=False, |
|
60
|
|
|
action="store_true", dest="rules_without_checks", |
|
61
|
|
|
help="print XCCDF Rules that do not include a check") |
|
62
|
|
|
parser.add_option("--rules-without-severity", default=False, |
|
63
|
|
|
action="store_true", dest="rules_without_severity", |
|
64
|
|
|
help="print XCCDF Rules that do not include a severity") |
|
65
|
|
|
parser.add_option("--rules-without-nistrefs", default=False, |
|
66
|
|
|
action="store_true", dest="rules_without_nistrefs", |
|
67
|
|
|
help="print XCCDF Rules which do not include any NIST 800-53 references") |
|
68
|
|
|
parser.add_option("--rules-without-disarefs", default=False, |
|
69
|
|
|
action="store_true", dest="rules_without_disarefs", |
|
70
|
|
|
help="print XCCDF Rules which do not include any DISA CCI references") |
|
71
|
|
|
parser.add_option("--rules-with-nistrefs-outside-profile", default=False, |
|
72
|
|
|
action="store_true", dest="nistrefs_not_in_profile", |
|
73
|
|
|
help="print XCCDF Rules which have a NIST reference, but are not part of the Profile specified") |
|
74
|
|
|
parser.add_option("--rules-with-disarefs-outside-profile", default=False, |
|
75
|
|
|
action="store_true", dest="disarefs_not_in_profile", |
|
76
|
|
|
help="print XCCDF Rules which have a DISA CCI reference, but are not part of the Profile specified") |
|
77
|
|
|
parser.add_option("--ovaldefs-unused", default=False, |
|
78
|
|
|
action="store_true", dest="ovaldefs_unused", |
|
79
|
|
|
help="print OVAL definitions which are not used by any XCCDF Rule") |
|
80
|
|
|
parser.add_option("--base-dir", default=False, action="store", dest="base_dir", |
|
81
|
|
|
help="path to the build directory") |
|
82
|
|
|
parser.add_option("--all-checks", default=False, action="store_true", |
|
83
|
|
|
dest="all_checks", |
|
84
|
|
|
help="perform all checks on the given XCCDF file") |
|
85
|
|
|
(options, args) = parser.parse_args() |
|
86
|
|
|
if len(args) < 1: |
|
87
|
|
|
parser.print_help() |
|
88
|
|
|
sys.exit(1) |
|
89
|
|
|
return (options, args) |
|
90
|
|
|
|
|
91
|
|
|
|
|
92
|
|
|
def get_ovalfiles(checks): |
|
93
|
|
|
global exit_value |
|
94
|
|
|
# Iterate over all checks, grab the OVAL files referenced within |
|
95
|
|
|
ovalfiles = set() |
|
96
|
|
|
for check in checks: |
|
97
|
|
|
if check.get("system") == oval_ns: |
|
98
|
|
|
checkcontentref = check.find("./{%s}check-content-ref" % xccdf_ns) |
|
99
|
|
|
href = checkcontentref.get("href") |
|
100
|
|
|
# Include the file in the particular check system only if it's NOT |
|
101
|
|
|
# a remotely located file (to allow OVAL checks to reference http:// |
|
102
|
|
|
# and https:// formatted URLs) or a file known as mapped to remote file |
|
103
|
|
|
if not is_remote_feed(href): |
|
104
|
|
|
ovalfiles.add(href) |
|
105
|
|
|
elif check.get("system") != ocil_cs and check.get("system") != sce_cs: |
|
106
|
|
|
print("ERROR: Non-OVAL checking system found: %s" |
|
107
|
|
|
% (check.get("system"))) |
|
108
|
|
|
exit_value = 1 |
|
109
|
|
|
return ovalfiles |
|
110
|
|
|
|
|
111
|
|
|
|
|
112
|
|
|
def get_profileruleids(xccdftree, profile_name): |
|
113
|
|
|
ruleids = [] |
|
114
|
|
|
|
|
115
|
|
|
while profile_name: |
|
116
|
|
|
profile = None |
|
117
|
|
|
for el in xccdftree.findall(".//{%s}Profile" % xccdf_ns): |
|
118
|
|
|
if el.get("id") != profile_name: |
|
119
|
|
|
continue |
|
120
|
|
|
profile = el |
|
121
|
|
|
break |
|
122
|
|
|
|
|
123
|
|
|
if profile is None: |
|
124
|
|
|
sys.exit("Specified XCCDF Profile %s was not found.") |
|
125
|
|
|
for select in profile.findall(".//{%s}select" % xccdf_ns): |
|
126
|
|
|
ruleids.append(select.get("idref")) |
|
127
|
|
|
profile_name = profile.get("extends") |
|
128
|
|
|
|
|
129
|
|
|
return ruleids |
|
130
|
|
|
|
|
131
|
|
|
|
|
132
|
|
|
def is_remote_feed(href): |
|
133
|
|
|
return href.startswith("http://") or \ |
|
134
|
|
|
href.startswith("https://") or \ |
|
135
|
|
|
href.startswith("security-data-oval-v2-") or \ |
|
136
|
|
|
href.startswith("security-data-oval-com.redhat.rhsa-") or \ |
|
137
|
|
|
href.startswith("security-oval-com.oracle") or \ |
|
138
|
|
|
href.startswith("-ubuntu-security-oval-com.ubuntu") or \ |
|
139
|
|
|
href.startswith("pub-projects-security-oval-suse") |
|
140
|
|
|
|
|
141
|
|
|
|
|
142
|
|
|
def main(): |
|
143
|
|
|
global exit_value |
|
144
|
|
|
(options, args) = parse_options() |
|
145
|
|
|
xccdffilename = args[0] |
|
146
|
|
|
|
|
147
|
|
|
# extract all of the rules within the xccdf |
|
148
|
|
|
xccdftree = ssg.xml.ElementTree.parse(xccdffilename) |
|
149
|
|
|
rules = xccdftree.findall(".//{%s}Rule" % xccdf_ns) |
|
150
|
|
|
|
|
151
|
|
|
# if a profile was specified, get rid of any Rules that aren't in it |
|
152
|
|
|
if options.profile_name: |
|
153
|
|
|
profile_ruleids = get_profileruleids(xccdftree, options.profile_name) |
|
154
|
|
|
prunedrules = rules[:] |
|
155
|
|
|
for rule in rules: |
|
156
|
|
|
if rule.get("id") not in profile_ruleids: |
|
157
|
|
|
prunedrules.remove(rule) |
|
158
|
|
|
rules = prunedrules |
|
159
|
|
|
|
|
160
|
|
|
# step over xccdf file, and find referenced oval files |
|
161
|
|
|
checks = xccdftree.findall(".//{%s}check" % xccdf_ns) |
|
162
|
|
|
ovalfiles = get_ovalfiles(checks) |
|
163
|
|
|
|
|
164
|
|
|
# this script only supports the inclusion of one OVAL file |
|
165
|
|
|
if len(ovalfiles) > 1: |
|
166
|
|
|
sys.exit("Referencing more than one OVAL file is not yet " + |
|
167
|
|
|
"supported by this script.") |
|
168
|
|
|
|
|
169
|
|
|
# find important elements within the XCCDF and the OVAL |
|
170
|
|
|
ovalfile = os.path.join(os.path.dirname(xccdffilename), ovalfiles.pop()) |
|
171
|
|
|
ovaltree = ssg.xml.ElementTree.parse(ovalfile) |
|
172
|
|
|
# collect all compliance checks (not inventory checks, which are |
|
173
|
|
|
# needed by CPE) |
|
174
|
|
|
ovaldefs = [] |
|
175
|
|
|
for el in ovaltree.findall(".//{%s}definition" % oval_ns): |
|
176
|
|
|
if el.get("class") != "compliance": |
|
177
|
|
|
continue |
|
178
|
|
|
|
|
179
|
|
|
ovaldefs.append(el) |
|
180
|
|
|
|
|
181
|
|
|
ovaldef_ids = [ovaldef.get("id") for ovaldef in ovaldefs] |
|
182
|
|
|
|
|
183
|
|
|
oval_extenddefs = ovaltree.findall(".//{%s}extend_definition" % oval_ns) |
|
184
|
|
|
ovaldef_ids_extended = [oval_extenddef.get("definition_ref") for oval_extenddef in oval_extenddefs] |
|
185
|
|
|
ovaldef_ids_extended = list(set(ovaldef_ids_extended)) |
|
186
|
|
|
|
|
187
|
|
|
check_content_refs = xccdftree.findall(".//{%s}check-content-ref" |
|
188
|
|
|
% xccdf_ns) |
|
189
|
|
|
xccdf_parent_map = dict((c, p) for p in xccdftree.iter() for c in p) |
|
190
|
|
|
# now we can actually do the verification work here |
|
191
|
|
|
if options.rules_with_invalid_checks or options.all_checks: |
|
192
|
|
|
for check_content_ref in check_content_refs: |
|
193
|
|
|
parent = xccdf_parent_map[check_content_ref] |
|
194
|
|
|
rule = xccdf_parent_map[parent] |
|
195
|
|
|
check_system = parent.get("system") |
|
196
|
|
|
# Skip those <check-content-ref> elements using OCIL as the checksystem |
|
197
|
|
|
# (since we are checking just referenced OVAL definitions) |
|
198
|
|
|
if check_system == ocil_cs: |
|
199
|
|
|
continue |
|
200
|
|
|
|
|
201
|
|
|
# Obtain the value of the 'href' attribute of particular |
|
202
|
|
|
# <check-content-ref> element |
|
203
|
|
|
href = check_content_ref.get("href") |
|
204
|
|
|
|
|
205
|
|
|
# Don't attempt to obtain refname on <check-content-ref> element |
|
206
|
|
|
# having its "href" attribute set either to "http://" or to |
|
207
|
|
|
# "https://" values (since the "name" attribute will be empty for |
|
208
|
|
|
# these two cases) |
|
209
|
|
|
# Also, skip known remote data files with CVE feeds. |
|
210
|
|
|
if is_remote_feed(href): |
|
211
|
|
|
continue |
|
212
|
|
|
|
|
213
|
|
|
if check_system == sce_cs: |
|
214
|
|
|
check_path = os.path.join(options.base_dir, href) |
|
215
|
|
|
if not os.path.exists(check_path): |
|
216
|
|
|
msg = "ERROR: Invalid or missing SCE definition (%s) " |
|
217
|
|
|
msg += "referenced by XCCDF Rule: %s" |
|
218
|
|
|
msg = msg % (check_path, rule.get("id")) |
|
219
|
|
|
print(msg) |
|
220
|
|
|
exit_value = 1 |
|
221
|
|
|
else: |
|
222
|
|
|
refname = check_content_ref.get("name") |
|
223
|
|
|
if refname not in ovaldef_ids: |
|
224
|
|
|
print("ERROR: Invalid OVAL definition referenced by XCCDF Rule: %s" |
|
225
|
|
|
% (rule.get("id"))) |
|
226
|
|
|
exit_value = 1 |
|
227
|
|
|
|
|
228
|
|
|
if options.rules_without_checks or options.all_checks: |
|
229
|
|
|
for rule in rules: |
|
230
|
|
|
check = rule.find("./{%s}check" % xccdf_ns) |
|
231
|
|
|
if check is None: |
|
232
|
|
|
print("ERROR: No reference to OVAL definition in XCCDF Rule: %s" |
|
233
|
|
|
% (rule.get("id"))) |
|
234
|
|
|
exit_value = 1 |
|
235
|
|
|
|
|
236
|
|
|
if options.rules_without_severity or options.all_checks: |
|
237
|
|
|
for rule in rules: |
|
238
|
|
|
if rule.get("severity") is None: |
|
239
|
|
|
print("ERROR: No severity assigned to XCCDF Rule: %s" |
|
240
|
|
|
% (rule.get("id"))) |
|
241
|
|
|
exit_value = 1 |
|
242
|
|
|
|
|
243
|
|
|
if options.rules_without_nistrefs or options.rules_without_disarefs or options.all_checks: |
|
244
|
|
|
for rule in rules: |
|
245
|
|
|
# find all references in the current rule |
|
246
|
|
|
refs = rule.findall(".//{%s}reference" % xccdf_ns) |
|
247
|
|
|
if refs is None: |
|
248
|
|
|
print("ERROR: No reference assigned to XCCDF Rule: %s" |
|
249
|
|
|
% (rule.get("id"))) |
|
250
|
|
|
exit_value = 1 |
|
251
|
|
|
else: |
|
252
|
|
|
# loop through the Rule's references and put their hrefs |
|
253
|
|
|
# in a list |
|
254
|
|
|
ref_href_list = [ref.get("href") for ref in refs] |
|
255
|
|
|
# print warning if rule does not have a NIST reference |
|
256
|
|
|
if (nist_ref_href not in ref_href_list) and options.rules_without_nistrefs: |
|
257
|
|
|
print("ERROR: No valid NIST reference in XCCDF Rule: " + |
|
258
|
|
|
rule.get("id")) |
|
259
|
|
|
exit_value = 1 |
|
260
|
|
|
# print warning if rule does not have a DISA reference |
|
261
|
|
|
if (disa_ref_href not in ref_href_list) and options.rules_without_disarefs: |
|
262
|
|
|
print("ERROR: No valid DISA CCI reference in XCCDF Rule: " + |
|
263
|
|
|
rule.get("id")) |
|
264
|
|
|
exit_value = 1 |
|
265
|
|
|
|
|
266
|
|
|
if options.disarefs_not_in_profile or options.nistrefs_not_in_profile: |
|
267
|
|
|
if options.profile_name is None: |
|
268
|
|
|
sys.exit("The options for finding Rules with a reference, " |
|
269
|
|
|
"but which are not in a Profile, requires specifying a Profile.") |
|
270
|
|
|
allrules = xccdftree.findall(".//{%s}Rule" % xccdf_ns) |
|
271
|
|
|
for rule in allrules: |
|
272
|
|
|
# find all references in the current rule |
|
273
|
|
|
refs = rule.findall(".//{%s}reference" % xccdf_ns) |
|
274
|
|
|
ref_href_list = [ref.get("href") for ref in refs] |
|
275
|
|
|
# print warning if Rule is outside Profile and has a NIST reference |
|
276
|
|
|
if options.nistrefs_not_in_profile: |
|
277
|
|
|
if (nist_ref_href in ref_href_list) and (rule.get("id") not in profile_ruleids): |
|
|
|
|
|
|
278
|
|
|
print("ERROR: XCCDF Rule found with NIST reference outside Profile %s: " |
|
279
|
|
|
% options.profile_name + rule.get("id")) |
|
280
|
|
|
exit_value = 1 |
|
281
|
|
|
# print warning if Rule is outside Profile and has a DISA reference |
|
282
|
|
|
if options.disarefs_not_in_profile: |
|
283
|
|
|
if (disa_ref_href in ref_href_list) and (rule.get("id") not in profile_ruleids): |
|
284
|
|
|
print("ERROR: XCCDF Rule found with DISA CCI reference outside Profile %s: " |
|
285
|
|
|
% options.profile_name + rule.get("id")) |
|
286
|
|
|
exit_value = 1 |
|
287
|
|
|
|
|
288
|
|
|
if options.ovaldefs_unused or options.all_checks: |
|
289
|
|
|
# create a list of all of the OVAL compliance check ids that are |
|
290
|
|
|
# defined in the oval file |
|
291
|
|
|
oval_checks_list = [ovaldef.get("id") for ovaldef in ovaldefs] |
|
292
|
|
|
# now loop through the xccdf rules; if a rule references an oval check |
|
293
|
|
|
# we remove the oval check from our list |
|
294
|
|
|
for check_content in check_content_refs: |
|
295
|
|
|
# remove from the list |
|
296
|
|
|
if check_content.get("name") in oval_checks_list: |
|
297
|
|
|
oval_checks_list.remove(check_content.get("name")) |
|
298
|
|
|
# the list should now contain the OVAL checks that are not referenced |
|
299
|
|
|
# by any XCCDF rule |
|
300
|
|
|
oval_checks_list.sort() |
|
301
|
|
|
for oval_id in oval_checks_list: |
|
302
|
|
|
# don't print out the OVAL defs that are extended by others, |
|
303
|
|
|
# as they're not unused |
|
304
|
|
|
if oval_id not in ovaldef_ids_extended: |
|
305
|
|
|
print("WARNING: OVAL Check is not referenced by XCCDF: %s" |
|
306
|
|
|
% (oval_id)) |
|
307
|
|
|
# Do not treat this as error but only as a warning |
|
308
|
|
|
#exit_value = 1 |
|
309
|
|
|
|
|
310
|
|
|
sys.exit(exit_value) |
|
311
|
|
|
|
|
312
|
|
|
if __name__ == "__main__": |
|
313
|
|
|
main() |
|
314
|
|
|
|