contentctl 4.4.7__py3-none-any.whl → 5.0.0__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/__init__.py +1 -1
- contentctl/actions/build.py +102 -57
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
- contentctl/actions/detection_testing/GitService.py +134 -76
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +9 -6
- contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +155 -108
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +33 -28
- contentctl/actions/validate.py +55 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +124 -90
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -53
- contentctl/input/director.py +68 -36
- contentctl/input/new_content_questions.py +27 -35
- contentctl/input/yml_reader.py +28 -18
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
- contentctl/objects/alert_action.py +10 -9
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +65 -54
- contentctl/objects/base_test.py +5 -3
- contentctl/objects/base_test_result.py +19 -11
- contentctl/objects/baseline.py +62 -30
- contentctl/objects/baseline_tags.py +30 -24
- contentctl/objects/config.py +790 -597
- contentctl/objects/constants.py +33 -56
- contentctl/objects/correlation_search.py +150 -136
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +16 -17
- contentctl/objects/deployment.py +43 -44
- contentctl/objects/deployment_email.py +3 -2
- contentctl/objects/deployment_notable.py +4 -2
- contentctl/objects/deployment_phantom.py +7 -6
- contentctl/objects/deployment_rba.py +3 -2
- contentctl/objects/deployment_scheduling.py +3 -2
- contentctl/objects/deployment_slack.py +3 -2
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +58 -103
- contentctl/objects/drilldown.py +66 -34
- contentctl/objects/enums.py +81 -100
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +59 -36
- contentctl/objects/investigation_tags.py +30 -19
- contentctl/objects/lookup.py +304 -101
- contentctl/objects/macro.py +55 -39
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +23 -13
- contentctl/objects/rba.py +96 -0
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +110 -160
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +54 -49
- contentctl/objects/story_tags.py +56 -45
- contentctl/objects/test_attack_data.py +2 -1
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +5 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +233 -220
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +235 -172
- contentctl/output/conf_writer.py +201 -125
- contentctl/output/data_source_writer.py +38 -26
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +21 -11
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/collections.j2 +1 -1
- contentctl/output/templates/doc_detections.j2 +0 -5
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -2
- contentctl/output/templates/savedsearches_detections.j2 +10 -11
- contentctl/output/templates/savedsearches_investigations.j2 +2 -2
- contentctl/output/templates/transforms.j2 +6 -8
- contentctl/output/yml_writer.py +29 -20
- contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
- contentctl/templates/stories/cobalt_strike.yml +1 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
- contentctl-5.0.0.dist-info/RECORD +168 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/event_source.py +0 -11
- contentctl/objects/observable.py +0 -37
- contentctl/output/detection_writer.py +0 -28
- contentctl/output/new_content_yml_output.py +0 -56
- contentctl/output/yml_output.py +0 -66
- contentctl-4.4.7.dist-info/RECORD +0 -173
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,91 +1,62 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import uuid
|
|
3
4
|
from typing import TYPE_CHECKING, List, Optional, Union
|
|
5
|
+
|
|
4
6
|
from pydantic import (
|
|
7
|
+
UUID4,
|
|
5
8
|
BaseModel,
|
|
9
|
+
ConfigDict,
|
|
6
10
|
Field,
|
|
7
|
-
NonNegativeInt,
|
|
8
|
-
PositiveInt,
|
|
9
|
-
computed_field,
|
|
10
|
-
UUID4,
|
|
11
11
|
HttpUrl,
|
|
12
|
-
ConfigDict,
|
|
13
|
-
field_validator,
|
|
14
12
|
ValidationInfo,
|
|
13
|
+
computed_field,
|
|
14
|
+
field_validator,
|
|
15
15
|
model_serializer,
|
|
16
|
-
model_validator
|
|
16
|
+
model_validator,
|
|
17
17
|
)
|
|
18
|
+
|
|
18
19
|
from contentctl.objects.story import Story
|
|
19
20
|
from contentctl.objects.throttling import Throttling
|
|
21
|
+
|
|
20
22
|
if TYPE_CHECKING:
|
|
21
23
|
from contentctl.input.director import DirectorOutputDto
|
|
22
24
|
|
|
23
|
-
from contentctl.objects.
|
|
25
|
+
from contentctl.objects.annotated_types import CVE_TYPE, MITRE_ATTACK_ID_TYPE
|
|
26
|
+
from contentctl.objects.atomic import AtomicEnrichment, AtomicTest
|
|
24
27
|
from contentctl.objects.constants import ATTACK_TACTICS_KILLCHAIN_MAPPING
|
|
25
|
-
from contentctl.objects.observable import Observable
|
|
26
28
|
from contentctl.objects.enums import (
|
|
27
|
-
Cis18Value,
|
|
28
29
|
AssetType,
|
|
29
|
-
|
|
30
|
-
RiskSeverity,
|
|
30
|
+
Cis18Value,
|
|
31
31
|
KillChainPhase,
|
|
32
32
|
NistCategory,
|
|
33
|
-
SecurityContentProductName
|
|
33
|
+
SecurityContentProductName,
|
|
34
|
+
SecurityDomain,
|
|
34
35
|
)
|
|
35
|
-
from contentctl.objects.
|
|
36
|
-
|
|
36
|
+
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
|
|
37
|
+
|
|
37
38
|
|
|
38
|
-
# TODO (#266): disable the use_enum_values configuration
|
|
39
39
|
class DetectionTags(BaseModel):
|
|
40
40
|
# detection spec
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(validate_default=False, extra="forbid")
|
|
42
43
|
analytic_story: list[Story] = Field(...)
|
|
43
44
|
asset_type: AssetType = Field(...)
|
|
44
|
-
|
|
45
|
-
confidence: NonNegativeInt = Field(..., le=100)
|
|
46
|
-
impact: NonNegativeInt = Field(..., le=100)
|
|
47
|
-
|
|
48
|
-
@computed_field
|
|
49
|
-
@property
|
|
50
|
-
def risk_score(self) -> int:
|
|
51
|
-
return round((self.confidence * self.impact)/100)
|
|
52
|
-
|
|
53
|
-
@computed_field
|
|
54
|
-
@property
|
|
55
|
-
def severity(self)->RiskSeverity:
|
|
56
|
-
if 0 <= self.risk_score <= 20:
|
|
57
|
-
return RiskSeverity.INFORMATIONAL
|
|
58
|
-
elif 20 < self.risk_score <= 40:
|
|
59
|
-
return RiskSeverity.LOW
|
|
60
|
-
elif 40 < self.risk_score <= 60:
|
|
61
|
-
return RiskSeverity.MEDIUM
|
|
62
|
-
elif 60 < self.risk_score <= 80:
|
|
63
|
-
return RiskSeverity.HIGH
|
|
64
|
-
elif 80 < self.risk_score <= 100:
|
|
65
|
-
return RiskSeverity.CRITICAL
|
|
66
|
-
else:
|
|
67
|
-
raise Exception(f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}")
|
|
68
|
-
|
|
45
|
+
group: list[str] = []
|
|
69
46
|
|
|
70
47
|
mitre_attack_id: List[MITRE_ATTACK_ID_TYPE] = []
|
|
71
48
|
nist: list[NistCategory] = []
|
|
72
49
|
|
|
73
|
-
# TODO (#249): Add pydantic validator to ensure observables are unique within a detection
|
|
74
|
-
observable: List[Observable] = []
|
|
75
|
-
message: str = Field(...)
|
|
76
50
|
product: list[SecurityContentProductName] = Field(..., min_length=1)
|
|
77
|
-
required_fields: list[str] = Field(min_length=1)
|
|
78
51
|
throttling: Optional[Throttling] = None
|
|
79
52
|
security_domain: SecurityDomain = Field(...)
|
|
80
53
|
cve: List[CVE_TYPE] = []
|
|
81
54
|
atomic_guid: List[AtomicTest] = []
|
|
82
|
-
|
|
83
55
|
|
|
84
56
|
# enrichment
|
|
85
|
-
mitre_attack_enrichments: List[MitreAttackEnrichment] = Field(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
evidence_str: Optional[str] = None
|
|
57
|
+
mitre_attack_enrichments: List[MitreAttackEnrichment] = Field(
|
|
58
|
+
[], validate_default=True
|
|
59
|
+
)
|
|
89
60
|
|
|
90
61
|
@computed_field
|
|
91
62
|
@property
|
|
@@ -114,55 +85,19 @@ class DetectionTags(BaseModel):
|
|
|
114
85
|
|
|
115
86
|
# TODO (#268): Validate manual_test has length > 0 if not None
|
|
116
87
|
manual_test: Optional[str] = None
|
|
117
|
-
|
|
118
|
-
# The following validator is temporarily disabled pending further discussions
|
|
119
|
-
# @validator('message')
|
|
120
|
-
# def validate_message(cls,v,values):
|
|
121
|
-
|
|
122
|
-
# observables:list[Observable] = values.get("observable",[])
|
|
123
|
-
# observable_names = set([o.name for o in observables])
|
|
124
|
-
# #find all of the observables used in the message by name
|
|
125
|
-
# name_match_regex = r"\$([^\s.]*)\$"
|
|
126
|
-
|
|
127
|
-
# message_observables = set()
|
|
128
|
-
|
|
129
|
-
# #Make sure that all observable names in
|
|
130
|
-
# for match in re.findall(name_match_regex, v):
|
|
131
|
-
# #Remove
|
|
132
|
-
# match_without_dollars = match.replace("$", "")
|
|
133
|
-
# message_observables.add(match_without_dollars)
|
|
134
|
-
|
|
135
|
-
# missing_observables = message_observables - observable_names
|
|
136
|
-
# unused_observables = observable_names - message_observables
|
|
137
|
-
# if len(missing_observables) > 0:
|
|
138
|
-
# raise ValueError(
|
|
139
|
-
# "The following observables are referenced in the message, but were not declared as"
|
|
140
|
-
# f" observables: {missing_observables}"
|
|
141
|
-
# )
|
|
142
|
-
|
|
143
|
-
# if len(unused_observables) > 0:
|
|
144
|
-
# raise ValueError(
|
|
145
|
-
# "The following observables were declared, but are not referenced in the message:"
|
|
146
|
-
# f" {unused_observables}"
|
|
147
|
-
# )
|
|
148
|
-
# return v
|
|
149
88
|
|
|
150
89
|
@model_serializer
|
|
151
90
|
def serialize_model(self):
|
|
152
91
|
# Since this field has no parent, there is no need to call super() serialization function
|
|
153
92
|
return {
|
|
154
93
|
"analytic_story": [story.name for story in self.analytic_story],
|
|
155
|
-
"asset_type": self.asset_type
|
|
94
|
+
"asset_type": self.asset_type,
|
|
156
95
|
"cis20": self.cis20,
|
|
157
96
|
"kill_chain_phases": self.kill_chain_phases,
|
|
158
97
|
"nist": self.nist,
|
|
159
|
-
"observable": self.observable,
|
|
160
|
-
"message": self.message,
|
|
161
|
-
"risk_score": self.risk_score,
|
|
162
98
|
"security_domain": self.security_domain,
|
|
163
|
-
"risk_severity": self.severity,
|
|
164
99
|
"mitre_attack_id": self.mitre_attack_id,
|
|
165
|
-
"mitre_attack_enrichments": self.mitre_attack_enrichments
|
|
100
|
+
"mitre_attack_enrichments": self.mitre_attack_enrichments,
|
|
166
101
|
}
|
|
167
102
|
|
|
168
103
|
@model_validator(mode="after")
|
|
@@ -176,9 +111,13 @@ class DetectionTags(BaseModel):
|
|
|
176
111
|
f" at runtime. Instead, this field contained: {self.mitre_attack_enrichments}"
|
|
177
112
|
)
|
|
178
113
|
|
|
179
|
-
output_dto: Union[DirectorOutputDto, None] = info.context.get(
|
|
114
|
+
output_dto: Union[DirectorOutputDto, None] = info.context.get(
|
|
115
|
+
"output_dto", None
|
|
116
|
+
)
|
|
180
117
|
if output_dto is None:
|
|
181
|
-
raise ValueError(
|
|
118
|
+
raise ValueError(
|
|
119
|
+
"Context not provided to detection.detection_tags model post validator"
|
|
120
|
+
)
|
|
182
121
|
|
|
183
122
|
if output_dto.attack_enrichment.use_enrichment is False:
|
|
184
123
|
return self
|
|
@@ -187,7 +126,9 @@ class DetectionTags(BaseModel):
|
|
|
187
126
|
missing_tactics: list[str] = []
|
|
188
127
|
for mitre_attack_id in self.mitre_attack_id:
|
|
189
128
|
try:
|
|
190
|
-
mitre_enrichments.append(
|
|
129
|
+
mitre_enrichments.append(
|
|
130
|
+
output_dto.attack_enrichment.getEnrichmentByMitreID(mitre_attack_id)
|
|
131
|
+
)
|
|
191
132
|
except Exception:
|
|
192
133
|
missing_tactics.append(mitre_attack_id)
|
|
193
134
|
|
|
@@ -198,7 +139,7 @@ class DetectionTags(BaseModel):
|
|
|
198
139
|
|
|
199
140
|
return self
|
|
200
141
|
|
|
201
|
-
|
|
142
|
+
"""
|
|
202
143
|
@field_validator('mitre_attack_enrichments', mode="before")
|
|
203
144
|
@classmethod
|
|
204
145
|
def addAttackEnrichments(cls, v:list[MitreAttackEnrichment], info:ValidationInfo)->list[MitreAttackEnrichment]:
|
|
@@ -216,31 +157,43 @@ class DetectionTags(BaseModel):
|
|
|
216
157
|
enrichments = []
|
|
217
158
|
|
|
218
159
|
return enrichments
|
|
219
|
-
|
|
160
|
+
"""
|
|
220
161
|
|
|
221
|
-
@field_validator(
|
|
162
|
+
@field_validator("analytic_story", mode="before")
|
|
222
163
|
@classmethod
|
|
223
|
-
def mapStoryNamesToStoryObjects(
|
|
164
|
+
def mapStoryNamesToStoryObjects(
|
|
165
|
+
cls, v: list[str], info: ValidationInfo
|
|
166
|
+
) -> list[Story]:
|
|
224
167
|
if info.context is None:
|
|
225
168
|
raise ValueError("ValidationInfo.context unexpectedly null")
|
|
226
169
|
|
|
227
|
-
return Story.mapNamesToSecurityContentObjects(
|
|
170
|
+
return Story.mapNamesToSecurityContentObjects(
|
|
171
|
+
v, info.context.get("output_dto", None)
|
|
172
|
+
)
|
|
228
173
|
|
|
229
174
|
def getAtomicGuidStringArray(self) -> List[str]:
|
|
230
|
-
return [
|
|
175
|
+
return [
|
|
176
|
+
str(atomic_guid.auto_generated_guid) for atomic_guid in self.atomic_guid
|
|
177
|
+
]
|
|
231
178
|
|
|
232
|
-
@field_validator(
|
|
179
|
+
@field_validator("atomic_guid", mode="before")
|
|
233
180
|
@classmethod
|
|
234
|
-
def mapAtomicGuidsToAtomicTests(
|
|
181
|
+
def mapAtomicGuidsToAtomicTests(
|
|
182
|
+
cls, v: List[UUID4], info: ValidationInfo
|
|
183
|
+
) -> List[AtomicTest]:
|
|
235
184
|
if len(v) == 0:
|
|
236
185
|
return []
|
|
237
186
|
|
|
238
187
|
if info.context is None:
|
|
239
188
|
raise ValueError("ValidationInfo.context unexpectedly null")
|
|
240
189
|
|
|
241
|
-
output_dto: Union[DirectorOutputDto, None] = info.context.get(
|
|
190
|
+
output_dto: Union[DirectorOutputDto, None] = info.context.get(
|
|
191
|
+
"output_dto", None
|
|
192
|
+
)
|
|
242
193
|
if output_dto is None:
|
|
243
|
-
raise ValueError(
|
|
194
|
+
raise ValueError(
|
|
195
|
+
"Context not provided to detection.detection_tags.atomic_guid validator"
|
|
196
|
+
)
|
|
244
197
|
|
|
245
198
|
atomic_enrichment: AtomicEnrichment = output_dto.atomic_enrichment
|
|
246
199
|
|
|
@@ -282,4 +235,6 @@ class DetectionTags(BaseModel):
|
|
|
282
235
|
elif len(missing_tests) > 0:
|
|
283
236
|
raise ValueError(missing_tests_string)
|
|
284
237
|
|
|
285
|
-
return matched_tests + [
|
|
238
|
+
return matched_tests + [
|
|
239
|
+
AtomicTest.AtomicTestWhenTestIsMissing(test) for test in missing_tests
|
|
240
|
+
]
|
contentctl/objects/drilldown.py
CHANGED
|
@@ -1,70 +1,102 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_serializer
|
|
6
|
+
|
|
4
7
|
if TYPE_CHECKING:
|
|
5
8
|
from contentctl.objects.detection import Detection
|
|
9
|
+
|
|
6
10
|
from contentctl.objects.enums import AnalyticsType
|
|
11
|
+
|
|
7
12
|
DRILLDOWN_SEARCH_PLACEHOLDER = "%original_detection_search%"
|
|
8
13
|
EARLIEST_OFFSET = "$info_min_time$"
|
|
9
14
|
LATEST_OFFSET = "$info_max_time$"
|
|
10
15
|
RISK_SEARCH = "index = risk starthoursago = 168 endhoursago = 0 | stats count values(search_name) values(risk_message) values(analyticstories) values(annotations._all) values(annotations.mitre_attack.mitre_tactic) "
|
|
11
16
|
|
|
17
|
+
|
|
12
18
|
class Drilldown(BaseModel):
|
|
13
19
|
name: str = Field(..., description="The name of the drilldown search", min_length=5)
|
|
14
|
-
search: str = Field(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
search: str = Field(
|
|
21
|
+
...,
|
|
22
|
+
description="The text of a drilldown search. This must be valid SPL.",
|
|
23
|
+
min_length=1,
|
|
24
|
+
)
|
|
25
|
+
earliest_offset: None | str = Field(
|
|
26
|
+
...,
|
|
27
|
+
description="Earliest offset time for the drilldown search. "
|
|
28
|
+
f"The most common value for this field is '{EARLIEST_OFFSET}', "
|
|
29
|
+
"but it is NOT the default value and must be supplied explicitly.",
|
|
30
|
+
min_length=1,
|
|
31
|
+
)
|
|
32
|
+
latest_offset: None | str = Field(
|
|
33
|
+
...,
|
|
34
|
+
description="Latest offset time for the driolldown search. "
|
|
35
|
+
f"The most common value for this field is '{LATEST_OFFSET}', "
|
|
36
|
+
"but it is NOT the default value and must be supplied explicitly.",
|
|
37
|
+
min_length=1,
|
|
38
|
+
)
|
|
25
39
|
|
|
26
40
|
@classmethod
|
|
27
41
|
def constructDrilldownsFromDetection(cls, detection: Detection) -> list[Drilldown]:
|
|
28
|
-
|
|
42
|
+
# Ensure the rba object is defined
|
|
43
|
+
if detection.rba is None:
|
|
44
|
+
raise NotImplementedError(
|
|
45
|
+
f"Unexpected error: Detection '{detection.name}' has no RBA objects associated "
|
|
46
|
+
"with it; cannot construct drilldowns."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
victim_observables = [o for o in detection.rba.risk_objects]
|
|
29
50
|
if len(victim_observables) == 0 or detection.type == AnalyticsType.Hunting:
|
|
30
51
|
# No victims, so no drilldowns
|
|
31
52
|
return []
|
|
32
53
|
print(f"Adding default drilldowns for [{detection.name}]")
|
|
33
|
-
variableNamesString =
|
|
54
|
+
variableNamesString = " and ".join([f"${o.field}$" for o in victim_observables])
|
|
34
55
|
nameField = f"View the detection results for {variableNamesString}"
|
|
35
|
-
appendedSearch =
|
|
56
|
+
appendedSearch = " | search " + " ".join(
|
|
57
|
+
[f"{o.field} = ${o.field}$" for o in victim_observables]
|
|
58
|
+
)
|
|
36
59
|
search_field = f"{detection.search}{appendedSearch}"
|
|
37
|
-
detection_results = cls(
|
|
38
|
-
|
|
39
|
-
|
|
60
|
+
detection_results = cls(
|
|
61
|
+
name=nameField,
|
|
62
|
+
earliest_offset=EARLIEST_OFFSET,
|
|
63
|
+
latest_offset=LATEST_OFFSET,
|
|
64
|
+
search=search_field,
|
|
65
|
+
)
|
|
66
|
+
|
|
40
67
|
nameField = f"View risk events for the last 7 days for {variableNamesString}"
|
|
41
|
-
fieldNamesListString =
|
|
68
|
+
fieldNamesListString = ", ".join([o.field for o in victim_observables])
|
|
42
69
|
search_field = f"{RISK_SEARCH}by {fieldNamesListString} {appendedSearch}"
|
|
43
|
-
risk_events_last_7_days = cls(
|
|
70
|
+
risk_events_last_7_days = cls(
|
|
71
|
+
name=nameField,
|
|
72
|
+
earliest_offset=None,
|
|
73
|
+
latest_offset=None,
|
|
74
|
+
search=search_field,
|
|
75
|
+
)
|
|
44
76
|
|
|
45
|
-
return [detection_results,risk_events_last_7_days]
|
|
46
|
-
|
|
77
|
+
return [detection_results, risk_events_last_7_days]
|
|
47
78
|
|
|
48
|
-
def perform_search_substitutions(self, detection:Detection)->None:
|
|
79
|
+
def perform_search_substitutions(self, detection: Detection) -> None:
|
|
49
80
|
"""Replaces the field DRILLDOWN_SEARCH_PLACEHOLDER (%original_detection_search%)
|
|
50
81
|
with the search contained in the detection. We do this so that the YML does not
|
|
51
82
|
need the search copy/pasted from the search field into the drilldown object.
|
|
52
83
|
|
|
53
84
|
Args:
|
|
54
85
|
detection (Detection): Detection to be used to update the search field of the drilldown
|
|
55
|
-
"""
|
|
56
|
-
self.search = self.search.replace(
|
|
57
|
-
|
|
86
|
+
"""
|
|
87
|
+
self.search = self.search.replace(
|
|
88
|
+
DRILLDOWN_SEARCH_PLACEHOLDER, detection.search
|
|
89
|
+
)
|
|
58
90
|
|
|
59
91
|
@model_serializer
|
|
60
|
-
def serialize_model(self) -> dict[str,str]:
|
|
61
|
-
#Call serializer for parent
|
|
62
|
-
model:dict[str,str] = {}
|
|
92
|
+
def serialize_model(self) -> dict[str, str]:
|
|
93
|
+
# Call serializer for parent
|
|
94
|
+
model: dict[str, str] = {}
|
|
63
95
|
|
|
64
|
-
model[
|
|
65
|
-
model[
|
|
96
|
+
model["name"] = self.name
|
|
97
|
+
model["search"] = self.search
|
|
66
98
|
if self.earliest_offset is not None:
|
|
67
|
-
model[
|
|
99
|
+
model["earliest_offset"] = self.earliest_offset
|
|
68
100
|
if self.latest_offset is not None:
|
|
69
|
-
model[
|
|
70
|
-
return model
|
|
101
|
+
model["latest_offset"] = self.latest_offset
|
|
102
|
+
return model
|