contentctl 4.0.4__py3-none-any.whl → 4.1.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/actions/inspect.py +1 -1
- contentctl/actions/new_content.py +18 -7
- contentctl/actions/validate.py +1 -0
- contentctl/api.py +137 -0
- contentctl/contentctl.py +28 -24
- contentctl/enrichments/cve_enrichment.py +43 -78
- contentctl/input/director.py +72 -72
- contentctl/input/new_content_questions.py +0 -5
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +77 -13
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -0
- contentctl/objects/baseline.py +0 -1
- contentctl/objects/config.py +4 -8
- contentctl/objects/detection_tags.py +1 -1
- contentctl/objects/macro.py +8 -7
- contentctl/objects/story_tags.py +2 -0
- contentctl/output/yml_writer.py +39 -1
- contentctl/templates/detections/application/.gitkeep +0 -0
- contentctl/templates/detections/cloud/.gitkeep +0 -0
- contentctl/templates/detections/network/.gitkeep +0 -0
- contentctl/templates/detections/web/.gitkeep +0 -0
- {contentctl-4.0.4.dist-info → contentctl-4.1.0.dist-info}/METADATA +7 -8
- {contentctl-4.0.4.dist-info → contentctl-4.1.0.dist-info}/RECORD +27 -25
- contentctl/actions/apav_deploy.py +0 -98
- contentctl/actions/api_deploy.py +0 -151
- contentctl/templates/app_template/default/distsearch.conf +0 -5
- /contentctl/actions/{acs_deploy.py → deploy_acs.py} +0 -0
- /contentctl/templates/detections/{anomalous_usage_of_7zip.yml → endpoint/anomalous_usage_of_7zip.yml} +0 -0
- {contentctl-4.0.4.dist-info → contentctl-4.1.0.dist-info}/LICENSE.md +0 -0
- {contentctl-4.0.4.dist-info → contentctl-4.1.0.dist-info}/WHEEL +0 -0
- {contentctl-4.0.4.dist-info → contentctl-4.1.0.dist-info}/entry_points.txt +0 -0
contentctl/input/director.py
CHANGED
|
@@ -5,9 +5,8 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from pydantic import ValidationError
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
from contentctl.input.yml_reader import YmlReader
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
|
|
9
|
+
|
|
11
10
|
from contentctl.objects.detection import Detection
|
|
12
11
|
from contentctl.objects.story import Story
|
|
13
12
|
|
|
@@ -28,29 +27,69 @@ from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
|
28
27
|
from contentctl.objects.config import validate
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
@dataclass()
|
|
30
|
+
@dataclass
|
|
33
31
|
class DirectorOutputDto:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
# Atomic Tests are first because parsing them
|
|
33
|
+
# is far quicker than attack_enrichment
|
|
34
|
+
atomic_tests: Union[list[AtomicTest],None]
|
|
35
|
+
attack_enrichment: AttackEnrichment
|
|
36
|
+
cve_enrichment: CveEnrichment
|
|
37
|
+
detections: list[Detection]
|
|
38
|
+
stories: list[Story]
|
|
39
|
+
baselines: list[Baseline]
|
|
40
|
+
investigations: list[Investigation]
|
|
41
|
+
playbooks: list[Playbook]
|
|
42
|
+
macros: list[Macro]
|
|
43
|
+
lookups: list[Lookup]
|
|
44
|
+
deployments: list[Deployment]
|
|
45
|
+
ssa_detections: list[SSADetection]
|
|
46
|
+
|
|
47
|
+
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
|
|
48
|
+
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
def addContentToDictMappings(self, content: SecurityContentObject):
|
|
51
|
+
content_name = content.name
|
|
52
|
+
if isinstance(content, SSADetection):
|
|
53
|
+
# Since SSA detections may have the same name as ESCU detection,
|
|
54
|
+
# for this function we prepend 'SSA ' to the name.
|
|
55
|
+
content_name = f"SSA {content_name}"
|
|
56
|
+
if content_name in self.name_to_content_map:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Duplicate name '{content_name}' with paths:\n"
|
|
59
|
+
f" - {content.file_path}\n"
|
|
60
|
+
f" - {self.name_to_content_map[content_name].file_path}"
|
|
61
|
+
)
|
|
62
|
+
elif content.id in self.uuid_to_content_map:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Duplicate id '{content.id}' with paths:\n"
|
|
65
|
+
f" - {content.file_path}\n"
|
|
66
|
+
f" - {self.name_to_content_map[content_name].file_path}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if isinstance(content, Lookup):
|
|
70
|
+
self.lookups.append(content)
|
|
71
|
+
elif isinstance(content, Macro):
|
|
72
|
+
self.macros.append(content)
|
|
73
|
+
elif isinstance(content, Deployment):
|
|
74
|
+
self.deployments.append(content)
|
|
75
|
+
elif isinstance(content, Playbook):
|
|
76
|
+
self.playbooks.append(content)
|
|
77
|
+
elif isinstance(content, Baseline):
|
|
78
|
+
self.baselines.append(content)
|
|
79
|
+
elif isinstance(content, Investigation):
|
|
80
|
+
self.investigations.append(content)
|
|
81
|
+
elif isinstance(content, Story):
|
|
82
|
+
self.stories.append(content)
|
|
83
|
+
elif isinstance(content, Detection):
|
|
84
|
+
self.detections.append(content)
|
|
85
|
+
elif isinstance(content, SSADetection):
|
|
86
|
+
self.ssa_detections.append(content)
|
|
87
|
+
else:
|
|
88
|
+
raise Exception(f"Unknown security content type: {type(content)}")
|
|
52
89
|
|
|
53
90
|
|
|
91
|
+
self.name_to_content_map[content_name] = content
|
|
92
|
+
self.uuid_to_content_map[content.id] = content
|
|
54
93
|
|
|
55
94
|
|
|
56
95
|
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
|
|
@@ -60,13 +99,6 @@ from contentctl.objects.enums import DetectionStatus
|
|
|
60
99
|
from contentctl.helper.utils import Utils
|
|
61
100
|
|
|
62
101
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
102
|
class Director():
|
|
71
103
|
input_dto: validate
|
|
72
104
|
output_dto: DirectorOutputDto
|
|
@@ -77,27 +109,7 @@ class Director():
|
|
|
77
109
|
def __init__(self, output_dto: DirectorOutputDto) -> None:
|
|
78
110
|
self.output_dto = output_dto
|
|
79
111
|
self.ssa_detection_builder = SSADetectionBuilder()
|
|
80
|
-
|
|
81
|
-
def addContentToDictMappings(self, content:SecurityContentObject):
|
|
82
|
-
content_name = content.name
|
|
83
|
-
if isinstance(content,SSADetection):
|
|
84
|
-
# Since SSA detections may have the same name as ESCU detection,
|
|
85
|
-
# for this function we prepend 'SSA ' to the name.
|
|
86
|
-
content_name = f"SSA {content_name}"
|
|
87
|
-
if content_name in self.output_dto.name_to_content_map:
|
|
88
|
-
raise ValueError(f"Duplicate name '{content_name}' with paths:\n"
|
|
89
|
-
f" - {content.file_path}\n"
|
|
90
|
-
f" - {self.output_dto.name_to_content_map[content_name].file_path}")
|
|
91
|
-
elif content.id in self.output_dto.uuid_to_content_map:
|
|
92
|
-
raise ValueError(f"Duplicate id '{content.id}' with paths:\n"
|
|
93
|
-
f" - {content.file_path}\n"
|
|
94
|
-
f" - {self.output_dto.name_to_content_map[content_name].file_path}")
|
|
95
|
-
|
|
96
|
-
self.output_dto.name_to_content_map[content_name] = content
|
|
97
|
-
self.output_dto.uuid_to_content_map[content.id] = content
|
|
98
|
-
|
|
99
112
|
|
|
100
|
-
|
|
101
113
|
def execute(self, input_dto: validate) -> None:
|
|
102
114
|
self.input_dto = input_dto
|
|
103
115
|
|
|
@@ -146,50 +158,41 @@ class Director():
|
|
|
146
158
|
|
|
147
159
|
if contentType == SecurityContentType.lookups:
|
|
148
160
|
lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
|
|
149
|
-
self.output_dto.
|
|
150
|
-
self.addContentToDictMappings(lookup)
|
|
161
|
+
self.output_dto.addContentToDictMappings(lookup)
|
|
151
162
|
|
|
152
163
|
elif contentType == SecurityContentType.macros:
|
|
153
164
|
macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
154
|
-
self.output_dto.
|
|
155
|
-
self.addContentToDictMappings(macro)
|
|
165
|
+
self.output_dto.addContentToDictMappings(macro)
|
|
156
166
|
|
|
157
167
|
elif contentType == SecurityContentType.deployments:
|
|
158
168
|
deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
159
|
-
self.output_dto.
|
|
160
|
-
self.addContentToDictMappings(deployment)
|
|
169
|
+
self.output_dto.addContentToDictMappings(deployment)
|
|
161
170
|
|
|
162
171
|
elif contentType == SecurityContentType.playbooks:
|
|
163
172
|
playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
164
|
-
self.output_dto.
|
|
165
|
-
self.addContentToDictMappings(playbook)
|
|
173
|
+
self.output_dto.addContentToDictMappings(playbook)
|
|
166
174
|
|
|
167
175
|
elif contentType == SecurityContentType.baselines:
|
|
168
176
|
baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
169
|
-
self.output_dto.
|
|
170
|
-
self.addContentToDictMappings(baseline)
|
|
177
|
+
self.output_dto.addContentToDictMappings(baseline)
|
|
171
178
|
|
|
172
179
|
elif contentType == SecurityContentType.investigations:
|
|
173
180
|
investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
174
|
-
self.output_dto.
|
|
175
|
-
self.addContentToDictMappings(investigation)
|
|
181
|
+
self.output_dto.addContentToDictMappings(investigation)
|
|
176
182
|
|
|
177
183
|
elif contentType == SecurityContentType.stories:
|
|
178
184
|
story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
179
|
-
self.output_dto.
|
|
180
|
-
self.addContentToDictMappings(story)
|
|
185
|
+
self.output_dto.addContentToDictMappings(story)
|
|
181
186
|
|
|
182
187
|
elif contentType == SecurityContentType.detections:
|
|
183
|
-
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
184
|
-
self.output_dto.
|
|
185
|
-
self.addContentToDictMappings(detection)
|
|
188
|
+
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
|
|
189
|
+
self.output_dto.addContentToDictMappings(detection)
|
|
186
190
|
|
|
187
191
|
elif contentType == SecurityContentType.ssa_detections:
|
|
188
192
|
self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
|
|
189
193
|
ssa_detection = self.ssa_detection_builder.getObject()
|
|
190
194
|
if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
|
|
191
|
-
self.output_dto.
|
|
192
|
-
self.addContentToDictMappings(ssa_detection)
|
|
195
|
+
self.output_dto.addContentToDictMappings(ssa_detection)
|
|
193
196
|
|
|
194
197
|
else:
|
|
195
198
|
raise Exception(f"Unsupported type: [{contentType}]")
|
|
@@ -228,6 +231,3 @@ class Director():
|
|
|
228
231
|
builder.addMappings()
|
|
229
232
|
builder.addUnitTest()
|
|
230
233
|
builder.addRBA()
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
@@ -27,11 +27,6 @@ class NewContentQuestions:
|
|
|
27
27
|
'message': 'enter author name',
|
|
28
28
|
'name': 'detection_author',
|
|
29
29
|
},
|
|
30
|
-
{
|
|
31
|
-
"type": "text",
|
|
32
|
-
"message": "enter author name",
|
|
33
|
-
"name": "detection_author",
|
|
34
|
-
},
|
|
35
30
|
{
|
|
36
31
|
"type": "select",
|
|
37
32
|
"message": "select a detection type",
|
|
@@ -26,7 +26,7 @@ from contentctl.objects.integration_test import IntegrationTest
|
|
|
26
26
|
|
|
27
27
|
#from contentctl.objects.playbook import Playbook
|
|
28
28
|
from contentctl.objects.enums import DataSource,ProvidingTechnology
|
|
29
|
-
from contentctl.enrichments.cve_enrichment import
|
|
29
|
+
from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
class Detection_Abstract(SecurityContentObject):
|
|
@@ -40,7 +40,6 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
40
40
|
search: Union[str, dict[str,Any]] = Field(...)
|
|
41
41
|
how_to_implement: str = Field(..., min_length=4)
|
|
42
42
|
known_false_positives: str = Field(..., min_length=4)
|
|
43
|
-
check_references: bool = False
|
|
44
43
|
#data_source: Optional[List[DataSource]] = None
|
|
45
44
|
|
|
46
45
|
enabled_by_default: bool = False
|
|
@@ -54,6 +53,58 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
54
53
|
# A list of groups of tests, relying on the same data
|
|
55
54
|
test_groups: Union[list[TestGroup], None] = Field(None,validate_default=True)
|
|
56
55
|
|
|
56
|
+
|
|
57
|
+
@field_validator("search", mode="before")
|
|
58
|
+
@classmethod
|
|
59
|
+
def validate_presence_of_filter_macro(cls, value:Union[str, dict[str,Any]], info:ValidationInfo)->Union[str, dict[str,Any]]:
|
|
60
|
+
"""
|
|
61
|
+
Validates that, if required to be present, the filter macro is present with the proper name.
|
|
62
|
+
The filter macro MUST be derived from the name of the detection
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
value (Union[str, dict[str,Any]]): The search. It can either be a string (and should be SPL)
|
|
67
|
+
or a dict, in which case it is Sigma-formatted.
|
|
68
|
+
info (ValidationInfo): The validation info can contain a number of different objects. Today it only contains the director.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Union[str, dict[str,Any]]: The search, either in sigma or SPL format.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
if isinstance(value,dict):
|
|
75
|
+
#If the search is a dict, then it is in Sigma format so return it
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
# Otherwise, the search is SPL.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# In the future, we will may add support that makes the inclusion of the
|
|
82
|
+
# filter macro optional or automatically generates it for searches that
|
|
83
|
+
# do not have it. For now, continue to require that all searches have a filter macro.
|
|
84
|
+
FORCE_FILTER_MACRO = True
|
|
85
|
+
if not FORCE_FILTER_MACRO:
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
# Get the required macro name, which is derived from the search name.
|
|
89
|
+
# Note that a separate validation ensures that the file name matches the content name
|
|
90
|
+
name:Union[str,None] = info.data.get("name",None)
|
|
91
|
+
if name is None:
|
|
92
|
+
#The search was sigma formatted (or failed other validation and was None), so we will not validate macros in it
|
|
93
|
+
raise ValueError("Cannot validate filter macro, field 'name' (which is required to validate the macro) was missing from the detection YML.")
|
|
94
|
+
|
|
95
|
+
#Get the file name without the extension. Note this is not a full path!
|
|
96
|
+
file_name = pathlib.Path(cls.contentNameToFileName(name)).stem
|
|
97
|
+
file_name_with_filter = f"`{file_name}_filter`"
|
|
98
|
+
|
|
99
|
+
if file_name_with_filter not in value:
|
|
100
|
+
raise ValueError(f"Detection does not contain the EXACT filter macro {file_name_with_filter}. "
|
|
101
|
+
"This filter macro MUST be present in the search. It usually placed at the end "
|
|
102
|
+
"of the search and is useful for environment-specific filtering of False Positive or noisy results.")
|
|
103
|
+
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
57
108
|
@field_validator("test_groups")
|
|
58
109
|
@classmethod
|
|
59
110
|
def validate_test_groups(cls, value:Union[None, List[TestGroup]], info:ValidationInfo) -> Union[List[TestGroup], None]:
|
|
@@ -144,17 +195,30 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
144
195
|
macros: list[Macro] = Field([],validate_default=True)
|
|
145
196
|
lookups: list[Lookup] = Field([],validate_default=True)
|
|
146
197
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
198
|
+
cve_enrichment: list[CveEnrichmentObj] = Field([], validate_default=True)
|
|
199
|
+
|
|
200
|
+
@model_validator(mode="after")
|
|
201
|
+
def cve_enrichment_func(self, info:ValidationInfo):
|
|
202
|
+
if len(self.cve_enrichment) > 0:
|
|
203
|
+
raise ValueError(f"Error, field 'cve_enrichment' should be empty and "
|
|
204
|
+
f"dynamically populated at runtime. Instead, this field contained: {self.cve_enrichment}")
|
|
205
|
+
|
|
206
|
+
output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
|
|
207
|
+
if output_dto is None:
|
|
208
|
+
raise ValueError("Context not provided to detection model post validator")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
enriched_cves:list[CveEnrichmentObj] = []
|
|
155
212
|
|
|
156
|
-
|
|
213
|
+
for cve_id in self.tags.cve:
|
|
214
|
+
try:
|
|
215
|
+
enriched_cves.append(output_dto.cve_enrichment.enrich_cve(cve_id, raise_exception_on_failure=False))
|
|
216
|
+
except Exception as e:
|
|
217
|
+
raise ValueError(f"{e}")
|
|
218
|
+
self.cve_enrichment = enriched_cves
|
|
219
|
+
return self
|
|
157
220
|
|
|
221
|
+
|
|
158
222
|
splunk_app_enrichment: Optional[List[dict]] = None
|
|
159
223
|
|
|
160
224
|
@computed_field
|
|
@@ -382,11 +446,11 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
382
446
|
filter_macro = Macro.model_validate({"name":filter_macro_name,
|
|
383
447
|
"definition":'search *',
|
|
384
448
|
"description":'Update this macro to limit the output results to filter out false positives.'})
|
|
385
|
-
director.
|
|
449
|
+
director.addContentToDictMappings(filter_macro)
|
|
386
450
|
|
|
387
451
|
macros_from_search = Macro.get_macros(search, director)
|
|
388
452
|
|
|
389
|
-
return macros_from_search
|
|
453
|
+
return macros_from_search
|
|
390
454
|
|
|
391
455
|
def get_content_dependencies(self)->list[SecurityContentObject]:
|
|
392
456
|
#Do this separately to satisfy type checker
|
|
@@ -12,6 +12,7 @@ import re
|
|
|
12
12
|
import abc
|
|
13
13
|
import uuid
|
|
14
14
|
import datetime
|
|
15
|
+
import pprint
|
|
15
16
|
from pydantic import BaseModel, field_validator, Field, ValidationInfo, FilePath, HttpUrl, NonNegativeInt, ConfigDict, model_validator, model_serializer
|
|
16
17
|
from typing import Tuple, Optional, List, Union
|
|
17
18
|
import pathlib
|
|
@@ -181,6 +182,22 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
|
|
|
181
182
|
for object in all_objects:
|
|
182
183
|
name_dict[str(pathlib.Path(object.file_path))] = object
|
|
183
184
|
return name_dict
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def __repr__(self)->str:
|
|
188
|
+
# Just use the model_dump functionality that
|
|
189
|
+
# has already been written. This loses some of the
|
|
190
|
+
# richness where objects reference themselves, but
|
|
191
|
+
# is usable
|
|
192
|
+
m = self.model_dump()
|
|
193
|
+
return pprint.pformat(m, indent=3)
|
|
194
|
+
|
|
195
|
+
def __str__(self)->str:
|
|
196
|
+
return(self.__repr__())
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
184
201
|
|
|
185
202
|
|
|
186
203
|
|
contentctl/objects/baseline.py
CHANGED
|
@@ -31,7 +31,6 @@ class Baseline(SecurityContentObject):
|
|
|
31
31
|
search: str = Field(..., min_length=4)
|
|
32
32
|
how_to_implement: str = Field(..., min_length=4)
|
|
33
33
|
known_false_positives: str = Field(..., min_length=4)
|
|
34
|
-
check_references: bool = False #Validation is done in order, this field must be defined first
|
|
35
34
|
tags: BaselineTags = Field(...)
|
|
36
35
|
|
|
37
36
|
# enrichment
|
contentctl/objects/config.py
CHANGED
|
@@ -154,6 +154,10 @@ class Config_Base(BaseModel):
|
|
|
154
154
|
|
|
155
155
|
path: DirectoryPath = Field(default=DirectoryPath("."), description="The root of your app.")
|
|
156
156
|
app:CustomApp = Field(default_factory=CustomApp)
|
|
157
|
+
verbose:bool = Field(default=False, description="Enable verbose error logging, including a stacktrace. "
|
|
158
|
+
"This option makes debugging contentctl errors much easier, but produces way more "
|
|
159
|
+
"output than is useful under most uses cases. "
|
|
160
|
+
"Please use this flag if you are submitting a bug report or issue on GitHub.")
|
|
157
161
|
|
|
158
162
|
@field_serializer('path',when_used='always')
|
|
159
163
|
def serialize_path(path: DirectoryPath)->str:
|
|
@@ -269,14 +273,6 @@ class Infrastructure(BaseModel):
|
|
|
269
273
|
instance_name: str = Field(...)
|
|
270
274
|
|
|
271
275
|
|
|
272
|
-
class deploy_rest(build):
|
|
273
|
-
model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
|
|
274
|
-
|
|
275
|
-
target:Infrastructure = Infrastructure(instance_name="splunk_target_host", instance_address="localhost")
|
|
276
|
-
#This will overwrite existing content without promprting for confirmation
|
|
277
|
-
overwrite_existing_content:bool = Field(default=True, description="Overwrite existing macros and savedsearches in your enviornment")
|
|
278
|
-
|
|
279
|
-
|
|
280
276
|
class Container(Infrastructure):
|
|
281
277
|
model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
|
|
282
278
|
instance_address:str = Field(default="localhost", description="Address of your splunk server.")
|
|
@@ -145,7 +145,7 @@ class DetectionTags(BaseModel):
|
|
|
145
145
|
@model_validator(mode="after")
|
|
146
146
|
def addAttackEnrichment(self, info:ValidationInfo):
|
|
147
147
|
if len(self.mitre_attack_enrichments) > 0:
|
|
148
|
-
raise ValueError(f"Error, field 'mitre_attack_enrichment' should be empty and dynamically populated at runtime. Instead, this field contained: {
|
|
148
|
+
raise ValueError(f"Error, field 'mitre_attack_enrichment' should be empty and dynamically populated at runtime. Instead, this field contained: {self.mitre_attack_enrichments}")
|
|
149
149
|
|
|
150
150
|
output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
|
|
151
151
|
if output_dto is None:
|
contentctl/objects/macro.py
CHANGED
|
@@ -9,13 +9,14 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
MACROS_TO_IGNORE
|
|
16
|
-
MACROS_TO_IGNORE.add("
|
|
17
|
-
MACROS_TO_IGNORE.add("
|
|
18
|
-
MACROS_TO_IGNORE.add("
|
|
12
|
+
#The following macros are included in commonly-installed apps.
|
|
13
|
+
#As such, we will ignore if they are missing from our app.
|
|
14
|
+
#Included in
|
|
15
|
+
MACROS_TO_IGNORE = set(["drop_dm_object_name"]) # Part of CIM/Splunk_SA_CIM
|
|
16
|
+
MACROS_TO_IGNORE.add("get_asset") #SA-IdentityManagement, part of Enterprise Security
|
|
17
|
+
MACROS_TO_IGNORE.add("get_risk_severity") #SA-ThreatIntelligence, part of Enterprise Security
|
|
18
|
+
MACROS_TO_IGNORE.add("cim_corporate_web_domain_search") #Part of CIM/Splunk_SA_CIM
|
|
19
|
+
#MACROS_TO_IGNORE.add("prohibited_processes")
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class Macro(SecurityContentObject):
|
contentctl/objects/story_tags.py
CHANGED
|
@@ -14,6 +14,8 @@ class StoryUseCase(str,Enum):
|
|
|
14
14
|
APPLICATION_SECURITY = "Application Security"
|
|
15
15
|
SECURITY_MONITORING = "Security Monitoring"
|
|
16
16
|
ADVANCED_THREAD_DETECTION = "Advanced Threat Detection"
|
|
17
|
+
INSIDER_THREAT = "Insider Threat"
|
|
18
|
+
OTHER = "Other"
|
|
17
19
|
|
|
18
20
|
class StoryTags(BaseModel):
|
|
19
21
|
model_config = ConfigDict(extra='forbid', use_enum_values=True)
|
contentctl/output/yml_writer.py
CHANGED
|
@@ -8,4 +8,42 @@ class YmlWriter:
|
|
|
8
8
|
def writeYmlFile(file_path : str, obj : dict[Any,Any]) -> None:
|
|
9
9
|
|
|
10
10
|
with open(file_path, 'w') as outfile:
|
|
11
|
-
yaml.safe_dump(obj, outfile, default_flow_style=False, sort_keys=False)
|
|
11
|
+
yaml.safe_dump(obj, outfile, default_flow_style=False, sort_keys=False)
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def writeDetection(file_path: str, obj: dict[Any,Any]) -> None:
|
|
15
|
+
output = dict()
|
|
16
|
+
output["name"] = obj["name"]
|
|
17
|
+
output["id"] = obj["id"]
|
|
18
|
+
output["version"] = obj["version"]
|
|
19
|
+
output["date"] = obj["date"]
|
|
20
|
+
output["author"] = obj["author"]
|
|
21
|
+
output["type"] = obj["type"]
|
|
22
|
+
output["status"] = obj["status"]
|
|
23
|
+
output["data_source"] = obj['data_sources']
|
|
24
|
+
output["description"] = obj["description"]
|
|
25
|
+
output["search"] = obj["search"]
|
|
26
|
+
output["how_to_implement"] = obj["how_to_implement"]
|
|
27
|
+
output["known_false_positives"] = obj["known_false_positives"]
|
|
28
|
+
output["references"] = obj["references"]
|
|
29
|
+
output["tags"] = obj["tags"]
|
|
30
|
+
output["tests"] = obj["tags"]
|
|
31
|
+
|
|
32
|
+
YmlWriter.writeYmlFile(file_path=file_path, obj=output)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def writeStory(file_path: str, obj: dict[Any,Any]) -> None:
|
|
36
|
+
output = dict()
|
|
37
|
+
output['name'] = obj['name']
|
|
38
|
+
output['id'] = obj['id']
|
|
39
|
+
output['version'] = obj['version']
|
|
40
|
+
output['date'] = obj['date']
|
|
41
|
+
output['author'] = obj['author']
|
|
42
|
+
output['description'] = obj['description']
|
|
43
|
+
output['narrative'] = obj['narrative']
|
|
44
|
+
output['references'] = obj['references']
|
|
45
|
+
output['tags'] = obj['tags']
|
|
46
|
+
|
|
47
|
+
YmlWriter.writeYmlFile(file_path=file_path, obj=output)
|
|
48
|
+
|
|
49
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: contentctl
|
|
3
|
-
Version: 4.0
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Splunk Content Control Tool
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Author: STRT
|
|
@@ -10,25 +10,24 @@ Classifier: License :: Other/Proprietary License
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
-
Requires-Dist: Jinja2 (>=3.1.
|
|
13
|
+
Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
|
|
14
14
|
Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
|
|
15
15
|
Requires-Dist: attackcti (>=0.3.7,<0.4.0)
|
|
16
16
|
Requires-Dist: bottle (>=0.12.25,<0.13.0)
|
|
17
17
|
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
|
18
18
|
Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
|
19
19
|
Requires-Dist: pycvesearch (>=1.2,<2.0)
|
|
20
|
-
Requires-Dist: pydantic (>=2.
|
|
20
|
+
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
21
21
|
Requires-Dist: pygit2 (>=1.14.1,<2.0.0)
|
|
22
|
-
Requires-Dist: pysigma (>=0.
|
|
23
|
-
Requires-Dist: pysigma-backend-splunk (>=1.0
|
|
22
|
+
Requires-Dist: pysigma (>=0.11.5,<0.12.0)
|
|
23
|
+
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,<71.0.0)
|
|
28
28
|
Requires-Dist: splunk-sdk (>=2.0.1,<3.0.0)
|
|
29
|
-
Requires-Dist: tqdm (>=4.66.
|
|
29
|
+
Requires-Dist: tqdm (>=4.66.4,<5.0.0)
|
|
30
30
|
Requires-Dist: tyro (>=0.8.3,<0.9.0)
|
|
31
|
-
Requires-Dist: validators (>=0.22.0,<0.23.0)
|
|
32
31
|
Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
|
|
33
32
|
Description-Content-Type: text/markdown
|
|
34
33
|
|