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.
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +41 -47
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +1 -1
- contentctl/actions/detection_testing/views/DetectionTestingView.py +1 -4
- contentctl/actions/validate.py +40 -1
- contentctl/enrichments/attack_enrichment.py +6 -8
- contentctl/enrichments/cve_enrichment.py +3 -3
- contentctl/helper/splunk_app.py +263 -0
- contentctl/input/director.py +1 -1
- contentctl/input/ssa_detection_builder.py +8 -6
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +362 -336
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +117 -103
- contentctl/objects/atomic.py +7 -10
- contentctl/objects/base_test.py +1 -1
- contentctl/objects/base_test_result.py +7 -5
- contentctl/objects/baseline_tags.py +2 -30
- contentctl/objects/config.py +5 -4
- contentctl/objects/correlation_search.py +316 -96
- contentctl/objects/data_source.py +7 -2
- contentctl/objects/detection_tags.py +128 -102
- contentctl/objects/errors.py +18 -0
- contentctl/objects/lookup.py +3 -1
- contentctl/objects/mitre_attack_enrichment.py +3 -3
- contentctl/objects/notable_event.py +20 -0
- contentctl/objects/observable.py +20 -26
- contentctl/objects/risk_analysis_action.py +2 -2
- contentctl/objects/risk_event.py +315 -0
- contentctl/objects/ssa_detection_tags.py +1 -1
- contentctl/objects/story_tags.py +2 -2
- contentctl/objects/unit_test.py +1 -9
- contentctl/output/data_source_writer.py +4 -4
- contentctl/output/templates/savedsearches_detections.j2 +0 -8
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/METADATA +5 -8
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/RECORD +36 -32
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/LICENSE.md +0 -0
- {contentctl-4.2.1.dist-info → contentctl-4.2.4.dist-info}/WHEEL +0 -0
- {contentctl-4.2.1.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 ([
|
|
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']}")
|
contentctl/objects/story_tags.py
CHANGED
|
@@ -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]
|
|
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:
|
contentctl/objects/unit_test.py
CHANGED
|
@@ -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
|
|
22
|
-
supported_TA_name = data_source.supported_TA[0].
|
|
23
|
-
supported_TA_version = data_source.supported_TA[0].
|
|
24
|
-
supported_TA_url = data_source.supported_TA[0].
|
|
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 = ''
|
|
@@ -57,15 +57,7 @@ cron_schedule = {{ detection.deployment.scheduling.cron_schedule }}
|
|
|
57
57
|
dispatch.earliest_time = {{ detection.deployment.scheduling.earliest_time }}
|
|
58
58
|
dispatch.latest_time = {{ detection.deployment.scheduling.latest_time }}
|
|
59
59
|
action.correlationsearch.enabled = 1
|
|
60
|
-
{% if detection.status == "deprecated" %}
|
|
61
|
-
action.correlationsearch.label = {{APP_NAME}} - Deprecated - {{ detection.name }} - Rule
|
|
62
|
-
{% elif detection.status == "experimental" %}
|
|
63
|
-
action.correlationsearch.label = {{APP_NAME}} - Experimental - {{ detection.name }} - Rule
|
|
64
|
-
{% elif detection.type | lower == "correlation" %}
|
|
65
|
-
action.correlationsearch.label = {{APP_NAME}} - RIR - {{ detection.name }} - Rule
|
|
66
|
-
{% else %}
|
|
67
60
|
action.correlationsearch.label = {{APP_NAME}} - {{ detection.name }} - Rule
|
|
68
|
-
{% endif %}
|
|
69
61
|
action.correlationsearch.annotations = {{ detection.annotations | tojson }}
|
|
70
62
|
action.correlationsearch.metadata = {{ detection.getMetadata() | tojson }}
|
|
71
63
|
{% if detection.deployment.scheduling.schedule_window is defined %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: contentctl
|
|
3
|
-
Version: 4.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,<
|
|
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
|
|
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.
|
|
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.
|
|
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=
|
|
9
|
-
contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
28
|
-
contentctl/enrichments/cve_enrichment.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
40
|
-
contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py,sha256=
|
|
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=
|
|
43
|
-
contentctl/objects/base_test.py,sha256=
|
|
44
|
-
contentctl/objects/base_test_result.py,sha256=
|
|
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=
|
|
47
|
-
contentctl/objects/config.py,sha256=
|
|
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=
|
|
50
|
-
contentctl/objects/data_source.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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/
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
@@ -123,7 +127,7 @@ contentctl/output/templates/header.j2,sha256=3usV7jm1q6J-QNnQrZzII9cN0XEGQjg_eVK
|
|
|
123
127
|
contentctl/output/templates/macros.j2,sha256=SLcQQ5X7TZS8j-2qP06BTXqdIcnwoYqTAaBLX2Dge7Y,390
|
|
124
128
|
contentctl/output/templates/panel.j2,sha256=Cw_W6p-14n6UivVfpS75KKJiJ2VpdGsSBceYsUYe9gk,221
|
|
125
129
|
contentctl/output/templates/savedsearches_baselines.j2,sha256=xr05J9WJSVdwpiBoPWEejZ1hmeqInyDKyDH4kjzHP6U,1743
|
|
126
|
-
contentctl/output/templates/savedsearches_detections.j2,sha256=
|
|
130
|
+
contentctl/output/templates/savedsearches_detections.j2,sha256=ZEY2oxn1NXrx28OR46azAs9coX_PhK7UGfyiLZh8g2c,6381
|
|
127
131
|
contentctl/output/templates/savedsearches_investigations.j2,sha256=aFIDK4NqtsZr3fb4F_tv9UQTQ2Z-n9pkP5rIocPA65Q,1259
|
|
128
132
|
contentctl/output/templates/transforms.j2,sha256=-cSoie0LgJwibtW-GMhc9BQlmS6h1s1Vykm9O2M0f9Y,1456
|
|
129
133
|
contentctl/output/templates/workflow_actions.j2,sha256=DFoZVnCa8dMRHjW2AdpoydBC0THgiH_W-Nx7WI4-uR4,925
|
|
@@ -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.
|
|
169
|
-
contentctl-4.2.
|
|
170
|
-
contentctl-4.2.
|
|
171
|
-
contentctl-4.2.
|
|
172
|
-
contentctl-4.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|