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,48 +1,66 @@
1
1
  from __future__ import annotations
2
2
  import uuid
3
3
  from typing import TYPE_CHECKING, List, Optional, Annotated, Union
4
- from pydantic import BaseModel,Field, NonNegativeInt, PositiveInt, computed_field, UUID4, HttpUrl, ConfigDict, field_validator, ValidationInfo, model_serializer, model_validator
4
+ from pydantic import (
5
+ BaseModel,
6
+ Field,
7
+ NonNegativeInt,
8
+ PositiveInt,
9
+ computed_field,
10
+ UUID4,
11
+ HttpUrl,
12
+ ConfigDict,
13
+ field_validator,
14
+ ValidationInfo,
15
+ model_serializer,
16
+ model_validator
17
+ )
5
18
  from contentctl.objects.story import Story
6
19
  if TYPE_CHECKING:
7
20
  from contentctl.input.director import DirectorOutputDto
8
21
 
9
-
10
-
11
22
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
12
- from contentctl.objects.constants import *
23
+ from contentctl.objects.constants import ATTACK_TACTICS_KILLCHAIN_MAPPING
13
24
  from contentctl.objects.observable import Observable
14
- from contentctl.objects.enums import Cis18Value, AssetType, SecurityDomain, RiskSeverity, KillChainPhase, NistCategory, RiskLevel, SecurityContentProductName
25
+ from contentctl.objects.enums import (
26
+ Cis18Value,
27
+ AssetType,
28
+ SecurityDomain,
29
+ RiskSeverity,
30
+ KillChainPhase,
31
+ NistCategory,
32
+ RiskLevel,
33
+ SecurityContentProductName
34
+ )
15
35
  from contentctl.objects.atomic import AtomicTest
16
36
 
17
37
 
18
-
19
38
  class DetectionTags(BaseModel):
20
39
  # detection spec
21
- model_config = ConfigDict(use_enum_values=True,validate_default=False)
40
+ model_config = ConfigDict(use_enum_values=True, validate_default=False)
22
41
  analytic_story: list[Story] = Field(...)
23
42
  asset_type: AssetType = Field(...)
24
-
25
-
26
- confidence: NonNegativeInt = Field(...,le=100)
27
- impact: NonNegativeInt = Field(...,le=100)
43
+
44
+ confidence: NonNegativeInt = Field(..., le=100)
45
+ impact: NonNegativeInt = Field(..., le=100)
46
+
28
47
  @computed_field
29
48
  @property
30
- def risk_score(self)->int:
49
+ def risk_score(self) -> int:
31
50
  return round((self.confidence * self.impact)/100)
32
-
33
-
34
- mitre_attack_id: List[Annotated[str, Field(pattern="^T[0-9]{4}(.[0-9]{3})?$")]] = []
51
+
52
+ mitre_attack_id: List[Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]] = []
35
53
  nist: list[NistCategory] = []
36
54
  observable: List[Observable] = []
37
- message: Optional[str] = Field(...)
38
- product: list[SecurityContentProductName] = Field(...,min_length=1)
55
+ message: str = Field(...)
56
+ product: list[SecurityContentProductName] = Field(..., min_length=1)
39
57
  required_fields: list[str] = Field(min_length=1)
40
-
58
+
41
59
  security_domain: SecurityDomain = Field(...)
42
60
 
43
61
  @computed_field
44
62
  @property
45
- def risk_severity(self)->RiskSeverity:
63
+ def risk_severity(self) -> RiskSeverity:
46
64
  if self.risk_score >= 80:
47
65
  return RiskSeverity('high')
48
66
  elif (self.risk_score >= 50 and self.risk_score <= 79):
@@ -50,83 +68,81 @@ class DetectionTags(BaseModel):
50
68
  else:
51
69
  return RiskSeverity('low')
52
70
 
53
-
54
-
55
- cve: List[Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"]] = []
71
+ cve: List[Annotated[str, r"^CVE-[1|2]\d{3}-\d+$"]] = []
56
72
  atomic_guid: List[AtomicTest] = []
57
73
  drilldown_search: Optional[str] = None
58
74
 
59
-
60
75
  # enrichment
61
- mitre_attack_enrichments: List[MitreAttackEnrichment] = Field([],validate_default=True)
62
- confidence_id: Optional[PositiveInt] = Field(None,ge=1,le=3)
63
- impact_id: Optional[PositiveInt] = Field(None,ge=1,le=5)
76
+ mitre_attack_enrichments: List[MitreAttackEnrichment] = Field([], validate_default=True)
77
+ confidence_id: Optional[PositiveInt] = Field(None, ge=1, le=3)
78
+ impact_id: Optional[PositiveInt] = Field(None, ge=1, le=5)
64
79
  # context_ids: list = None
65
- risk_level_id: Optional[NonNegativeInt] = Field(None,le=4)
80
+ risk_level_id: Optional[NonNegativeInt] = Field(None, le=4)
66
81
  risk_level: Optional[RiskLevel] = None
67
- #observable_str: str = None
82
+ # observable_str: str = None
68
83
  evidence_str: Optional[str] = None
69
84
 
70
85
  @computed_field
71
86
  @property
72
- def kill_chain_phases(self)->list[KillChainPhase]:
73
- if self.mitre_attack_enrichments is None:
74
- return []
75
- phases:set[KillChainPhase] = set()
87
+ def kill_chain_phases(self) -> list[KillChainPhase]:
88
+ phases: set[KillChainPhase] = set()
76
89
  for enrichment in self.mitre_attack_enrichments:
77
90
  for tactic in enrichment.mitre_attack_tactics:
78
91
  phase = KillChainPhase(ATTACK_TACTICS_KILLCHAIN_MAPPING[tactic])
79
92
  phases.add(phase)
80
93
  return sorted(list(phases))
81
-
82
- #enum is intentionally Cis18 even though field is named cis20 for legacy reasons
94
+
95
+ # enum is intentionally Cis18 even though field is named cis20 for legacy reasons
83
96
  @computed_field
84
97
  @property
85
- def cis20(self)->list[Cis18Value]:
98
+ def cis20(self) -> list[Cis18Value]:
86
99
  if self.security_domain == SecurityDomain.NETWORK:
87
100
  return [Cis18Value.CIS_13]
88
101
  else:
89
102
  return [Cis18Value.CIS_10]
90
103
 
91
-
92
104
  research_site_url: Optional[HttpUrl] = None
93
105
  event_schema: str = "ocsf"
106
+ # TODO (#221): mappings should be fleshed out into a proper class
94
107
  mappings: Optional[List] = None
95
- #annotations: Optional[dict] = None
108
+ # annotations: Optional[dict] = None
96
109
  manual_test: Optional[str] = None
97
-
98
-
110
+
99
111
  # The following validator is temporarily disabled pending further discussions
100
112
  # @validator('message')
101
113
  # def validate_message(cls,v,values):
102
-
114
+
103
115
  # observables:list[Observable] = values.get("observable",[])
104
116
  # observable_names = set([o.name for o in observables])
105
117
  # #find all of the observables used in the message by name
106
118
  # name_match_regex = r"\$([^\s.]*)\$"
107
-
119
+
108
120
  # message_observables = set()
109
121
 
110
- # #Make sure that all observable names in
122
+ # #Make sure that all observable names in
111
123
  # for match in re.findall(name_match_regex, v):
112
124
  # #Remove
113
125
  # match_without_dollars = match.replace("$", "")
114
126
  # message_observables.add(match_without_dollars)
115
-
116
127
 
117
128
  # missing_observables = message_observables - observable_names
118
129
  # unused_observables = observable_names - message_observables
119
130
  # if len(missing_observables) > 0:
120
- # raise ValueError(f"The following observables are referenced in the message, but were not declared as observables: {missing_observables}")
121
-
131
+ # raise ValueError(
132
+ # "The following observables are referenced in the message, but were not declared as"
133
+ # f" observables: {missing_observables}"
134
+ # )
135
+
122
136
  # if len(unused_observables) > 0:
123
- # raise ValueError(f"The following observables were declared, but are not referenced in the message: {unused_observables}")
137
+ # raise ValueError(
138
+ # "The following observables were declared, but are not referenced in the message:"
139
+ # f" {unused_observables}"
140
+ # )
124
141
  # return v
125
142
 
126
-
127
143
  @model_serializer
128
144
  def serialize_model(self):
129
- #Since this field has no parent, there is no need to call super() serialization function
145
+ # Since this field has no parent, there is no need to call super() serialization function
130
146
  return {
131
147
  "analytic_story": [story.name for story in self.analytic_story],
132
148
  "asset_type": self.asset_type.value,
@@ -141,34 +157,38 @@ class DetectionTags(BaseModel):
141
157
  "mitre_attack_id": self.mitre_attack_id,
142
158
  "mitre_attack_enrichments": self.mitre_attack_enrichments
143
159
  }
144
-
145
-
160
+
146
161
  @model_validator(mode="after")
147
- def addAttackEnrichment(self, info:ValidationInfo):
162
+ def addAttackEnrichment(self, info: ValidationInfo):
163
+ if info.context is None:
164
+ raise ValueError("ValidationInfo.context unexpectedly null")
165
+
148
166
  if len(self.mitre_attack_enrichments) > 0:
149
- raise ValueError(f"Error, field 'mitre_attack_enrichment' should be empty and dynamically populated at runtime. Instead, this field contained: {self.mitre_attack_enrichments}")
150
-
151
- output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
167
+ raise ValueError(
168
+ "Error, field 'mitre_attack_enrichment' should be empty and dynamically populated"
169
+ f" at runtime. Instead, this field contained: {self.mitre_attack_enrichments}"
170
+ )
171
+
172
+ output_dto: Union[DirectorOutputDto, None] = info.context.get("output_dto", None)
152
173
  if output_dto is None:
153
174
  raise ValueError("Context not provided to detection.detection_tags model post validator")
154
-
175
+
155
176
  if output_dto.attack_enrichment.use_enrichment is False:
156
177
  return self
157
-
158
178
 
159
- mitre_enrichments = []
160
- missing_tactics = []
179
+ mitre_enrichments: list[MitreAttackEnrichment] = []
180
+ missing_tactics: list[str] = []
161
181
  for mitre_attack_id in self.mitre_attack_id:
162
182
  try:
163
183
  mitre_enrichments.append(output_dto.attack_enrichment.getEnrichmentByMitreID(mitre_attack_id))
164
- except Exception as e:
165
- missing_tactics.append(mitre_attack_id)
166
-
184
+ except Exception:
185
+ missing_tactics.append(mitre_attack_id)
186
+
167
187
  if len(missing_tactics) > 0:
168
188
  raise ValueError(f"Missing Mitre Attack IDs. {missing_tactics} not found.")
169
189
  else:
170
190
  self.mitre_attack_enrichments = mitre_enrichments
171
-
191
+
172
192
  return self
173
193
 
174
194
  '''
@@ -176,77 +196,83 @@ class DetectionTags(BaseModel):
176
196
  @classmethod
177
197
  def addAttackEnrichments(cls, v:list[MitreAttackEnrichment], info:ValidationInfo)->list[MitreAttackEnrichment]:
178
198
  if len(v) > 0:
179
- raise ValueError(f"Error, field 'mitre_attack_enrichment' should be empty and dynamically populated at runtime. Instead, this field contained: {str(v)}")
180
-
181
-
199
+ raise ValueError(
200
+ f"Error, field 'mitre_attack_enrichment' should be empty and dynamically populated"
201
+ f" at runtime. Instead, this field contained: {str(v)}"
202
+ )
203
+
204
+
182
205
  output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
183
206
  if output_dto is None:
184
207
  raise ValueError("Context not provided to detection.detection_tags.mitre_attack_enrichments")
185
-
186
- enrichments = []
187
-
188
208
 
209
+ enrichments = []
189
210
 
190
-
191
211
  return enrichments
192
212
  '''
193
213
 
194
- @field_validator('analytic_story',mode="before")
214
+ @field_validator('analytic_story', mode="before")
195
215
  @classmethod
196
- def mapStoryNamesToStoryObjects(cls, v:list[str], info:ValidationInfo)->list[Story]:
197
- return Story.mapNamesToSecurityContentObjects(v, info.context.get("output_dto",None))
198
-
199
- def getAtomicGuidStringArray(self)->List[str]:
216
+ def mapStoryNamesToStoryObjects(cls, v: list[str], info: ValidationInfo) -> list[Story]:
217
+ if info.context is None:
218
+ raise ValueError("ValidationInfo.context unexpectedly null")
219
+
220
+ return Story.mapNamesToSecurityContentObjects(v, info.context.get("output_dto", None))
221
+
222
+ def getAtomicGuidStringArray(self) -> List[str]:
200
223
  return [str(atomic_guid.auto_generated_guid) for atomic_guid in self.atomic_guid]
201
-
202
224
 
203
- @field_validator('atomic_guid',mode="before")
225
+ @field_validator('atomic_guid', mode="before")
204
226
  @classmethod
205
- def mapAtomicGuidsToAtomicTests(cls, v:List[UUID4], info:ValidationInfo)->List[AtomicTest]:
227
+ def mapAtomicGuidsToAtomicTests(cls, v: List[UUID4], info: ValidationInfo) -> List[AtomicTest]:
206
228
  if len(v) == 0:
207
229
  return []
208
-
209
- output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
230
+
231
+ if info.context is None:
232
+ raise ValueError("ValidationInfo.context unexpectedly null")
233
+
234
+ output_dto: Union[DirectorOutputDto, None] = info.context.get("output_dto", None)
210
235
  if output_dto is None:
211
236
  raise ValueError("Context not provided to detection.detection_tags.atomic_guid validator")
212
237
 
213
-
214
- all_tests:List[AtomicTest]= output_dto.atomic_tests
215
-
216
- matched_tests:List[AtomicTest] = []
217
- missing_tests:List[UUID4] = []
218
- badly_formatted_guids:List[str] = []
238
+ all_tests: None | List[AtomicTest] = output_dto.atomic_tests
239
+
240
+ matched_tests: List[AtomicTest] = []
241
+ missing_tests: List[UUID4] = []
242
+ badly_formatted_guids: List[str] = []
219
243
  for atomic_guid_str in v:
220
244
  try:
221
- #Ensure that this is a valid UUID
245
+ # Ensure that this is a valid UUID
222
246
  atomic_guid = uuid.UUID(str(atomic_guid_str))
223
- except Exception as e:
224
- #We will not try to load a test for this since it was invalid
247
+ except Exception:
248
+ # We will not try to load a test for this since it was invalid
225
249
  badly_formatted_guids.append(str(atomic_guid_str))
226
250
  continue
227
251
  try:
228
- matched_tests.append(AtomicTest.getAtomicByAtomicGuid(atomic_guid,all_tests))
229
- except Exception as _:
252
+ matched_tests.append(AtomicTest.getAtomicByAtomicGuid(atomic_guid, all_tests))
253
+ except Exception:
230
254
  missing_tests.append(atomic_guid)
231
255
 
232
-
233
-
234
-
235
256
  if len(missing_tests) > 0:
236
- missing_tests_string = f"\n\tWARNING: Failed to find [{len(missing_tests)}] Atomic Test(s) with the following atomic_guids (called auto_generated_guid in the ART Repo)."\
237
- f"\n\tPlease review the output above for potential exception(s) when parsing the Atomic Red Team Repo."\
238
- f"\n\tVerify that these auto_generated_guid exist and try updating/pulling the repo again.: {[str(guid) for guid in missing_tests]}"
257
+ missing_tests_string = (
258
+ f"\n\tWARNING: Failed to find [{len(missing_tests)}] Atomic Test(s) with the "
259
+ "following atomic_guids (called auto_generated_guid in the ART Repo)."
260
+ f"\n\tPlease review the output above for potential exception(s) when parsing the "
261
+ "Atomic Red Team Repo."
262
+ "\n\tVerify that these auto_generated_guid exist and try updating/pulling the "
263
+ f"repo again.: {[str(guid) for guid in missing_tests]}"
264
+ )
239
265
  else:
240
266
  missing_tests_string = ""
241
-
242
267
 
243
268
  if len(badly_formatted_guids) > 0:
244
- if len(badly_formatted_guids) > 0:
245
- bad_guids_string = f"The following [{len(badly_formatted_guids)}] value(s) are not properly formatted UUIDs: {badly_formatted_guids}\n"
246
- raise ValueError(f"{bad_guids_string}{missing_tests_string}")
247
-
269
+ bad_guids_string = (
270
+ f"The following [{len(badly_formatted_guids)}] value(s) are not properly "
271
+ f"formatted UUIDs: {badly_formatted_guids}\n"
272
+ )
273
+ raise ValueError(f"{bad_guids_string}{missing_tests_string}")
274
+
248
275
  elif len(missing_tests) > 0:
249
276
  print(missing_tests_string)
250
277
 
251
278
  return matched_tests + [AtomicTest.AtomicTestWhenTestIsMissing(test) for test in missing_tests]
252
-
@@ -0,0 +1,18 @@
1
+ class ValidationFailed(Exception):
2
+ """Indicates not an error in execution, but a validation failure"""
3
+ pass
4
+
5
+
6
+ class IntegrationTestingError(Exception):
7
+ """Base exception class for integration testing"""
8
+ pass
9
+
10
+
11
+ class ServerError(IntegrationTestingError):
12
+ """An error encounterd during integration testing, as provided by the server (Splunk instance)"""
13
+ pass
14
+
15
+
16
+ class ClientError(IntegrationTestingError):
17
+ """An error encounterd during integration testing, on the client's side (locally)"""
18
+ pass
@@ -8,19 +8,21 @@ if TYPE_CHECKING:
8
8
  from contentctl.objects.config import validate
9
9
  from contentctl.objects.security_content_object import SecurityContentObject
10
10
 
11
-
11
+ # This section is used to ignore lookups that are NOT shipped with ESCU app but are used in the detections. Adding exclusions here will so that contentctl builds will not fail.
12
12
  LOOKUPS_TO_IGNORE = set(["outputlookup"])
13
13
  LOOKUPS_TO_IGNORE.add("ut_shannon_lookup") #In the URL toolbox app which is recommended for ESCU
14
14
  LOOKUPS_TO_IGNORE.add("identity_lookup_expanded") #Shipped with the Asset and Identity Framework
15
15
  LOOKUPS_TO_IGNORE.add("cim_corporate_web_domain_lookup") #Shipped with the Asset and Identity Framework
16
16
  LOOKUPS_TO_IGNORE.add("alexa_lookup_by_str") #Shipped with the Asset and Identity Framework
17
17
  LOOKUPS_TO_IGNORE.add("interesting_ports_lookup") #Shipped with the Asset and Identity Framework
18
+ LOOKUPS_TO_IGNORE.add("admon_groups_def") #Shipped with the SA-admon addon
18
19
 
19
20
  #Special case for the Detection "Exploit Public Facing Application via Apache Commons Text"
20
21
  LOOKUPS_TO_IGNORE.add("=")
21
22
  LOOKUPS_TO_IGNORE.add("other_lookups")
22
23
 
23
24
 
25
+ # TODO (#220): Split Lookup into 2 classes
24
26
  class Lookup(SecurityContentObject):
25
27
 
26
28
  collection: Optional[str] = None
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from pydantic import BaseModel, Field, ConfigDict
3
- from typing import Set,List,Annotated
3
+ from typing import List, Annotated
4
4
  from enum import StrEnum
5
5
 
6
6
 
@@ -23,10 +23,10 @@ class MitreTactics(StrEnum):
23
23
 
24
24
  class MitreAttackEnrichment(BaseModel):
25
25
  ConfigDict(use_enum_values=True)
26
- mitre_attack_id: Annotated[str, Field(pattern="^T\d{4}(.\d{3})?$")] = Field(...)
26
+ mitre_attack_id: Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")] = Field(...)
27
27
  mitre_attack_technique: str = Field(...)
28
28
  mitre_attack_tactics: List[MitreTactics] = Field(...)
29
29
  mitre_attack_groups: List[str] = Field(...)
30
30
 
31
31
  def __hash__(self) -> int:
32
- return id(self)
32
+ return id(self)
@@ -0,0 +1,20 @@
1
+ from pydantic import BaseModel
2
+
3
+ from contentctl.objects.detection import Detection
4
+
5
+
6
+ # TODO (PEX-434): implement deeper notable validation
7
+ class NotableEvent(BaseModel):
8
+ # The search name (e.g. "ESCU - Windows Modify Registry EnableLinkedConnections - Rule")
9
+ search_name: str
10
+
11
+ # The search ID that found that generated this risk event
12
+ orig_sid: str
13
+
14
+ class Config:
15
+ # Allowing fields that aren't explicitly defined to be passed since some of the risk event's
16
+ # fields vary depending on the SPL which generated them
17
+ extra = 'allow'
18
+
19
+ def validate_against_detection(self, detection: Detection) -> None:
20
+ raise NotImplementedError()
@@ -1,8 +1,5 @@
1
- from __future__ import annotations
2
- from pydantic import BaseModel, validator
3
-
4
- from contentctl.objects.constants import *
5
-
1
+ from pydantic import BaseModel, field_validator
2
+ from contentctl.objects.constants import SES_OBSERVABLE_TYPE_MAPPING, SES_OBSERVABLE_ROLE_MAPPING
6
3
 
7
4
 
8
5
  class Observable(BaseModel):
@@ -10,32 +7,29 @@ class Observable(BaseModel):
10
7
  type: str
11
8
  role: list[str]
12
9
 
13
-
14
-
15
- @validator('name')
16
- def check_name(cls, v, values):
10
+ @field_validator('name')
11
+ def check_name(cls, v: str):
17
12
  if v == "":
18
13
  raise ValueError("No name provided for observable")
19
14
  return v
20
-
21
- @validator('type')
22
- def check_type(cls, v, values):
15
+
16
+ @field_validator('type')
17
+ def check_type(cls, v: str):
23
18
  if v not in SES_OBSERVABLE_TYPE_MAPPING.keys():
24
- raise ValueError(f"Invalid type '{v}' provided for observable. Valid observable types are {SES_OBSERVABLE_TYPE_MAPPING.keys()}")
19
+ raise ValueError(
20
+ f"Invalid type '{v}' provided for observable. Valid observable types are "
21
+ f"{SES_OBSERVABLE_TYPE_MAPPING.keys()}"
22
+ )
25
23
  return v
26
24
 
27
-
28
- @validator('role', each_item=False)
29
- def check_roles_not_empty(cls, v, values):
25
+ @field_validator('role')
26
+ def check_roles(cls, v: list[str]):
30
27
  if len(v) == 0:
31
- raise ValueError("At least one role must be defined for observable")
28
+ raise ValueError("Error, at least 1 role must be listed for Observable.")
29
+ for role in v:
30
+ if role not in SES_OBSERVABLE_ROLE_MAPPING.keys():
31
+ raise ValueError(
32
+ f"Invalid role '{role}' provided for observable. Valid observable types are "
33
+ f"{SES_OBSERVABLE_ROLE_MAPPING.keys()}"
34
+ )
32
35
  return v
33
-
34
- @validator('role', each_item=True)
35
- def check_roles(cls, v, values):
36
- if v not in SES_OBSERVABLE_ROLE_MAPPING.keys():
37
- raise ValueError(f"Invalid role '{v}' provided for observable. Valid observable types are {SES_OBSERVABLE_ROLE_MAPPING.keys()}")
38
- return v
39
-
40
-
41
-
@@ -7,7 +7,7 @@ from contentctl.objects.risk_object import RiskObject
7
7
  from contentctl.objects.threat_object import ThreatObject
8
8
 
9
9
 
10
- # TODO (cmcginley): add logic which reports concretely that integration testing failed (or would fail)
10
+ # TODO (#231): add logic which reports concretely that integration testing failed (or would fail)
11
11
  # as a result of a missing victim observable
12
12
  class RiskAnalysisAction(BaseModel):
13
13
  """Representation of a risk analysis action
@@ -77,7 +77,7 @@ class RiskAnalysisAction(BaseModel):
77
77
  risk_objects: list[RiskObject] = []
78
78
  threat_objects: list[ThreatObject] = []
79
79
 
80
- # TODO (cmcginley): add validation ensuring at least 1 risk objects
80
+ # TODO (#231): add validation ensuring at least 1 risk objects
81
81
  for entry in object_dicts:
82
82
  if "risk_object_field" in entry:
83
83
  risk_objects.append(RiskObject(