contentctl 4.4.7__py3-none-any.whl → 5.0.0a0__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 (69) hide show
  1. contentctl/actions/build.py +39 -27
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +0 -1
  3. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +32 -26
  4. contentctl/actions/detection_testing/progress_bar.py +6 -6
  5. contentctl/actions/detection_testing/views/DetectionTestingView.py +4 -4
  6. contentctl/actions/new_content.py +98 -81
  7. contentctl/actions/test.py +4 -5
  8. contentctl/actions/validate.py +2 -1
  9. contentctl/contentctl.py +114 -79
  10. contentctl/helper/utils.py +0 -14
  11. contentctl/input/director.py +5 -5
  12. contentctl/input/new_content_questions.py +2 -2
  13. contentctl/input/yml_reader.py +11 -6
  14. contentctl/objects/abstract_security_content_objects/detection_abstract.py +228 -120
  15. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +5 -7
  16. contentctl/objects/alert_action.py +2 -1
  17. contentctl/objects/atomic.py +1 -0
  18. contentctl/objects/base_test.py +4 -3
  19. contentctl/objects/base_test_result.py +3 -3
  20. contentctl/objects/baseline.py +26 -6
  21. contentctl/objects/baseline_tags.py +2 -3
  22. contentctl/objects/config.py +26 -45
  23. contentctl/objects/constants.py +4 -1
  24. contentctl/objects/correlation_search.py +89 -95
  25. contentctl/objects/data_source.py +5 -6
  26. contentctl/objects/deployment.py +2 -10
  27. contentctl/objects/deployment_email.py +2 -1
  28. contentctl/objects/deployment_notable.py +2 -1
  29. contentctl/objects/deployment_phantom.py +2 -1
  30. contentctl/objects/deployment_rba.py +2 -1
  31. contentctl/objects/deployment_scheduling.py +2 -1
  32. contentctl/objects/deployment_slack.py +2 -1
  33. contentctl/objects/detection_tags.py +7 -42
  34. contentctl/objects/drilldown.py +1 -0
  35. contentctl/objects/enums.py +21 -58
  36. contentctl/objects/investigation.py +6 -5
  37. contentctl/objects/investigation_tags.py +2 -3
  38. contentctl/objects/lookup.py +145 -63
  39. contentctl/objects/macro.py +2 -3
  40. contentctl/objects/mitre_attack_enrichment.py +2 -2
  41. contentctl/objects/observable.py +3 -1
  42. contentctl/objects/playbook_tags.py +5 -1
  43. contentctl/objects/rba.py +90 -0
  44. contentctl/objects/risk_event.py +87 -144
  45. contentctl/objects/story_tags.py +1 -2
  46. contentctl/objects/test_attack_data.py +2 -1
  47. contentctl/objects/unit_test_baseline.py +2 -1
  48. contentctl/output/api_json_output.py +233 -220
  49. contentctl/output/conf_output.py +51 -44
  50. contentctl/output/conf_writer.py +201 -125
  51. contentctl/output/data_source_writer.py +0 -1
  52. contentctl/output/json_writer.py +2 -4
  53. contentctl/output/svg_output.py +1 -1
  54. contentctl/output/templates/analyticstories_detections.j2 +1 -1
  55. contentctl/output/templates/collections.j2 +1 -1
  56. contentctl/output/templates/doc_detections.j2 +0 -5
  57. contentctl/output/templates/savedsearches_detections.j2 +8 -3
  58. contentctl/output/templates/transforms.j2 +4 -4
  59. contentctl/output/yml_writer.py +15 -0
  60. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  61. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/METADATA +5 -4
  62. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/RECORD +65 -68
  63. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/WHEEL +1 -1
  64. contentctl/objects/event_source.py +0 -11
  65. contentctl/output/detection_writer.py +0 -28
  66. contentctl/output/new_content_yml_output.py +0 -56
  67. contentctl/output/yml_output.py +0 -66
  68. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/LICENSE.md +0 -0
  69. {contentctl-4.4.7.dist-info → contentctl-5.0.0a0.dist-info}/entry_points.txt +0 -0
@@ -83,9 +83,9 @@ class MitreAttackGroup(BaseModel):
83
83
  return []
84
84
  return contributors
85
85
 
86
- # TODO (#266): disable the use_enum_values configuration
87
86
  class MitreAttackEnrichment(BaseModel):
88
- ConfigDict(use_enum_values=True)
87
+
88
+ ConfigDict(extra='forbid')
89
89
  mitre_attack_id: MITRE_ATTACK_ID_TYPE = Field(...)
90
90
  mitre_attack_technique: str = Field(...)
91
91
  mitre_attack_tactics: List[MitreTactics] = Field(...)
@@ -1,8 +1,10 @@
1
- from pydantic import BaseModel, field_validator
1
+ from pydantic import BaseModel, field_validator, ConfigDict
2
2
  from contentctl.objects.constants import SES_OBSERVABLE_TYPE_MAPPING, RBA_OBSERVABLE_ROLE_MAPPING
3
3
 
4
+ # TODO (cmcginley): should this class be removed?
4
5
 
5
6
  class Observable(BaseModel):
7
+ model_config = ConfigDict(extra="forbid")
6
8
  name: str
7
9
  type: str
8
10
  role: list[str]
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from typing import TYPE_CHECKING, Optional, List
3
- from pydantic import BaseModel, Field
3
+ from pydantic import BaseModel, Field,ConfigDict
4
4
  import enum
5
5
  from contentctl.objects.detection import Detection
6
6
 
@@ -36,6 +36,7 @@ class DefendTechnique(str,enum.Enum):
36
36
  D3_SRA = "D3-SRA"
37
37
  D3_RUAA = "D3-RUAA"
38
38
  class PlaybookTag(BaseModel):
39
+ model_config = ConfigDict(extra="forbid")
39
40
  analytic_story: Optional[list] = None
40
41
  detections: Optional[list] = None
41
42
  platform_tags: list[str] = Field(...,min_length=0)
@@ -46,5 +47,8 @@ class PlaybookTag(BaseModel):
46
47
  use_cases: list[PlaybookUseCase] = Field([],min_length=0)
47
48
  defend_technique_id: Optional[List[DefendTechnique]] = None
48
49
 
50
+ labels:list[str] = []
51
+ playbook_outputs:list[str] = []
52
+
49
53
  detection_objects: list[Detection] = []
50
54
 
@@ -0,0 +1,90 @@
1
+ from enum import Enum
2
+ from pydantic import BaseModel, computed_field, Field
3
+ from abc import ABC
4
+ from typing import Set, Annotated
5
+ from contentctl.objects.enums import RiskSeverity
6
+
7
+
8
+ RiskScoreValue_Type = Annotated[int, Field(ge=1, le=100)]
9
+
10
+ class RiskObjectType(str, Enum):
11
+ SYSTEM = "system"
12
+ USER = "user"
13
+ OTHER = "other"
14
+
15
+ class ThreatObjectType(str, Enum):
16
+ CERTIFICATE_COMMON_NAME = "certificate_common_name"
17
+ CERTIFICATE_ORGANIZATION = "certificate_organization"
18
+ CERTIFICATE_SERIAL = "certificate_serial"
19
+ CERTIFICATE_UNIT = "certificate_unit"
20
+ COMMAND = "command"
21
+ DOMAIN = "domain"
22
+ EMAIL_ADDRESS = "email_address"
23
+ EMAIL_SUBJECT = "email_subject"
24
+ FILE_HASH = "file_hash"
25
+ FILE_NAME = "file_name"
26
+ FILE_PATH = "file_path"
27
+ HTTP_USER_AGENT = "http_user_agent"
28
+ IP_ADDRESS = "ip_address"
29
+ PROCESS = "process"
30
+ PROCESS_NAME = "process_name"
31
+ PARENT_PROCESS = "parent_process"
32
+ PARENT_PROCESS_NAME = "parent_process_name"
33
+ PROCESS_HASH = "process_hash"
34
+ REGISTRY_PATH = "registry_path"
35
+ REGISTRY_VALUE_NAME = "registry_value_name"
36
+ REGISTRY_VALUE_TEXT = "registry_value_text"
37
+ SERVICE = "service"
38
+ SIGNATURE = "signature"
39
+ SYSTEM = "system"
40
+ TLS_HASH = "tls_hash"
41
+ URL = "url"
42
+
43
+ class RiskObject(BaseModel):
44
+ field: str
45
+ type: RiskObjectType
46
+ score: RiskScoreValue_Type
47
+
48
+ def __hash__(self):
49
+ return hash((self.field, self.type, self.score))
50
+
51
+ class ThreatObject(BaseModel):
52
+ field: str
53
+ type: ThreatObjectType
54
+
55
+ def __hash__(self):
56
+ return hash((self.field, self.type))
57
+
58
+ class RBAObject(BaseModel, ABC):
59
+ message: str
60
+ risk_objects: Annotated[Set[RiskObject], Field(min_length=1)]
61
+ threat_objects: Set[ThreatObject]
62
+
63
+
64
+
65
+ @computed_field
66
+ @property
67
+ def risk_score(self)->RiskScoreValue_Type:
68
+ # First get the maximum score associated with
69
+ # a risk object. If there are no objects, then
70
+ # we should throw an exception.
71
+ if len(self.risk_objects) == 0:
72
+ raise Exception("There must be at least one Risk Object present to get Severity.")
73
+ return max([risk_object.score for risk_object in self.risk_objects])
74
+
75
+ @computed_field
76
+ @property
77
+ def severity(self)->RiskSeverity:
78
+ if 0 <= self.risk_score <= 20:
79
+ return RiskSeverity.INFORMATIONAL
80
+ elif 20 < self.risk_score <= 40:
81
+ return RiskSeverity.LOW
82
+ elif 40 < self.risk_score <= 60:
83
+ return RiskSeverity.MEDIUM
84
+ elif 60 < self.risk_score <= 80:
85
+ return RiskSeverity.HIGH
86
+ elif 80 < self.risk_score <= 100:
87
+ return RiskSeverity.CRITICAL
88
+ else:
89
+ raise Exception(f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}")
90
+
@@ -4,48 +4,7 @@ from functools import cached_property
4
4
  from pydantic import ConfigDict, BaseModel, Field, PrivateAttr, field_validator, computed_field
5
5
  from contentctl.objects.errors import ValidationFailed
6
6
  from contentctl.objects.detection import Detection
7
- from contentctl.objects.observable import Observable
8
-
9
- # TODO (#259): Map our observable types to more than user/system
10
- # TODO (#247): centralize this mapping w/ usage of SES_OBSERVABLE_TYPE_MAPPING (see
11
- # observable.py) and the ad hoc mapping made in detection_abstract.py (see the risk property func)
12
- TYPE_MAP: dict[str, list[str]] = {
13
- "system": [
14
- "Hostname",
15
- "IP Address",
16
- "Endpoint"
17
- ],
18
- "user": [
19
- "User",
20
- "User Name",
21
- "Email Address",
22
- "Email"
23
- ],
24
- "hash_values": [],
25
- "network_artifacts": [],
26
- "host_artifacts": [],
27
- "tools": [],
28
- "other": [
29
- "Process",
30
- "URL String",
31
- "Unknown",
32
- "Process Name",
33
- "MAC Address",
34
- "File Name",
35
- "File Hash",
36
- "Resource UID",
37
- "Uniform Resource Locator",
38
- "File",
39
- "Geo Location",
40
- "Container",
41
- "Registry Key",
42
- "Registry Value",
43
- "Other"
44
- ]
45
- }
46
-
47
- # Roles that should not generate risks
48
- IGNORE_ROLES: list[str] = ["Attacker"]
7
+ from contentctl.objects.rba import RiskObject
49
8
 
50
9
 
51
10
  class RiskEvent(BaseModel):
@@ -55,10 +14,12 @@ class RiskEvent(BaseModel):
55
14
  search_name: str
56
15
 
57
16
  # The subject of the risk event (e.g. a username, process name, system name, account ID, etc.)
58
- risk_object: int | str
17
+ # (not to be confused w/ the risk object from the detection)
18
+ es_risk_object: int | str
59
19
 
60
- # The type of the risk object (e.g. user, system, or other)
61
- risk_object_type: str
20
+ # The type of the risk object from ES (e.g. user, system, or other) (not to be confused w/
21
+ # the risk object from the detection)
22
+ es_risk_object_type: str
62
23
 
63
24
  # The level of risk associated w/ the risk event
64
25
  risk_score: int
@@ -79,11 +40,11 @@ class RiskEvent(BaseModel):
79
40
  )
80
41
 
81
42
  # Contributing events search query (we use this to derive the corresponding field from the
82
- # observables)
43
+ # detection's risk object definition)
83
44
  contributing_events_search: str
84
45
 
85
- # Private attribute caching the observable this RiskEvent is mapped to
86
- _matched_observable: Observable | None = PrivateAttr(default=None)
46
+ # Private attribute caching the risk object this RiskEvent is mapped to
47
+ _matched_risk_object: RiskObject | None = PrivateAttr(default=None)
87
48
 
88
49
  # Allowing fields that aren't explicitly defined to be passed since some of the risk event's
89
50
  # fields vary depending on the SPL which generated them
@@ -108,7 +69,7 @@ class RiskEvent(BaseModel):
108
69
  def source_field_name(self) -> str:
109
70
  """
110
71
  A cached derivation of the source field name the risk event corresponds to in the relevant
111
- event(s). Useful for mapping back to an observable in the detection.
72
+ event(s). Useful for mapping back to a risk object in the detection.
112
73
  """
113
74
  pattern = re.compile(
114
75
  r"\| savedsearch \"" + self.search_name + r"\" \| search (?P<field>[^=]+)=.+"
@@ -128,13 +89,6 @@ class RiskEvent(BaseModel):
128
89
  :param detection: the detection associated w/ this risk event
129
90
  :raises: ValidationFailed
130
91
  """
131
- # Check risk_score
132
- if self.risk_score != detection.tags.risk_score:
133
- raise ValidationFailed(
134
- f"Risk score observed in risk event ({self.risk_score}) does not match risk score in "
135
- f"detection ({detection.tags.risk_score})."
136
- )
137
-
138
92
  # Check analyticstories
139
93
  self.validate_analyticstories(detection)
140
94
 
@@ -151,8 +105,15 @@ class RiskEvent(BaseModel):
151
105
  # Check risk_message
152
106
  self.validate_risk_message(detection)
153
107
 
154
- # Check several conditions against the observables
155
- self.validate_risk_against_observables(detection.tags.observable)
108
+ # Ensure the rba object is defined
109
+ if detection.rba is None:
110
+ raise ValidationFailed(
111
+ f"Unexpected error: Detection '{detection.name}' has no RBA objects associated "
112
+ "with it; cannot validate."
113
+ )
114
+
115
+ # Check several conditions against the detection's risk objects
116
+ self.validate_risk_against_risk_objects(detection.rba.risk_objects)
156
117
 
157
118
  def validate_mitre_ids(self, detection: Detection) -> None:
158
119
  """
@@ -177,7 +138,7 @@ class RiskEvent(BaseModel):
177
138
  if sorted(self.analyticstories) != sorted(detection_analytic_story):
178
139
  raise ValidationFailed(
179
140
  f"Analytic stories in risk event ({self.analyticstories}) do not match those"
180
- f" in detection ({detection.tags.analytic_story})."
141
+ f" in detection ({[x.name for x in detection.tags.analytic_story]})."
181
142
  )
182
143
 
183
144
  def validate_risk_message(self, detection: Detection) -> None:
@@ -186,10 +147,20 @@ class RiskEvent(BaseModel):
186
147
  :param detection: the detection associated w/ this risk event
187
148
  :raises: ValidationFailed
188
149
  """
150
+ # Ensure the rba object is defined
151
+ if detection.rba is None:
152
+ raise ValidationFailed(
153
+ f"Unexpected error: Detection '{detection.name}' has no RBA objects associated "
154
+ "with it; cannot validate."
155
+ )
156
+
189
157
  # Extract the field replacement tokens ("$...$")
190
158
  field_replacement_pattern = re.compile(r"\$\S+\$")
191
- tokens = field_replacement_pattern.findall(detection.tags.message)
159
+ tokens = field_replacement_pattern.findall(detection.rba.message)
192
160
 
161
+ # TODO (#346): could expand this to get the field values from the raw events and check
162
+ # to see that allexpected strings ARE in the risk message (as opposed to checking only
163
+ # that unexpected strings aren't)
193
164
  # Check for the presence of each token in the message from the risk event
194
165
  for token in tokens:
195
166
  if token in self.risk_message:
@@ -205,7 +176,7 @@ class RiskEvent(BaseModel):
205
176
  escaped_source_message_with_placeholder: str = re.escape(
206
177
  field_replacement_pattern.sub(
207
178
  tmp_placeholder,
208
- detection.tags.message
179
+ detection.rba.message
209
180
  )
210
181
  )
211
182
  placeholder_replacement_pattern = re.compile(tmp_placeholder)
@@ -221,114 +192,86 @@ class RiskEvent(BaseModel):
221
192
  raise ValidationFailed(
222
193
  "Risk message in event does not match the pattern set by the detection. Message in "
223
194
  f"risk event: \"{self.risk_message}\". Message in detection: "
224
- f"\"{detection.tags.message}\"."
195
+ f"\"{detection.rba.message}\"."
225
196
  )
226
197
 
227
- def validate_risk_against_observables(self, observables: list[Observable]) -> None:
198
+ def validate_risk_against_risk_objects(self, risk_objects: set[RiskObject]) -> None:
228
199
  """
229
- Given the observables from the associated detection, validate the risk event against those
230
- observables
231
- :param observables: the Observable objects from the detection
200
+ Given the risk objects from the associated detection, validate the risk event against those
201
+ risk objects
202
+ :param risk_objects: the risk objects from the detection
232
203
  :raises: ValidationFailed
233
204
  """
234
- # Get the matched observable; will raise validation errors if no match can be made or if
235
- # risk is missing values associated w/ observables
236
- matched_observable = self.get_matched_observable(observables)
205
+ # Get the matched risk object; will raise validation errors if no match can be made or if
206
+ # risk is missing values associated w/ risk objects
207
+ matched_risk_object = self.get_matched_risk_object(risk_objects)
237
208
 
238
- # The risk object type should match our mapping of observable types to risk types
239
- expected_type = RiskEvent.observable_type_to_risk_type(matched_observable.type)
240
- if self.risk_object_type != expected_type:
209
+ # The risk object type from the risk event should match our mapping of internal risk object
210
+ # types
211
+ if self.es_risk_object_type != matched_risk_object.type.value:
241
212
  raise ValidationFailed(
242
- f"The risk object type ({self.risk_object_type}) does not match the expected type "
243
- f"based on the matched observable ({matched_observable.type}->{expected_type}): "
244
- f"risk=(object={self.risk_object}, type={self.risk_object_type}, "
245
- f"source_field_name={self.source_field_name}), "
246
- f"observable=(name={matched_observable.name}, type={matched_observable.type}, "
247
- f"role={matched_observable.role})"
213
+ f"The risk object type from the risk event ({self.es_risk_object_type}) does not match"
214
+ " the expected type based on the matched risk object "
215
+ f"({matched_risk_object.type.value}): risk event=(object={self.es_risk_object}, "
216
+ f"type={self.es_risk_object_type}, source_field_name={self.source_field_name}), "
217
+ f"risk object=(name={matched_risk_object.field}, "
218
+ f"type={matched_risk_object.type.value})"
248
219
  )
249
220
 
250
- @staticmethod
251
- def observable_type_to_risk_type(observable_type: str) -> str:
252
- """
253
- Given a string representing the observable type, use our mapping to convert it to the
254
- expected type in the risk event
255
- :param observable_type: the type of the observable
256
- :returns: a string (the risk object type)
257
- :raises ValueError: if the observable type has not yet been mapped to a risk object type
258
- """
259
- # Iterate over the map and search the lists for a match
260
- for risk_type in TYPE_MAP:
261
- if observable_type in TYPE_MAP[risk_type]:
262
- return risk_type
263
-
264
- raise ValueError(
265
- f"Observable type {observable_type} does not have a mapping to a risk type in TYPE_MAP"
266
- )
221
+ # Check risk_score
222
+ if self.risk_score != matched_risk_object.score:
223
+ raise ValidationFailed(
224
+ f"Risk score observed in risk event ({self.risk_score}) does not match risk score in "
225
+ f"matched risk object from detection ({matched_risk_object.score})."
226
+ )
267
227
 
268
- @staticmethod
269
- def ignore_observable(observable: Observable) -> bool:
270
- """
271
- Given an observable, determine based on its roles if it should be ignored in risk/observable
272
- matching (e.g. Attacker role observables should not generate risk events)
273
- :param observable: the Observable object we are checking the roles of
274
- :returns: a bool indicating whether this observable should be ignored or not
275
- """
276
- ignore = False
277
- for role in observable.role:
278
- if role in IGNORE_ROLES:
279
- ignore = True
280
- break
281
- return ignore
282
-
283
- def get_matched_observable(self, observables: list[Observable]) -> Observable:
228
+ def get_matched_risk_object(self, risk_objects: set[RiskObject]) -> RiskObject:
284
229
  """
285
- Given a list of observables, return the one this risk event matches
286
- :param observables: the list of Observable objects we are checking against
287
- :returns: the matched Observable object
230
+ Given a set of risk objects, return the one this risk event matches
231
+ :param risk_objects: the list of risk objects we are checking against
232
+ :returns: the matched risk object
288
233
  :raises ValidationFailed: if a match could not be made or if an expected field (based on
289
- one of the observables) could not be found in the risk event
234
+ one of the risk objects) could not be found in the risk event
290
235
  """
291
236
  # Return the cached match if already found
292
- if self._matched_observable is not None:
293
- return self._matched_observable
237
+ if self._matched_risk_object is not None:
238
+ return self._matched_risk_object
294
239
 
295
- matched_observable: Observable | None = None
240
+ matched_risk_object: RiskObject | None = None
296
241
 
297
242
  # Iterate over the obervables and check for a match
298
- for observable in observables:
243
+ for risk_object in risk_objects:
299
244
  # TODO (#252): Refactor and re-enable per-field validation of risk events
300
- # Each the field name used in each observable shoud be present in the risk event
301
- # if not hasattr(self, observable.name):
245
+ # Each the field name used in each risk object shoud be present in the risk event
246
+ # if not hasattr(self, risk_object.field):
302
247
  # raise ValidationFailed(
303
- # f"Observable field \"{observable.name}\" not found in risk event."
248
+ # f"Risk object field \"{risk_object.field}\" not found in risk event."
304
249
  # )
305
250
 
306
- # Try to match the risk_object against a specific observable for the obervables with
307
- # a valid role (some, like Attacker, shouldn't get converted to risk events)
308
- if self.source_field_name == observable.name:
309
- if matched_observable is not None:
251
+ # Try to match the risk_object against a specific risk object
252
+ if self.source_field_name == risk_object.field:
253
+ # TODO (#347): enforce that field names are not repeated across risk objects as
254
+ # part of build/validate
255
+ if matched_risk_object is not None:
310
256
  raise ValueError(
311
- "Unexpected conditon: we don't expect the source event field "
312
- "corresponding to an observables field name to be repeated."
257
+ "Unexpected conditon: we don't expect multiple risk objects to use the "
258
+ "same field name, so we should not be able match the risk event to "
259
+ "multiple risk objects."
313
260
  )
314
261
 
315
- # Report any risk events we find that shouldn't be there
316
- if RiskEvent.ignore_observable(observable):
317
- raise ValidationFailed(
318
- "Risk event matched an observable with an invalid role: "
319
- f"(name={observable.name}, type={observable.type}, role={observable.role})")
320
- # NOTE: we explicitly do not break early as we want to check each observable
321
- matched_observable = observable
262
+ # NOTE: we explicitly do not break early as we want to check each risk object
263
+ matched_risk_object = risk_object
322
264
 
323
- # Ensure we were able to match the risk event to a specific observable
324
- if matched_observable is None:
265
+ # Ensure we were able to match the risk event to a specific risk object
266
+ if matched_risk_object is None:
325
267
  raise ValidationFailed(
326
- f"Unable to match risk event (object={self.risk_object}, type="
327
- f"{self.risk_object_type}, source_field_name={self.source_field_name}) to an "
328
- "observable; please check for errors in the observable roles/types for this "
329
- "detection, as well as the risk event build process in contentctl."
268
+ f"Unable to match risk event (object={self.es_risk_object}, type="
269
+ f"{self.es_risk_object_type}, source_field_name={self.source_field_name}) to a "
270
+ "risk object in the detection; please check for errors in the risk object types for this "
271
+ "detection, as well as the risk event build process in contentctl (e.g. threat "
272
+ "objects aren't being converted to risk objects somehow)."
330
273
  )
331
274
 
332
- # Cache and return the matched observable
333
- self._matched_observable = matched_observable
334
- return self._matched_observable
275
+ # Cache and return the matched risk object
276
+ self._matched_risk_object = matched_risk_object
277
+ return self._matched_risk_object
@@ -18,9 +18,8 @@ class StoryUseCase(str,Enum):
18
18
  OTHER = "Other"
19
19
 
20
20
 
21
- # TODO (#266): disable the use_enum_values configuration
22
21
  class StoryTags(BaseModel):
23
- model_config = ConfigDict(extra='forbid', use_enum_values=True)
22
+ model_config = ConfigDict(extra='forbid')
24
23
  category: List[StoryCategory] = Field(...,min_length=1)
25
24
  product: List[SecurityContentProductName] = Field(...,min_length=1)
26
25
  usecase: StoryUseCase = Field(...)
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, HttpUrl, FilePath, Field
2
+ from pydantic import BaseModel, HttpUrl, FilePath, Field, ConfigDict
3
3
 
4
4
 
5
5
  class TestAttackData(BaseModel):
6
+ model_config = ConfigDict(extra="forbid")
6
7
  data: HttpUrl | FilePath = Field(...)
7
8
  # TODO - should source and sourcetype should be mapped to a list
8
9
  # of supported source and sourcetypes in a given environment?
@@ -1,9 +1,10 @@
1
1
 
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel,ConfigDict
4
4
  from typing import Union
5
5
 
6
6
  class UnitTestBaseline(BaseModel):
7
+ model_config = ConfigDict(extra="forbid")
7
8
  name: str
8
9
  file: str
9
10
  pass_condition: str