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.
Files changed (36) hide show
  1. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +41 -47
  2. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +1 -1
  3. contentctl/actions/detection_testing/views/DetectionTestingView.py +1 -4
  4. contentctl/actions/validate.py +40 -1
  5. contentctl/enrichments/attack_enrichment.py +6 -8
  6. contentctl/enrichments/cve_enrichment.py +3 -3
  7. contentctl/helper/splunk_app.py +263 -0
  8. contentctl/input/director.py +1 -1
  9. contentctl/input/ssa_detection_builder.py +8 -6
  10. contentctl/objects/abstract_security_content_objects/detection_abstract.py +362 -336
  11. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +117 -103
  12. contentctl/objects/atomic.py +7 -10
  13. contentctl/objects/base_test.py +1 -1
  14. contentctl/objects/base_test_result.py +7 -5
  15. contentctl/objects/baseline_tags.py +2 -30
  16. contentctl/objects/config.py +5 -4
  17. contentctl/objects/correlation_search.py +316 -96
  18. contentctl/objects/data_source.py +7 -2
  19. contentctl/objects/detection_tags.py +128 -102
  20. contentctl/objects/errors.py +18 -0
  21. contentctl/objects/lookup.py +3 -1
  22. contentctl/objects/mitre_attack_enrichment.py +3 -3
  23. contentctl/objects/notable_event.py +20 -0
  24. contentctl/objects/observable.py +20 -26
  25. contentctl/objects/risk_analysis_action.py +2 -2
  26. contentctl/objects/risk_event.py +315 -0
  27. contentctl/objects/ssa_detection_tags.py +1 -1
  28. contentctl/objects/story_tags.py +2 -2
  29. contentctl/objects/unit_test.py +1 -9
  30. contentctl/output/data_source_writer.py +4 -4
  31. contentctl/output/templates/savedsearches_detections.j2 +0 -8
  32. {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/METADATA +5 -8
  33. {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/RECORD +36 -32
  34. {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/LICENSE.md +0 -0
  35. {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/WHEEL +0 -0
  36. {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!", self.start_time)
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: Union[float, None],
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 id not provided
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 (cmcginley): break up the execute routines for integration/unit tests some more to remove
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 (cmcginley): right now, we are creating one CorrelationSearch instance for each
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
- detection_name=detection.name,
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 FAILED: unhandled exception in CorrelationSearch",
886
+ message="TEST ERROR: unhandled exception in CorrelationSearch",
896
887
  exception=e,
897
888
  status=TestResultStatus.ERROR
898
889
  )
899
890
 
900
- # TODO (cmcginley): when in interactive mode, consider maybe making the cleanup routine in
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, test_group.name, "Copying Data", test_group_start_time)
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)
@@ -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=str(self.global_config.getContainerAppDir()),
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 (cmcginley): add stats around total test cases and unit/integration test
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": {
@@ -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 Union,Annotated
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})?$")])->Union[MitreAttackEnrichment,None]:
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
- return None
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 ValueError(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
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 ValueError(f"Error, trying to redefine MITRE ID '{technique_id}'")
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 is False:
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][0-9]{3}-[0-9]+$"]
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