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): |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
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 |