contentctl 4.3.4__py3-none-any.whl → 4.3.5__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/initialize.py +28 -12
- contentctl/actions/inspect.py +189 -91
- contentctl/actions/validate.py +3 -7
- contentctl/api.py +1 -1
- contentctl/contentctl.py +3 -0
- contentctl/enrichments/attack_enrichment.py +49 -81
- contentctl/helper/splunk_app.py +141 -10
- contentctl/input/director.py +5 -12
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +8 -6
- contentctl/objects/atomic.py +51 -77
- contentctl/objects/config.py +142 -19
- contentctl/objects/constants.py +4 -1
- contentctl/objects/detection_metadata.py +71 -0
- contentctl/objects/detection_stanza.py +79 -0
- contentctl/objects/detection_tags.py +6 -6
- contentctl/objects/enums.py +0 -2
- contentctl/objects/errors.py +187 -0
- contentctl/objects/savedsearches_conf.py +196 -0
- contentctl/output/conf_writer.py +4 -1
- contentctl/output/new_content_yml_output.py +4 -9
- {contentctl-4.3.4.dist-info → contentctl-4.3.5.dist-info}/METADATA +4 -4
- {contentctl-4.3.4.dist-info → contentctl-4.3.5.dist-info}/RECORD +25 -26
- contentctl/objects/ssa_detection.py +0 -157
- contentctl/objects/ssa_detection_tags.py +0 -138
- contentctl/objects/unit_test_old.py +0 -10
- contentctl/objects/unit_test_ssa.py +0 -31
- {contentctl-4.3.4.dist-info → contentctl-4.3.5.dist-info}/LICENSE.md +0 -0
- {contentctl-4.3.4.dist-info → contentctl-4.3.5.dist-info}/WHEEL +0 -0
- {contentctl-4.3.4.dist-info → contentctl-4.3.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, ClassVar
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
import tarfile
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
9
|
+
|
|
10
|
+
from contentctl.objects.detection_stanza import DetectionStanza
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SavedsearchesConf(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
A model of the savedsearches.conf file, represented as a set of stanzas
|
|
16
|
+
|
|
17
|
+
NOTE: At present, this model only parses the detections themselves from the .conf; thing like
|
|
18
|
+
baselines or response tasks are left alone currently
|
|
19
|
+
"""
|
|
20
|
+
# The path to the conf file
|
|
21
|
+
path: Path = Field(...)
|
|
22
|
+
|
|
23
|
+
# The app label (used for pattern matching in the conf) (e.g. ESCU)
|
|
24
|
+
app_label: str = Field(...)
|
|
25
|
+
|
|
26
|
+
# A dictionary mapping rule names to a model of the corresponding stanza in the conf
|
|
27
|
+
detection_stanzas: dict[str, DetectionStanza] = Field(default={}, init=False)
|
|
28
|
+
|
|
29
|
+
# A internal flag indicating whether we are currently in the detections portion of the conf
|
|
30
|
+
# during parsing
|
|
31
|
+
_in_detections: bool = PrivateAttr(default=False)
|
|
32
|
+
|
|
33
|
+
# A internal flag indicating whether we are currently in a specific section of the conf
|
|
34
|
+
# during parsing
|
|
35
|
+
_in_section: bool = PrivateAttr(default=False)
|
|
36
|
+
|
|
37
|
+
# A running list of the accumulated lines identified as part of the current section
|
|
38
|
+
_current_section_lines: list[str] = PrivateAttr(default=[])
|
|
39
|
+
|
|
40
|
+
# The name of the current section
|
|
41
|
+
_current_section_name: str | None = PrivateAttr(default=None)
|
|
42
|
+
|
|
43
|
+
# The current line number as we continue to parse the file
|
|
44
|
+
_current_line_no: int = PrivateAttr(default=0)
|
|
45
|
+
|
|
46
|
+
# A format string for the path to the savedsearches.conf in the app package
|
|
47
|
+
PACKAGE_CONF_PATH_FMT_STR: ClassVar[str] = "{appid}/default/savedsearches.conf"
|
|
48
|
+
|
|
49
|
+
def model_post_init(self, __context: Any) -> None:
|
|
50
|
+
super().model_post_init(__context)
|
|
51
|
+
self._parse_detection_stanzas()
|
|
52
|
+
|
|
53
|
+
def is_section_header(self, line: str) -> bool:
|
|
54
|
+
"""
|
|
55
|
+
Given a line, determine if the line is a section header, indicating the start of a new
|
|
56
|
+
section
|
|
57
|
+
|
|
58
|
+
:param line: a line from the conf file
|
|
59
|
+
:type line: str
|
|
60
|
+
|
|
61
|
+
:returns: a bool indicating whether the current line is a section header or not
|
|
62
|
+
:rtype: bool
|
|
63
|
+
"""
|
|
64
|
+
# Compile the pattern based on the app name
|
|
65
|
+
pattern = re.compile(r"\[" + self.app_label + r" - .+ - Rule\]")
|
|
66
|
+
if pattern.match(line):
|
|
67
|
+
return True
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def section_start(self, line: str) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Given a line, adjust the state to track a new section
|
|
73
|
+
|
|
74
|
+
:param line: a line from the conf file
|
|
75
|
+
:type line: str
|
|
76
|
+
"""
|
|
77
|
+
# Determine the new section name:
|
|
78
|
+
new_section_name = line.strip().strip("[").strip("]")
|
|
79
|
+
|
|
80
|
+
# Raise if we are in a section already according to the state (we cannot statr a new section
|
|
81
|
+
# before ending the previous section)
|
|
82
|
+
if self._in_section:
|
|
83
|
+
raise Exception(
|
|
84
|
+
"Attempting to start a new section w/o ending the current one; check for "
|
|
85
|
+
f"parsing/serialization errors: (current section: '{self._current_section_name}', "
|
|
86
|
+
f"new section: '{new_section_name}') [see line {self._current_line_no} in "
|
|
87
|
+
f"{self.path}]"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Capture the name of this section, reset the lines, and indicate that we are now in a
|
|
91
|
+
# section
|
|
92
|
+
self._current_section_name = new_section_name
|
|
93
|
+
self._current_section_lines = [line]
|
|
94
|
+
self._in_section = True
|
|
95
|
+
|
|
96
|
+
def section_end(self) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Adjust the state end the section we were enumerating; parse the lines as a DetectionStanza
|
|
99
|
+
"""
|
|
100
|
+
# Name should have been set during section start
|
|
101
|
+
if self._current_section_name is None:
|
|
102
|
+
raise Exception(
|
|
103
|
+
"Name for the current section was never set; check for parsing/serialization "
|
|
104
|
+
f"errors [see line {self._current_line_no} in {self.path}]."
|
|
105
|
+
)
|
|
106
|
+
elif self._current_section_name in self.detection_stanzas:
|
|
107
|
+
# Each stanza should be unique, so the name should not already be in the dict
|
|
108
|
+
raise Exception(
|
|
109
|
+
f"Name '{self._current_section_name}' already in set of stanzas [see line "
|
|
110
|
+
f"{self._current_line_no} in {self.path}]."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Build the stanza model from the accumulated lines and adjust the state to end this section
|
|
114
|
+
self.detection_stanzas[self._current_section_name] = DetectionStanza(
|
|
115
|
+
name=self._current_section_name,
|
|
116
|
+
lines=self._current_section_lines
|
|
117
|
+
)
|
|
118
|
+
self._in_section = False
|
|
119
|
+
|
|
120
|
+
def _parse_detection_stanzas(self) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Open the conf file, and parse out DetectionStanza objects from the raw conf stanzas
|
|
123
|
+
"""
|
|
124
|
+
# We don't want to parse the stanzas twice (non-atomic operation)
|
|
125
|
+
if len(self.detection_stanzas) != 0:
|
|
126
|
+
raise Exception(
|
|
127
|
+
f"{len(self.detection_stanzas)} stanzas have already been parsed from this conf; we"
|
|
128
|
+
" do not need to parse them again"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Open the conf file and iterate over the lines
|
|
132
|
+
with open(self.path, "r") as file:
|
|
133
|
+
for line in file:
|
|
134
|
+
self._current_line_no += 1
|
|
135
|
+
|
|
136
|
+
# Break when we get to the end of the app detections
|
|
137
|
+
if line.strip() == f"### END {self.app_label} DETECTIONS ###":
|
|
138
|
+
break
|
|
139
|
+
elif self._in_detections:
|
|
140
|
+
# Check if we are in the detections portion of the conf, and then if we are in a
|
|
141
|
+
# section
|
|
142
|
+
if self._in_section:
|
|
143
|
+
# If we are w/in a section and have hit an empty line, close the section
|
|
144
|
+
if line.strip() == "":
|
|
145
|
+
self.section_end()
|
|
146
|
+
elif self.is_section_header(line):
|
|
147
|
+
# Raise if we encounter a section header w/in a section
|
|
148
|
+
raise Exception(
|
|
149
|
+
"Encountered section header while already in section (current "
|
|
150
|
+
f"section: '{self._current_section_name}') [see line "
|
|
151
|
+
f"{self._current_line_no} in {self.path}]."
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
# Otherwise, append the line
|
|
155
|
+
self._current_section_lines.append(line)
|
|
156
|
+
elif self.is_section_header(line):
|
|
157
|
+
# If we encounter a section header while not already in a section, start a
|
|
158
|
+
# new one
|
|
159
|
+
self.section_start(line)
|
|
160
|
+
elif line.strip() != "":
|
|
161
|
+
# If we are not in a section and have encountered anything other than an
|
|
162
|
+
# empty line, something is wrong
|
|
163
|
+
raise Exception(
|
|
164
|
+
"Found a non-empty line outside a stanza [see line "
|
|
165
|
+
f"{self._current_line_no} in {self.path}]."
|
|
166
|
+
)
|
|
167
|
+
elif line.strip() == f"### {self.app_label} DETECTIONS ###":
|
|
168
|
+
# We have hit the detections portion of the conf and we adjust the state
|
|
169
|
+
# accordingly
|
|
170
|
+
self._in_detections = True
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def init_from_package(package_path: Path, app_name: str, appid: str) -> "SavedsearchesConf":
|
|
174
|
+
"""
|
|
175
|
+
Alternate constructor which can take an app package, and extract the savedsearches.conf from
|
|
176
|
+
a temporary file.
|
|
177
|
+
|
|
178
|
+
:param package_path: Path to the app package
|
|
179
|
+
:type package_path: :class:`pathlib.Path`
|
|
180
|
+
:param app_name: the name of the app (e.g. ESCU)
|
|
181
|
+
:type app_name: str
|
|
182
|
+
|
|
183
|
+
:returns: a SavedsearchesConf object
|
|
184
|
+
:rtype: :class:`contentctl.objects.savedsearches_conf.SavedsearchesConf`
|
|
185
|
+
"""
|
|
186
|
+
# Create a temporary directory
|
|
187
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
188
|
+
# Open the tar/gzip archive
|
|
189
|
+
with tarfile.open(package_path) as package:
|
|
190
|
+
# Extract the savedsearches.conf and use it to init the model
|
|
191
|
+
package_conf_path = SavedsearchesConf.PACKAGE_CONF_PATH_FMT_STR.format(appid=appid)
|
|
192
|
+
package.extract(package_conf_path, path=tmpdir)
|
|
193
|
+
return SavedsearchesConf(
|
|
194
|
+
path=Path(tmpdir, package_conf_path),
|
|
195
|
+
app_label=app_name
|
|
196
|
+
)
|
contentctl/output/conf_writer.py
CHANGED
|
@@ -34,7 +34,10 @@ class ConfWriter():
|
|
|
34
34
|
# Failing to do so will result in an improperly formatted conf files that
|
|
35
35
|
# cannot be parsed
|
|
36
36
|
if isinstance(obj,str):
|
|
37
|
-
|
|
37
|
+
# Remove leading and trailing characters. Conf parsers may erroneously
|
|
38
|
+
# Parse fields if they have leading or trailing newlines/whitespace and we
|
|
39
|
+
# probably don't want that anyway as it doesn't look good in output
|
|
40
|
+
return obj.strip().replace(f"\n"," \\\n")
|
|
38
41
|
else:
|
|
39
42
|
return obj
|
|
40
43
|
|
|
@@ -39,11 +39,8 @@ class NewContentYmlOutput():
|
|
|
39
39
|
.replace('.','_') \
|
|
40
40
|
.replace('/','_') \
|
|
41
41
|
.lower()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
file_name = 'ssa___' + file_name + '.yml'
|
|
45
|
-
else:
|
|
46
|
-
file_name = file_name + '.yml'
|
|
42
|
+
|
|
43
|
+
file_name = file_name + '.yml'
|
|
47
44
|
return file_name
|
|
48
45
|
|
|
49
46
|
|
|
@@ -54,8 +51,6 @@ class NewContentYmlOutput():
|
|
|
54
51
|
.replace('.','_') \
|
|
55
52
|
.replace('/','_') \
|
|
56
53
|
.lower()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
else:
|
|
60
|
-
file_name = file_name + '.test.yml'
|
|
54
|
+
|
|
55
|
+
file_name = file_name + '.test.yml'
|
|
61
56
|
return file_name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: contentctl
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.5
|
|
4
4
|
Summary: Splunk Content Control Tool
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Author: STRT
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
13
13
|
Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
|
|
14
14
|
Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
|
|
15
15
|
Requires-Dist: attackcti (>=0.4.0,<0.5.0)
|
|
16
|
-
Requires-Dist: bottle (>=0.12.25,<0.
|
|
16
|
+
Requires-Dist: bottle (>=0.12.25,<0.14.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)
|
|
@@ -22,7 +22,7 @@ Requires-Dist: pygit2 (>=1.15.1,<2.0.0)
|
|
|
22
22
|
Requires-Dist: questionary (>=2.0.1,<3.0.0)
|
|
23
23
|
Requires-Dist: requests (>=2.32.3,<2.33.0)
|
|
24
24
|
Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
|
|
25
|
-
Requires-Dist: setuptools (>=69.5.1,<
|
|
25
|
+
Requires-Dist: setuptools (>=69.5.1,<76.0.0)
|
|
26
26
|
Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
|
|
27
27
|
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
28
28
|
Requires-Dist: tyro (>=0.8.3,<0.9.0)
|
|
@@ -165,7 +165,7 @@ This section is under active development. It will allow you to a [MITRE Map](ht
|
|
|
165
165
|
Choose TYPE {detection, story} to create new content for the Content Pack. The tool will interactively ask a series of questions required for generating a basic piece of content and automatically add it to the Content Pack.
|
|
166
166
|
|
|
167
167
|
### contentctl inspect
|
|
168
|
-
This section is under development.
|
|
168
|
+
This section is under development. The inspect action performs a number of post-build validations. Primarily, it will enable the user to perform an appinspect of the content pack in preparation for deployment onto a Splunk Instance or via Splunk Cloud. It also compares detections in the new build against a prior build, confirming that any changed detections have had their versions incremented (this comparison happens at the savedsearch.conf level, which is why it must happen after the build). Please also note that new versions of contentctl may result in the generation of different savedsearches.conf files without any content changes in YML (new keys at the .conf level which will necessitate bumping of the version in the YML file).
|
|
169
169
|
|
|
170
170
|
### contentctl deploy
|
|
171
171
|
The reason to build content is so that it can be deployed to your environment. However, deploying content to multiple servers and different types of infrastructure can be tricky and time-consuming. contentctl makes this easy by supporting a number of different deployment mechanisms. Deployment targets can be defined in [contentctl.yml](/contentctl/templates/contentctl_default.yml).
|
|
@@ -13,37 +13,37 @@ contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py,sha256=v5F
|
|
|
13
13
|
contentctl/actions/detection_testing/views/DetectionTestingViewFile.py,sha256=3mBCQy3hYuX8bNqh3al0nANlMwq9sxbQjkhwA1V5LOA,1090
|
|
14
14
|
contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py,sha256=6mecacXFoTJxcHiRZSnlHos5Hca1jdedEEZfiIAhaJg,4706
|
|
15
15
|
contentctl/actions/doc_gen.py,sha256=YNc1VYA0ikL1hWDHYjfEOmUkfhy8PEIdvTyC4ZLxQRY,863
|
|
16
|
-
contentctl/actions/initialize.py,sha256=
|
|
16
|
+
contentctl/actions/initialize.py,sha256=wEO3u8vJYP8Xh2OSJ_HxfMV6mqOdkPyWbUzNGEqMTNA,3055
|
|
17
17
|
contentctl/actions/initialize_old.py,sha256=0qXbW_fNDvkcnEeL6Zpte8d-hpTu1REyzHsXOCY-YB8,9333
|
|
18
|
-
contentctl/actions/inspect.py,sha256=
|
|
18
|
+
contentctl/actions/inspect.py,sha256=kxExmA4dn4-JXl_PiPVmGObeqQmYd04nKjFNvjFyFYc,17232
|
|
19
19
|
contentctl/actions/new_content.py,sha256=o5ZYBQ216RN6TnW_wRxVGJybx2SsJ7ht4PAi1dw45Yg,6076
|
|
20
20
|
contentctl/actions/release_notes.py,sha256=akkFfLhsJuaPUyjsb6dLlKt9cUM-JApAjTFQMbYoXeM,13115
|
|
21
21
|
contentctl/actions/reporting.py,sha256=MJEmvmoA1WnSFZEU9QM6daL_W94oOX0WXAcX1qAM2As,1583
|
|
22
22
|
contentctl/actions/test.py,sha256=jv12UO_PTjZwvo4G-Dr8fE2gsuWvuvAmO2QQM4q7TL0,5917
|
|
23
|
-
contentctl/actions/validate.py,sha256=
|
|
24
|
-
contentctl/api.py,sha256=
|
|
25
|
-
contentctl/contentctl.py,sha256=
|
|
26
|
-
contentctl/enrichments/attack_enrichment.py,sha256=
|
|
23
|
+
contentctl/actions/validate.py,sha256=TL_zUU8Lo2ygf28F_EtaKWTFRBrbg-31XN5j2feNFKM,5524
|
|
24
|
+
contentctl/api.py,sha256=O0dNE3-WkWs2zuOeAQnIicgOtBX5s2bGBhRVo3j69-8,6327
|
|
25
|
+
contentctl/contentctl.py,sha256=CLYQ1kpVcUkOXPGrGyE7SwAkEtvjq2kHENWyy81gwsM,10400
|
|
26
|
+
contentctl/enrichments/attack_enrichment.py,sha256=i0p5ud7EqA2SMB7Gc8JQdIonUTjAeDN-hxKBV4XV-Rg,6391
|
|
27
27
|
contentctl/enrichments/cve_enrichment.py,sha256=rRdf62sKkBzCBLCNwzAmEhxNiPV2px1VS6MzDiS-uBw,2337
|
|
28
28
|
contentctl/enrichments/splunk_app_enrichment.py,sha256=zDNHFLZTi2dJ1gdnh0sHkD6F1VtkblqFnhacFcCMBfc,3418
|
|
29
29
|
contentctl/helper/link_validator.py,sha256=-XorhxfGtjLynEL1X4hcpRMiyemogf2JEnvLwhHq80c,7139
|
|
30
30
|
contentctl/helper/logger.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
-
contentctl/helper/splunk_app.py,sha256=
|
|
31
|
+
contentctl/helper/splunk_app.py,sha256=5KoacltgQ2J1BdxqvZYhr6GCXFl2tsy8TEWNc2gXkqw,14187
|
|
32
32
|
contentctl/helper/utils.py,sha256=8ICRvE7DUiNL9BK4Hw71hCLFbd3R2u86OwKeDOdaBTY,19454
|
|
33
|
-
contentctl/input/director.py,sha256=
|
|
33
|
+
contentctl/input/director.py,sha256=Z_NV6nyfFHDcWUaXi9Q88Xv-V_patuzQ39YsFzJoXQE,10434
|
|
34
34
|
contentctl/input/new_content_questions.py,sha256=o4prlBoUhEMxqpZukquI9WKbzfFJfYhEF7a8m2q_BEE,5565
|
|
35
35
|
contentctl/input/yml_reader.py,sha256=hyVUYhx4Ka8C618kP2D_E3sDUKEQGC6ty_QZQArHKd4,1489
|
|
36
|
-
contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=
|
|
36
|
+
contentctl/objects/abstract_security_content_objects/detection_abstract.py,sha256=2TOIfDVZm1uQbHFrP9YFOy7pXDPkIWCxzm-qCzK9Twc,39061
|
|
37
37
|
contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py,sha256=vdZvybF34Zlxf6XOjw400gYbpkPUkOtlu-JiWlAof40,9877
|
|
38
38
|
contentctl/objects/alert_action.py,sha256=E9gjCn5C31h0sN7k90KNe4agRxFFSnMW_Z-Ri_3YQss,1335
|
|
39
39
|
contentctl/objects/annotated_types.py,sha256=jnX02BQT4dHbd_DCIjik0PNN3kgsvb7sxAz_1Jy8TOY,259
|
|
40
|
-
contentctl/objects/atomic.py,sha256=
|
|
40
|
+
contentctl/objects/atomic.py,sha256=L9QSmwmmSFFfvUykPk_nXwz9XDz-Gn6e0rrDxxRO8uY,7292
|
|
41
41
|
contentctl/objects/base_test.py,sha256=qUtKQJrqCto_fwCBdiH68_tXqokhcv9ceu2fQlBxsjA,1045
|
|
42
42
|
contentctl/objects/base_test_result.py,sha256=jVroyGLb9GD6Wm2QzvgIEA3SWCZqxPsHp9PzxSvpyIs,5101
|
|
43
43
|
contentctl/objects/baseline.py,sha256=Lb1vJKtDdlDrzWgrdkC9oQao_TnRrOxSwOWHf4trtaU,2150
|
|
44
44
|
contentctl/objects/baseline_tags.py,sha256=fVhLF-NmisavybB_idu3N0Con0Ymj8clKfRMkWzBB-k,1762
|
|
45
|
-
contentctl/objects/config.py,sha256=
|
|
46
|
-
contentctl/objects/constants.py,sha256=
|
|
45
|
+
contentctl/objects/config.py,sha256=q6-zGzKXi_etiAOJDgKKrU31WfmJkA9_Yjnx2QccScA,49808
|
|
46
|
+
contentctl/objects/constants.py,sha256=389Gna6BtukAkXfOKiHEg-FtPRVEVReV4pEMeLuq7o8,3653
|
|
47
47
|
contentctl/objects/correlation_search.py,sha256=ZZVoO3M594qCy_aAMhQiOPWn8FiSFbRShUCCLx6zhNc,48434
|
|
48
48
|
contentctl/objects/data_source.py,sha256=aRr6lHu-EtGmi6J2nXKD7i2ozUPtp7X-vDkQiutvD3I,1545
|
|
49
49
|
contentctl/objects/deployment.py,sha256=Qc6M4yeOvxjqFKR8sfjd4CG06AbVheTOqP1mwqo4t8s,2651
|
|
@@ -54,9 +54,11 @@ contentctl/objects/deployment_rba.py,sha256=YFLSKzLU7s8Bt1cJkSBWlfCsc_2MfgiwyaDi
|
|
|
54
54
|
contentctl/objects/deployment_scheduling.py,sha256=bQjbJHNaUGdU1VAGV8-nFOHzHutbIlt7FZpUvR1CV4Y,198
|
|
55
55
|
contentctl/objects/deployment_slack.py,sha256=P6z8OLHDKcDWx7nbKWasqBc3dFRatGcpO2GtmxzVV8I,135
|
|
56
56
|
contentctl/objects/detection.py,sha256=3W41cXf3ECjWuPqWrseqSLC3PAA7O5_nENWWM6MPK0Y,620
|
|
57
|
-
contentctl/objects/
|
|
58
|
-
contentctl/objects/
|
|
59
|
-
contentctl/objects/
|
|
57
|
+
contentctl/objects/detection_metadata.py,sha256=eCsru2cymc3VINjt9MpDyGw2zXa2HyVEPv-XiGAcAeQ,2236
|
|
58
|
+
contentctl/objects/detection_stanza.py,sha256=842fHPfGDdddHF5UzgftYr8OlYblWhMWZxPQsTu2wKg,3066
|
|
59
|
+
contentctl/objects/detection_tags.py,sha256=90-dGSMwZH-6VYReb2_f81s3pZ4dJ2PBQZog4GMZcE4,11030
|
|
60
|
+
contentctl/objects/enums.py,sha256=xY-pESjN8AUeP_ELCtMDUxQO7OzMJbK-QSl4UJfaqGQ,14016
|
|
61
|
+
contentctl/objects/errors.py,sha256=WURmJCqhy2CZNXXCypXVtwnjSBx-VIcB6W9oFJmzoFk,5762
|
|
60
62
|
contentctl/objects/event_source.py,sha256=G9P7rtcN5hcBNQx6DG37mR3QyQufx--T6kgQGNqQuKk,415
|
|
61
63
|
contentctl/objects/integration_test.py,sha256=UBBx85f517MpQXOM7-iEasACEQ0-Ia7W4rDChOHZfno,1319
|
|
62
64
|
contentctl/objects/integration_test_result.py,sha256=9oVWka57alIVPiCDbNgy-OmJcBicyYbrr6anL52Wgks,278
|
|
@@ -75,9 +77,8 @@ contentctl/objects/playbook_tags.py,sha256=NrhTGcgoYSGEZggrfebko0GBOXN9x05IadRUU
|
|
|
75
77
|
contentctl/objects/risk_analysis_action.py,sha256=Glzcq99DAqqOJ2eZYCkUI3R5hA5cZGU0ZuCSinFf2R8,4278
|
|
76
78
|
contentctl/objects/risk_event.py,sha256=b5Smh3w5Hecmi7E-Ub5DvO8iOPwnVg2ux47u7oemxX4,14041
|
|
77
79
|
contentctl/objects/risk_object.py,sha256=yY4NmEwEKaRl4sLzCRZb1n8kdpV3HzYbQVQ1ClQWYHw,904
|
|
80
|
+
contentctl/objects/savedsearches_conf.py,sha256=tCyZHqAQ9azgwIyySViY2BdM4To5Cb_GeYEEHPwR4Zc,8604
|
|
78
81
|
contentctl/objects/security_content_object.py,sha256=j8KNDwSMfZsSIzJucC3NuZo0SlFVpqHfDc6y3-YHjHI,234
|
|
79
|
-
contentctl/objects/ssa_detection.py,sha256=ud0T6lq-5XUlmeK8Jzw_aNLe6podVcA1o7THDYvWbik,5934
|
|
80
|
-
contentctl/objects/ssa_detection_tags.py,sha256=9aRwbpQHi79NIS9rofjgxDJpw7cWXqG534_kSbvHJh8,5220
|
|
81
82
|
contentctl/objects/story.py,sha256=FXe11LV19xJTtCgx7DKdvV9cL0gKeryUnE3yjpnDmrU,4957
|
|
82
83
|
contentctl/objects/story_tags.py,sha256=cOL8PUzdlFdLPQHc54_-9sdI8nCE1D04oKY7KriOssI,2293
|
|
83
84
|
contentctl/objects/test_attack_data.py,sha256=9OgErjdPR4S-SJpQePt0uwBLPYHYPtqKDd-auhjz7Uc,430
|
|
@@ -85,20 +86,18 @@ contentctl/objects/test_group.py,sha256=DCtm4ChGYksOwZQVHsioaweOvI37CSlTZJzKvBX-
|
|
|
85
86
|
contentctl/objects/threat_object.py,sha256=S8B7RQFfLxN_g7yKPrDTuYhIy9JvQH3YwJ_T5LUZIa4,711
|
|
86
87
|
contentctl/objects/unit_test.py,sha256=eMFehpHhmZA5WYBqhWUNRF_LpxuLM9VooAxjXeNbrxY,1144
|
|
87
88
|
contentctl/objects/unit_test_baseline.py,sha256=XHvOm7qLYfqrP6uC5U_pfgw_pf8-S2RojuNmbo6lXlM,227
|
|
88
|
-
contentctl/objects/unit_test_old.py,sha256=IfvytHG4ZnUhsvXgdczECZbiwv6YLViYdsk9AqeDBjQ,199
|
|
89
89
|
contentctl/objects/unit_test_result.py,sha256=POQfvvPpSw-jQzINBz1_IszUMJ4Wbopu8HRS1Qe6P2M,2940
|
|
90
|
-
contentctl/objects/unit_test_ssa.py,sha256=RURqXb3e0CuI5nNX8PvFucxatAvMmGSUDngVbqNpoiY,653
|
|
91
90
|
contentctl/output/api_json_output.py,sha256=n3OTd5z-Vkmsn7ny6QCAar_jSMNuuJfzAQa7xq_9if4,9085
|
|
92
91
|
contentctl/output/attack_nav_output.py,sha256=95iKV8U9BMMgqh6cCOw1S89Ln73xmJGgJPHTYR0L7hA,2304
|
|
93
92
|
contentctl/output/attack_nav_writer.py,sha256=64ILZLmNbh2XLmbopgENkeo6t-4SRRG8xZXBmtpNd4g,2219
|
|
94
93
|
contentctl/output/conf_output.py,sha256=7HcHM9pJLNnan1Kq_7ozvs5iOgfzqdKbO6gwxUZJVnc,9994
|
|
95
|
-
contentctl/output/conf_writer.py,sha256=
|
|
94
|
+
contentctl/output/conf_writer.py,sha256=uMxWrdu-4paiTgUGu_FUWMjT-r_IpdZSTUSDZUGC6k8,8541
|
|
96
95
|
contentctl/output/data_source_writer.py,sha256=ubFjm6XJ4T2d3oqfKwDFasITHeDj3HFmegqVN--5_ME,1635
|
|
97
96
|
contentctl/output/detection_writer.py,sha256=AzxbssNLmsNIOaYKotew5-ONoyq1cQpKSGy3pe191B0,960
|
|
98
97
|
contentctl/output/doc_md_output.py,sha256=gf7osH1uSrC6js3D_I72g4uDe9TaB3tsvtqCHi5znp0,3238
|
|
99
98
|
contentctl/output/jinja_writer.py,sha256=bdiqr9FaXYxth4wZ1A52zTMAS5stHNGpezTkaS5pres,1119
|
|
100
99
|
contentctl/output/json_writer.py,sha256=Z-iVLnZb8tzYATxbQtXax0dz572lVPFMNVTx-vWbnog,1007
|
|
101
|
-
contentctl/output/new_content_yml_output.py,sha256=
|
|
100
|
+
contentctl/output/new_content_yml_output.py,sha256=KvP0FffQBPznSKqJyRQMtehf4XYEVK5jiPlUwnkekUc,2061
|
|
102
101
|
contentctl/output/svg_output.py,sha256=T2p4S085MKj5VPZKvo4tWBVOmYme32J9L7kMEBm3SwQ,2751
|
|
103
102
|
contentctl/output/templates/analyticstories_detections.j2,sha256=MYefoyWAq4b7dth3OlbMWNhFnH3_nnMKaOfw0lMkxT4,917
|
|
104
103
|
contentctl/output/templates/analyticstories_investigations.j2,sha256=7bwt_6U3dr9hbxOUkp0a1KnRJohNgC7GE1zRg_N_awI,515
|
|
@@ -166,8 +165,8 @@ contentctl/templates/detections/web/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
|
|
166
165
|
contentctl/templates/macros/security_content_ctime.yml,sha256=Gg1YNllHVsX_YB716H1SJLWzxXZEfuJlnsgB2fuyoHU,159
|
|
167
166
|
contentctl/templates/macros/security_content_summariesonly.yml,sha256=9BYUxAl2E4Nwh8K19F3AJS8Ka7ceO6ZDBjFiO3l3LY0,162
|
|
168
167
|
contentctl/templates/stories/cobalt_strike.yml,sha256=rlaXxMN-5k8LnKBLPafBoksyMtlmsPMHPJOjTiMiZ-M,3063
|
|
169
|
-
contentctl-4.3.
|
|
170
|
-
contentctl-4.3.
|
|
171
|
-
contentctl-4.3.
|
|
172
|
-
contentctl-4.3.
|
|
173
|
-
contentctl-4.3.
|
|
168
|
+
contentctl-4.3.5.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
|
|
169
|
+
contentctl-4.3.5.dist-info/METADATA,sha256=Ja_S233rBxi4ZWj0ihjS7XdybxUirZFKwC2sZvwvOaI,21489
|
|
170
|
+
contentctl-4.3.5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
171
|
+
contentctl-4.3.5.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
|
|
172
|
+
contentctl-4.3.5.dist-info/RECORD,,
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
import uuid
|
|
3
|
-
import string
|
|
4
|
-
import requests
|
|
5
|
-
import time
|
|
6
|
-
from pydantic import BaseModel, validator, root_validator
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from typing import Union
|
|
10
|
-
import re
|
|
11
|
-
|
|
12
|
-
from contentctl.objects.abstract_security_content_objects.detection_abstract import Detection_Abstract
|
|
13
|
-
from contentctl.objects.enums import AnalyticsType
|
|
14
|
-
from contentctl.objects.enums import DataModel
|
|
15
|
-
from contentctl.objects.enums import DetectionStatus
|
|
16
|
-
from contentctl.objects.deployment import Deployment
|
|
17
|
-
from contentctl.objects.ssa_detection_tags import SSADetectionTags
|
|
18
|
-
from contentctl.objects.unit_test_ssa import UnitTestSSA
|
|
19
|
-
from contentctl.objects.unit_test_old import UnitTestOld
|
|
20
|
-
from contentctl.objects.macro import Macro
|
|
21
|
-
from contentctl.objects.lookup import Lookup
|
|
22
|
-
from contentctl.objects.baseline import Baseline
|
|
23
|
-
from contentctl.objects.playbook import Playbook
|
|
24
|
-
from contentctl.helper.link_validator import LinkValidator
|
|
25
|
-
from contentctl.objects.enums import SecurityContentType
|
|
26
|
-
|
|
27
|
-
class SSADetection(BaseModel):
|
|
28
|
-
# detection spec
|
|
29
|
-
name: str
|
|
30
|
-
id: str
|
|
31
|
-
version: int
|
|
32
|
-
date: str
|
|
33
|
-
author: str
|
|
34
|
-
type: AnalyticsType = ...
|
|
35
|
-
status: DetectionStatus = ...
|
|
36
|
-
detection_type: str = None
|
|
37
|
-
description: str
|
|
38
|
-
data_source: list[str]
|
|
39
|
-
search: Union[str, dict]
|
|
40
|
-
how_to_implement: str
|
|
41
|
-
known_false_positives: str
|
|
42
|
-
references: list
|
|
43
|
-
tags: SSADetectionTags
|
|
44
|
-
tests: list[UnitTestSSA] = None
|
|
45
|
-
|
|
46
|
-
# enrichments
|
|
47
|
-
annotations: dict = None
|
|
48
|
-
risk: list = None
|
|
49
|
-
mappings: dict = None
|
|
50
|
-
file_path: str = None
|
|
51
|
-
source: str = None
|
|
52
|
-
test: Union[UnitTestSSA, dict, UnitTestOld] = None
|
|
53
|
-
runtime: str = None
|
|
54
|
-
internalVersion: int = None
|
|
55
|
-
|
|
56
|
-
# @validator('name')v
|
|
57
|
-
# def name_max_length(cls, v, values):
|
|
58
|
-
# if len(v) > 67:
|
|
59
|
-
# raise ValueError('name is longer then 67 chars: ' + v)
|
|
60
|
-
# return v
|
|
61
|
-
|
|
62
|
-
# TODO (#266): disable the use_enum_values configuration
|
|
63
|
-
class Config:
|
|
64
|
-
use_enum_values = True
|
|
65
|
-
|
|
66
|
-
'''
|
|
67
|
-
@validator("name")
|
|
68
|
-
def name_invalid_chars(cls, v):
|
|
69
|
-
invalidChars = set(string.punctuation.replace("-", ""))
|
|
70
|
-
if any(char in invalidChars for char in v):
|
|
71
|
-
raise ValueError("invalid chars used in name: " + v)
|
|
72
|
-
return v
|
|
73
|
-
|
|
74
|
-
@validator("id")
|
|
75
|
-
def id_check(cls, v, values):
|
|
76
|
-
try:
|
|
77
|
-
uuid.UUID(str(v))
|
|
78
|
-
except:
|
|
79
|
-
raise ValueError("uuid is not valid: " + values["name"])
|
|
80
|
-
return v
|
|
81
|
-
|
|
82
|
-
@validator("date")
|
|
83
|
-
def date_valid(cls, v, values):
|
|
84
|
-
try:
|
|
85
|
-
datetime.strptime(v, "%Y-%m-%d")
|
|
86
|
-
except:
|
|
87
|
-
raise ValueError("date is not in format YYYY-MM-DD: " + values["name"])
|
|
88
|
-
return v
|
|
89
|
-
|
|
90
|
-
# @validator("type")
|
|
91
|
-
# def type_valid(cls, v, values):
|
|
92
|
-
# if v.lower() not in [el.name.lower() for el in AnalyticsType]:
|
|
93
|
-
# raise ValueError("not valid analytics type: " + values["name"])
|
|
94
|
-
# return v
|
|
95
|
-
|
|
96
|
-
@validator("description", "how_to_implement")
|
|
97
|
-
def encode_error(cls, v, values, field):
|
|
98
|
-
try:
|
|
99
|
-
v.encode("ascii")
|
|
100
|
-
except UnicodeEncodeError:
|
|
101
|
-
raise ValueError("encoding error in " + field.name + ": " + values["name"])
|
|
102
|
-
return v
|
|
103
|
-
|
|
104
|
-
# @root_validator
|
|
105
|
-
# def search_validation(cls, values):
|
|
106
|
-
# if 'ssa_' not in values['file_path']:
|
|
107
|
-
# if not '_filter' in values['search']:
|
|
108
|
-
# raise ValueError('filter macro missing in: ' + values["name"])
|
|
109
|
-
# if any(x in values['search'] for x in ['eventtype=', 'sourcetype=', ' source=', 'index=']):
|
|
110
|
-
# if not 'index=_internal' in values['search']:
|
|
111
|
-
# raise ValueError('Use source macro instead of eventtype, sourcetype, source or index in detection: ' + values["name"])
|
|
112
|
-
# return values
|
|
113
|
-
|
|
114
|
-
@root_validator
|
|
115
|
-
def name_max_length(cls, values):
|
|
116
|
-
# Check max length only for ESCU searches, SSA does not have that constraint
|
|
117
|
-
if "ssa_" not in values["file_path"]:
|
|
118
|
-
if len(values["name"]) > 67:
|
|
119
|
-
raise ValueError("name is longer then 67 chars: " + values["name"])
|
|
120
|
-
return values
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
@root_validator
|
|
124
|
-
def new_line_check(cls, values):
|
|
125
|
-
# Check if there is a new line in description and how to implement that is not escaped
|
|
126
|
-
pattern = r'(?<!\\)\n'
|
|
127
|
-
if re.search(pattern, values["description"]):
|
|
128
|
-
match_obj = re.search(pattern,values["description"])
|
|
129
|
-
words = values["description"][:match_obj.span()[0]].split()[-10:]
|
|
130
|
-
newline_context = ' '.join(words)
|
|
131
|
-
raise ValueError(f"Field named 'description' contains new line that is not escaped using backslash. Add backslash at the end of the line after the words: '{newline_context}' in '{values['name']}'")
|
|
132
|
-
if re.search(pattern, values["how_to_implement"]):
|
|
133
|
-
match_obj = re.search(pattern,values["how_to_implement"])
|
|
134
|
-
words = values["how_to_implement"][:match_obj.span()[0]].split()[-10:]
|
|
135
|
-
newline_context = ' '.join(words)
|
|
136
|
-
raise ValueError(f"Field named 'how_to_implement' contains new line that is not escaped using backslash. Add backslash at the end of the line after the words: '{newline_context}' in '{values['name']}'")
|
|
137
|
-
return values
|
|
138
|
-
|
|
139
|
-
# @validator('references')
|
|
140
|
-
# def references_check(cls, v, values):
|
|
141
|
-
# return LinkValidator.SecurityContentObject_validate_references(v, values)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@validator("search")
|
|
145
|
-
def search_validate(cls, v, values):
|
|
146
|
-
# write search validator
|
|
147
|
-
return v
|
|
148
|
-
|
|
149
|
-
@validator("tests")
|
|
150
|
-
def tests_validate(cls, v, values):
|
|
151
|
-
if (values.get("status","") in [DetectionStatus.production.value, DetectionStatus.validation.value]) and not v:
|
|
152
|
-
raise ValueError(
|
|
153
|
-
"At least one test is required for a production or validation detection: " + values["name"]
|
|
154
|
-
)
|
|
155
|
-
return v
|
|
156
|
-
|
|
157
|
-
'''
|