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
@@ -1,49 +1,48 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING, Self
2
+ from typing import TYPE_CHECKING, Self, Any
3
3
 
4
4
  if TYPE_CHECKING:
5
- from contentctl.objects.deployment import Deployment
5
+ from contentctl.objects.deployment import Deployment
6
6
  from contentctl.objects.security_content_object import SecurityContentObject
7
- from contentctl.objects.config import Config
8
7
  from contentctl.input.director import DirectorOutputDto
9
-
8
+
10
9
  from contentctl.objects.enums import AnalyticsType
11
- import re
12
10
  import abc
13
11
  import uuid
14
12
  import datetime
15
13
  import pprint
16
- from pydantic import BaseModel, field_validator, Field, ValidationInfo, FilePath, HttpUrl, NonNegativeInt, ConfigDict, model_validator, model_serializer
14
+ from pydantic import (
15
+ BaseModel,
16
+ field_validator,
17
+ Field,
18
+ ValidationInfo,
19
+ FilePath,
20
+ HttpUrl,
21
+ NonNegativeInt,
22
+ ConfigDict,
23
+ model_serializer
24
+ )
17
25
  from typing import Tuple, Optional, List, Union
18
26
  import pathlib
19
-
20
-
21
-
22
27
 
23
28
 
24
29
  NO_FILE_NAME = "NO_FILE_NAME"
25
30
 
26
31
 
27
32
  class SecurityContentObject_Abstract(BaseModel, abc.ABC):
28
- model_config = ConfigDict(use_enum_values=True,validate_default=True)
29
- # name: str = ...
30
- # author: str = Field(...,max_length=255)
31
- # date: datetime.date = Field(...)
32
- # version: NonNegativeInt = ...
33
- # id: uuid.UUID = Field(default_factory=uuid.uuid4) #we set a default here until all content has a uuid
34
- # description: str = Field(...,max_length=1000)
35
- # file_path: FilePath = Field(...)
36
- # references: Optional[List[HttpUrl]] = None
37
-
38
- name: str = Field("NO_NAME")
39
- author: str = Field("Content Author",max_length=255)
33
+ model_config = ConfigDict(use_enum_values=True, validate_default=True)
34
+
35
+ name: str = Field(...)
36
+ author: str = Field("Content Author", max_length=255)
40
37
  date: datetime.date = Field(datetime.date.today())
41
38
  version: NonNegativeInt = 1
42
- id: uuid.UUID = Field(default_factory=uuid.uuid4) #we set a default here until all content has a uuid
43
- description: str = Field("Enter Description Here",max_length=10000)
39
+ id: uuid.UUID = Field(default_factory=uuid.uuid4) # we set a default here until all content has a uuid
40
+ description: str = Field("Enter Description Here", max_length=10000)
44
41
  file_path: Optional[FilePath] = None
45
42
  references: Optional[List[HttpUrl]] = None
46
43
 
44
+ def model_post_init(self, __context: Any) -> None:
45
+ self.ensureFileNameMatchesSearchName()
47
46
 
48
47
  @model_serializer
49
48
  def serialize_model(self):
@@ -58,33 +57,32 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
58
57
  }
59
58
 
60
59
  @staticmethod
61
- def objectListToNameList(objects:list[SecurityContentObject], config:Optional[Config]=None)->list[str]:
62
- return [object.getName(config) for object in objects]
60
+ def objectListToNameList(objects: list[SecurityContentObject]) -> list[str]:
61
+ return [object.getName() for object in objects]
63
62
 
64
63
  # This function is overloadable by specific types if they want to redefine names, for example
65
64
  # to have the format ESCU - NAME - Rule (config.tag - self.name - Rule)
66
- def getName(self, config:Optional[Config])->str:
65
+ def getName(self) -> str:
67
66
  return self.name
68
67
 
69
-
70
68
  @classmethod
71
- def contentNameToFileName(cls, content_name:str)->str:
69
+ def contentNameToFileName(cls, content_name: str) -> str:
72
70
  return content_name \
73
71
  .replace(' ', '_') \
74
- .replace('-','_') \
75
- .replace('.','_') \
76
- .replace('/','_') \
72
+ .replace('-', '_') \
73
+ .replace('.', '_') \
74
+ .replace('/', '_') \
77
75
  .lower() + ".yml"
78
76
 
79
-
80
- @model_validator(mode="after")
81
77
  def ensureFileNameMatchesSearchName(self):
82
78
  file_name = self.contentNameToFileName(self.name)
83
-
79
+
84
80
  if (self.file_path is not None and file_name != self.file_path.name):
85
- raise ValueError(f"The file name MUST be based off the content 'name' field:\n"\
86
- f"\t- Expected File Name: {file_name}\n"\
87
- f"\t- Actual File Name : {self.file_path.name}")
81
+ raise ValueError(
82
+ f"The file name MUST be based off the content 'name' field:\n"
83
+ f"\t- Expected File Name: {file_name}\n"
84
+ f"\t- Actual File Name : {self.file_path.name}"
85
+ )
88
86
 
89
87
  return self
90
88
 
@@ -92,99 +90,120 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
92
90
  @classmethod
93
91
  def file_path_valid(cls, v: Optional[pathlib.PosixPath], info: ValidationInfo):
94
92
  if not v:
95
- #It's possible that the object has no file path - for example filter macros that are created at runtime
93
+ # It's possible that the object has no file path - for example filter macros that are created at runtime
96
94
  return v
97
95
  if not v.name.endswith(".yml"):
98
- raise ValueError(f"All Security Content Objects must be YML files and end in .yml. The following file does not: '{v}'")
96
+ raise ValueError(
97
+ "All Security Content Objects must be YML files and end in .yml. The following"
98
+ f" file does not: '{v}'"
99
+ )
99
100
  return v
100
101
 
101
- def getReferencesListForJson(self)->List[str]:
102
+ def getReferencesListForJson(self) -> List[str]:
102
103
  return [str(url) for url in self.references or []]
103
-
104
+
104
105
  @classmethod
105
- def mapNamesToSecurityContentObjects(cls, v: list[str], director:Union[DirectorOutputDto,None])->list[Self]:
106
+ def mapNamesToSecurityContentObjects(cls, v: list[str], director: Union[DirectorOutputDto, None]) -> list[Self]:
106
107
  if director is not None:
107
108
  name_map = director.name_to_content_map
108
109
  else:
109
110
  name_map = {}
110
-
111
-
112
111
 
113
112
  mappedObjects: list[Self] = []
114
113
  mistyped_objects: list[SecurityContentObject_Abstract] = []
115
114
  missing_objects: list[str] = []
116
115
  for object_name in v:
117
- found_object = name_map.get(object_name,None)
116
+ found_object = name_map.get(object_name, None)
118
117
  if not found_object:
119
118
  missing_objects.append(object_name)
120
- elif not isinstance(found_object,cls):
119
+ elif not isinstance(found_object, cls):
121
120
  mistyped_objects.append(found_object)
122
121
  else:
123
122
  mappedObjects.append(found_object)
124
-
125
- errors:list[str] = []
123
+
124
+ errors: list[str] = []
126
125
  if len(missing_objects) > 0:
127
126
  errors.append(f"Failed to find the following '{cls.__name__}': {missing_objects}")
128
127
  if len(mistyped_objects) > 0:
129
128
  for mistyped_object in mistyped_objects:
130
- errors.append(f"'{mistyped_object.name}' expected to have type '{cls}', but actually had type '{type(mistyped_object)}'")
131
-
129
+ errors.append(
130
+ f"'{mistyped_object.name}' expected to have type '{cls}', but actually "
131
+ f"had type '{type(mistyped_object)}'"
132
+ )
133
+
132
134
  if len(errors) > 0:
133
135
  error_string = "\n - ".join(errors)
134
- raise ValueError(f"Found {len(errors)} issues when resolving references Security Content Object names:\n - {error_string}")
135
-
136
- #Sort all objects sorted by name
136
+ raise ValueError(
137
+ f"Found {len(errors)} issues when resolving references Security Content Object "
138
+ f"names:\n - {error_string}")
139
+
140
+ # Sort all objects sorted by name
137
141
  return sorted(mappedObjects, key=lambda o: o.name)
138
142
 
139
143
  @staticmethod
140
- def getDeploymentFromType(typeField:Union[str,None], info:ValidationInfo)->Deployment:
144
+ def getDeploymentFromType(typeField: Union[str, None], info: ValidationInfo) -> Deployment:
141
145
  if typeField is None:
142
146
  raise ValueError("'type:' field is missing from YML.")
143
- director: Optional[DirectorOutputDto] = info.context.get("output_dto",None)
147
+
148
+ if info.context is None:
149
+ raise ValueError("ValidationInfo.context unexpectedly null")
150
+
151
+ director: Optional[DirectorOutputDto] = info.context.get("output_dto", None)
144
152
  if not director:
145
153
  raise ValueError("Cannot set deployment - DirectorOutputDto not passed to Detection Constructor in context")
146
-
147
- type_to_deployment_name_map = {AnalyticsType.TTP.value:"ESCU Default Configuration TTP",
148
- AnalyticsType.Hunting.value:"ESCU Default Configuration Hunting",
149
- AnalyticsType.Correlation.value: "ESCU Default Configuration Correlation",
150
- AnalyticsType.Anomaly.value: "ESCU Default Configuration Anomaly",
151
- "Baseline": "ESCU Default Configuration Baseline",
154
+
155
+ type_to_deployment_name_map = {
156
+ AnalyticsType.TTP.value: "ESCU Default Configuration TTP",
157
+ AnalyticsType.Hunting.value: "ESCU Default Configuration Hunting",
158
+ AnalyticsType.Correlation.value: "ESCU Default Configuration Correlation",
159
+ AnalyticsType.Anomaly.value: "ESCU Default Configuration Anomaly",
160
+ "Baseline": "ESCU Default Configuration Baseline"
152
161
  }
153
162
  converted_type_field = type_to_deployment_name_map[typeField]
154
-
155
- #TODO: This is clunky, but is imported here to resolve some circular import errors
156
- from contentctl.objects.deployment import Deployment
163
+
164
+ # TODO: This is clunky, but is imported here to resolve some circular import errors
165
+ from contentctl.objects.deployment import Deployment
157
166
 
158
167
  deployments = Deployment.mapNamesToSecurityContentObjects([converted_type_field], director)
159
168
  if len(deployments) == 1:
160
169
  return deployments[0]
161
170
  elif len(deployments) == 0:
162
- raise ValueError(f"Failed to find Deployment for type '{converted_type_field}' "\
163
- f"from possible {[deployment.type for deployment in director.deployments]}")
171
+ raise ValueError(
172
+ f"Failed to find Deployment for type '{converted_type_field}' "
173
+ f"from possible {[deployment.type for deployment in director.deployments]}"
174
+ )
164
175
  else:
165
- raise ValueError(f"Found more than 1 ({len(deployments)}) Deployment for type '{converted_type_field}' "\
166
- f"from possible {[deployment.type for deployment in director.deployments]}")
167
-
168
-
169
-
176
+ raise ValueError(
177
+ f"Found more than 1 ({len(deployments)}) Deployment for type '{converted_type_field}' "
178
+ f"from possible {[deployment.type for deployment in director.deployments]}"
179
+ )
170
180
 
171
181
  @staticmethod
172
- def get_objects_by_name(names_to_find:set[str], objects_to_search:list[SecurityContentObject_Abstract])->Tuple[list[SecurityContentObject_Abstract], set[str]]:
182
+ def get_objects_by_name(
183
+ names_to_find: set[str],
184
+ objects_to_search: list[SecurityContentObject_Abstract]
185
+ ) -> Tuple[list[SecurityContentObject_Abstract], set[str]]:
173
186
  raise Exception("get_objects_by_name deprecated")
174
187
  found_objects = list(filter(lambda obj: obj.name in names_to_find, objects_to_search))
175
188
  found_names = set([obj.name for obj in found_objects])
176
189
  missing_names = names_to_find - found_names
177
- return found_objects,missing_names
178
-
190
+ return found_objects, missing_names
191
+
179
192
  @staticmethod
180
- def create_filename_to_content_dict(all_objects:list[SecurityContentObject_Abstract])->dict[str,SecurityContentObject_Abstract]:
181
- name_dict:dict[str,SecurityContentObject_Abstract] = dict()
193
+ def create_filename_to_content_dict(
194
+ all_objects: list[SecurityContentObject_Abstract]
195
+ ) -> dict[str, SecurityContentObject_Abstract]:
196
+ name_dict: dict[str, SecurityContentObject_Abstract] = dict()
182
197
  for object in all_objects:
198
+ # If file_path is None, this function has been called on an inappropriate
199
+ # SecurityContentObject (e.g. filter macros that are created at runtime but have no
200
+ # actual file associated)
201
+ if object.file_path is None:
202
+ raise ValueError(f"SecurityContentObject is missing a file_path: {object.name}")
183
203
  name_dict[str(pathlib.Path(object.file_path))] = object
184
204
  return name_dict
185
205
 
186
-
187
- def __repr__(self)->str:
206
+ def __repr__(self) -> str:
188
207
  # Just use the model_dump functionality that
189
208
  # has already been written. This loses some of the
190
209
  # richness where objects reference themselves, but
@@ -192,40 +211,35 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
192
211
  m = self.model_dump()
193
212
  return pprint.pformat(m, indent=3)
194
213
 
195
- def __str__(self)->str:
196
- return(self.__repr__())
214
+ def __str__(self) -> str:
215
+ return self.__repr__()
197
216
 
198
- def __lt__(self, other:object)->bool:
199
- if not isinstance(other,SecurityContentObject_Abstract):
217
+ def __lt__(self, other: object) -> bool:
218
+ if not isinstance(other, SecurityContentObject_Abstract):
200
219
  raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
201
- return self.name < other.name
220
+ return self.name < other.name
202
221
 
203
- def __eq__(self, other:object)->bool:
204
- if not isinstance(other,SecurityContentObject_Abstract):
222
+ def __eq__(self, other: object) -> bool:
223
+ if not isinstance(other, SecurityContentObject_Abstract):
205
224
  raise Exception(f"SecurityContentObject can only be compared to each other, not to {type(other)}")
206
-
225
+
207
226
  if id(self) == id(other) and self.name == other.name and self.id == other.id:
208
227
  # Yes, this is the same object
209
228
  return True
210
-
229
+
211
230
  elif id(self) == id(other) or self.name == other.name or self.id == other.id:
212
- raise Exception("Attempted to compare two SecurityContentObjects, but their fields indicate they were not globally unique:"
213
- f"\n\tid(obj1) : {id(self)}"
214
- f"\n\tid(obj2) : {id(other)}"
215
- f"\n\tobj1.name : {self.name}"
216
- f"\n\tobj2.name : {other.name}"
217
- f"\n\tobj1.id : {self.id}"
218
- f"\n\tobj2.id : {other.id}")
231
+ raise Exception(
232
+ "Attempted to compare two SecurityContentObjects, but their fields indicate they "
233
+ "were not globally unique:"
234
+ f"\n\tid(obj1) : {id(self)}"
235
+ f"\n\tid(obj2) : {id(other)}"
236
+ f"\n\tobj1.name : {self.name}"
237
+ f"\n\tobj2.name : {other.name}"
238
+ f"\n\tobj1.id : {self.id}"
239
+ f"\n\tobj2.id : {other.id}"
240
+ )
219
241
  else:
220
242
  return False
221
-
243
+
222
244
  def __hash__(self) -> NonNegativeInt:
223
245
  return id(self)
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
@@ -3,10 +3,7 @@ from contentctl.input.yml_reader import YmlReader
3
3
  from pydantic import BaseModel, model_validator, ConfigDict, FilePath, UUID4
4
4
  from typing import List, Optional, Dict, Union, Self
5
5
  import pathlib
6
- # We should determine if we want to use StrEnum, which is only present in Python3.11+
7
- # Alternatively, we can use
8
- # class SupportedPlatform(str, enum.Enum):
9
- # or install the StrEnum library from pip
6
+
10
7
 
11
8
  from enum import StrEnum, auto
12
9
 
@@ -48,7 +45,7 @@ class AtomicExecutor(BaseModel):
48
45
  cleanup_command: Optional[str] = None
49
46
 
50
47
  @model_validator(mode='after')
51
- def ensure_mutually_exclusive_fields(self)->AtomicExecutor:
48
+ def ensure_mutually_exclusive_fields(self)->Self:
52
49
  if self.command is not None and self.steps is not None:
53
50
  raise ValueError("command and steps cannot both be defined in the executor section. Exactly one must be defined.")
54
51
  elif self.command is None and self.steps is None:
@@ -88,7 +85,7 @@ class AtomicTest(BaseModel):
88
85
  dependency_executor_name: Optional[DependencyExecutorType] = None
89
86
 
90
87
  @staticmethod
91
- def AtomicTestWhenEnrichmentIsDisabled(auto_generated_guid: UUID4)->Self:
88
+ def AtomicTestWhenEnrichmentIsDisabled(auto_generated_guid: UUID4) -> AtomicTest:
92
89
  return AtomicTest(name="Placeholder Atomic Test (enrichment disabled)",
93
90
  auto_generated_guid=auto_generated_guid,
94
91
  description="This is a placeholder AtomicTest. Because enrichments were not enabled, it has not been validated against the real Atomic Red Team Repo.",
@@ -97,17 +94,17 @@ class AtomicTest(BaseModel):
97
94
  command="Placeholder command (enrichment disabled)"))
98
95
 
99
96
  @staticmethod
100
- def AtomicTestWhenTestIsMissing(auto_generated_guid: UUID4)->Self:
97
+ def AtomicTestWhenTestIsMissing(auto_generated_guid: UUID4) -> AtomicTest:
101
98
  return AtomicTest(name="Missing Atomic",
102
99
  auto_generated_guid=auto_generated_guid,
103
- description="This is a placeholder AtomicTest. Either the auto_generated_guid is incorrect or it there was an exception while parsing its AtomicFile..",
100
+ description="This is a placeholder AtomicTest. Either the auto_generated_guid is incorrect or it there was an exception while parsing its AtomicFile.",
104
101
  supported_platforms=[],
105
102
  executor=AtomicExecutor(name="Placeholder Executor (failed to find auto_generated_guid)",
106
103
  command="Placeholder command (failed to find auto_generated_guid)"))
107
104
 
108
105
 
109
106
  @classmethod
110
- def getAtomicByAtomicGuid(cls, guid: UUID4, all_atomics:Union[List[AtomicTest],None])->AtomicTest:
107
+ def getAtomicByAtomicGuid(cls, guid: UUID4, all_atomics:list[AtomicTest] | None)->AtomicTest:
111
108
  if all_atomics is None:
112
109
  return AtomicTest.AtomicTestWhenEnrichmentIsDisabled(guid)
113
110
  matching_atomics = [atomic for atomic in all_atomics if atomic.auto_generated_guid == guid]
@@ -152,7 +149,7 @@ class AtomicTest(BaseModel):
152
149
  return atomic_file
153
150
 
154
151
  @classmethod
155
- def getAtomicTestsFromArtRepo(cls, repo_path:pathlib.Path, enabled:bool=True)->Union[List[AtomicTest],None]:
152
+ def getAtomicTestsFromArtRepo(cls, repo_path:pathlib.Path, enabled:bool=True)->list[AtomicTest] | None:
156
153
  # Get all the atomic files. Note that if the ART repo is not found, we will not throw an error,
157
154
  # but will not have any atomics. This means that if atomic_guids are referenced during validation,
158
155
  # validation for those detections will fail
@@ -18,7 +18,7 @@ class TestType(str, Enum):
18
18
  return self.value
19
19
 
20
20
 
21
- # TODO (cmcginley): enforce distinct test names w/in detections
21
+ # TODO (#224): enforce distinct test names w/in detections
22
22
  class BaseTest(BaseModel, ABC):
23
23
  """
24
24
  A test case for a detection
@@ -1,4 +1,4 @@
1
- from typing import Union
1
+ from typing import Union, Any
2
2
  from enum import Enum
3
3
 
4
4
  from pydantic import BaseModel
@@ -7,6 +7,8 @@ from splunklib.data import Record
7
7
  from contentctl.helper.utils import Utils
8
8
 
9
9
 
10
+ # TODO (PEX-432): add status "UNSET" so that we can make sure the result is always of this enum
11
+ # type; remove mypy ignores associated w/ these typing issues once we do
10
12
  class TestResultStatus(str, Enum):
11
13
  """Enum for test status (e.g. pass/fail)"""
12
14
  # Test failed (detection did NOT fire appropriately)
@@ -26,7 +28,7 @@ class TestResultStatus(str, Enum):
26
28
  return self.value
27
29
 
28
30
 
29
- # TODO (cmcginley): add validator to BaseTestResult which makes a lack of exception incompatible
31
+ # TODO (#225): add validator to BaseTestResult which makes a lack of exception incompatible
30
32
  # with status ERROR
31
33
  class BaseTestResult(BaseModel):
32
34
  """
@@ -94,7 +96,7 @@ class BaseTestResult(BaseModel):
94
96
  "success", "exception", "message", "sid_link", "status", "duration", "wait_duration"
95
97
  ],
96
98
  job_fields: list[str] = ["search", "resultCount", "runDuration"],
97
- ) -> dict:
99
+ ) -> dict[str, Any]:
98
100
  """
99
101
  Aggregates a dictionary summarizing the test result model
100
102
  :param model_fields: the fields of the test result to gather
@@ -102,7 +104,7 @@ class BaseTestResult(BaseModel):
102
104
  :returns: a dict summary
103
105
  """
104
106
  # Init the summary dict
105
- summary_dict = {}
107
+ summary_dict: dict[str, Any] = {}
106
108
 
107
109
  # Grab the fields required
108
110
  for field in model_fields:
@@ -122,7 +124,7 @@ class BaseTestResult(BaseModel):
122
124
  # Grab the job content fields required
123
125
  for field in job_fields:
124
126
  if self.job_content is not None:
125
- value = self.job_content.get(field, None)
127
+ value: Any = self.job_content.get(field, None) # type: ignore
126
128
 
127
129
  # convert runDuration to a fixed width string representation of a float
128
130
  if field == "runDuration":
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
17
17
  class BaselineTags(BaseModel):
18
18
  analytic_story: list[Story] = Field(...)
19
19
  #deployment: Deployment = Field('SET_IN_GET_DEPLOYMENT_FUNCTION')
20
+ # TODO (#223): can we remove str from the possible types here?
20
21
  detections: List[Union[Detection,str]] = Field(...)
21
22
  product: list[SecurityContentProductName] = Field(...,min_length=1)
22
23
  required_fields: List[str] = Field(...,min_length=1)
@@ -42,33 +43,4 @@ class BaselineTags(BaseModel):
42
43
 
43
44
 
44
45
  #return the model
45
- return model
46
-
47
- def replaceDetectionNameWithDetectionObject(self, detection:Detection)->bool:
48
-
49
- pass
50
-
51
-
52
-
53
-
54
- # @field_validator("deployment", mode="before")
55
- # def getDeployment(cls, v:Any, info:ValidationInfo)->Deployment:
56
- # if v != 'SET_IN_GET_DEPLOYMENT_FUNCTION':
57
- # print(f"Deployment defined in YML: {v}")
58
- # return v
59
-
60
- # director: Optional[DirectorOutputDto] = info.context.get("output_dto",None)
61
- # if not director:
62
- # raise ValueError("Cannot set deployment - DirectorOutputDto not passed to Detection Constructor in context")
63
-
64
- # typeField = "Baseline"
65
- # deps = [deployment for deployment in director.deployments if deployment.type == typeField]
66
- # if len(deps) == 1:
67
- # return deps[0]
68
- # elif len(deps) == 0:
69
- # raise ValueError(f"Failed to find Deployment for type '{typeField}' "\
70
- # f"from possible {[deployment.type for deployment in director.deployments]}")
71
- # else:
72
- # raise ValueError(f"Found more than 1 ({len(deps)}) Deployment for type '{typeField}' "\
73
- # f"from possible {[deployment.type for deployment in director.deployments]}")
74
-
46
+ return model
@@ -176,6 +176,7 @@ class validate(Config_Base):
176
176
  build_app: bool = Field(default=True, description="Should an app be built and output in the build_path?")
177
177
  build_api: bool = Field(default=False, description="Should api objects be built and output in the build_path?")
178
178
  build_ssa: bool = Field(default=False, description="Should ssa objects be built and output in the build_path?")
179
+ data_source_TA_validation: bool = Field(default=False, description="Validate latest TA information from Splunkbase")
179
180
 
180
181
  def getAtomicRedTeamRepoPath(self, atomic_red_team_repo_name:str = "atomic-red-team"):
181
182
  return self.path/atomic_red_team_repo_name
@@ -601,13 +602,13 @@ class test_common(build):
601
602
 
602
603
 
603
604
  def getLocalAppDir(self)->pathlib.Path:
604
- #docker really wants abolsute paths
605
+ # docker really wants absolute paths
605
606
  path = self.path / "apps"
606
607
  return path.absolute()
607
608
 
608
609
  def getContainerAppDir(self)->pathlib.Path:
609
- #docker really wants abolsute paths
610
- return pathlib.Path("/tmp/apps").absolute()
610
+ # docker really wants absolute paths
611
+ return pathlib.Path("/tmp/apps")
611
612
 
612
613
  def enterpriseSecurityInApps(self)->bool:
613
614
 
@@ -739,7 +740,7 @@ class test(test_common):
739
740
  if path.startswith(SPLUNKBASE_URL):
740
741
  container_paths.append(path)
741
742
  else:
742
- container_paths.append(str(self.getContainerAppDir()/pathlib.Path(path).name))
743
+ container_paths.append((self.getContainerAppDir()/pathlib.Path(path).name).as_posix())
743
744
 
744
745
  return ','.join(container_paths)
745
746