Repository navigation
Expand file tree
/
Copy pathsummarize_json.py
More file actions
247 lines (215 loc) · 8.59 KB
/
Copy pathsummarize_json.py
File metadata and controls
247 lines (215 loc) · 8.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
import json
from collections import OrderedDict
import argparse
import sys
import json
from modules import validate_args
import os.path
from operator import itemgetter
import copy
def outputResultsJSON(
output_filename: str,
data: list[dict],
baseline: OrderedDict,
failure_manifest_filename="detection_failure_manifest.json",
output_folder: str = "",
summarization_reproduce_failure_config: dict = {},
) -> tuple[bool, int, int, int, int]:
success = True
try:
test_count = len(data)
# Passed
pass_count = len([x for x in data if x["success"] == True])
# A failure or an error
fail_count = len([x for x in data if x["success"] == False])
# An error (every error is also a failure)
fail_and_error_count = len([x for x in data if x["error"] == True])
# A failure without an error
fail_without_error_count = len(
[x for x in data if x["success"] == False and x["error"] == False]
)
# This number should always be zero...
error_and_success_count = len(
[x for x in data if x["success"] == True and x["error"] == True]
)
if error_and_success_count > 0:
print(
"Error - a test was successful, but also included an error. This should be impossible.",
file=sys.stderr,
)
success = False
if test_count != (pass_count + fail_count):
print(
"Error - the total tests [%d] does not equal the pass[%d]/fails[%d]"
% (test_count, pass_count, fail_count)
)
success = False
if fail_count > 0:
result = "FAIL for %d detections" % (fail_count)
success = False
else:
result = "PASS for all %d detections" % (pass_count)
summary = {
"TOTAL_TESTS": test_count,
"TESTS_PASSED": pass_count,
"TOTAL_FAILURES": fail_count,
"FAIL_ONLY": fail_without_error_count,
"PASS_RATE": calculate_pass_rate(pass_count, test_count),
"FAIL_AND_ERROR": fail_and_error_count,
}
data_sorted = sorted(
data, key=lambda k: (-k["error"], k["success"], k["detection_file"])
)
with open(os.path.join(output_folder, output_filename), "w") as jsonFile:
json.dump(
{"summary": summary, "baseline": baseline, "results": data_sorted},
jsonFile,
indent=" ",
)
# Generate a failure that the user can download to reproduce and test ONLY the failures locally.
# This makes it easy to test and debug ONLY those that failed. No need to test the ones
# that succeeded!
fail_list = [
os.path.join("security_content/detections", x["detection_file"])
for x in data_sorted
if x["success"] == False
]
if len(fail_list) > 0:
print("FAILURES:")
for failed_test in fail_list:
print(f"\t{failed_test}")
failures_test_override = copy.deepcopy(
summarization_reproduce_failure_config
)
# Force all tests to be interactive, even if they don't fail (because they failed on this test)
failures_test_override.update(
{
"detections_list": fail_list,
"no_interactive_failure": False,
"interactive": True,
"num_containers": 1,
"branch": baseline["branch"],
"commit_hash": baseline["commit_hash"],
"mode": "selected",
"show_splunk_app_password": True,
}
)
with open(
os.path.join(output_folder, failure_manifest_filename), "w"
) as failures:
validate_args.validate_and_write(failures_test_override, failures)
except Exception as e:
print(
"There was an error generating [%s]: [%s]" % (output_filename, str(e)),
file=sys.stderr,
)
print(data)
raise (e)
# success = False
# return success, False
# note that total failures is fail_count, fail_and_error count is JUST errors (and every error is also a failure)
return success, test_count, pass_count, fail_count, fail_and_error_count
def calculate_pass_rate(pass_count: int, test_count: int) -> float:
if test_count == 0:
# Assume this means 100% pass rate to avoid divide by zero
pass_rate = 1
else:
pass_rate = pass_count / test_count
return pass_rate
def print_summary(
test_count: int, pass_count: int, fail_count: int, error_count: int
) -> None:
print(
"Summary:"
f"\n\tTotal Tests: {test_count}"
f"\n\tTotal Pass : {pass_count}"
f"\n\tTotal Fail : {fail_count} ({error_count} of these were ERRORS))"
f"\n\tPass Rate : {calculate_pass_rate(pass_count, test_count):.3f}"
)
def exit_with_status(
test_pass: bool, test_count: int, pass_count: int, fail_count: int, error_count: int
) -> None:
if not test_pass:
print("Result: FAIL")
# print("DURING TESTING, THIS WILL STILL EXIT WITH AN EXIT CODE OF 0 (SUCCESS) TO ALLOW THE WORKFLOW "
# "TO PASS AND CI/CD TO CONTINUE. THIS WILL BE CHANGED IN A FUTURE VERSION.")
# sys.exit(0)
sys.exit(1)
else:
print("Result: PASS!")
sys.exit(0)
def finish(
test_pass: bool, test_count: int, pass_count: int, fail_count: int, error_count: int
) -> None:
print_summary(test_count, pass_count, fail_count, error_count)
exit_with_status(test_pass, test_count, pass_count, fail_count, error_count)
def main():
parser = argparse.ArgumentParser(description="Results Merger")
parser.add_argument(
"-f",
"--files",
type=argparse.FileType("r"),
required=True,
nargs="+",
help="The json files you would like to combine into a single file",
)
parser.add_argument(
"-o",
"--output_filename",
type=str,
required=True,
help="The name of the output file",
)
parser.add_argument("--smoketest", action=argparse.BooleanOptionalAction)
args = parser.parse_args()
all_data = OrderedDict()
try:
print("We will summarize the files: %s" % (str([f.name for f in args.files])))
for f in args.files:
if not f.name.endswith(".json"):
print(
"Error: passed in file must end in .json - you passed in [%s].\n\tQuitting..."
% (f.name)
)
sys.exit(1)
data = json.loads(f.read())
if "baseline" in all_data:
# everything has the same baseline, only need to do it once
pass
else:
all_data["baseline"] = data["baseline"]
if "results" in all_data:
# this is a list of dictionaries, so add to it
all_data["results"].extend(data["results"])
else:
all_data["results"] = data["results"]
IGNORE_MESSAGES = [
"Model does not exist", # model not generated by baseline
"Data model 'Identity_Management' was not found", # missing datamodel included with es
"get_asset", # missing macro included with es
"Failed to load model", # when running a model that has not been downloaded separately
"UEBA", # Another missing ES asset
]
if args.smoketest:
new_results = []
for result in all_data["results"]:
if result.get("detection_error", None):
message = result.get("detection_error", None)
ignore = False
for ignore_message in IGNORE_MESSAGES:
if ignore_message in message:
ignore = True
break
if not ignore:
new_results.append(result)
pass
all_data["results"] = new_results
test_pass, test_count, pass_count, fail_count, error_count = outputResultsJSON(
args.output_filename, all_data["results"], all_data["baseline"]
)
finish(test_pass, test_count, pass_count, fail_count, error_count)
except Exception as e:
print("Error generating the summary file: [%s].\n\tQuitting..." % (str(e)))
sys.exit(1)
if __name__ == "__main__":
main()