Issues (70)

build-scripts/verify_references.py (1 issue)

Severity
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
The variable profile_ruleids does not seem to be defined in case options.profile_name on line 152 is False. Are you sure this can never be the case?
Loading history...
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