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.
@@ -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
+ )
@@ -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
- return obj.replace(f"\n"," \\\n")
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
- if 'Splunk Behavioral Analytics' in product:
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
- if 'Splunk Behavioral Analytics' in product:
58
- file_name = 'ssa___' + file_name + '.test.yml'
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.4
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.13.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,<75.0.0)
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. 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.
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=Ifi13REBwQyUfCHma6IzjM_Z8uYEZ3Qz8kmP0WIFbJQ,1975
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=6gVVKmV5CUUYOkNNVTMPKj9bM1uXVthgGCoFKZGDeS8,12628
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=2MQ8yumCKj7zD8iUuA5gfFEMcE-GPRzYqkvuOexn0JA,5633
24
- contentctl/api.py,sha256=FBOpRhbBCBdjORmwe_8MPQ3PRZ6T0KrrFcfKovVFkug,6343
25
- contentctl/contentctl.py,sha256=JXbUD5l1PziRRJxUc1UHrveM33CHryZPmc0RxudDpIs,10328
26
- contentctl/enrichments/attack_enrichment.py,sha256=XEcLRnXKfJeChax5gfHDGea5D5MCFjP4bWp8hRWn3d8,7871
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=PZf60Z3ALQLJQ6I--cbWTCzvOMPGsjZSns1BFrZu4S4,9549
31
+ contentctl/helper/splunk_app.py,sha256=5KoacltgQ2J1BdxqvZYhr6GCXFl2tsy8TEWNc2gXkqw,14187
32
32
  contentctl/helper/utils.py,sha256=8ICRvE7DUiNL9BK4Hw71hCLFbd3R2u86OwKeDOdaBTY,19454
33
- contentctl/input/director.py,sha256=kTqdN_rCzRMn4dR32hPaVyx2llhAxyhJgoGjowhsHzs,10887
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=U3IvEQO3D5ab7YPUz8JnAnUCNtN--INOs2AP-ew5qn8,38867
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=BP27gP8KHeODp6UazhVFxwDQ64wuJCARGsLfIH34h7U,8768
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=_DRRMdtDKxjg2u-7iEbBrvKwtABxtlrmAEC8XYBQGk8,44487
46
- contentctl/objects/constants.py,sha256=lfCcr1DsTZvANHj4Ee1_sEV-SebHwAn41-5EvmoEX2E,3537
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/detection_tags.py,sha256=r7nIYMMspPk68aQx5q04jQaFGO4zTYG1P1UAUrX9qtU,11023
58
- contentctl/objects/enums.py,sha256=37v7w8xCg5j5hxP3kod0S3HQ9BY-CqZulPiwhnTtEvs,14052
59
- contentctl/objects/errors.py,sha256=gnD99z4O00EBbMerUjt4368q8mohm3Zb9HByG3CP_A0,525
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=2TaCAPEtU-bMa7A2m7xOxh93PMpzIdhwiHiPLUCeCB4,8281
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=ktZ9miHluqkw8jD-pn-62bjVp1sQqqQ7B53xy18DHU8,2321
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.4.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
170
- contentctl-4.3.4.dist-info/METADATA,sha256=YgRlkSBe1UQmgQfU3wIVwH0lufqLvfhjnnhY2qBNxiU,20925
171
- contentctl-4.3.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
172
- contentctl-4.3.4.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
173
- contentctl-4.3.4.dist-info/RECORD,,
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
- '''