contentctl 4.2.1__py3-none-any.whl → 4.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +41 -47
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +1 -1
- contentctl/actions/detection_testing/views/DetectionTestingView.py +1 -4
- contentctl/actions/validate.py +40 -1
- contentctl/enrichments/attack_enrichment.py +6 -8
- contentctl/enrichments/cve_enrichment.py +3 -3
- contentctl/helper/splunk_app.py +263 -0
- contentctl/input/director.py +1 -1
- contentctl/input/ssa_detection_builder.py +8 -6
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +362 -336
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +117 -103
- contentctl/objects/atomic.py +7 -10
- contentctl/objects/base_test.py +1 -1
- contentctl/objects/base_test_result.py +7 -5
- contentctl/objects/baseline_tags.py +2 -30
- contentctl/objects/config.py +5 -4
- contentctl/objects/correlation_search.py +316 -96
- contentctl/objects/data_source.py +7 -2
- contentctl/objects/detection_tags.py +128 -102
- contentctl/objects/errors.py +18 -0
- contentctl/objects/lookup.py +3 -1
- contentctl/objects/mitre_attack_enrichment.py +3 -3
- contentctl/objects/notable_event.py +20 -0
- contentctl/objects/observable.py +20 -26
- contentctl/objects/risk_analysis_action.py +2 -2
- contentctl/objects/risk_event.py +315 -0
- contentctl/objects/ssa_detection_tags.py +1 -1
- contentctl/objects/story_tags.py +2 -2
- contentctl/objects/unit_test.py +1 -9
- contentctl/output/data_source_writer.py +4 -4
- contentctl/output/templates/savedsearches_detections.j2 +0 -8
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/METADATA +5 -8
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/RECORD +36 -32
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/LICENSE.md +0 -0
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/WHEEL +0 -0
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/entry_points.txt +0 -0
|
@@ -85,7 +85,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
85
85
|
hec_channel: str = ""
|
|
86
86
|
_conn: client.Service = PrivateAttr()
|
|
87
87
|
pbar: tqdm.tqdm = None
|
|
88
|
-
start_time: float = None
|
|
88
|
+
start_time: Optional[float] = None
|
|
89
89
|
|
|
90
90
|
class Config:
|
|
91
91
|
arbitrary_types_allowed = True
|
|
@@ -136,7 +136,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
136
136
|
TestReportingType.SETUP,
|
|
137
137
|
self.get_name(),
|
|
138
138
|
msg,
|
|
139
|
-
self.start_time,
|
|
140
139
|
update_sync_status=True,
|
|
141
140
|
)
|
|
142
141
|
func()
|
|
@@ -147,7 +146,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
147
146
|
self.finish()
|
|
148
147
|
return
|
|
149
148
|
|
|
150
|
-
self.format_pbar_string(TestReportingType.SETUP, self.get_name(), "Finished Setup!"
|
|
149
|
+
self.format_pbar_string(TestReportingType.SETUP, self.get_name(), "Finished Setup!")
|
|
151
150
|
|
|
152
151
|
def wait_for_ui_ready(self):
|
|
153
152
|
self.get_conn()
|
|
@@ -216,7 +215,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
216
215
|
TestReportingType.SETUP,
|
|
217
216
|
self.get_name(),
|
|
218
217
|
"Waiting for reboot",
|
|
219
|
-
self.start_time,
|
|
220
218
|
update_sync_status=True,
|
|
221
219
|
)
|
|
222
220
|
else:
|
|
@@ -236,18 +234,12 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
236
234
|
self.pbar.write(
|
|
237
235
|
f"Error getting API connection (not quitting) '{type(e).__name__}': {str(e)}"
|
|
238
236
|
)
|
|
239
|
-
print("wow")
|
|
240
|
-
# self.pbar.write(
|
|
241
|
-
# f"Unhandled exception getting connection to splunk server: {str(e)}"
|
|
242
|
-
# )
|
|
243
|
-
# self.sync_obj.terminate = True
|
|
244
237
|
|
|
245
238
|
for _ in range(sleep_seconds):
|
|
246
239
|
self.format_pbar_string(
|
|
247
240
|
TestReportingType.SETUP,
|
|
248
241
|
self.get_name(),
|
|
249
242
|
"Getting API Connection",
|
|
250
|
-
self.start_time,
|
|
251
243
|
update_sync_status=True,
|
|
252
244
|
)
|
|
253
245
|
time.sleep(1)
|
|
@@ -318,7 +310,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
318
310
|
TestReportingType.SETUP,
|
|
319
311
|
self.get_name(),
|
|
320
312
|
"Configuring Datamodels",
|
|
321
|
-
self.start_time,
|
|
322
313
|
)
|
|
323
314
|
|
|
324
315
|
def configure_conf_file_datamodels(self, APP_NAME: str = "Splunk_SA_CIM"):
|
|
@@ -424,7 +415,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
424
415
|
TestReportingType.GROUP,
|
|
425
416
|
test_group.name,
|
|
426
417
|
FinalTestingStates.SKIP.value,
|
|
427
|
-
time.time(),
|
|
418
|
+
start_time=time.time(),
|
|
428
419
|
set_pbar=False,
|
|
429
420
|
)
|
|
430
421
|
)
|
|
@@ -465,7 +456,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
465
456
|
TestReportingType.GROUP,
|
|
466
457
|
test_group.name,
|
|
467
458
|
TestingStates.DONE_GROUP.value,
|
|
468
|
-
setup_results.start_time,
|
|
459
|
+
start_time=setup_results.start_time,
|
|
469
460
|
set_pbar=False,
|
|
470
461
|
)
|
|
471
462
|
)
|
|
@@ -486,7 +477,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
486
477
|
TestReportingType.GROUP,
|
|
487
478
|
test_group.name,
|
|
488
479
|
TestingStates.BEGINNING_GROUP.value,
|
|
489
|
-
setup_start_time
|
|
480
|
+
start_time=setup_start_time
|
|
490
481
|
)
|
|
491
482
|
# https://github.com/WoLpH/python-progressbar/issues/164
|
|
492
483
|
# Use NullBar if there is more than 1 container or we are running
|
|
@@ -526,7 +517,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
526
517
|
TestReportingType.GROUP,
|
|
527
518
|
test_group.name,
|
|
528
519
|
TestingStates.DELETING.value,
|
|
529
|
-
test_group_start_time,
|
|
520
|
+
start_time=test_group_start_time,
|
|
530
521
|
)
|
|
531
522
|
|
|
532
523
|
# TODO: do we want to clean up even if replay failed? Could have been partial failure?
|
|
@@ -544,7 +535,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
544
535
|
test_reporting_type: TestReportingType,
|
|
545
536
|
test_name: str,
|
|
546
537
|
state: str,
|
|
547
|
-
start_time:
|
|
538
|
+
start_time: Optional[float] = None,
|
|
548
539
|
set_pbar: bool = True,
|
|
549
540
|
update_sync_status: bool = False,
|
|
550
541
|
) -> str:
|
|
@@ -559,8 +550,13 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
559
550
|
:param update_sync_status: bool indicating whether a sync status update should be queued
|
|
560
551
|
:returns: a formatted string for use w/ pbar
|
|
561
552
|
"""
|
|
562
|
-
# set start time
|
|
553
|
+
# set start time if not provided
|
|
563
554
|
if start_time is None:
|
|
555
|
+
# if self.start_time is still None, something went wrong
|
|
556
|
+
if self.start_time is None:
|
|
557
|
+
raise ValueError(
|
|
558
|
+
"self.start_time is still None; a function may have been called before self.setup()"
|
|
559
|
+
)
|
|
564
560
|
start_time = self.start_time
|
|
565
561
|
|
|
566
562
|
# invoke the helper method
|
|
@@ -575,7 +571,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
575
571
|
|
|
576
572
|
# update sync status if needed
|
|
577
573
|
if update_sync_status:
|
|
578
|
-
self.sync_obj.currentTestingQueue[self.get_name()] = {
|
|
574
|
+
self.sync_obj.currentTestingQueue[self.get_name()] = { # type: ignore
|
|
579
575
|
"name": state,
|
|
580
576
|
"search": "N/A",
|
|
581
577
|
}
|
|
@@ -621,7 +617,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
621
617
|
TestReportingType.UNIT,
|
|
622
618
|
f"{detection.name}:{test.name}",
|
|
623
619
|
TestingStates.BEGINNING_TEST,
|
|
624
|
-
test_start_time,
|
|
620
|
+
start_time=test_start_time,
|
|
625
621
|
)
|
|
626
622
|
|
|
627
623
|
# if the replay failed, record the test failure and return
|
|
@@ -690,14 +686,14 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
690
686
|
res = "ERROR"
|
|
691
687
|
link = detection.search
|
|
692
688
|
else:
|
|
693
|
-
res = test.result.status.value.upper()
|
|
689
|
+
res = test.result.status.value.upper() # type: ignore
|
|
694
690
|
link = test.result.get_summary_dict()["sid_link"]
|
|
695
691
|
|
|
696
692
|
self.format_pbar_string(
|
|
697
693
|
TestReportingType.UNIT,
|
|
698
694
|
f"{detection.name}:{test.name}",
|
|
699
695
|
f"{res} - {link} (CTRL+D to continue)",
|
|
700
|
-
test_start_time,
|
|
696
|
+
start_time=test_start_time,
|
|
701
697
|
)
|
|
702
698
|
|
|
703
699
|
# Wait for user input
|
|
@@ -722,7 +718,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
722
718
|
TestReportingType.UNIT,
|
|
723
719
|
f"{detection.name}:{test.name}",
|
|
724
720
|
FinalTestingStates.PASS.value,
|
|
725
|
-
test_start_time,
|
|
721
|
+
start_time=test_start_time,
|
|
726
722
|
set_pbar=False,
|
|
727
723
|
)
|
|
728
724
|
)
|
|
@@ -744,7 +740,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
744
740
|
TestReportingType.UNIT,
|
|
745
741
|
f"{detection.name}:{test.name}",
|
|
746
742
|
FinalTestingStates.FAIL.value,
|
|
747
|
-
test_start_time,
|
|
743
|
+
start_time=test_start_time,
|
|
748
744
|
set_pbar=False,
|
|
749
745
|
)
|
|
750
746
|
)
|
|
@@ -755,7 +751,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
755
751
|
TestReportingType.UNIT,
|
|
756
752
|
f"{detection.name}:{test.name}",
|
|
757
753
|
FinalTestingStates.ERROR.value,
|
|
758
|
-
test_start_time,
|
|
754
|
+
start_time=test_start_time,
|
|
759
755
|
set_pbar=False,
|
|
760
756
|
)
|
|
761
757
|
)
|
|
@@ -770,7 +766,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
770
766
|
stdout.flush()
|
|
771
767
|
test.result.duration = round(time.time() - test_start_time, 2)
|
|
772
768
|
|
|
773
|
-
# TODO (
|
|
769
|
+
# TODO (#227): break up the execute routines for integration/unit tests some more to remove
|
|
774
770
|
# code w/ similar structure
|
|
775
771
|
def execute_integration_test(
|
|
776
772
|
self,
|
|
@@ -837,7 +833,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
837
833
|
TestReportingType.INTEGRATION,
|
|
838
834
|
f"{detection.name}:{test.name}",
|
|
839
835
|
TestingStates.BEGINNING_TEST,
|
|
840
|
-
test_start_time,
|
|
836
|
+
start_time=test_start_time,
|
|
841
837
|
)
|
|
842
838
|
|
|
843
839
|
# if the replay failed, record the test failure and return
|
|
@@ -874,15 +870,10 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
874
870
|
start_time=test_start_time
|
|
875
871
|
)
|
|
876
872
|
|
|
877
|
-
# TODO (
|
|
878
|
-
# test case; typically, there is only one unit test, and thus one integration test,
|
|
879
|
-
# per detection, so this is not an issue. However, if we start having many test cases
|
|
880
|
-
# per detection, we will be duplicating some effort & network calls that we don't need
|
|
881
|
-
# to. Consider refactoring in order to re-use CorrelationSearch objects across tests
|
|
882
|
-
# in such a case
|
|
873
|
+
# TODO (#228): consider reusing CorrelationSearch instances across test cases
|
|
883
874
|
# Instantiate the CorrelationSearch
|
|
884
875
|
correlation_search = CorrelationSearch(
|
|
885
|
-
|
|
876
|
+
detection=detection,
|
|
886
877
|
service=self.get_conn(),
|
|
887
878
|
pbar_data=pbar_data,
|
|
888
879
|
)
|
|
@@ -892,14 +883,12 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
892
883
|
except Exception as e:
|
|
893
884
|
# Catch and report and unhandled exceptions in integration testing
|
|
894
885
|
test.result = IntegrationTestResult(
|
|
895
|
-
message="TEST
|
|
886
|
+
message="TEST ERROR: unhandled exception in CorrelationSearch",
|
|
896
887
|
exception=e,
|
|
897
888
|
status=TestResultStatus.ERROR
|
|
898
889
|
)
|
|
899
890
|
|
|
900
|
-
# TODO (
|
|
901
|
-
# correlation_search happen after the user breaks the interactivity; currently
|
|
902
|
-
# risk/notable indexes are dumped before the user can inspect
|
|
891
|
+
# TODO (#229): when in interactive mode, cleanup should happen after user interaction
|
|
903
892
|
# Pause here if the terminate flag has NOT been set AND either of the below are true:
|
|
904
893
|
# 1. the behavior is always_pause
|
|
905
894
|
# 2. the behavior is pause_on_failure and the test failed
|
|
@@ -908,7 +897,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
908
897
|
if test.result is None:
|
|
909
898
|
res = "ERROR"
|
|
910
899
|
else:
|
|
911
|
-
res = test.result.status.value.upper()
|
|
900
|
+
res = test.result.status.value.upper() # type: ignore
|
|
912
901
|
|
|
913
902
|
# Get the link to the saved search in this specific instance
|
|
914
903
|
link = f"https://{self.infrastructure.instance_address}:{self.infrastructure.web_ui_port}"
|
|
@@ -917,7 +906,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
917
906
|
TestReportingType.INTEGRATION,
|
|
918
907
|
f"{detection.name}:{test.name}",
|
|
919
908
|
f"{res} - {link} (CTRL+D to continue)",
|
|
920
|
-
test_start_time,
|
|
909
|
+
start_time=test_start_time,
|
|
921
910
|
)
|
|
922
911
|
|
|
923
912
|
# Wait for user input
|
|
@@ -1036,8 +1025,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1036
1025
|
search = f"{detection.search} {test.pass_condition}"
|
|
1037
1026
|
|
|
1038
1027
|
# Ensure searches that do not begin with '|' must begin with 'search '
|
|
1039
|
-
if not search.strip().startswith("|"):
|
|
1040
|
-
if not search.strip().startswith("search "):
|
|
1028
|
+
if not search.strip().startswith("|"): # type: ignore
|
|
1029
|
+
if not search.strip().startswith("search "): # type: ignore
|
|
1041
1030
|
search = f"search {search}"
|
|
1042
1031
|
|
|
1043
1032
|
# exponential backoff for wait time
|
|
@@ -1054,7 +1043,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1054
1043
|
TestReportingType.UNIT,
|
|
1055
1044
|
f"{detection.name}:{test.name}",
|
|
1056
1045
|
TestingStates.PROCESSING.value,
|
|
1057
|
-
start_time
|
|
1046
|
+
start_time=start_time
|
|
1058
1047
|
)
|
|
1059
1048
|
|
|
1060
1049
|
time.sleep(1)
|
|
@@ -1063,7 +1052,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1063
1052
|
TestReportingType.UNIT,
|
|
1064
1053
|
f"{detection.name}:{test.name}",
|
|
1065
1054
|
TestingStates.SEARCHING.value,
|
|
1066
|
-
start_time,
|
|
1055
|
+
start_time=start_time,
|
|
1067
1056
|
)
|
|
1068
1057
|
|
|
1069
1058
|
# Execute the search and read the results
|
|
@@ -1079,7 +1068,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1079
1068
|
test.result = UnitTestResult()
|
|
1080
1069
|
|
|
1081
1070
|
# Initialize the collection of fields that are empty that shouldn't be
|
|
1082
|
-
empty_fields = set()
|
|
1071
|
+
empty_fields: set[str] = set()
|
|
1083
1072
|
|
|
1084
1073
|
# Filter out any messages in the results
|
|
1085
1074
|
for result in results:
|
|
@@ -1194,10 +1183,15 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1194
1183
|
):
|
|
1195
1184
|
tempfile = mktemp(dir=tmp_dir)
|
|
1196
1185
|
|
|
1186
|
+
|
|
1197
1187
|
if not (str(attack_data_file.data).startswith("http://") or
|
|
1198
1188
|
str(attack_data_file.data).startswith("https://")) :
|
|
1199
1189
|
if pathlib.Path(str(attack_data_file.data)).is_file():
|
|
1200
|
-
self.format_pbar_string(TestReportingType.GROUP,
|
|
1190
|
+
self.format_pbar_string(TestReportingType.GROUP,
|
|
1191
|
+
test_group.name,
|
|
1192
|
+
"Copying Data",
|
|
1193
|
+
test_group_start_time)
|
|
1194
|
+
|
|
1201
1195
|
try:
|
|
1202
1196
|
copyfile(str(attack_data_file.data), tempfile)
|
|
1203
1197
|
except Exception as e:
|
|
@@ -1221,7 +1215,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1221
1215
|
TestReportingType.GROUP,
|
|
1222
1216
|
test_group.name,
|
|
1223
1217
|
TestingStates.DOWNLOADING.value,
|
|
1224
|
-
test_group_start_time
|
|
1218
|
+
start_time=test_group_start_time
|
|
1225
1219
|
)
|
|
1226
1220
|
|
|
1227
1221
|
Utils.download_file_from_http(
|
|
@@ -1240,7 +1234,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1240
1234
|
TestReportingType.GROUP,
|
|
1241
1235
|
test_group.name,
|
|
1242
1236
|
TestingStates.REPLAYING.value,
|
|
1243
|
-
test_group_start_time
|
|
1237
|
+
start_time=test_group_start_time
|
|
1244
1238
|
)
|
|
1245
1239
|
|
|
1246
1240
|
self.hec_raw_replay(tempfile, attack_data_file)
|
contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py
CHANGED
|
@@ -75,7 +75,7 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
75
75
|
mounts = [
|
|
76
76
|
docker.types.Mount(
|
|
77
77
|
source=str(self.global_config.getLocalAppDir()),
|
|
78
|
-
target=
|
|
78
|
+
target=(self.global_config.getContainerAppDir()).as_posix(),
|
|
79
79
|
type="bind",
|
|
80
80
|
read_only=True,
|
|
81
81
|
)
|
|
@@ -152,10 +152,7 @@ class DetectionTestingView(BaseModel, abc.ABC):
|
|
|
152
152
|
total_pass, total_detections-total_skipped, 1
|
|
153
153
|
)
|
|
154
154
|
|
|
155
|
-
# TODO (
|
|
156
|
-
# sucess/failure? maybe configurable reporting? add section to summary called
|
|
157
|
-
# "testwise_summary" listing per test metrics (e.g. total test, total tests passed, ...);
|
|
158
|
-
# also list num skipped at both detection and test level
|
|
155
|
+
# TODO (#230): expand testing metrics reported
|
|
159
156
|
# Construct and return the larger results dict
|
|
160
157
|
result_dict = {
|
|
161
158
|
"summary": {
|
contentctl/actions/validate.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
|
|
2
2
|
import pathlib
|
|
3
|
+
|
|
3
4
|
from contentctl.input.director import Director, DirectorOutputDto
|
|
4
5
|
from contentctl.objects.config import validate
|
|
5
6
|
from contentctl.enrichments.attack_enrichment import AttackEnrichment
|
|
6
7
|
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
7
8
|
from contentctl.objects.atomic import AtomicTest
|
|
8
9
|
from contentctl.helper.utils import Utils
|
|
10
|
+
from contentctl.objects.data_source import DataSource
|
|
11
|
+
from contentctl.helper.splunk_app import SplunkApp
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Validate:
|
|
@@ -33,6 +36,9 @@ class Validate:
|
|
|
33
36
|
director = Director(director_output_dto)
|
|
34
37
|
director.execute(input_dto)
|
|
35
38
|
self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto)
|
|
39
|
+
if input_dto.data_source_TA_validation:
|
|
40
|
+
self.validate_latest_TA_information(director_output_dto.data_sources)
|
|
41
|
+
|
|
36
42
|
return director_output_dto
|
|
37
43
|
|
|
38
44
|
|
|
@@ -72,4 +78,37 @@ class Validate:
|
|
|
72
78
|
if len(unusedLookupFiles) > 0:
|
|
73
79
|
raise Exception(f"The following .csv or .mlmodel files exist in '{lookupsDirectory}', but are not referenced by a lookup file: {[str(path) for path in unusedLookupFiles]}")
|
|
74
80
|
return
|
|
75
|
-
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_latest_TA_information(self, data_sources: list[DataSource]) -> None:
|
|
84
|
+
validated_TAs: list[tuple[str, str]] = []
|
|
85
|
+
errors:list[str] = []
|
|
86
|
+
print("----------------------")
|
|
87
|
+
print("Validating latest TA:")
|
|
88
|
+
print("----------------------")
|
|
89
|
+
for data_source in data_sources:
|
|
90
|
+
for supported_TA in data_source.supported_TA:
|
|
91
|
+
ta_identifier = (supported_TA.name, supported_TA.version)
|
|
92
|
+
if ta_identifier in validated_TAs:
|
|
93
|
+
continue
|
|
94
|
+
if supported_TA.url is not None:
|
|
95
|
+
validated_TAs.append(ta_identifier)
|
|
96
|
+
uid = int(str(supported_TA.url).rstrip('/').split("/")[-1])
|
|
97
|
+
try:
|
|
98
|
+
splunk_app = SplunkApp(app_uid=uid)
|
|
99
|
+
if splunk_app.latest_version != supported_TA.version:
|
|
100
|
+
errors.append(f"Version mismatch in '{data_source.file_path}' supported TA '{supported_TA.name}'"
|
|
101
|
+
f"\n Latest version on Splunkbase : {splunk_app.latest_version}"
|
|
102
|
+
f"\n Version specified in data source: {supported_TA.version}")
|
|
103
|
+
except Exception as e:
|
|
104
|
+
errors.append(f"Error processing checking version of TA {supported_TA.name}: {str(e)}")
|
|
105
|
+
|
|
106
|
+
if len(errors) > 0:
|
|
107
|
+
errorString = '\n\n'.join(errors)
|
|
108
|
+
raise Exception(f"[{len(errors)}] or more TA versions are out of date or have other errors."
|
|
109
|
+
f"Please update the following data sources with the latest versions of "
|
|
110
|
+
f"their supported tas:\n\n{errorString}")
|
|
111
|
+
print("All TA versions are up to date.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
@@ -2,14 +2,12 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
import csv
|
|
4
4
|
import os
|
|
5
|
-
from posixpath import split
|
|
6
|
-
from typing import Optional
|
|
7
5
|
import sys
|
|
8
6
|
from attackcti import attack_client
|
|
9
7
|
import logging
|
|
10
8
|
from pydantic import BaseModel, Field
|
|
11
9
|
from dataclasses import field
|
|
12
|
-
from typing import
|
|
10
|
+
from typing import Annotated
|
|
13
11
|
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
|
|
14
12
|
from contentctl.objects.config import validate
|
|
15
13
|
logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
|
|
@@ -25,15 +23,15 @@ class AttackEnrichment(BaseModel):
|
|
|
25
23
|
_ = enrichment.get_attack_lookup(str(config.path))
|
|
26
24
|
return enrichment
|
|
27
25
|
|
|
28
|
-
def getEnrichmentByMitreID(self, mitre_id:Annotated[str, Field(pattern="^T\d{4}(.\d{3})?$")])->
|
|
26
|
+
def getEnrichmentByMitreID(self, mitre_id:Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")])->MitreAttackEnrichment:
|
|
29
27
|
if not self.use_enrichment:
|
|
30
|
-
|
|
28
|
+
raise Exception(f"Error, trying to add Mitre Enrichment, but use_enrichment was set to False")
|
|
31
29
|
|
|
32
30
|
enrichment = self.data.get(mitre_id, None)
|
|
33
31
|
if enrichment is not None:
|
|
34
32
|
return enrichment
|
|
35
33
|
else:
|
|
36
|
-
raise
|
|
34
|
+
raise Exception(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
def addMitreID(self, technique:dict, tactics:list[str], groups:list[str])->None:
|
|
@@ -44,7 +42,7 @@ class AttackEnrichment(BaseModel):
|
|
|
44
42
|
groups.sort()
|
|
45
43
|
|
|
46
44
|
if technique_id in self.data:
|
|
47
|
-
raise
|
|
45
|
+
raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
|
|
48
46
|
|
|
49
47
|
self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
|
|
50
48
|
mitre_attack_technique=technique_obj,
|
|
@@ -53,7 +51,7 @@ class AttackEnrichment(BaseModel):
|
|
|
53
51
|
|
|
54
52
|
|
|
55
53
|
def get_attack_lookup(self, input_path: str, store_csv: bool = False, force_cached_or_offline: bool = False, skip_enrichment:bool = False) -> dict:
|
|
56
|
-
if self.use_enrichment
|
|
54
|
+
if not self.use_enrichment:
|
|
57
55
|
return {}
|
|
58
56
|
print("Getting MITRE Attack Enrichment Data. This may take some time...")
|
|
59
57
|
attack_lookup = dict()
|
|
@@ -18,9 +18,9 @@ CVESSEARCH_API_URL = 'https://cve.circl.lu'
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class CveEnrichmentObj(BaseModel):
|
|
21
|
-
id:Annotated[str, "^CVE-[1|2]
|
|
22
|
-
cvss:Annotated[Decimal, Field(ge=.1, le=10, decimal_places=1)]
|
|
23
|
-
summary:str
|
|
21
|
+
id: Annotated[str, r"^CVE-[1|2]\d{3}-\d+$"]
|
|
22
|
+
cvss: Annotated[Decimal, Field(ge=.1, le=10, decimal_places=1)]
|
|
23
|
+
summary: str
|
|
24
24
|
|
|
25
25
|
@computed_field
|
|
26
26
|
@property
|