Test Failed
Pull Request — master (#8570)
by Matthew
06:02 queued 03:30
created

utils.create_srg_export   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 547
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
eloc 467
dl 0
loc 547
ccs 0
cts 252
cp 0
rs 5.5199
c 0
b 0
f 0
wmc 56

22 Functions

Rating   Name   Duplication   Size   Complexity  
A handle_variables() 0 3 1
A parse_args() 0 22 1
A get_iacontrol() 0 8 3
A replace_variables() 0 11 4
A get_srg_dict() 0 23 4
A get_description_root() 0 9 1
A handle_rule_yaml() 0 7 1
A html_plain_text() 0 13 2
A get_severity() 0 7 3
A get_rule_json() 0 4 2
B handle_control() 0 29 7
A create_base_row() 0 21 2
A no_selections_row() 0 10 1
A check_paths() 0 10 3
A get_policy() 0 4 1
A setup_csv_writer() 0 4 1
A handle_csv_output() 0 5 3
A handle_output() 0 9 4
A get_env_yaml() 0 5 1
A handle_xlsx_output() 0 6 2
A handle_html_output() 0 6 2
A main() 0 16 2

1 Method

Rating   Name   Duplication   Size   Complexity  
A DisaStatus.from_string() 0 9 5

How to fix   Complexity   

Complexity

Complex classes like utils.create_srg_export often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/python3
2
3
import argparse
4
import csv
5
import datetime
6
import json
7
import pathlib
8
import os
9
import re
10
import sys
11
import string
12
from typing.io import TextIO
13
import xml.etree.ElementTree as ET
14
15
import convert_srg_export_to_xlsx
16
import convert_srg_export_to_html
17
18
try:
19
    import ssg.build_yaml
20
    import ssg.constants
21
    import ssg.controls
22
    import ssg.environment
23
    import ssg.rules
24
    import ssg.yaml
25
except (ModuleNotFoundError, ImportError):
26
    sys.stderr.write("Unable to load ssg python modules.\n")
27
    sys.stderr.write("Hint: run source ./.pyenv.sh\n")
28
    exit(3)
29
30
SSG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
31
RULES_JSON = os.path.join(SSG_ROOT, "build", "rule_dirs.json")
32
BUILD_CONFIG = os.path.join(SSG_ROOT, "build", "build_config.yml")
33
OUTPUT = os.path.join(SSG_ROOT, 'build',
34
                      f'{datetime.datetime.now().strftime("%s")}_stig_export.csv')
35
SRG_PATH = os.path.join(SSG_ROOT, 'shared', 'references', 'disa-os-srg-v2r2.xml')
36
NS = {'scap': ssg.constants.datastream_namespace,
37
      'xccdf-1.2': ssg.constants.XCCDF12_NS,
38
      'xccdf-1.1': ssg.constants.XCCDF11_NS}
39
SEVERITY = {'low': 'CAT III', 'medium': 'CAT II', 'high': 'CAT I'}
40
41
HEADERS = [
42
    'IA Control', 'CCI', 'SRGID', 'STIGID', 'SRG Requirement', 'Requirement',
43
    'SRG VulDiscussion', 'Vul Discussion', 'Status', 'SRG Check', 'Check', 'SRG Fix',
44
    'Fix', 'Severity', 'Mitigation', 'Artifact Description', 'Status Justification'
45
    ]
46
47
COLUMNS = string.ascii_uppercase[:17]  # A-Q uppercase letters
48
49
COLUMN_MAPPINGS = dict(zip(COLUMNS, HEADERS))
50
51
srgid_to_iacontrol = {
52
    'SRG-OS-000001-GPOS-00001': 'AC-2 (1)',
53
    'SRG-OS-000002-GPOS-00002': 'AC-2 (2)',
54
    'SRG-OS-000004-GPOS-00004': 'AC-2 (4)',
55
    'SRG-OS-000021-GPOS-00005': 'AC-7 a',
56
    'SRG-OS-000023-GPOS-00006': 'AC-8 a',
57
    'SRG-OS-000024-GPOS-00007': 'AC-8 b',
58
    'SRG-OS-000027-GPOS-00008': 'AC-10',
59
    'SRG-OS-000028-GPOS-00009': 'AC-11 b',
60
    'SRG-OS-000029-GPOS-00010': 'AC-11 a',
61
    'SRG-OS-000030-GPOS-00011': 'AC-11 a',
62
    'SRG-OS-000031-GPOS-00012': 'AC-11 (1)',
63
    'SRG-OS-000032-GPOS-00013': 'AC-17 (1)',
64
    'SRG-OS-000033-GPOS-00014': 'AC-17 (2)',
65
    'SRG-OS-000037-GPOS-00015': 'AU-3',
66
    'SRG-OS-000038-GPOS-00016': 'AU-3',
67
    'SRG-OS-000039-GPOS-00017': 'AU-3',
68
    'SRG-OS-000040-GPOS-00018': 'AU-3',
69
    'SRG-OS-000041-GPOS-00019': 'AU-3',
70
    'SRG-OS-000042-GPOS-00020': 'AU-3 (1)',
71
    'SRG-OS-000042-GPOS-00021': 'AU-3 (1)',
72
    'SRG-OS-000046-GPOS-00022': 'AU-5 a',
73
    'SRG-OS-000047-GPOS-00023': 'AU-5 b',
74
    'SRG-OS-000051-GPOS-00024': 'AU-6 (4)',
75
    'SRG-OS-000054-GPOS-00025': 'AU-7 (1)',
76
    'SRG-OS-000055-GPOS-00026': 'AU-8 a',
77
    'SRG-OS-000057-GPOS-00027': 'AU-9',
78
    'SRG-OS-000058-GPOS-00028': 'AU-9',
79
    'SRG-OS-000059-GPOS-00029': 'AU-9',
80
    'SRG-OS-000062-GPOS-00031': 'AU-12 a',
81
    'SRG-OS-000063-GPOS-00032': 'AU-12 b',
82
    'SRG-OS-000064-GPOS-00033': 'AU-12 c',
83
    'SRG-OS-000066-GPOS-00034': 'IA-5 (2) (a)',
84
    'SRG-OS-000067-GPOS-00035': 'IA-5 (2) (b)',
85
    'SRG-OS-000068-GPOS-00036': 'IA-5 (2) (c)',
86
    'SRG-OS-000069-GPOS-00037': 'IA-5 (1) (a)',
87
    'SRG-OS-000070-GPOS-00038': 'IA-5 (1) (a)',
88
    'SRG-OS-000071-GPOS-00039': 'IA-5 (1) (a)',
89
    'SRG-OS-000072-GPOS-00040': 'IA-5 (1) (b)',
90
    'SRG-OS-000073-GPOS-00041': 'IA-5 (1) (c)',
91
    'SRG-OS-000074-GPOS-00042': 'IA-5 (1) (c)',
92
    'SRG-OS-000075-GPOS-00043': 'IA-5 (1) (d)',
93
    'SRG-OS-000076-GPOS-00044': 'IA-5 (1) (d)',
94
    'SRG-OS-000077-GPOS-00045': 'IA-5 (1) (e)',
95
    'SRG-OS-000078-GPOS-00046': 'IA-5 (1) (a)',
96
    'SRG-OS-000079-GPOS-00047': 'IA-6',
97
    'SRG-OS-000080-GPOS-00048': 'AC-3',
98
    'SRG-OS-000095-GPOS-00049': 'CM-7 a',
99
    'SRG-OS-000096-GPOS-00050': 'CM-7 b',
100
    'SRG-OS-000104-GPOS-00051': 'IA-2',
101
    'SRG-OS-000105-GPOS-00052': 'IA-2 (1)',
102
    'SRG-OS-000106-GPOS-00053': 'IA-2 (2)',
103
    'SRG-OS-000107-GPOS-00054': 'IA-2 (3)',
104
    'SRG-OS-000108-GPOS-00055': 'IA-2 (4)',
105
    'SRG-OS-000109-GPOS-00056': 'IA-2 (5)',
106
    'SRG-OS-000112-GPOS-00057': 'IA-2 (8)',
107
    'SRG-OS-000113-GPOS-00058': 'IA-2 (9)',
108
    'SRG-OS-000114-GPOS-00059': 'IA-3',
109
    'SRG-OS-000118-GPOS-00060': 'IA-4 e',
110
    'SRG-OS-000120-GPOS-00061': 'IA-7',
111
    'SRG-OS-000121-GPOS-00062': 'IA-8',
112
    'SRG-OS-000122-GPOS-00063': 'AU-7 a',
113
    'SRG-OS-000123-GPOS-00064': 'AC-2 (2)',
114
    'SRG-OS-000125-GPOS-00065': 'MA-4 c',
115
    'SRG-OS-000126-GPOS-00066': 'MA-4 e',
116
    'SRG-OS-000132-GPOS-00067': 'SC-2',
117
    'SRG-OS-000134-GPOS-00068': 'SC-3',
118
    'SRG-OS-000138-GPOS-00069': 'SC-4',
119
    'SRG-OS-000142-GPOS-00071': 'SC-5 (2)',
120
    'SRG-OS-000163-GPOS-00072': 'SC-10',
121
    'SRG-OS-000184-GPOS-00078': 'SC-24',
122
    'SRG-OS-000185-GPOS-00079': 'SC-28',
123
    'SRG-OS-000191-GPOS-00080': 'SI-2 (2)',
124
    'SRG-OS-000205-GPOS-00083': 'SI-11 a',
125
    'SRG-OS-000206-GPOS-00084': 'SI-11 b',
126
    'SRG-OS-000228-GPOS-00088': 'AC-8 c 1, AC-8 c 2, AC-8 c 3',
127
    'SRG-OS-000239-GPOS-00089': 'AC-2 (4)',
128
    'SRG-OS-000240-GPOS-00090': 'AC-2 (4)',
129
    'SRG-OS-000241-GPOS-00091': 'AC-2 (4)',
130
    'SRG-OS-000250-GPOS-00093': 'AC-17 (2)',
131
    'SRG-OS-000254-GPOS-00095': 'AU-14 (1)',
132
    'SRG-OS-000255-GPOS-00096': 'AU-3',
133
    'SRG-OS-000256-GPOS-00097': 'AU-9',
134
    'SRG-OS-000257-GPOS-00098': 'AU-9',
135
    'SRG-OS-000258-GPOS-00099': 'AU-9',
136
    'SRG-OS-000259-GPOS-00100': 'CM-5 (6)',
137
    'SRG-OS-000266-GPOS-00101': 'IA-5 (1) (a)',
138
    'SRG-OS-000269-GPOS-00103': 'SC-24',
139
    'SRG-OS-000274-GPOS-00104': 'AC-2 (4)',
140
    'SRG-OS-000275-GPOS-00105': 'AC-2 (4)',
141
    'SRG-OS-000276-GPOS-00106': 'AC-2 (4)',
142
    'SRG-OS-000277-GPOS-00107': 'AC-2 (4)',
143
    'SRG-OS-000278-GPOS-00108': 'AU-9 (3)',
144
    'SRG-OS-000279-GPOS-00109': 'AC-12',
145
    'SRG-OS-000280-GPOS-00110': 'AC-12 (1)',
146
    'SRG-OS-000281-GPOS-00111': 'AC-12 (1)',
147
    'SRG-OS-000297-GPOS-00115': 'AC-17 (1)',
148
    'SRG-OS-000298-GPOS-00116': 'AC-17 (9)',
149
    'SRG-OS-000299-GPOS-00117': 'AC-18 (1)',
150
    'SRG-OS-000300-GPOS-00118': 'AC-18 (1)',
151
    'SRG-OS-000303-GPOS-00120': 'AC-2 (4)',
152
    'SRG-OS-000304-GPOS-00121': 'AC-2 (4)',
153
    'SRG-OS-000312-GPOS-00122': 'AC-3 (4)',
154
    'SRG-OS-000312-GPOS-00123': 'AC-3 (4)',
155
    'SRG-OS-000312-GPOS-00124': 'AC-3 (4)',
156
    'SRG-OS-000324-GPOS-00125': 'AC-6 (10)',
157
    'SRG-OS-000326-GPOS-00126': 'AC-6 (8)',
158
    'SRG-OS-000327-GPOS-00127': 'AC-6 (9)',
159
    'SRG-OS-000329-GPOS-00128': 'AC-7 b',
160
    'SRG-OS-000337-GPOS-00129': 'AU-12 (3)',
161
    'SRG-OS-000341-GPOS-00132': 'AU-4',
162
    'SRG-OS-000342-GPOS-00133': 'AU-4 (1)',
163
    'SRG-OS-000343-GPOS-00134': 'AU-5 (1)',
164
    'SRG-OS-000344-GPOS-00135': 'AU-5 (2)',
165
    'SRG-OS-000348-GPOS-00136': 'AU-7 a',
166
    'SRG-OS-000349-GPOS-00137': 'AU-7 a',
167
    'SRG-OS-000350-GPOS-00138': 'AU-7 a',
168
    'SRG-OS-000351-GPOS-00139': 'AU-7 a',
169
    'SRG-OS-000352-GPOS-00140': 'AU-7 a',
170
    'SRG-OS-000353-GPOS-00141': 'AU-7 b',
171
    'SRG-OS-000354-GPOS-00142': 'AU-7 b',
172
    'SRG-OS-000355-GPOS-00143': 'AU-8 (1) (a)',
173
    'SRG-OS-000356-GPOS-00144': 'AU-8 (1) (b)',
174
    'SRG-OS-000358-GPOS-00145': 'AU-8 b',
175
    'SRG-OS-000359-GPOS-00146': 'AU-8 b',
176
    'SRG-OS-000360-GPOS-00147': 'AU-9 (5), CM-6 b',
177
    'SRG-OS-000362-GPOS-00149': 'CM-11 (2)',
178
    'SRG-OS-000363-GPOS-00150': 'CM-3 (5)',
179
    'SRG-OS-000364-GPOS-00151': 'CM-5 (1)',
180
    'SRG-OS-000365-GPOS-00152': 'CM-5 (1)',
181
    'SRG-OS-000366-GPOS-00153': 'CM-5 (3)',
182
    'SRG-OS-000368-GPOS-00154': 'CM-7 (2)',
183
    'SRG-OS-000370-GPOS-00155': 'CM-7 (5) (b)',
184
    'SRG-OS-000373-GPOS-00156': 'IA-11',
185
    'SRG-OS-000373-GPOS-00157': 'IA-11',
186
    'SRG-OS-000373-GPOS-00158': 'IA-11',
187
    'SRG-OS-000374-GPOS-00159': 'IA-11',
188
    'SRG-OS-000375-GPOS-00160': 'IA-2 (11)',
189
    'SRG-OS-000376-GPOS-00161': 'IA-2 (12)',
190
    'SRG-OS-000377-GPOS-00162': 'IA-2 (12)',
191
    'SRG-OS-000378-GPOS-00163': 'IA-3',
192
    'SRG-OS-000379-GPOS-00164': 'IA-3 (1)',
193
    'SRG-OS-000380-GPOS-00165': 'IA-5 (1) (f)',
194
    'SRG-OS-000383-GPOS-00166': 'IA-5 (13)',
195
    'SRG-OS-000384-GPOS-00167': 'IA-5 (2) (d)',
196
    'SRG-OS-000392-GPOS-00172': 'MA-4 (1) (a)',
197
    'SRG-OS-000393-GPOS-00173': 'MA-4 (6)',
198
    'SRG-OS-000394-GPOS-00174': 'MA-4 (6)',
199
    'SRG-OS-000395-GPOS-00175': 'MA-4 (7)',
200
    'SRG-OS-000396-GPOS-00176': 'SC-13',
201
    'SRG-OS-000403-GPOS-00182': 'SC-23 (5)',
202
    'SRG-OS-000404-GPOS-00183': 'SC-28 (1)',
203
    'SRG-OS-000405-GPOS-00184': 'SC-28 (1)',
204
    'SRG-OS-000420-GPOS-00186': 'SC-5',
205
    'SRG-OS-000423-GPOS-00187': 'SC-8',
206
    'SRG-OS-000424-GPOS-00188': 'SC-8 (1)',
207
    'SRG-OS-000425-GPOS-00189': 'SC-8 (2)',
208
    'SRG-OS-000426-GPOS-00190': 'SC-8 (2)',
209
    'SRG-OS-000432-GPOS-00191': 'SI-10 (3)',
210
    'SRG-OS-000433-GPOS-00192': 'SI-16',
211
    'SRG-OS-000433-GPOS-00193': 'SI-16',
212
    'SRG-OS-000437-GPOS-00194': 'SI-2 (6)',
213
    'SRG-OS-000445-GPOS-00199': 'SI-6 a',
214
    'SRG-OS-000446-GPOS-00200': 'SI-6 b',
215
    'SRG-OS-000447-GPOS-00201': 'SI-6 d',
216
    'SRG-OS-000458-GPOS-00203': 'AU-12 c',
217
    'SRG-OS-000461-GPOS-00205': 'AU-12 c',
218
    'SRG-OS-000462-GPOS-00206': 'AU-12 c',
219
    'SRG-OS-000463-GPOS-00207': 'AU-12 c',
220
    'SRG-OS-000465-GPOS-00209': 'AU-12 c',
221
    'SRG-OS-000466-GPOS-00210': 'AU-12 c',
222
    'SRG-OS-000467-GPOS-00211': 'AU-12 c',
223
    'SRG-OS-000468-GPOS-00212': 'AU-12 c',
224
    'SRG-OS-000470-GPOS-00214': 'AU-12 c',
225
    'SRG-OS-000471-GPOS-00215': 'AU-12 c',
226
    'SRG-OS-000471-GPOS-00216': 'AU-12 c',
227
    'SRG-OS-000472-GPOS-00217': 'AU-12 c',
228
    'SRG-OS-000473-GPOS-00218': 'AU-12 c',
229
    'SRG-OS-000474-GPOS-00219': 'AU-12 c',
230
    'SRG-OS-000475-GPOS-00220': 'AU-12 c',
231
    'SRG-OS-000476-GPOS-00221': 'AU-12 c',
232
    'SRG-OS-000477-GPOS-00222': 'AU-12 c',
233
    'SRG-OS-000478-GPOS-00223': 'SC-13',
234
    'SRG-OS-000479-GPOS-00224': 'AU-4 (1)',
235
    'SRG-OS-000480-GPOS-00225': 'CM-6 b',
236
    'SRG-OS-000480-GPOS-00226': 'CM-6 b',
237
    'SRG-OS-000480-GPOS-00227': 'CM-6 b',
238
    'SRG-OS-000480-GPOS-00228': 'CM-6 b',
239
    'SRG-OS-000480-GPOS-00229': 'CM-6 b',
240
    'SRG-OS-000480-GPOS-00230': 'CM-6 b',
241
    'SRG-OS-000480-GPOS-00232': 'CM-6 b',
242
    'SRG-OS-000481-GPOS-000481': 'SC-8'}
243
244
245
def get_iacontrol(srg_str: str) -> str:
246
    srgs = srg_str.split(',')
247
    result = list()
248
    for srg in srgs:
249
        if srg in srgid_to_iacontrol:
250
            result.append(srgid_to_iacontrol[srg])
251
    result_set = set(result)
252
    return ','.join(str(srg) for srg in result_set)
253
254
255
def get_severity(input_severity: str) -> str:
256
    if input_severity not in ['CAT I', 'CAT II', 'CAT III', 'low', 'medium', 'high']:
257
        raise ValueError(f'Severity of {input_severity} is not valid')
258
    elif input_severity in ['CAT I', 'CAT II', 'CAT III']:
259
        return input_severity
260
    else:
261
        return SEVERITY[input_severity]
262
263
264
class DisaStatus:
265
    PENDING = "pending"
266
    PLANNED = "planned"
267
    NOT_APPLICABLE = "Not Applicable"
268
    INHERENTLY_MET = "Applicable - Inherently Met"
269
    DOCUMENTATION = "documentation"
270
    PARTIAL = "Applicable - Configurable"
271
    SUPPORTED = "supported"
272
    AUTOMATED = "Applicable - Configurable"
273
    DOES_NOT_MEET = "Applicable - Does Not Meet"
274
275
    @staticmethod
276
    def from_string(source: str) -> str:
277
        if source == ssg.controls.Status.INHERENTLY_MET:
278
            return DisaStatus.INHERENTLY_MET
279
        elif source == ssg.controls.Status.DOES_NOT_MEET:
280
            return DisaStatus.DOES_NOT_MEET
281
        elif source == ssg.controls.Status.AUTOMATED or ssg.controls.Status.MANUAL:
282
            return DisaStatus.AUTOMATED
283
        return source
284
285
    STATUSES = {AUTOMATED, INHERENTLY_MET, DOES_NOT_MEET, NOT_APPLICABLE}
286
287
288
def html_plain_text(source: str) -> str:
289
    if source is None:
290
        return ""
291
    # Quick and dirty way to clean up HTML fields.
292
    # Add line breaks
293
    result = source.replace("<br />", "\n")
294
    result = result.replace("<tt>", '"')
295
    result = result.replace("</tt>", '"')
296
    result = result.replace("&gt;", ">")
297
    result = result.replace("&lt;", "<")
298
    # Remove all other tags
299
    result = re.sub(r"(?s)<.*?>", " ", result)
300
    return result
301
302
303
def replace_variables(source: str, variables: dict) -> str:
304
    result = source
305
    if source:
306
        sub_element_regex = r'<sub idref="([a-z0-9_]+)" \/>'
307
        matches = re.finditer(sub_element_regex, source, re.MULTILINE)
308
309
        if matches:
310
            for match in matches:
311
                name = re.findall(sub_element_regex, source)[0]
312
                result = result.replace(match.group(), variables.get(name, ''))
313
    return result
314
315
316
def handle_variables(source: str, variables: dict) -> str:
317
    result = replace_variables(source, variables)
318
    return html_plain_text(result)
319
320
321
def get_description_root(srg: ET.Element) -> ET.Element:
322
    # DISA adds escaped XML to the description field
323
    # This method unescapes that XML and parses it
324
    description_xml = "<root>"
325
    description_xml += srg.find('xccdf-1.1:description', NS).text.replace('&lt;', '<') \
326
        .replace('&gt;', '>').replace(' & ', '')
327
    description_xml += "</root>"
328
    description_root = ET.ElementTree(ET.fromstring(description_xml)).getroot()
329
    return description_root
330
331
332
def get_srg_dict(xml_path: str) -> dict:
333
    if not pathlib.Path(xml_path).exists():
334
        sys.stderr.write("XML for SRG was not found\n")
335
        sys.stderr.write(f"Could not open file {xml_path} \n")
336
        exit(1)
337
    root = ET.parse(xml_path).getroot()
338
    srgs = dict()
339
    for group in root.findall('xccdf-1.1:Group', NS):
340
        for srg in group.findall('xccdf-1.1:Rule', NS):
341
            srg_id = srg.find('xccdf-1.1:version', NS).text
342
            srgs[srg_id] = dict()
343
            srgs[srg_id]['severity'] = get_severity(srg.get('severity'))
344
            srgs[srg_id]['title'] = srg.find('xccdf-1.1:title', NS).text
345
            description_root = get_description_root(srg)
346
            srgs[srg_id]['vuln_discussion'] = \
347
                html_plain_text(description_root.find('VulnDiscussion').text)
348
            srgs[srg_id]['cci'] = \
349
                srg.find("xccdf-1.1:ident[@system='http://cyber.mil/cci']", NS).text
350
            srgs[srg_id]['fix'] = srg.find('xccdf-1.1:fix', NS).text
351
            srgs[srg_id]['check'] = \
352
                html_plain_text(srg.find('xccdf-1.1:check/xccdf-1.1:check-content', NS).text)
353
            srgs[srg_id]['ia_controls'] = description_root.find('IAControls').text
354
    return srgs
355
356
357
def handle_rule_yaml(product: str, rule_dir: str, env_yaml: dict) -> ssg.build_yaml.Rule:
358
    rule_file = ssg.rules.get_rule_dir_yaml(rule_dir)
359
360
    rule_yaml = ssg.build_yaml.Rule.from_yaml(rule_file, env_yaml=env_yaml)
361
    rule_yaml.normalize(product)
362
363
    return rule_yaml
364
365
366
def parse_args() -> argparse.Namespace:
367
    parser = argparse.ArgumentParser()
368
    parser.add_argument('-c', '--control', type=str, action="store", required=True,
369
                        help="The control file to parse")
370
    parser.add_argument('-o', '--output', type=str,
371
                        help=f"The path to the output. Defaults to {OUTPUT}",
372
                        default=OUTPUT)
373
    parser.add_argument("-r", "--root", type=str, action="store", default=SSG_ROOT,
374
                        help=f"Path to SSG root directory (defaults to {SSG_ROOT})")
375
    parser.add_argument("-j", "--json", type=str, action="store", default=RULES_JSON,
376
                        help=f"Path to the rules_dir.json (defaults to {RULES_JSON})")
377
    parser.add_argument("-p", "--product", type=str, action="store", required=True,
378
                        help="What product to get STIGs for")
379
    parser.add_argument("-b", "--build-config-yaml", default=BUILD_CONFIG,
380
                        help="YAML file with information about the build configuration.")
381
    parser.add_argument("-m", "--manual", type=str, action="store",
382
                        help="Path to XML XCCDF manual file to use as the source of the SRGs",
383
                        default=SRG_PATH)
384
    parser.add_argument("-f", "--out-format", type=str, choices=("csv", "xlsx", "html"),
385
                        action="store", help="The format the output should take. Defaults to csv",
386
                        default="csv")
387
    return parser.parse_args()
388
389
390
def handle_control(product: str, control: ssg.controls.Control, env_yaml: ssg.environment,
391
                   rule_json: dict, srgs: dict, used_rules: list) -> dict:
392
393
    if len(control.selections) > 0:
394
        for selection in control.selections:
395
            if selection not in used_rules and selection in control.selected:
396
                rule_object = handle_rule_yaml(product, rule_json[selection]['dir'], env_yaml)
397
                row = create_base_row(control, srgs, rule_object)
398
                if control.levels is not None:
399
                    row['Severity'] = get_severity(control.levels[0])
400
                row['Requirement'] = control.title
401
                row['Vul Discussion'] = handle_variables(rule_object.rationale, control.variables)
402
                ocil_var = handle_variables(rule_object.ocil, control.variables)
403
                ocil_clause_var = handle_variables(rule_object.ocil_clause, control.variables)
404
                row['Check'] = f'{ocil_var}\n\n' \
405
                               f'If {ocil_clause_var} then this is a finding.'
406
                row['Fix'] = handle_variables(rule_object.fixtext, control.variables)
407
                row['STIGID'] = rule_object.identifiers.get('cce', "")
408
                if control.status is not None:
409
                    row['Status'] = DisaStatus.from_string(control.status)
410
                else:
411
                    row['Status'] = DisaStatus.AUTOMATED
412
                used_rules.append(selection)
413
                return row
414
            else:
415
                return no_selections_row(control, srgs)
416
417
    else:
418
        return no_selections_row(control, srgs)
419
420
421
def no_selections_row(control, srgs):
422
    row = create_base_row(control, srgs, ssg.build_yaml.Rule('null'))
423
    row['Requirement'] = control.title
424
    row['Status'] = DisaStatus.from_string(control.status)
425
    row['Vul Discussion'] = control.rationale
426
    row['Fix'] = control.fixtext
427
    row['Check'] = control.check
428
    row['Vul Discussion'] = html_plain_text(control.rationale)
429
    row["STIGID"] = ""
430
    return row
431
432
433
def create_base_row(item: ssg.controls.Control, srgs: dict,
434
                    rule_object: ssg.build_yaml.Rule) -> dict:
435
    row = dict()
436
    srg_id = item.id
437
    if srg_id not in srgs:
438
        print(f"Unable to find SRG {srg_id}. Id in the control must be a valid SRGID.")
439
        exit(4)
440
    srg = srgs[srg_id]
441
442
    row['SRGID'] = rule_object.references.get('srg', srg_id)
443
    row['CCI'] = rule_object.references.get('disa', srg['cci'])
444
    row['SRG Requirement'] = srg['title']
445
    row['SRG VulDiscussion'] = srg['vuln_discussion']
446
    row['SRG Check'] = srg['check']
447
    row['SRG Fix'] = srg['fix']
448
    row['Severity'] = get_severity(srg.get('severity'))
449
    row['IA Control'] = get_iacontrol(row['SRGID'])
450
    row['Mitigation'] = item.mitigation
451
    row['Artifact Description'] = item.artifact_description
452
    row['Status Justification'] = item.status_justification
453
    return row
454
455
456
def setup_csv_writer(csv_file: TextIO) -> csv.DictWriter:
457
    csv_writer = csv.DictWriter(csv_file, HEADERS)
458
    csv_writer.writeheader()
459
    return csv_writer
460
461
462
def get_rule_json(json_path: str) -> dict:
463
    with open(json_path, 'r') as json_file:
464
        rule_json = json.load(json_file)
465
    return rule_json
466
467
468
def check_paths(control_path: str, rule_json_path: str) -> None:
469
    control_full_path = pathlib.Path(control_path).absolute()
470
    if not pathlib.Path.exists(control_full_path):
471
        sys.stderr.write(f"Unable to find control file {control_full_path}\n")
472
        exit(5)
473
    rule_json_full_path = pathlib.Path(rule_json_path).absolute()
474
    if not os.path.exists(rule_json_full_path):
475
        sys.stderr.write(f"Unable to find rule_dirs.json file {rule_json_full_path}\n")
476
        sys.stderr.write("Hint: run ./utils/rule_dir_json.py\n")
477
        exit(2)
478
479
480
def get_policy(args, env_yaml) -> ssg.controls.Policy:
481
    policy = ssg.controls.Policy(args.control, env_yaml=env_yaml)
482
    policy.load()
483
    return policy
484
485
486
def handle_csv_output(output, results):
487
    with open(output, 'w') as csv_file:
488
        csv_writer = setup_csv_writer(csv_file)
489
        for row in results:
490
            csv_writer.writerow(row)
491
492
493
def handle_xlsx_output(output, product, results):
494
    output = output.replace('.csv', '.xlsx')
495
    for row in results:
496
        row['IA Control'] = get_iacontrol(row['SRGID'])
497
    convert_srg_export_to_xlsx.handle_dict(results, output, f'{product} SRG Mapping')
498
    return output
499
500
501
def handle_html_output(output, product, results):
502
    for row in results:
503
        row['IA Control'] = get_iacontrol(row['SRGID'])
504
    output = output.replace('.csv', '.html')
505
    convert_srg_export_to_html.handle_dict(results, output, f'{product} SRG Mapping')
506
    return output
507
508
509
def handle_output(output: str, results: list, format_type: str, product: str) -> None:
510
    if format_type == 'csv':
511
        handle_csv_output(output, results)
512
    elif format_type == 'xlsx':
513
        output = handle_xlsx_output(output, product, results)
514
    elif format_type == 'html':
515
        output = handle_html_output(output, product, results)
516
517
    print(f'Wrote output to {output}')
518
519
520
def get_env_yaml(root: str, product: str, build_config_yaml: str) -> dict:
521
    product_dir = os.path.join(root, "products", product)
522
    product_yaml_path = os.path.join(product_dir, "product.yml")
523
    env_yaml = ssg.environment.open_environment(build_config_yaml, str(product_yaml_path))
524
    return env_yaml
525
526
527
def main() -> None:
528
    args = parse_args()
529
    check_paths(args.control, args.json)
530
531
    srgs = get_srg_dict(args.manual)
532
    env_yaml = get_env_yaml(args.root, args.product, args.build_config_yaml)
533
    policy = get_policy(args, env_yaml)
534
    rule_json = get_rule_json(args.json)
535
536
    used_rules = list()
537
    results = list()
538
    for control in policy.controls:
539
        row = handle_control(args.product, control, env_yaml, rule_json, srgs, used_rules)
540
        results.append(row)
541
542
    handle_output(args.output, results, args.out_format, args.product)
543
544
545
if __name__ == '__main__':
546
    main()
547