contentctl 3.6.0__py3-none-any.whl → 4.0.2__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 (142) hide show
  1. contentctl/actions/build.py +89 -0
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
  3. contentctl/actions/detection_testing/GitService.py +148 -230
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
  5. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
  6. contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
  7. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
  8. contentctl/actions/doc_gen.py +1 -1
  9. contentctl/actions/initialize.py +28 -65
  10. contentctl/actions/inspect.py +260 -0
  11. contentctl/actions/new_content.py +106 -13
  12. contentctl/actions/release_notes.py +168 -144
  13. contentctl/actions/reporting.py +24 -13
  14. contentctl/actions/test.py +39 -20
  15. contentctl/actions/validate.py +25 -48
  16. contentctl/contentctl.py +196 -754
  17. contentctl/enrichments/attack_enrichment.py +69 -19
  18. contentctl/enrichments/cve_enrichment.py +28 -13
  19. contentctl/helper/link_validator.py +24 -26
  20. contentctl/helper/utils.py +7 -3
  21. contentctl/input/director.py +139 -201
  22. contentctl/input/new_content_questions.py +63 -61
  23. contentctl/input/sigma_converter.py +1 -2
  24. contentctl/input/ssa_detection_builder.py +16 -7
  25. contentctl/input/yml_reader.py +4 -3
  26. contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
  27. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
  28. contentctl/objects/alert_action.py +40 -0
  29. contentctl/objects/atomic.py +212 -0
  30. contentctl/objects/baseline.py +44 -43
  31. contentctl/objects/baseline_tags.py +69 -20
  32. contentctl/objects/config.py +857 -125
  33. contentctl/objects/constants.py +0 -1
  34. contentctl/objects/correlation_search.py +1 -1
  35. contentctl/objects/data_source.py +2 -4
  36. contentctl/objects/deployment.py +61 -21
  37. contentctl/objects/deployment_email.py +2 -2
  38. contentctl/objects/deployment_notable.py +4 -4
  39. contentctl/objects/deployment_phantom.py +2 -2
  40. contentctl/objects/deployment_rba.py +3 -4
  41. contentctl/objects/deployment_scheduling.py +2 -3
  42. contentctl/objects/deployment_slack.py +2 -2
  43. contentctl/objects/detection.py +1 -5
  44. contentctl/objects/detection_tags.py +210 -119
  45. contentctl/objects/enums.py +312 -24
  46. contentctl/objects/integration_test.py +1 -1
  47. contentctl/objects/integration_test_result.py +0 -2
  48. contentctl/objects/investigation.py +62 -53
  49. contentctl/objects/investigation_tags.py +30 -6
  50. contentctl/objects/lookup.py +80 -31
  51. contentctl/objects/macro.py +29 -45
  52. contentctl/objects/mitre_attack_enrichment.py +29 -5
  53. contentctl/objects/observable.py +3 -7
  54. contentctl/objects/playbook.py +60 -30
  55. contentctl/objects/playbook_tags.py +45 -8
  56. contentctl/objects/security_content_object.py +1 -5
  57. contentctl/objects/ssa_detection.py +8 -4
  58. contentctl/objects/ssa_detection_tags.py +19 -26
  59. contentctl/objects/story.py +142 -44
  60. contentctl/objects/story_tags.py +46 -33
  61. contentctl/objects/unit_test.py +7 -2
  62. contentctl/objects/unit_test_attack_data.py +10 -19
  63. contentctl/objects/unit_test_baseline.py +1 -1
  64. contentctl/objects/unit_test_old.py +4 -3
  65. contentctl/objects/unit_test_result.py +5 -3
  66. contentctl/objects/unit_test_ssa.py +31 -0
  67. contentctl/output/api_json_output.py +202 -130
  68. contentctl/output/attack_nav_output.py +20 -9
  69. contentctl/output/attack_nav_writer.py +3 -3
  70. contentctl/output/ba_yml_output.py +3 -3
  71. contentctl/output/conf_output.py +125 -391
  72. contentctl/output/conf_writer.py +169 -31
  73. contentctl/output/jinja_writer.py +2 -2
  74. contentctl/output/json_writer.py +17 -5
  75. contentctl/output/new_content_yml_output.py +8 -7
  76. contentctl/output/svg_output.py +17 -27
  77. contentctl/output/templates/analyticstories_detections.j2 +8 -4
  78. contentctl/output/templates/analyticstories_investigations.j2 +1 -1
  79. contentctl/output/templates/analyticstories_stories.j2 +6 -6
  80. contentctl/output/templates/app.conf.j2 +2 -2
  81. contentctl/output/templates/app.manifest.j2 +2 -2
  82. contentctl/output/templates/detection_coverage.j2 +6 -8
  83. contentctl/output/templates/doc_detection_page.j2 +2 -2
  84. contentctl/output/templates/doc_detections.j2 +2 -2
  85. contentctl/output/templates/doc_stories.j2 +1 -1
  86. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  87. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  88. contentctl/output/templates/header.j2 +2 -1
  89. contentctl/output/templates/macros.j2 +6 -10
  90. contentctl/output/templates/savedsearches_baselines.j2 +5 -5
  91. contentctl/output/templates/savedsearches_detections.j2 +36 -33
  92. contentctl/output/templates/savedsearches_investigations.j2 +4 -4
  93. contentctl/output/templates/transforms.j2 +4 -4
  94. contentctl/output/yml_writer.py +2 -2
  95. contentctl/templates/app_template/README.md +7 -0
  96. contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
  97. contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
  98. contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
  99. contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
  100. contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
  101. contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
  102. contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
  103. contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
  104. contentctl/templates/stories/cobalt_strike.yml +0 -1
  105. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
  106. contentctl-4.0.2.dist-info/RECORD +168 -0
  107. contentctl/actions/detection_testing/DataManipulation.py +0 -149
  108. contentctl/actions/generate.py +0 -91
  109. contentctl/helper/config_handler.py +0 -75
  110. contentctl/input/baseline_builder.py +0 -66
  111. contentctl/input/basic_builder.py +0 -58
  112. contentctl/input/detection_builder.py +0 -370
  113. contentctl/input/investigation_builder.py +0 -42
  114. contentctl/input/new_content_generator.py +0 -95
  115. contentctl/input/playbook_builder.py +0 -68
  116. contentctl/input/story_builder.py +0 -106
  117. contentctl/objects/app.py +0 -214
  118. contentctl/objects/repo_config.py +0 -163
  119. contentctl/objects/test_config.py +0 -630
  120. contentctl/output/templates/macros_detections.j2 +0 -7
  121. contentctl/output/templates/splunk_app/README.md +0 -7
  122. contentctl-3.6.0.dist-info/RECORD +0 -176
  123. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
  124. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
  125. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
  126. /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
  127. /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
  128. /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
  129. /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
  130. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
  131. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
  132. /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
  133. /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
  134. /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
  135. /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
  136. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
  137. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
  138. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
  139. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
  140. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
  141. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
  142. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
@@ -1,49 +1,147 @@
1
- import string
2
- import uuid
3
- import requests
4
-
5
- from pydantic import BaseModel, validator, ValidationError
6
- from datetime import datetime
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING,List
3
+ from contentctl.objects.story_tags import StoryTags
4
+ from pydantic import Field, model_serializer,computed_field, model_validator
5
+ import re
6
+ if TYPE_CHECKING:
7
+ from contentctl.objects.detection import Detection
8
+ from contentctl.objects.investigation import Investigation
9
+ from contentctl.objects.baseline import Baseline
10
+
7
11
 
8
12
  from contentctl.objects.security_content_object import SecurityContentObject
9
- from contentctl.objects.story_tags import StoryTags
10
- from contentctl.helper.link_validator import LinkValidator
11
- from contentctl.objects.enums import SecurityContentType
13
+
14
+
15
+
16
+
17
+
18
+ #from contentctl.objects.investigation import Investigation
19
+
20
+
21
+
12
22
  class Story(SecurityContentObject):
13
- # story spec
14
- #name: str
15
- #id: str
16
- #version: int
17
- #date: str
18
- #author: str
19
- #description: str
20
- #contentType: SecurityContentType = SecurityContentType.stories
21
- narrative: str
22
- check_references: bool = False #Validation is done in order, this field must be defined first
23
- references: list
24
- tags: StoryTags
23
+ narrative: str = Field(...)
24
+ tags: StoryTags = Field(...)
25
25
 
26
26
  # enrichments
27
- detection_names: list = None
28
- investigation_names: list = None
29
- baseline_names: list = None
30
- author_company: str = None
31
- author_name: str = None
32
- detections: list = None
33
- investigations: list = None
34
-
35
-
36
- # Allow long names for macros
37
- @validator('name',check_fields=False)
38
- def name_max_length(cls, v):
39
- #if len(v) > 67:
40
- # raise ValueError('name is longer then 67 chars: ' + v)
41
- return v
42
-
43
- @validator('narrative')
44
- def encode_error(cls, v, values, field):
45
- return SecurityContentObject.free_text_field_valid(cls,v,values,field)
46
-
47
- @validator('references')
48
- def references_check(cls, v, values):
49
- return LinkValidator.SecurityContentObject_validate_references(v, values)
27
+ #detection_names: List[str] = []
28
+ #investigation_names: List[str] = []
29
+ #baseline_names: List[str] = []
30
+
31
+ # These are updated when detection and investigation objects are created.
32
+ # Specifically in the model_post_init functions
33
+ detections:List[Detection] = []
34
+ investigations: List[Investigation] = []
35
+ baselines: List[Baseline] = []
36
+
37
+
38
+ def storyAndInvestigationNamesWithApp(self, app_name:str)->List[str]:
39
+ return [f"{app_name} - {name} - Rule" for name in self.detection_names] + \
40
+ [f"{app_name} - {name} - Response Task" for name in self.investigation_names]
41
+
42
+ @model_serializer
43
+ def serialize_model(self):
44
+ #Call serializer for parent
45
+ super_fields = super().serialize_model()
46
+
47
+ #All fields custom to this model
48
+ model= {
49
+ "narrative": self.narrative,
50
+ "tags": self.tags.model_dump(),
51
+ "detection_names": self.detection_names,
52
+ "investigation_names": self.investigation_names,
53
+ "baseline_names": self.baseline_names,
54
+ "author_company": self.author_company,
55
+ "author_name":self.author_name
56
+ }
57
+ detections = []
58
+ for detection in self.detections:
59
+ new_detection = {
60
+ "name":detection.name,
61
+ "source":detection.source,
62
+ "type":detection.type
63
+ }
64
+ if self.tags.mitre_attack_enrichments is not None:
65
+ new_detection['tags'] = {"mitre_attack_enrichments": [{"mitre_attack_technique": enrichment.mitre_attack_technique} for enrichment in detection.tags.mitre_attack_enrichments]}
66
+ detections.append(new_detection)
67
+
68
+ model['detections'] = detections
69
+ #Combine fields from this model with fields from parent
70
+ super_fields.update(model)
71
+
72
+ #return the model
73
+ return super_fields
74
+
75
+ @model_validator(mode="after")
76
+ def setTagsFields(self):
77
+
78
+ enrichments = []
79
+ for detection in self.detections:
80
+ enrichments.extend(detection.tags.mitre_attack_enrichments)
81
+ self.tags.mitre_attack_enrichments = list(set(enrichments))
82
+
83
+
84
+ tactics = []
85
+ for enrichment in self.tags.mitre_attack_enrichments:
86
+ tactics.extend(enrichment.mitre_attack_tactics)
87
+ self.tags.mitre_attack_tactics = set(tactics)
88
+
89
+
90
+
91
+ datamodels = []
92
+ for detection in self.detections:
93
+ datamodels.extend(detection.datamodel)
94
+ self.tags.datamodels = set(datamodels)
95
+
96
+
97
+
98
+ kill_chain_phases = []
99
+ for detection in self.detections:
100
+ kill_chain_phases.extend(detection.tags.kill_chain_phases)
101
+ self.tags.kill_chain_phases = set(kill_chain_phases)
102
+
103
+ return self
104
+
105
+
106
+ @computed_field
107
+ @property
108
+ def author_name(self)->str:
109
+ match_author = re.search(r'^([^,]+)', self.author)
110
+ if match_author is None:
111
+ return 'no'
112
+ else:
113
+ return match_author.group(1)
114
+
115
+ @computed_field
116
+ @property
117
+ def author_company(self)->str:
118
+ match_company = re.search(r',\s?(.*)$', self.author)
119
+ if match_company is None:
120
+ return 'no'
121
+ else:
122
+ return match_company.group(1)
123
+
124
+ @computed_field
125
+ @property
126
+ def author_email(self)->str:
127
+ return "-"
128
+
129
+ @computed_field
130
+ @property
131
+ def detection_names(self)->List[str]:
132
+ return [detection.name for detection in self.detections]
133
+
134
+ @computed_field
135
+ @property
136
+ def investigation_names(self)->List[str]:
137
+ return [investigation.name for investigation in self.investigations]
138
+
139
+ @computed_field
140
+ @property
141
+ def baseline_names(self)->List[str]:
142
+ return [baseline.name for baseline in self.baselines]
143
+
144
+
145
+
146
+
147
+
@@ -1,38 +1,51 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel, Field, model_serializer, ConfigDict
3
+ from typing import List,Set,Optional, Annotated
1
4
 
5
+ from enum import Enum
2
6
 
3
- from pydantic import BaseModel, validator, ValidationError
4
7
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
5
- from contentctl.objects.enums import StoryCategory
8
+ from contentctl.objects.enums import StoryCategory, DataModel, KillChainPhase, SecurityContentProductName
9
+
10
+
11
+ class StoryUseCase(str,Enum):
12
+ FRAUD_DETECTION = "Fraud Detection"
13
+ COMPLIANCE = "Compliance"
14
+ APPLICATION_SECURITY = "Application Security"
15
+ SECURITY_MONITORING = "Security Monitoring"
16
+ ADVANCED_THREAD_DETECTION = "Advanced Threat Detection"
6
17
 
7
18
  class StoryTags(BaseModel):
8
- # story spec
9
- name: str
10
- analytic_story: str
11
- category: list[StoryCategory]
12
- product: list
13
- usecase: str
14
-
15
- # enrichment
16
- mitre_attack_enrichments: list[MitreAttackEnrichment] = []
17
- mitre_attack_tactics: list = []
18
- datamodels: list = []
19
- kill_chain_phases: list = []
20
-
21
-
22
- @validator('product')
23
- def tags_product(cls, v, values):
24
- valid_products = [
25
- "Splunk Enterprise", "Splunk Enterprise Security", "Splunk Cloud",
26
- "Splunk Security Analytics for AWS", "Splunk Behavioral Analytics"
27
- ]
28
-
29
- for value in v:
30
- if value not in valid_products:
31
- raise ValueError('product is not valid for ' + values['name'] + '. valid products are ' + str(valid_products))
32
- return v
33
-
34
- @validator('category')
35
- def category_validate(cls,v,values):
36
- if len(v) == 0:
37
- raise ValueError(f"Error for Story '{values['name']}' - at least one 'category' MUST be provided.")
38
- return v
19
+ model_config = ConfigDict(extra='forbid', use_enum_values=True)
20
+ category: List[StoryCategory] = Field(...,min_length=1)
21
+ product: List[SecurityContentProductName] = Field(...,min_length=1)
22
+ usecase: StoryUseCase = Field(...)
23
+
24
+ # enrichment
25
+ mitre_attack_enrichments: Optional[List[MitreAttackEnrichment]] = None
26
+ mitre_attack_tactics: Optional[Set[Annotated[str, Field(pattern="^T\d{4}(.\d{3})?$")]]] = None
27
+ datamodels: Optional[Set[DataModel]] = None
28
+ kill_chain_phases: Optional[Set[KillChainPhase]] = None
29
+ cve: List[Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"]] = []
30
+ group: List[str] = Field([], description="A list of groups who leverage the techniques list in this Analytic Story.")
31
+
32
+ def getCategory_conf(self) -> str:
33
+ #if len(self.category) > 1:
34
+ # print("Story with more than 1 category. We can only have 1 category, fix it!")
35
+ return list(self.category)[0]
36
+
37
+ @model_serializer
38
+ def serialize_model(self):
39
+ #no super to call
40
+ return {
41
+ "category": list(self.category),
42
+ "product": list(self.product),
43
+ "usecase": self.usecase,
44
+ "mitre_attack_enrichments": self.mitre_attack_enrichments,
45
+ "mitre_attack_tactics": list(self.mitre_attack_tactics) if self.mitre_attack_tactics is not None else None,
46
+ "datamodels": list(self.datamodels) if self.datamodels is not None else None,
47
+ "kill_chain_phases": list(self.kill_chain_phases) if self.kill_chain_phases is not None else None
48
+ }
49
+
50
+
51
+
@@ -1,4 +1,9 @@
1
-
1
+ from __future__ import annotations
2
+ from pydantic import Field
3
+ from typing import TYPE_CHECKING
4
+ if TYPE_CHECKING:
5
+ from contentctl.objects.unit_test_attack_data import UnitTestAttackData
6
+ from contentctl.objects.unit_test_result import UnitTestResult
2
7
 
3
8
  from typing import Union
4
9
 
@@ -20,7 +25,7 @@ class UnitTest(BaseTest):
20
25
  # contentType: SecurityContentType = SecurityContentType.unit_tests
21
26
 
22
27
  # The test type (unit)
23
- test_type: TestType = Field(TestType.UNIT, const=True)
28
+ test_type: TestType = Field(TestType.UNIT)
24
29
 
25
30
  # The condition to check if the search was successful
26
31
  pass_condition: Union[str, None] = None
@@ -1,22 +1,13 @@
1
- from pydantic import BaseModel, validator, ValidationError
2
- from contentctl.helper.utils import Utils
3
- from typing import Union
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel, HttpUrl, FilePath, Field
3
+ from typing import Union, Optional
4
4
 
5
5
 
6
6
  class UnitTestAttackData(BaseModel):
7
- file_name: str = None
8
- data: str = None
9
- source: str = None
10
- sourcetype: str = None
11
- update_timestamp: bool = None
12
- custom_index: str = None
13
- host: str = None
14
-
15
- @validator("data", always=True)
16
- def validate_data(cls, v, values):
17
- return v
18
- try:
19
- Utils.verify_file_exists(v)
20
- except Exception as e:
21
- raise (ValueError(f"Cannot find file {v}: {str(e)}"))
22
- return v
7
+ data: Union[HttpUrl, FilePath] = Field(...)
8
+ # TODO - should source and sourcetype should be mapped to a list
9
+ # of supported source and sourcetypes in a given environment?
10
+ source: str = Field(...)
11
+ sourcetype: str = Field(...)
12
+ custom_index: Optional[str] = None
13
+ host: Optional[str] = None
@@ -1,6 +1,6 @@
1
1
 
2
2
 
3
- from pydantic import BaseModel, validator, ValidationError
3
+ from pydantic import BaseModel
4
4
  from typing import Union
5
5
 
6
6
  class UnitTestBaseline(BaseModel):
@@ -1,9 +1,10 @@
1
- from pydantic import BaseModel, validator, ValidationError
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
2
3
 
3
4
 
4
- from contentctl.objects.unit_test import UnitTest
5
+ from contentctl.objects.unit_test_ssa import UnitTestSSA
5
6
 
6
7
 
7
8
  class UnitTestOld(BaseModel):
8
9
  name: str
9
- tests: list[UnitTest]
10
+ tests: list[UnitTestSSA]
@@ -1,10 +1,12 @@
1
- from typing import Union
1
+ from __future__ import annotations
2
2
 
3
+ from typing import Union,TYPE_CHECKING
3
4
  from splunklib.data import Record
4
-
5
- from contentctl.objects.test_config import Infrastructure
6
5
  from contentctl.objects.base_test_result import BaseTestResult, TestResultStatus
7
6
 
7
+ if TYPE_CHECKING:
8
+ from contentctl.objects.config import Infrastructure
9
+
8
10
  FORCE_TEST_FAILURE_FOR_MISSING_OBSERVABLE = False
9
11
 
10
12
  NO_SID = "Testing Failed, NO Search ID"
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from pydantic import BaseModel, Field
4
+ from pydantic import Field
5
+
6
+
7
+ class UnitTestAttackDataSSA(BaseModel):
8
+ file_name:Optional[str] = None
9
+ data: str = Field(...)
10
+ # TODO - should source and sourcetype should be mapped to a list
11
+ # of supported source and sourcetypes in a given environment?
12
+ source: str = Field(...)
13
+
14
+ sourcetype: Optional[str] = None
15
+
16
+
17
+ class UnitTestSSA(BaseModel):
18
+ """
19
+ A unit test for a detection
20
+ """
21
+ name: str
22
+
23
+ # The attack data to be ingested for the unit test
24
+ attack_data: list[UnitTestAttackDataSSA] = Field(...)
25
+
26
+
27
+
28
+
29
+
30
+
31
+