contentctl 4.2.2__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 (35) 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 +1 -0
  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-4.2.2.dist-info → contentctl-4.2.4.dist-info}/METADATA +5 -8
  32. {contentctl-4.2.2.dist-info → contentctl-4.2.4.dist-info}/RECORD +35 -31
  33. {contentctl-4.2.2.dist-info → contentctl-4.2.4.dist-info}/LICENSE.md +0 -0
  34. {contentctl-4.2.2.dist-info → contentctl-4.2.4.dist-info}/WHEEL +0 -0
  35. {contentctl-4.2.2.dist-info → contentctl-4.2.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,315 @@
1
+ import re
2
+ from typing import Union, Optional
3
+
4
+ from pydantic import BaseModel, Field, PrivateAttr, field_validator
5
+
6
+ from contentctl.objects.errors import ValidationFailed
7
+ from contentctl.objects.detection import Detection
8
+ from contentctl.objects.observable import Observable
9
+
10
+ # TODO (PEX-433): use SES_OBSERVABLE_TYPE_MAPPING
11
+ TYPE_MAP: dict[str, list[str]] = {
12
+ "user": ["User"],
13
+ "system": ["Hostname", "IP Address", "Endpoint"],
14
+ "other": ["Process", "URL String", "Unknown", "Process Name"],
15
+ }
16
+ # TODO (PEX-433): 'Email Address', 'File Name', 'File Hash', 'Other', 'User Name', 'File',
17
+ # 'Process Name'
18
+
19
+ # TODO (PEX-433): use SES_OBSERVABLE_ROLE_MAPPING
20
+ IGNORE_ROLES: list[str] = ["Attacker"]
21
+ # Known valid roles: Victim, Parent Process, Child Process
22
+ # TODO (PEX-433): 'Other', 'Target', 'Unknown'
23
+ # TODO (PEX-433): is Other a valid role
24
+
25
+ # TODO (PEX-433): do we need User Name in conjunction w/ User? User Name doesn't get mapped to
26
+ # "user" in risk events
27
+ # TODO (PEX-433): similarly, do we need Process and Process Name?
28
+
29
+ RESERVED_FIELDS = ["host"]
30
+
31
+
32
+ class RiskEvent(BaseModel):
33
+ """Model for risk event in ES"""
34
+
35
+ # The search name (e.g. "ESCU - Windows Modify Registry EnableLinkedConnections - Rule")
36
+ search_name: str
37
+
38
+ # The subject of the risk event (e.g. a username, process name, system name, account ID, etc.)
39
+ risk_object: Union[int, str]
40
+
41
+ # The type of the risk object (e.g. user, system, or other)
42
+ risk_object_type: str
43
+
44
+ # The level of risk associated w/ the risk event
45
+ risk_score: int
46
+
47
+ # The search ID that found that generated this risk event
48
+ orig_sid: str
49
+
50
+ # The message for the risk event
51
+ risk_message: str
52
+
53
+ # The analytic stories applicable to this risk event
54
+ analyticstories: list[str] = Field(default=[])
55
+
56
+ # The MITRE ATT&CK IDs
57
+ annotations_mitre_attack: list[str] = Field(
58
+ alias="annotations.mitre_attack",
59
+ default=[]
60
+ )
61
+
62
+ # Private attribute caching the observable this RiskEvent is mapped to
63
+ _matched_observable: Optional[Observable] = PrivateAttr(default=None)
64
+
65
+ class Config:
66
+ # Allowing fields that aren't explicitly defined to be passed since some of the risk event's
67
+ # fields vary depending on the SPL which generated them
68
+ extra = "allow"
69
+
70
+ @field_validator("annotations_mitre_attack", "analyticstories", mode="before")
71
+ @classmethod
72
+ def _convert_str_value_to_singleton(cls, v: Union[str, list[str]]) -> list[str]:
73
+ """
74
+ Given a value, determine if its a list or a single str value; if a single value, return as a
75
+ singleton. Do nothing if anything else.
76
+ """
77
+ if isinstance(v, list):
78
+ return v
79
+ else:
80
+ return [v]
81
+
82
+ def validate_against_detection(self, detection: Detection) -> None:
83
+ """
84
+ Given the associated detection, validate the risk event against its fields
85
+ :param detection: the detection associated w/ this risk event
86
+ :raises: ValidationFailed
87
+ """
88
+ # Check risk_score
89
+ if self.risk_score != detection.tags.risk_score:
90
+ raise ValidationFailed(
91
+ f"Risk score observed in risk event ({self.risk_score}) does not match risk score in "
92
+ f"detection ({detection.tags.risk_score})."
93
+ )
94
+
95
+ # Check analyticstories
96
+ self.validate_analyticstories(detection)
97
+
98
+ # Check annotations.mitre_attack
99
+ self.validate_mitre_ids(detection)
100
+
101
+ # Check search_name
102
+ if self.search_name != f"ESCU - {detection.name} - Rule":
103
+ raise ValidationFailed(
104
+ f"Saved Search name in risk event ({self.search_name}) does not match detection name "
105
+ f"({detection.name})."
106
+ )
107
+
108
+ # Check risk_message
109
+ self.validate_risk_message(detection)
110
+
111
+ # TODO (PEX-433): Re-enable this check once we have refined the logic and reduced the false
112
+ # positive rate in risk/obseravble matching
113
+ # Check several conditions against the observables
114
+ # self.validate_risk_against_observables(detection.tags.observable)
115
+
116
+ def validate_mitre_ids(self, detection: Detection) -> None:
117
+ """
118
+ Given the associated detection, validate the risk event's MITRE attack IDs
119
+ :param detection: the detection associated w/ this risk event
120
+ :raises: ValidationFailed
121
+ """
122
+ if sorted(self.annotations_mitre_attack) != sorted(detection.tags.mitre_attack_id):
123
+ raise ValidationFailed(
124
+ f"MITRE ATT&CK IDs in risk event ({self.annotations_mitre_attack}) do not match those"
125
+ f" in detection ({detection.tags.mitre_attack_id})."
126
+ )
127
+
128
+ def validate_analyticstories(self, detection: Detection) -> None:
129
+ """
130
+ Given the associated detection, validate the risk event's MITRE analytic stories
131
+ :param detection: the detection associated w/ this risk event
132
+ :raises: ValidationFailed
133
+ """
134
+ # Render the detection analytic_story to a list of strings before comparing
135
+ detection_analytic_story = [story.name for story in detection.tags.analytic_story]
136
+ if sorted(self.analyticstories) != sorted(detection_analytic_story):
137
+ raise ValidationFailed(
138
+ f"Analytic stories in risk event ({self.analyticstories}) do not match those"
139
+ f" in detection ({detection.tags.analytic_story})."
140
+ )
141
+
142
+ def validate_risk_message(self, detection: Detection) -> None:
143
+ """
144
+ Given the associated detection, validate the risk event's message
145
+ :param detection: the detection associated w/ this risk event
146
+ :raises: ValidationFailed
147
+ """
148
+ # Extract the field replacement tokens ("$...$")
149
+ field_replacement_pattern = re.compile(r"\$\S+\$")
150
+ tokens = field_replacement_pattern.findall(detection.tags.message)
151
+
152
+ # Check for the presence of each token in the message from the risk event
153
+ for token in tokens:
154
+ if token in self.risk_message:
155
+ raise ValidationFailed(
156
+ f"Unreplaced field replacement string ('{token}') found in risk message:"
157
+ f" {self.risk_message}"
158
+ )
159
+
160
+ # Convert detection source message to regex pattern; we need to first sub in a placeholder
161
+ # so we can escape the string, and then swap in the actual regex elements in place of the
162
+ # placeholder
163
+ tmp_placeholder = "PLACEHOLDERPATTERNFORESCAPING"
164
+ escaped_source_message_with_placeholder: str = re.escape(
165
+ field_replacement_pattern.sub(
166
+ tmp_placeholder,
167
+ detection.tags.message
168
+ )
169
+ )
170
+ placeholder_replacement_pattern = re.compile(tmp_placeholder)
171
+ final_risk_message_pattern = re.compile(
172
+ placeholder_replacement_pattern.sub(
173
+ r"[\\s\\S]*\\S[\\s\\S]*",
174
+ escaped_source_message_with_placeholder
175
+ )
176
+ )
177
+
178
+ # Check created regex pattern againt the observed risk message
179
+ if final_risk_message_pattern.match(self.risk_message) is None:
180
+ raise ValidationFailed(
181
+ "Risk message in event does not match the pattern set by the detection. Message in "
182
+ f"risk event: \"{self.risk_message}\". Message in detection: "
183
+ f"\"{detection.tags.message}\"."
184
+ )
185
+
186
+ def validate_risk_against_observables(self, observables: list[Observable]) -> None:
187
+ """
188
+ Given the observables from the associated detection, validate the risk event against those
189
+ observables
190
+ :param observables: the Observable objects from the detection
191
+ :raises: ValidationFailed
192
+ """
193
+ # Get the matched observable; will raise validation errors if no match can be made or if
194
+ # risk is missing values associated w/ observables
195
+ matched_observable = self.get_matched_observable(observables)
196
+
197
+ # The risk object type should match our mapping of observable types to risk types
198
+ expected_type = RiskEvent.observable_type_to_risk_type(matched_observable.type)
199
+ if self.risk_object_type != expected_type:
200
+ raise ValidationFailed(
201
+ f"The risk object type ({self.risk_object_type}) does not match the expected type "
202
+ f"based on the matched observable ({matched_observable.type}=={expected_type})."
203
+ )
204
+
205
+ @staticmethod
206
+ def observable_type_to_risk_type(observable_type: str) -> str:
207
+ """
208
+ Given a string representing the observable type, use our mapping to convert it to the
209
+ expected type in the risk event
210
+ :param observable_type: the type of the observable
211
+ :returns: a string (the risk object type)
212
+ :raises ValueError: if the observable type has not yet been mapped to a risk object type
213
+ """
214
+ # Iterate over the map and search the lists for a match
215
+ for risk_type in TYPE_MAP:
216
+ if observable_type in TYPE_MAP[risk_type]:
217
+ return risk_type
218
+
219
+ raise ValueError(
220
+ f"Observable type {observable_type} does not have a mapping to a risk type in TYPE_MAP"
221
+ )
222
+
223
+ # TODO (PEX-433): should this be an observable instance method? It feels less relevant to
224
+ # observables themselves, as it's really only relevant to the handling of risk events
225
+ @staticmethod
226
+ def ignore_observable(observable: Observable) -> bool:
227
+ """
228
+ Given an observable, determine based on its roles if it should be ignored in risk/observable
229
+ matching (e.g. Attacker role observables should not generate risk events)
230
+ :param observable: the Observable object we are checking the roles of
231
+ :returns: a bool indicating whether this observable should be ignored or not
232
+ """
233
+ # TODO (PEX-433): could there be a case where an observable has both an Attacker and Victim
234
+ # (or equivalent) role? If so, how should we handle ignoring it?
235
+ ignore = False
236
+ for role in observable.role:
237
+ if role in IGNORE_ROLES:
238
+ ignore = True
239
+ break
240
+ return ignore
241
+
242
+ # TODO (PEX-433): two possibilities: alway check for the field itself and the field prefixed
243
+ # w/ "orig_" OR more explicitly maintain a list of known "reserved fields", like "host". I
244
+ # think I like option 2 better as it can have fewer unknown side effects
245
+ def matches_observable(self, observable: Observable) -> bool:
246
+ """
247
+ Given an observable, check if the risk event matches is
248
+ :param observable: the Observable object we are comparing the risk event against
249
+ :returns: bool indicating a match or not
250
+ """
251
+ # When field names collide w/ reserved fields in Splunk events (e.g. sourcetype or host)
252
+ # they get prefixed w/ "orig_"
253
+ attribute_name = observable.name
254
+ if attribute_name in RESERVED_FIELDS:
255
+ attribute_name = f"orig_{attribute_name}"
256
+
257
+ # Retrieve the value of this attribute and see if it matches the risk_object
258
+ value: Union[str, list[str]] = getattr(self, attribute_name)
259
+ if isinstance(value, str):
260
+ value = [value]
261
+
262
+ # The value of the attribute may be a list of values, so check for any matches
263
+ return self.risk_object in value
264
+
265
+ def get_matched_observable(self, observables: list[Observable]) -> Observable:
266
+ """
267
+ Given a list of observables, return the one this risk event matches
268
+ :param observables: the list of Observable objects we are checking against
269
+ :returns: the matched Observable object
270
+ :raises ValidationFailed: if a match could not be made or if an expected field (based on
271
+ one of the observables) could not be found in the risk event
272
+ """
273
+ # Return the cached match if already found
274
+ if self._matched_observable is not None:
275
+ return self._matched_observable
276
+
277
+ matched_observable: Optional[Observable] = None
278
+
279
+ # Iterate over the obervables and check for a match
280
+ for observable in observables:
281
+ # Each the field name used in each observable shoud be present in the risk event
282
+ # TODO (PEX-433): this check is redundant I think; earlier in the unit test, observable
283
+ # field
284
+ # names are compared against the search result set, ensuring all are present; if all
285
+ # are present in the result set, all are present in the risk event
286
+ if not hasattr(self, observable.name):
287
+ raise ValidationFailed(
288
+ f"Observable field \"{observable.name}\" not found in risk event."
289
+ )
290
+
291
+ # Try to match the risk_object against a specific observable for the obervables with
292
+ # a valid role (some, like Attacker, don't get converted to risk events)
293
+ if not RiskEvent.ignore_observable(observable):
294
+ if self.matches_observable(observable):
295
+ # TODO (PEX-433): This check fails as there are some instances where this is
296
+ # true (e.g. we have an observable for process and parent_process and both
297
+ # have the same name like "cmd.exe")
298
+ if matched_observable is not None:
299
+ raise ValueError(
300
+ "Unexpected conditon: we don't expect the value corresponding to an "
301
+ "observables field name to be repeated"
302
+ )
303
+ # NOTE: we explicitly do not break early as we want to check each observable
304
+ matched_observable = observable
305
+
306
+ # Ensure we were able to match the risk event to a specific observable
307
+ if matched_observable is None:
308
+ raise ValidationFailed(
309
+ f"Unable to match risk event ({self.risk_object}, {self.risk_object_type}) to an "
310
+ "appropriate observable"
311
+ )
312
+
313
+ # Cache and return the matched observable
314
+ self._matched_observable = matched_observable
315
+ return self._matched_observable
@@ -53,7 +53,7 @@ class SSADetectionTags(BaseModel):
53
53
 
54
54
  @validator('cis20')
55
55
  def tags_cis20(cls, v, values):
56
- pattern = '^CIS ([0-9]|1[0-9]|20)$' #DO NOT match leading zeroes and ensure no extra characters before or after the string
56
+ pattern = r'^CIS ([\d|1\d|20)$' #DO NOT match leading zeroes and ensure no extra characters before or after the string
57
57
  for value in v:
58
58
  if not re.match(pattern, value):
59
59
  raise ValueError(f"CIS control '{value}' is not a valid Control ('CIS 1' -> 'CIS 20'): {values['name']}")
@@ -25,10 +25,10 @@ class StoryTags(BaseModel):
25
25
 
26
26
  # enrichment
27
27
  mitre_attack_enrichments: Optional[List[MitreAttackEnrichment]] = None
28
- mitre_attack_tactics: Optional[Set[Annotated[str, Field(pattern="^T\d{4}(.\d{3})?$")]]] = None
28
+ mitre_attack_tactics: Optional[Set[Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]]] = None
29
29
  datamodels: Optional[Set[DataModel]] = None
30
30
  kill_chain_phases: Optional[Set[KillChainPhase]] = None
31
- cve: List[Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"]] = []
31
+ cve: List[Annotated[str, r"^CVE-[1|2]\d{3}-\d+$"]] = []
32
32
  group: List[str] = Field([], description="A list of groups who leverage the techniques list in this Analytic Story.")
33
33
 
34
34
  def getCategory_conf(self) -> str:
@@ -1,16 +1,8 @@
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
7
-
8
2
  from typing import Union
9
3
 
10
4
  from pydantic import Field
11
5
 
12
- # from contentctl.objects.security_content_object import SecurityContentObject
13
- # from contentctl.objects.enums import SecurityContentType
14
6
  from contentctl.objects.unit_test_baseline import UnitTestBaseline
15
7
  from contentctl.objects.unit_test_attack_data import UnitTestAttackData
16
8
  from contentctl.objects.unit_test_result import UnitTestResult
@@ -44,7 +36,7 @@ class UnitTest(BaseTest):
44
36
  Skip the test by setting its result status
45
37
  :param message: the reason for skipping
46
38
  """
47
- self.result = UnitTestResult(
39
+ self.result = UnitTestResult( # type: ignore
48
40
  message=message,
49
41
  status=TestResultStatus.SKIP
50
42
  )
@@ -18,10 +18,10 @@ class DataSourceWriter:
18
18
  ])
19
19
  # Write the data
20
20
  for data_source in data_source_objects:
21
- if data_source.supported_TA and isinstance(data_source.supported_TA, list) and len(data_source.supported_TA) > 0:
22
- supported_TA_name = data_source.supported_TA[0].get('name', '')
23
- supported_TA_version = data_source.supported_TA[0].get('version', '')
24
- supported_TA_url = data_source.supported_TA[0].get('url', '')
21
+ if len(data_source.supported_TA) > 0:
22
+ supported_TA_name = data_source.supported_TA[0].name
23
+ supported_TA_version = data_source.supported_TA[0].version
24
+ supported_TA_url = data_source.supported_TA[0].url or ''
25
25
  else:
26
26
  supported_TA_name = ''
27
27
  supported_TA_version = ''
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: contentctl
3
- Version: 4.2.2
3
+ Version: 4.2.4
4
4
  Summary: Splunk Content Control Tool
5
5
  License: Apache 2.0
6
6
  Author: STRT
@@ -24,7 +24,7 @@ Requires-Dist: pysigma-backend-splunk (>=1.1.0,<2.0.0)
24
24
  Requires-Dist: questionary (>=2.0.1,<3.0.0)
25
25
  Requires-Dist: requests (>=2.32.2,<2.33.0)
26
26
  Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
27
- Requires-Dist: setuptools (>=69.5.1,<71.0.0)
27
+ Requires-Dist: setuptools (>=69.5.1,<73.0.0)
28
28
  Requires-Dist: splunk-sdk (>=2.0.1,<3.0.0)
29
29
  Requires-Dist: tqdm (>=4.66.4,<5.0.0)
30
30
  Requires-Dist: tyro (>=0.8.3,<0.9.0)
@@ -98,10 +98,7 @@ Testing is run using [GitHub Hosted Runners](https://docs.github.com/en/actions/
98
98
 
99
99
  | Requirement | Supported | Description | Passing Integration Tests |
100
100
  | --------------------- | ----- | ---- | ------ |
101
- | Python <3.9 | No | No support planned. contentctl tool uses modern language constructs not supported ion Python3.8 and below | N/A |
102
- | Python 3.9 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
103
- | Python 3.10 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
104
- | Python 3.11 | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
101
+ | Python 3.11+ | Yes | contentctl tool is written in Python | Yes (locally + GitHub Actions) |
105
102
  | Docker (local) | Yes | A running Splunk Server is required for Dynamic Testing. contentctl can automatically create, configure, and destroy this server as a Splunk container during the lifetime of a test. | (locally + GitHub Actions) |
106
103
  | Docker (remote) | Planned | A running Splunk Server is required for Dynamic Testing. contentctl can automatically create, configure, and destroy this server as a Splunk container during the lifetime of a test. | No |
107
104
 
@@ -113,7 +110,7 @@ It is typically recommended to install poetry to the Global Python Environment.*
113
110
 
114
111
  #### Install via pip (recommended):
115
112
  ```
116
- python3.9 -m venv .venv
113
+ python3.11 -m venv .venv
117
114
  source .venv/bin/activate
118
115
  pip install contentctl
119
116
  ```
@@ -122,7 +119,7 @@ pip install contentctl
122
119
  ```
123
120
  git clone https://github.com/splunk/contentctl
124
121
  cd contentctl
125
- python3.9 -m pip install poetry
122
+ python3.11 -m pip install poetry
126
123
  poetry install
127
124
  poetry shell
128
125
  contentctl --help
@@ -5,11 +5,11 @@ contentctl/actions/deploy_acs.py,sha256=mf3uk495H1EU_LNN-TiOsYCo18HMGoEBMb6ojeTr
5
5
  contentctl/actions/detection_testing/DetectionTestingManager.py,sha256=zg8JasDjCpSC-yhseEyUwO8qbDJIUJbhlus9Li9ZAnA,8818
6
6
  contentctl/actions/detection_testing/GitService.py,sha256=W1vnDDt8JvIL7Z1Lve3D3RS7h8qwMxrW0BMXVGuDZDM,9007
7
7
  contentctl/actions/detection_testing/generate_detection_coverage_badge.py,sha256=N5mznaeErVak3mOBwsd0RDBFJO3bku0EZvpayCyU-uk,2259
8
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=VFhSHdw_0N6ol668hDkaj7yFjPsZqBoFNC8FKzWKICc,53141
9
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py,sha256=HVGWCXy0GQeBqu2cVJn5H-I8GY8rwgkkc53ilO1TfZA,6846
8
+ contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=fDiyntUFXGi3OKNCL02Pr-4PLzX3dKWcD5UiTYoOkYA,53002
9
+ contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py,sha256=REM3WB-DQAczeknGAKMzJhnvHgnt-u9yDG2UKGVj2vM,6854
10
10
  contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py,sha256=Q1ZfCYOp54O39bgTScZMInkmZiU-bGAM9Hiwr2mq5ms,370
11
11
  contentctl/actions/detection_testing/progress_bar.py,sha256=OK9oRnPlzPAswt9KZNYID-YLHxqaYPY821kIE4-rCeA,3244
12
- contentctl/actions/detection_testing/views/DetectionTestingView.py,sha256=yneZxGnpMvkbWPCTFSWM6hoTCA-JndTMctgTGsLGNNU,7013
12
+ contentctl/actions/detection_testing/views/DetectionTestingView.py,sha256=4UIA3BqjGpR-N4c03en1Iu5sHaiFBzfrPsnUVPaBM7A,6725
13
13
  contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py,sha256=Mos0VV2CTSHtIqMPLwtEJlMEU7LE7TXFjM6GUA1G6hM,2050
14
14
  contentctl/actions/detection_testing/views/DetectionTestingViewFile.py,sha256=OJgmQgoVnzy7p1MN9bDyKGUhFWKzQc6ejc4F87uZG1I,1123
15
15
  contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py,sha256=6mecacXFoTJxcHiRZSnlHos5Hca1jdedEEZfiIAhaJg,4706
@@ -21,33 +21,34 @@ contentctl/actions/new_content.py,sha256=o5ZYBQ216RN6TnW_wRxVGJybx2SsJ7ht4PAi1dw
21
21
  contentctl/actions/release_notes.py,sha256=akkFfLhsJuaPUyjsb6dLlKt9cUM-JApAjTFQMbYoXeM,13115
22
22
  contentctl/actions/reporting.py,sha256=MJEmvmoA1WnSFZEU9QM6daL_W94oOX0WXAcX1qAM2As,1583
23
23
  contentctl/actions/test.py,sha256=dx7f750_MrlvysxOmOdIro1bH0iVKF4K54TSwhvU2MU,5146
24
- contentctl/actions/validate.py,sha256=HfHfUTaRNx8eItociAEgQt8BEOVy9jb2yc8bKAGSsFA,3574
24
+ contentctl/actions/validate.py,sha256=2iFhyhh_LXyMAXtkxnYai7CONSVx4Hb8RftEs_Z_7mI,5649
25
25
  contentctl/api.py,sha256=FBOpRhbBCBdjORmwe_8MPQ3PRZ6T0KrrFcfKovVFkug,6343
26
26
  contentctl/contentctl.py,sha256=Vr2cuvaPjpJpYvD9kVoYq7iD6rhLQEpTKmcGoq4emhA,10470
27
- contentctl/enrichments/attack_enrichment.py,sha256=EkEloG3hMmPTloPyYiVkhq3iT_BieXaJmprJ5stfyRw,6732
28
- contentctl/enrichments/cve_enrichment.py,sha256=IzkKSdnQi3JrAUUyLpcGA_Y2g_B7latq9bOIMlaMpGg,2315
27
+ contentctl/enrichments/attack_enrichment.py,sha256=dVwXcULSeZJuQbeTlPpKDyEB9Y6uCy0UGWI83gPLTI0,6735
28
+ contentctl/enrichments/cve_enrichment.py,sha256=SjiytaZktVNbfICXcZ2vZzBiQpOkug5taPtiJK-S1OE,2313
29
29
  contentctl/enrichments/splunk_app_enrichment.py,sha256=zDNHFLZTi2dJ1gdnh0sHkD6F1VtkblqFnhacFcCMBfc,3418
30
30
  contentctl/helper/link_validator.py,sha256=-XorhxfGtjLynEL1X4hcpRMiyemogf2JEnvLwhHq80c,7139
31
31
  contentctl/helper/logger.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
+ contentctl/helper/splunk_app.py,sha256=PZf60Z3ALQLJQ6I--cbWTCzvOMPGsjZSns1BFrZu4S4,9549
32
33
  contentctl/helper/utils.py,sha256=8ICRvE7DUiNL9BK4Hw71hCLFbd3R2u86OwKeDOdaBTY,19454
33
34
  contentctl/input/backend_splunk_ba.py,sha256=Y70tJqgaUM0nzfm2SiGMof4HkhY84feqf-xnRx1xPb4,5861
34
- contentctl/input/director.py,sha256=36Licm8TV62oDk1s97Y7EFGvTKAP1ryXi7NL4BXP9kU,12603
35
+ contentctl/input/director.py,sha256=w-3aMrFGmfLb8vRzI-rP6K-JlmqYOwZS7OLjU_cOlck,12598
35
36
  contentctl/input/new_content_questions.py,sha256=o4prlBoUhEMxqpZukquI9WKbzfFJfYhEF7a8m2q_BEE,5565
36
37
  contentctl/input/sigma_converter.py,sha256=ATFNW7boNngp5dmWM7Gr4rMZrUKjvKW2_qu28--FdiU,19391
37
- contentctl/input/ssa_detection_builder.py,sha256=dke9mPn2VQVSpYiaGWjZn3PkqVJTe58gcT2Vifv9_yc,8159
38
+ contentctl/input/ssa_detection_builder.py,sha256=4wjgV-WQaJltPHxqd455lNU_8Dn-OlEaqYO8dvIsZ6c,8279
38
39
  contentctl/input/yml_reader.py,sha256=hyVUYhx4Ka8C618kP2D_E3sDUKEQGC6ty_QZQArHKd4,1489
39
- contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=gpyl-hnDNAxxMAHDMBZJMK10yZOq78Y6-ZMCStvrgQM,35354
40
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py,sha256=zpteipbBAwBe90SOmq5ky0KIztS806jnrg1vsBr5PsM,9778
40
+ contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=TP2FAbcJ3B1xTTKSRh8-p2FfNgnTVIruprp_WMNyJGw,35388
41
+ contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py,sha256=7tv-WEiUUOvZkao272J9l1IvL0y12kJ6SWLsMeWv9VE,9820
41
42
  contentctl/objects/alert_action.py,sha256=E9gjCn5C31h0sN7k90KNe4agRxFFSnMW_Z-Ri_3YQss,1335
42
- contentctl/objects/atomic.py,sha256=a_G_iliAm86BunpAAG86aAL3LAEGpd9Crp7t7-PxYvI,8979
43
- contentctl/objects/base_test.py,sha256=6hCL9K-N_jJx1zLbuZQCsB93_XWj6JcGGs2PbbjzJWo,1028
44
- contentctl/objects/base_test_result.py,sha256=dPupudgeXW64Emk9YJfS5JhUXbZwpEZrrx_DiqbRgvU,4752
43
+ contentctl/objects/atomic.py,sha256=BP27gP8KHeODp6UazhVFxwDQ64wuJCARGsLfIH34h7U,8768
44
+ contentctl/objects/base_test.py,sha256=7kAV0njoXaasA-Mt3Zxeq-NFwFF5Z9U85k5cEYW1iY8,1023
45
+ contentctl/objects/base_test_result.py,sha256=ZEAC2IUwUrW_-zHoaS7zp-uBBKIVTS8TcMXjkMByjF4,5006
45
46
  contentctl/objects/baseline.py,sha256=Lb1vJKtDdlDrzWgrdkC9oQao_TnRrOxSwOWHf4trtaU,2150
46
- contentctl/objects/baseline_tags.py,sha256=JLdlCUc_DEccMQD6f-sa2qD8pcxYiwMUT_sRZEhW7ZA,2978
47
- contentctl/objects/config.py,sha256=tK0BY4A9Go5jp8tpOSwgczuOAyu9dMPvC0nyOHeO-74,43642
47
+ contentctl/objects/baseline_tags.py,sha256=fVhLF-NmisavybB_idu3N0Con0Ymj8clKfRMkWzBB-k,1762
48
+ contentctl/objects/config.py,sha256=ha18aqKmkYqAvM8YI124q6JYxesYRon9rc0NMWFzCS4,43762
48
49
  contentctl/objects/constants.py,sha256=1LjiK9A7t0aHHkJz2mrW-DImdW1P98GPssTwmwNNI_M,3468
49
- contentctl/objects/correlation_search.py,sha256=B97vCt2Ew7PGgqd5Y9l6RD3DJdy51Eh7Gzkxxs2xqZ0,36891
50
- contentctl/objects/data_source.py,sha256=ThZivI3NoQybD0C0fS_f-FvhjhD5_n09IML913e1fEY,1459
50
+ contentctl/objects/correlation_search.py,sha256=QmYUS_yIkLT6sdAodsbc_aHuLHcL9CmY1uBcQZJB8OY,47933
51
+ contentctl/objects/data_source.py,sha256=aRr6lHu-EtGmi6J2nXKD7i2ozUPtp7X-vDkQiutvD3I,1545
51
52
  contentctl/objects/deployment.py,sha256=Qc6M4yeOvxjqFKR8sfjd4CG06AbVheTOqP1mwqo4t8s,2651
52
53
  contentctl/objects/deployment_email.py,sha256=Zu9cXZdfOP6noa_mZpiK1GrYCTgi3Mim94iLGjE674c,147
53
54
  contentctl/objects/deployment_notable.py,sha256=QhOI7HEkUuuqk0fum9SD8IpYBlbwIsJUff8s3kCKKj4,198
@@ -56,30 +57,33 @@ contentctl/objects/deployment_rba.py,sha256=YFLSKzLU7s8Bt1cJkSBWlfCsc_2MfgiwyaDi
56
57
  contentctl/objects/deployment_scheduling.py,sha256=bQjbJHNaUGdU1VAGV8-nFOHzHutbIlt7FZpUvR1CV4Y,198
57
58
  contentctl/objects/deployment_slack.py,sha256=P6z8OLHDKcDWx7nbKWasqBc3dFRatGcpO2GtmxzVV8I,135
58
59
  contentctl/objects/detection.py,sha256=3W41cXf3ECjWuPqWrseqSLC3PAA7O5_nENWWM6MPK0Y,620
59
- contentctl/objects/detection_tags.py,sha256=nAHRuBtltx4Rsx9htPtxizRlmQOSypYysbzqn3CQZ_I,10321
60
+ contentctl/objects/detection_tags.py,sha256=b9dav1KJMkGXDtQLn2S7jVwnjOiMz2G5_GPd1PkGI6c,10788
60
61
  contentctl/objects/enums.py,sha256=37v7w8xCg5j5hxP3kod0S3HQ9BY-CqZulPiwhnTtEvs,14052
62
+ contentctl/objects/errors.py,sha256=gnD99z4O00EBbMerUjt4368q8mohm3Zb9HByG3CP_A0,525
61
63
  contentctl/objects/event_source.py,sha256=G9P7rtcN5hcBNQx6DG37mR3QyQufx--T6kgQGNqQuKk,415
62
64
  contentctl/objects/integration_test.py,sha256=W_VksBN_cRo7DTXdr1aLujjS9mgkEp0uvoNpmL0dVnQ,1273
63
65
  contentctl/objects/integration_test_result.py,sha256=DrIZRRlILSHGcsK_Rlm3KJLnbKPtIen8uEPFi4ZdJ8s,370
64
66
  contentctl/objects/investigation.py,sha256=JRoZxc_qi1fu_VFTRaxOc3B7zzSzCfEURsNzWPUCrtY,2620
65
67
  contentctl/objects/investigation_tags.py,sha256=nFpMRKBVBsW21YW_vy2G1lXaSARX-kfFyrPoCyE77Q8,1280
66
- contentctl/objects/lookup.py,sha256=YQiQKhWC07IUQti6l9nh3jhsQUD9vDD11JnoqkCtuho,7176
68
+ contentctl/objects/lookup.py,sha256=oZwBiHfRRrv2ZXdGyWIJWSWZMpuUbsXydaDDfpenk-4,7219
67
69
  contentctl/objects/macro.py,sha256=9nE-bxkFhtaltHOUCr0luU8jCCthmglHjhKs6Q2YzLU,2684
68
- contentctl/objects/mitre_attack_enrichment.py,sha256=bWrMG-Xj3knmULR5q2YZk7mloJBdQUzU1moZfEw9lQM,1073
70
+ contentctl/objects/mitre_attack_enrichment.py,sha256=JqSDnKF0-ZTaxUgvhdYNzIAt-7kNaEBvGr_5Bbfdwr8,1072
69
71
  contentctl/objects/notable_action.py,sha256=ValkblBaG-60TF19y_vSnNzoNZ3eg48wIfr0qZxyKTA,1605
70
- contentctl/objects/observable.py,sha256=-nbVASkwyLpstWQk9Za1Hyjg0etGHiZArg7doEOS02k,1156
72
+ contentctl/objects/notable_event.py,sha256=ITcwLzeatSGpe8267PYN-EhgqOSoWTfciCBVu8zjOXE,682
73
+ contentctl/objects/observable.py,sha256=loEkmo7RPl383Jq-i5BmSnAqpTeh80d6ai7PDeWuxF0,1211
71
74
  contentctl/objects/playbook.py,sha256=hSYYpdMhctgpp7uwaPciFqu1yuFI4M1NHy1WBBLyvzM,2469
72
75
  contentctl/objects/playbook_tags.py,sha256=NrhTGcgoYSGEZggrfebko0GBOXN9x05IadRUUL_CVfQ,1436
73
- contentctl/objects/risk_analysis_action.py,sha256=bySNQX5SBIR8L7SDnlTQr_Jn29YqrPFZtSc0KxQox4Y,4288
76
+ contentctl/objects/risk_analysis_action.py,sha256=Glzcq99DAqqOJ2eZYCkUI3R5hA5cZGU0ZuCSinFf2R8,4278
77
+ contentctl/objects/risk_event.py,sha256=LnFg0BKnt7rMJvzxZoaFeInKP4w5onvJwOUxMWWDk6w,14303
74
78
  contentctl/objects/risk_object.py,sha256=yY4NmEwEKaRl4sLzCRZb1n8kdpV3HzYbQVQ1ClQWYHw,904
75
79
  contentctl/objects/security_content_object.py,sha256=j8KNDwSMfZsSIzJucC3NuZo0SlFVpqHfDc6y3-YHjHI,234
76
80
  contentctl/objects/ssa_detection.py,sha256=-G6tXfVVlZgPWS64hIIy3M-aMePANAuQvdpXPlgUyUs,5873
77
- contentctl/objects/ssa_detection_tags.py,sha256=u8annjzo3MYZ-16wyFnuR8qJJzRa4LEhdprMIrQ47G0,5224
81
+ contentctl/objects/ssa_detection_tags.py,sha256=9aRwbpQHi79NIS9rofjgxDJpw7cWXqG534_kSbvHJh8,5220
78
82
  contentctl/objects/story.py,sha256=FXe11LV19xJTtCgx7DKdvV9cL0gKeryUnE3yjpnDmrU,4957
79
- contentctl/objects/story_tags.py,sha256=0oF1OePLBxa-RQPb438tXrrfosa939CP8UbNV0_S8XY,2225
83
+ contentctl/objects/story_tags.py,sha256=puF-g61YA6eGZy9eLjp4l-5IblMrekcYtQX8EYFOvk0,2221
80
84
  contentctl/objects/test_group.py,sha256=Yb1sqGom6SkVL8B3czPndz8w3CK8WdwZ39V_cn0_JZQ,2600
81
85
  contentctl/objects/threat_object.py,sha256=S8B7RQFfLxN_g7yKPrDTuYhIy9JvQH3YwJ_T5LUZIa4,711
82
- contentctl/objects/unit_test.py,sha256=5EDsPNUct1UY5OtfX-VwFzhET83OmLA6XcaQiZWL1Uo,1655
86
+ contentctl/objects/unit_test.py,sha256=AQcGdi4zEMl9PqZTRnBI87_VU7ySaHrPiBHOlquoxrM,1372
83
87
  contentctl/objects/unit_test_attack_data.py,sha256=ZmHA83O8i9VZveDAliNp_XVKOuH5ytGN9l3X8v8jm4o,480
84
88
  contentctl/objects/unit_test_baseline.py,sha256=XHvOm7qLYfqrP6uC5U_pfgw_pf8-S2RojuNmbo6lXlM,227
85
89
  contentctl/objects/unit_test_old.py,sha256=IfvytHG4ZnUhsvXgdczECZbiwv6YLViYdsk9AqeDBjQ,199
@@ -91,7 +95,7 @@ contentctl/output/attack_nav_writer.py,sha256=64ILZLmNbh2XLmbopgENkeo6t-4SRRG8xZ
91
95
  contentctl/output/ba_yml_output.py,sha256=Lrk13Q9-f71i3c0oNrT50G94PxdogG4k4-MI-rTMOAo,5950
92
96
  contentctl/output/conf_output.py,sha256=qCRT77UKNFCe4AufeBV8Uz9lkPqgpGzU1Y149RuEnis,10147
93
97
  contentctl/output/conf_writer.py,sha256=2TaCAPEtU-bMa7A2m7xOxh93PMpzIdhwiHiPLUCeCB4,8281
94
- contentctl/output/data_source_writer.py,sha256=55gi6toAMcjj0AxOmYMwkTHcANCfK6dezQs5aIQTW4k,1737
98
+ contentctl/output/data_source_writer.py,sha256=ubFjm6XJ4T2d3oqfKwDFasITHeDj3HFmegqVN--5_ME,1635
95
99
  contentctl/output/detection_writer.py,sha256=AzxbssNLmsNIOaYKotew5-ONoyq1cQpKSGy3pe191B0,960
96
100
  contentctl/output/doc_md_output.py,sha256=gf7osH1uSrC6js3D_I72g4uDe9TaB3tsvtqCHi5znp0,3238
97
101
  contentctl/output/finding_report_writer.py,sha256=bjJR7NAxLE8vt8uU3zSDhazQzqzOdtCsUu95lVdzU_w,3939
@@ -165,8 +169,8 @@ contentctl/templates/detections/web/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
165
169
  contentctl/templates/macros/security_content_ctime.yml,sha256=Gg1YNllHVsX_YB716H1SJLWzxXZEfuJlnsgB2fuyoHU,159
166
170
  contentctl/templates/macros/security_content_summariesonly.yml,sha256=9BYUxAl2E4Nwh8K19F3AJS8Ka7ceO6ZDBjFiO3l3LY0,162
167
171
  contentctl/templates/stories/cobalt_strike.yml,sha256=rlaXxMN-5k8LnKBLPafBoksyMtlmsPMHPJOjTiMiZ-M,3063
168
- contentctl-4.2.2.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
169
- contentctl-4.2.2.dist-info/METADATA,sha256=3C5Himg-l9Kz95K6QwOCfjsScGQrds2dgGTMD6PGkIQ,19706
170
- contentctl-4.2.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
171
- contentctl-4.2.2.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
172
- contentctl-4.2.2.dist-info/RECORD,,
172
+ contentctl-4.2.4.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
173
+ contentctl-4.2.4.dist-info/METADATA,sha256=3RsDM2IVtmjpNfbLXXS8MTkQnLYEjngx6yQyJxOeJoY,19386
174
+ contentctl-4.2.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
175
+ contentctl-4.2.4.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
176
+ contentctl-4.2.4.dist-info/RECORD,,