|
1
|
|
|
#!/usr/bin/env python3 |
|
2
|
|
|
|
|
3
|
|
|
import argparse |
|
4
|
|
|
import os |
|
5
|
|
|
import sys |
|
6
|
|
|
|
|
7
|
|
|
try: |
|
8
|
|
|
import ssg.xml |
|
9
|
|
|
from ssg.constants import PREFIX_TO_NS, SSG_REF_URIS, XCCDF12_NS |
|
10
|
|
|
except ImportError: |
|
11
|
|
|
print("The ssg module could not be found.") |
|
12
|
|
|
print("Run .pyenv.sh available in the project root directory," |
|
13
|
|
|
" or add it to PYTHONPATH manually.") |
|
14
|
|
|
print("$ source .pyenv.sh") |
|
15
|
|
|
exit(1) |
|
16
|
|
|
|
|
17
|
|
|
from xml.etree import ElementTree as ElementTree |
|
18
|
|
|
|
|
19
|
|
|
# So we don't hard code "xccdf-1.2" |
|
20
|
|
|
XCCDF12 = list(PREFIX_TO_NS.keys())[list(PREFIX_TO_NS.values()).index(XCCDF12_NS)] |
|
21
|
|
|
|
|
22
|
|
|
|
|
23
|
|
|
class Status: |
|
24
|
|
|
PASS = "pass" |
|
25
|
|
|
FAIL = "fail" |
|
26
|
|
|
ERROR = "error" |
|
27
|
|
|
NOT_CHECKED = "notchecked" |
|
28
|
|
|
NOT_SELECTED = "notselected" |
|
29
|
|
|
NOT_APPLICABLE = "notapplicable" |
|
30
|
|
|
INFORMATION = "informational" |
|
31
|
|
|
|
|
32
|
|
|
@classmethod |
|
33
|
|
|
def get_wining_status(cls, current_status: str, proposed: str) -> str: |
|
34
|
|
|
if current_status == cls.ERROR: |
|
35
|
|
|
return current_status |
|
36
|
|
|
elif current_status == cls.FAIL: |
|
37
|
|
|
return current_status |
|
38
|
|
|
elif current_status == cls.NOT_APPLICABLE: |
|
39
|
|
|
return current_status |
|
40
|
|
|
elif current_status == cls.NOT_SELECTED: |
|
41
|
|
|
return current_status |
|
42
|
|
|
elif current_status == cls.NOT_CHECKED: |
|
43
|
|
|
return current_status |
|
44
|
|
|
elif current_status == cls.INFORMATION: |
|
45
|
|
|
return current_status |
|
46
|
|
|
return proposed |
|
47
|
|
|
|
|
48
|
|
|
|
|
49
|
|
|
def parse_args() -> argparse.Namespace: |
|
50
|
|
|
parser = argparse.ArgumentParser(description='Compare two result ARF files.') |
|
51
|
|
|
parser.add_argument('base', help='Path to the first ARF file to compare') |
|
52
|
|
|
parser.add_argument('target', help='Path to the second ARF file to compare') |
|
53
|
|
|
return parser.parse_args() |
|
54
|
|
|
|
|
55
|
|
|
|
|
56
|
|
|
def is_file_disa(xml: ElementTree.ElementTree) -> bool: |
|
57
|
|
|
return 'DISA' in xml.find(f'.//{XCCDF12}:metadata/dc:creator', PREFIX_TO_NS).text |
|
58
|
|
|
|
|
59
|
|
|
|
|
60
|
|
|
def is_file_ssg(xml: ElementTree.ElementTree) -> bool: |
|
61
|
|
|
return 'SCAP Security Guide Project' in \ |
|
62
|
|
|
xml.find(f'.//{XCCDF12}:metadata/dc:creator', PREFIX_TO_NS).text |
|
63
|
|
|
|
|
64
|
|
|
|
|
65
|
|
|
def check_file(path: str) -> bool: |
|
66
|
|
|
if not os.path.exists(path): |
|
67
|
|
|
sys.stderr.write(f"File not found: {path}") |
|
68
|
|
|
exit(1) |
|
69
|
|
|
return True |
|
70
|
|
|
|
|
71
|
|
|
|
|
72
|
|
|
def get_rule_to_stig_dict(xml: ElementTree.ElementTree, is_disa: bool) -> dict: |
|
73
|
|
|
rules = dict() |
|
74
|
|
|
for group in xml.findall(f'{XCCDF12}:Group', PREFIX_TO_NS): |
|
75
|
|
|
for sub_groups in group.findall(f'{XCCDF12}:Group', PREFIX_TO_NS): |
|
76
|
|
|
rules.update(get_rule_to_stig_dict(ElementTree.ElementTree(sub_groups), is_disa)) |
|
77
|
|
|
for rule in group.findall(f'{XCCDF12}:Rule', PREFIX_TO_NS): |
|
78
|
|
|
rule_id = rule.attrib['id'] |
|
79
|
|
|
if is_disa: |
|
80
|
|
|
stig_id = rule.find(f'{XCCDF12}:version', PREFIX_TO_NS).text |
|
81
|
|
|
else: |
|
82
|
|
|
elm = rule.find(f"{XCCDF12}:reference[@href='{SSG_REF_URIS['stigid']}']", |
|
83
|
|
|
PREFIX_TO_NS) |
|
84
|
|
|
if elm is not None: |
|
85
|
|
|
stig_id = elm.text |
|
86
|
|
|
else: |
|
87
|
|
|
continue |
|
88
|
|
|
if stig_id in rules: |
|
89
|
|
|
rules[stig_id].append(rule_id) |
|
90
|
|
|
else: |
|
91
|
|
|
rules[stig_id] = [rule_id] |
|
92
|
|
|
return rules |
|
93
|
|
|
|
|
94
|
|
|
|
|
95
|
|
|
def get_results(xml: ElementTree.ElementTree) -> dict: |
|
96
|
|
|
rules = dict() |
|
97
|
|
|
|
|
98
|
|
|
results_xml = xml.findall('.//xccdf-1.2:TestResult/xccdf-1.2:rule-result', |
|
99
|
|
|
ssg.constants.PREFIX_TO_NS) |
|
100
|
|
|
for result in results_xml: |
|
101
|
|
|
idref = result.attrib['idref'] |
|
102
|
|
|
rules[idref] = result.find('xccdf-1.2:result', ssg.constants.PREFIX_TO_NS).text |
|
103
|
|
|
|
|
104
|
|
|
return rules |
|
105
|
|
|
|
|
106
|
|
|
|
|
107
|
|
|
def file_a_different_type(base_tree: ElementTree.ElementTree, |
|
108
|
|
|
target_tree: ElementTree.ElementTree) \ |
|
109
|
|
|
-> bool: |
|
110
|
|
|
""" |
|
111
|
|
|
Check if both files are the same type. |
|
112
|
|
|
|
|
113
|
|
|
:param base_tree: base tree to check |
|
114
|
|
|
:param target_tree: target tree to check |
|
115
|
|
|
:return: true if the give trees are not the same type, otherwise return false. |
|
116
|
|
|
""" |
|
117
|
|
|
return is_file_disa(base_tree) != is_file_disa(target_tree) or \ |
|
118
|
|
|
is_file_ssg(base_tree) != is_file_ssg(target_tree) |
|
119
|
|
|
|
|
120
|
|
|
|
|
121
|
|
|
def flatten_stig_results(stig_results: dict) -> dict: |
|
122
|
|
|
base_stig_flat_results = dict() |
|
123
|
|
|
for stig, results in stig_results.items(): |
|
124
|
|
|
if len(results) == 1: |
|
125
|
|
|
base_stig_flat_results[stig] = results[0] |
|
126
|
|
|
status = results[0] |
|
127
|
|
|
for result in results: |
|
128
|
|
|
if result == status: |
|
129
|
|
|
continue |
|
130
|
|
|
else: |
|
131
|
|
|
status = Status.get_wining_status(status, result) |
|
132
|
|
|
base_stig_flat_results[stig] = status |
|
133
|
|
|
return base_stig_flat_results |
|
134
|
|
|
|
|
135
|
|
|
|
|
136
|
|
|
def get_results_by_stig(results: dict, stigs: dict) -> dict: |
|
137
|
|
|
base_stig_results = dict() |
|
138
|
|
|
for base_stig, rules in stigs.items(): |
|
139
|
|
|
base_stig_results[base_stig] = list() |
|
140
|
|
|
for rule in rules: |
|
141
|
|
|
base_stig_results[base_stig].append(results.get(rule)) |
|
142
|
|
|
return base_stig_results |
|
143
|
|
|
|
|
144
|
|
|
|
|
145
|
|
|
def print_summary(base_stig_flat_results: dict, different_results: dict, missing_in_target: list, |
|
146
|
|
|
same_status: list) -> None: |
|
147
|
|
|
print(f'Missing in target: {len(missing_in_target)}') |
|
148
|
|
|
for rule in missing_in_target: |
|
149
|
|
|
print(f'\t{rule}') |
|
150
|
|
|
print(f'Same Status: {len(same_status)}') |
|
151
|
|
|
for rule in same_status: |
|
152
|
|
|
print(f'\t{rule}\t\t{base_stig_flat_results[rule]}') |
|
153
|
|
|
print(f'Different results: {len(different_results)}') |
|
154
|
|
|
for rule, value in different_results.items(): |
|
155
|
|
|
print(f'\t{rule}\t\t{value[0]} - {value[1]}') |
|
156
|
|
|
|
|
157
|
|
|
|
|
158
|
|
|
def process_stig_results(base_results: dict, target_results: dict, |
|
159
|
|
|
base_tree: ElementTree.ElementTree, |
|
160
|
|
|
target_tree: ElementTree.ElementTree) -> (dict, dict): |
|
161
|
|
|
base_stigs = get_rule_to_stig_dict(base_tree, is_file_disa(base_tree)) |
|
162
|
|
|
target_stigs = get_rule_to_stig_dict(target_tree, is_file_disa(target_tree)) |
|
163
|
|
|
base_stig_results = get_results_by_stig(base_results, base_stigs) |
|
164
|
|
|
target_stig_results = get_results_by_stig(target_results, target_stigs) |
|
165
|
|
|
base_stig_flat_results = flatten_stig_results(base_stig_results) |
|
166
|
|
|
target_stig_flat_results = flatten_stig_results(target_stig_results) |
|
167
|
|
|
return base_stig_flat_results, target_stig_flat_results |
|
168
|
|
|
|
|
169
|
|
|
|
|
170
|
|
|
def do_compare(base_results: dict, target_results: dict) -> None: |
|
171
|
|
|
same_status = list() |
|
172
|
|
|
missing_in_target = list() |
|
173
|
|
|
different_results = dict() |
|
174
|
|
|
for base_result_id, base_result in base_results.items(): |
|
175
|
|
|
target_result = target_results.get(base_result_id) |
|
176
|
|
|
if not target_result: |
|
177
|
|
|
missing_in_target.append(base_result_id) |
|
178
|
|
|
continue |
|
179
|
|
|
if base_result != target_result: |
|
180
|
|
|
different_results[base_result_id] = (base_result, target_result) |
|
181
|
|
|
else: |
|
182
|
|
|
same_status.append(base_result_id) |
|
183
|
|
|
print_summary(base_results, different_results, missing_in_target, same_status) |
|
184
|
|
|
|
|
185
|
|
|
|
|
186
|
|
|
def match_results(base_tree: ElementTree.ElementTree, target_tree: ElementTree.ElementTree): |
|
187
|
|
|
diff_type = file_a_different_type(base_tree, target_tree) |
|
188
|
|
|
base_results = get_results(base_tree) |
|
189
|
|
|
target_results = get_results(target_tree) |
|
190
|
|
|
|
|
191
|
|
|
if diff_type: |
|
192
|
|
|
base_stig_flat_results, target_stig_flat_results = process_stig_results(base_results, |
|
193
|
|
|
target_results, |
|
194
|
|
|
base_tree, |
|
195
|
|
|
target_tree) |
|
196
|
|
|
|
|
197
|
|
|
do_compare(base_stig_flat_results, target_stig_flat_results) |
|
198
|
|
|
exit(0) |
|
199
|
|
|
|
|
200
|
|
|
do_compare(base_results, target_results) |
|
201
|
|
|
|
|
202
|
|
|
|
|
203
|
|
|
def main(): |
|
204
|
|
|
args = parse_args() |
|
205
|
|
|
check_file(args.base) |
|
206
|
|
|
check_file(args.target) |
|
207
|
|
|
base_tree = ssg.xml.open_xml(args.base) |
|
208
|
|
|
target_tree = ssg.xml.open_xml(args.target) |
|
209
|
|
|
match_results(base_tree, target_tree) |
|
210
|
|
|
|
|
211
|
|
|
|
|
212
|
|
|
if __name__ == '__main__': |
|
213
|
|
|
main() |
|
214
|
|
|
|