1 | #!/usr/bin/python3 |
||
2 | |||
3 | from __future__ import print_function |
||
4 | |||
5 | import sys |
||
6 | import argparse |
||
7 | import os |
||
8 | import ssg |
||
9 | import ssg.xml |
||
10 | import xml.dom.minidom |
||
11 | |||
12 | from ssg.constants import XCCDF11_NS, XCCDF12_NS, OSCAP_RULE |
||
13 | from ssg.utils import mkdir_p |
||
14 | |||
15 | |||
16 | ET = ssg.xml.ElementTree |
||
17 | |||
18 | |||
19 | owner = "disastig" |
||
20 | stig_ns = ["https://public.cyber.mil/stigs/downloads/?_dl_facet_stigs=operating-systems%2Cunix-linux", |
||
21 | "https://public.cyber.mil/stigs/downloads/?_dl_facet_stigs=operating-systems%2Cgeneral-purpose-os", |
||
22 | "https://public.cyber.mil/stigs/downloads/?_dl_facet_stigs=app-security%2Capplication-servers", |
||
23 | "https://public.cyber.mil/stigs/downloads/?_dl_facet_stigs=app-security%2Capp-security-dev"] |
||
24 | dc_ns = "http://purl.org/dc/elements/1.1/" |
||
25 | outfile = "stig_overlay.xml" |
||
26 | |||
27 | |||
28 | def yes_no_prompt(): |
||
29 | prompt = "Would you like to proceed? (Y/N): " |
||
30 | |||
31 | while True: |
||
32 | data = str(input(prompt)).lower() |
||
33 | |||
34 | if data in ("yes", "y"): |
||
35 | return True |
||
36 | elif data in ("n", "no"): |
||
37 | return False |
||
38 | |||
39 | |||
40 | def element_value(element, element_obj): |
||
41 | for elem in element_obj.findall("./{%s}%s" % (XCCDF11_NS, element)): |
||
42 | elem = elem.text |
||
43 | try: |
||
44 | return elem |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
45 | except UnboundLocalError as e: |
||
46 | return "" |
||
47 | |||
48 | |||
49 | def ssg_xccdf_stigid_mapping(ssgtree): |
||
50 | xccdf_ns = ssg.xml.determine_xccdf_tree_namespace(ssgtree) |
||
51 | xccdftostig_idmapping = {} |
||
52 | |||
53 | for rule in ssgtree.findall(".//{%s}Rule" % xccdf_ns): |
||
54 | srgs = [] |
||
55 | rhid = [] |
||
56 | |||
57 | xccdfid = rule.get("id") |
||
58 | if xccdf_ns == XCCDF12_NS: |
||
59 | xccdfid = xccdfid.replace(OSCAP_RULE, "") |
||
60 | if xccdfid is not None: |
||
61 | for references in stig_ns: |
||
62 | stig = [ids for ids in rule.findall(".//{%s}reference[@href='%s']" % (xccdf_ns, references))] |
||
63 | for ref in reversed(stig): |
||
64 | if not ref.text.startswith("SRG-"): |
||
65 | rhid.append(ref.text) |
||
66 | else: |
||
67 | srgs.append(ref.text) |
||
68 | for id in rhid: |
||
69 | xccdftostig_idmapping.update({id: {xccdfid: srgs}}) |
||
70 | |||
71 | return xccdftostig_idmapping |
||
72 | |||
73 | |||
74 | def getkey(elem): |
||
75 | return elem.get("ownerid") |
||
76 | |||
77 | |||
78 | def new_stig_overlay(xccdftree, ssgtree, outfile, quiet): |
||
79 | if not ssgtree: |
||
80 | ssg_mapping = False |
||
81 | else: |
||
82 | ssg_mapping = ssg_xccdf_stigid_mapping(ssgtree) |
||
83 | |||
84 | new_stig_overlay = ET.Element("overlays", xmlns=XCCDF11_NS) |
||
85 | for group in xccdftree.findall("./{%s}Group" % XCCDF11_NS): |
||
86 | vkey = group.get("id").strip('V-') |
||
87 | for title in group.findall("./{%s}title" % XCCDF11_NS): |
||
88 | srg = title.text |
||
89 | for rule in group.findall("./{%s}Rule" % XCCDF11_NS): |
||
90 | svkey_raw = rule.get("id") |
||
91 | svkey = svkey_raw.strip()[3:9] |
||
92 | severity = rule.get("severity") |
||
93 | release = svkey_raw.strip()[10:-5] |
||
94 | version = element_value("version", rule) |
||
95 | rule_title = element_value("title", rule) |
||
96 | ident = element_value("ident", rule).strip("CCI-").lstrip("0") |
||
97 | |||
98 | if not ssgtree: |
||
99 | mapped_id = "XXXX" |
||
100 | else: |
||
101 | try: |
||
102 | mapped_id = ''.join(ssg_mapping[version].keys()) |
||
0 ignored issues
–
show
|
|||
103 | except KeyError as e: |
||
104 | mapped_id = "XXXX" |
||
105 | |||
106 | overlay = ET.SubElement(new_stig_overlay, "overlay", owner=owner, |
||
107 | ruleid=mapped_id, ownerid=version, disa=ident, |
||
0 ignored issues
–
show
|
|||
108 | severity=severity) |
||
0 ignored issues
–
show
|
|||
109 | vmsinfo = ET.SubElement(overlay, "VMSinfo", VKey=vkey, |
||
110 | SVKey=svkey, VRelease=release) |
||
0 ignored issues
–
show
|
|||
111 | title = ET.SubElement(overlay, "title", text=rule_title) |
||
0 ignored issues
–
show
|
|||
112 | |||
113 | lines = new_stig_overlay.findall("overlay") |
||
114 | new_stig_overlay[:] = sorted(lines, key=getkey) |
||
115 | |||
116 | try: |
||
117 | et_str = ET.tostring(new_stig_overlay, encoding="UTF-8", xml_declaration=True) |
||
118 | except TypeError: |
||
119 | et_str = ET.tostring(new_stig_overlay, encoding="UTF-8") |
||
120 | |||
121 | dom = xml.dom.minidom.parseString(et_str) |
||
122 | pretty_xml_as_string = dom.toprettyxml(indent=' ', encoding="UTF-8") |
||
123 | |||
124 | overlay_directory = os.path.dirname(outfile) |
||
125 | if mkdir_p(overlay_directory) and not quiet: |
||
126 | print("\nOverlay directory created: %s" % overlay_directory) |
||
127 | |||
128 | with open(outfile, 'wb') as f: |
||
129 | f.write(pretty_xml_as_string) |
||
130 | |||
131 | if not quiet: |
||
132 | print("\nGenerated the new STIG overlay file: %s" % outfile) |
||
133 | |||
134 | |||
135 | def parse_args(): |
||
136 | parser = argparse.ArgumentParser() |
||
137 | parser.add_argument("--disa-xccdf", default=False, required=True, |
||
138 | action="store", dest="disa_xccdf_filename", |
||
139 | help="A DISA generated XCCDF Manual checks file. \ |
||
140 | For example: disa-stig-rhel8-v1r12-xccdf-manual.xml") |
||
141 | parser.add_argument("--ssg-xccdf", default=None, |
||
142 | action="store", dest="ssg_xccdf_filename", |
||
143 | help="A SSG generated XCCDF file. Can be XCCDF 1.1 or XCCDF 1.2 \ |
||
144 | For example: ssg-rhel8-xccdf.xml") |
||
145 | parser.add_argument("-o", "--output", required=True, |
||
146 | action="store", dest="output_file", |
||
147 | help="STIG overlay XML content file \ |
||
148 | [default: %s]" % outfile) |
||
149 | parser.add_argument("-q", "--quiet", dest="quiet", default=False, |
||
150 | action="store_true", help="Do not print anything and assume yes for everything") |
||
151 | |||
152 | return parser.parse_args() |
||
153 | |||
154 | |||
155 | def main(): |
||
156 | args = parse_args() |
||
157 | |||
158 | disa_xccdftree = ET.parse(args.disa_xccdf_filename) |
||
159 | |||
160 | if not args.ssg_xccdf_filename: |
||
161 | prompt = True |
||
162 | if not args.quiet: |
||
163 | print("WARNING: You are generating a STIG overlay XML file without mapping it " |
||
164 | "to existing SSG content.") |
||
165 | prompt = yes_no_prompt() |
||
166 | if not prompt: |
||
167 | sys.exit(0) |
||
168 | ssg_xccdftree = False |
||
169 | else: |
||
170 | ssg_xccdftree = ET.parse(args.ssg_xccdf_filename) |
||
171 | ssg = ssg_xccdftree.find(".//{%s}publisher" % dc_ns).text |
||
172 | if ssg != "SCAP Security Guide Project": |
||
173 | if not args.quiet: |
||
174 | sys.exit("%s is not a valid SSG generated XCCDF file." % args.ssg_xccdf_filename) |
||
175 | else: |
||
176 | sys.exit(1) |
||
177 | |||
178 | disa = disa_xccdftree.find(".//{%s}source" % dc_ns).text |
||
179 | if disa != "STIG.DOD.MIL": |
||
180 | if not args.quiet: |
||
181 | sys.exit("%s is not a valid DISA generated manual XCCDF file." % args.disa_xccdf_filename) |
||
182 | else: |
||
183 | sys.exit(2) |
||
184 | |||
185 | new_stig_overlay(disa_xccdftree, ssg_xccdftree, args.output_file, args.quiet) |
||
186 | |||
187 | |||
188 | if __name__ == "__main__": |
||
189 | main() |
||
190 |