contentctl 4.3.3__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.
Files changed (36) hide show
  1. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -6
  2. contentctl/actions/initialize.py +28 -12
  3. contentctl/actions/inspect.py +189 -91
  4. contentctl/actions/validate.py +3 -7
  5. contentctl/api.py +1 -1
  6. contentctl/contentctl.py +3 -0
  7. contentctl/enrichments/attack_enrichment.py +51 -82
  8. contentctl/enrichments/cve_enrichment.py +2 -2
  9. contentctl/helper/splunk_app.py +141 -10
  10. contentctl/input/director.py +5 -12
  11. contentctl/objects/abstract_security_content_objects/detection_abstract.py +11 -8
  12. contentctl/objects/annotated_types.py +6 -0
  13. contentctl/objects/atomic.py +51 -77
  14. contentctl/objects/config.py +145 -22
  15. contentctl/objects/constants.py +4 -1
  16. contentctl/objects/correlation_search.py +35 -28
  17. contentctl/objects/detection_metadata.py +71 -0
  18. contentctl/objects/detection_stanza.py +79 -0
  19. contentctl/objects/detection_tags.py +11 -9
  20. contentctl/objects/enums.py +0 -2
  21. contentctl/objects/errors.py +187 -0
  22. contentctl/objects/mitre_attack_enrichment.py +2 -1
  23. contentctl/objects/risk_event.py +94 -76
  24. contentctl/objects/savedsearches_conf.py +196 -0
  25. contentctl/objects/story_tags.py +3 -3
  26. contentctl/output/conf_writer.py +4 -1
  27. contentctl/output/new_content_yml_output.py +4 -9
  28. {contentctl-4.3.3.dist-info → contentctl-4.3.5.dist-info}/METADATA +4 -4
  29. {contentctl-4.3.3.dist-info → contentctl-4.3.5.dist-info}/RECORD +32 -32
  30. contentctl/objects/ssa_detection.py +0 -157
  31. contentctl/objects/ssa_detection_tags.py +0 -138
  32. contentctl/objects/unit_test_old.py +0 -10
  33. contentctl/objects/unit_test_ssa.py +0 -31
  34. {contentctl-4.3.3.dist-info → contentctl-4.3.5.dist-info}/LICENSE.md +0 -0
  35. {contentctl-4.3.3.dist-info → contentctl-4.3.5.dist-info}/WHEEL +0 -0
  36. {contentctl-4.3.3.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
+ )
@@ -6,7 +6,7 @@ from enum import Enum
6
6
 
7
7
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
8
8
  from contentctl.objects.enums import StoryCategory, DataModel, KillChainPhase, SecurityContentProductName
9
-
9
+ from contentctl.objects.annotated_types import CVE_TYPE,MITRE_ATTACK_ID_TYPE
10
10
 
11
11
  class StoryUseCase(str,Enum):
12
12
  FRAUD_DETECTION = "Fraud Detection"
@@ -27,10 +27,10 @@ class StoryTags(BaseModel):
27
27
 
28
28
  # enrichment
29
29
  mitre_attack_enrichments: Optional[List[MitreAttackEnrichment]] = None
30
- mitre_attack_tactics: Optional[Set[Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]]] = None
30
+ mitre_attack_tactics: Optional[Set[MITRE_ATTACK_ID_TYPE]] = None
31
31
  datamodels: Optional[Set[DataModel]] = None
32
32
  kill_chain_phases: Optional[Set[KillChainPhase]] = None
33
- cve: List[Annotated[str, r"^CVE-[1|2]\d{3}-\d+$"]] = []
33
+ cve: List[CVE_TYPE] = []
34
34
  group: List[str] = Field([], description="A list of groups who leverage the techniques list in this Analytic Story.")
35
35
 
36
36
  def getCategory_conf(self) -> str:
@@ -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.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.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,<74.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).
@@ -4,7 +4,7 @@ contentctl/actions/deploy_acs.py,sha256=mf3uk495H1EU_LNN-TiOsYCo18HMGoEBMb6ojeTr
4
4
  contentctl/actions/detection_testing/DetectionTestingManager.py,sha256=zg8JasDjCpSC-yhseEyUwO8qbDJIUJbhlus9Li9ZAnA,8818
5
5
  contentctl/actions/detection_testing/GitService.py,sha256=W1vnDDt8JvIL7Z1Lve3D3RS7h8qwMxrW0BMXVGuDZDM,9007
6
6
  contentctl/actions/detection_testing/generate_detection_coverage_badge.py,sha256=N5mznaeErVak3mOBwsd0RDBFJO3bku0EZvpayCyU-uk,2259
7
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=w_ULKp-RqE7HXMSzWZ-a4ha0_45GwAG7RLixJjv_gKQ,55718
7
+ contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py,sha256=00ymK5PyAn_FREi8Cj0HqpUt-U6XMpSHrN0QNqIrbDA,55190
8
8
  contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py,sha256=REM3WB-DQAczeknGAKMzJhnvHgnt-u9yDG2UKGVj2vM,6854
9
9
  contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py,sha256=Q1ZfCYOp54O39bgTScZMInkmZiU-bGAM9Hiwr2mq5ms,370
10
10
  contentctl/actions/detection_testing/progress_bar.py,sha256=OK9oRnPlzPAswt9KZNYID-YLHxqaYPY821kIE4-rCeA,3244
@@ -13,37 +13,38 @@ 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=HsfHfcrRmsHT6pILN457jmCGOCdAhOlRBGfAP8aZY78,7834
27
- contentctl/enrichments/cve_enrichment.py,sha256=SjiytaZktVNbfICXcZ2vZzBiQpOkug5taPtiJK-S1OE,2313
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
+ 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=OD0QFdBDtAlPt3vUpozeS6F0fs2iTKg6ovKJc6F-Se4,38721
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
- contentctl/objects/atomic.py,sha256=BP27gP8KHeODp6UazhVFxwDQ64wuJCARGsLfIH34h7U,8768
39
+ contentctl/objects/annotated_types.py,sha256=jnX02BQT4dHbd_DCIjik0PNN3kgsvb7sxAz_1Jy8TOY,259
40
+ contentctl/objects/atomic.py,sha256=L9QSmwmmSFFfvUykPk_nXwz9XDz-Gn6e0rrDxxRO8uY,7292
40
41
  contentctl/objects/base_test.py,sha256=qUtKQJrqCto_fwCBdiH68_tXqokhcv9ceu2fQlBxsjA,1045
41
42
  contentctl/objects/base_test_result.py,sha256=jVroyGLb9GD6Wm2QzvgIEA3SWCZqxPsHp9PzxSvpyIs,5101
42
43
  contentctl/objects/baseline.py,sha256=Lb1vJKtDdlDrzWgrdkC9oQao_TnRrOxSwOWHf4trtaU,2150
43
44
  contentctl/objects/baseline_tags.py,sha256=fVhLF-NmisavybB_idu3N0Con0Ymj8clKfRMkWzBB-k,1762
44
- contentctl/objects/config.py,sha256=8oP68b_wnPLXBMdvemmWFazaBssASW4jFZjFbTNrboY,44507
45
- contentctl/objects/constants.py,sha256=lfCcr1DsTZvANHj4Ee1_sEV-SebHwAn41-5EvmoEX2E,3537
46
- contentctl/objects/correlation_search.py,sha256=QZp1u-dwTZl9hkUOlJdHQ9h4Hp2bDHWWCKtrp3mvIUY,48310
45
+ contentctl/objects/config.py,sha256=q6-zGzKXi_etiAOJDgKKrU31WfmJkA9_Yjnx2QccScA,49808
46
+ contentctl/objects/constants.py,sha256=389Gna6BtukAkXfOKiHEg-FtPRVEVReV4pEMeLuq7o8,3653
47
+ contentctl/objects/correlation_search.py,sha256=ZZVoO3M594qCy_aAMhQiOPWn8FiSFbRShUCCLx6zhNc,48434
47
48
  contentctl/objects/data_source.py,sha256=aRr6lHu-EtGmi6J2nXKD7i2ozUPtp7X-vDkQiutvD3I,1545
48
49
  contentctl/objects/deployment.py,sha256=Qc6M4yeOvxjqFKR8sfjd4CG06AbVheTOqP1mwqo4t8s,2651
49
50
  contentctl/objects/deployment_email.py,sha256=Zu9cXZdfOP6noa_mZpiK1GrYCTgi3Mim94iLGjE674c,147
@@ -53,9 +54,11 @@ contentctl/objects/deployment_rba.py,sha256=YFLSKzLU7s8Bt1cJkSBWlfCsc_2MfgiwyaDi
53
54
  contentctl/objects/deployment_scheduling.py,sha256=bQjbJHNaUGdU1VAGV8-nFOHzHutbIlt7FZpUvR1CV4Y,198
54
55
  contentctl/objects/deployment_slack.py,sha256=P6z8OLHDKcDWx7nbKWasqBc3dFRatGcpO2GtmxzVV8I,135
55
56
  contentctl/objects/detection.py,sha256=3W41cXf3ECjWuPqWrseqSLC3PAA7O5_nENWWM6MPK0Y,620
56
- contentctl/objects/detection_tags.py,sha256=esJF7Uvblynm0uJj03EV9Hkr6Jko1LobSnJK2keJXUE,10913
57
- contentctl/objects/enums.py,sha256=37v7w8xCg5j5hxP3kod0S3HQ9BY-CqZulPiwhnTtEvs,14052
58
- 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
59
62
  contentctl/objects/event_source.py,sha256=G9P7rtcN5hcBNQx6DG37mR3QyQufx--T6kgQGNqQuKk,415
60
63
  contentctl/objects/integration_test.py,sha256=UBBx85f517MpQXOM7-iEasACEQ0-Ia7W4rDChOHZfno,1319
61
64
  contentctl/objects/integration_test_result.py,sha256=9oVWka57alIVPiCDbNgy-OmJcBicyYbrr6anL52Wgks,278
@@ -65,39 +68,36 @@ contentctl/objects/lookup.py,sha256=oZwBiHfRRrv2ZXdGyWIJWSWZMpuUbsXydaDDfpenk-4,
65
68
  contentctl/objects/macro.py,sha256=9nE-bxkFhtaltHOUCr0luU8jCCthmglHjhKs6Q2YzLU,2684
66
69
  contentctl/objects/manual_test.py,sha256=YNquEQ0UCzZGJ0uvHBgJ3Efho-F80ZG885ABLtqB7TI,1022
67
70
  contentctl/objects/manual_test_result.py,sha256=C4AYW3jlMsxVzCPzCA5dpAcbKgCpmDO43JmptFm--Q4,155
68
- contentctl/objects/mitre_attack_enrichment.py,sha256=4c5zapPm1Dpmcg_bgUAjZRY5zS3yPFKYzz5zllb-u1o,3350
71
+ contentctl/objects/mitre_attack_enrichment.py,sha256=4_9hvrxCXnGfyWqoj7C-0pCfGXEBJXfhrcSfb1cmPjs,3387
69
72
  contentctl/objects/notable_action.py,sha256=ValkblBaG-60TF19y_vSnNzoNZ3eg48wIfr0qZxyKTA,1605
70
73
  contentctl/objects/notable_event.py,sha256=ITcwLzeatSGpe8267PYN-EhgqOSoWTfciCBVu8zjOXE,682
71
74
  contentctl/objects/observable.py,sha256=pw0Ehi_KMb7nXzw2kuw1FnCknpD8zDkCAqBTa-M_F28,1313
72
75
  contentctl/objects/playbook.py,sha256=hSYYpdMhctgpp7uwaPciFqu1yuFI4M1NHy1WBBLyvzM,2469
73
76
  contentctl/objects/playbook_tags.py,sha256=NrhTGcgoYSGEZggrfebko0GBOXN9x05IadRUUL_CVfQ,1436
74
77
  contentctl/objects/risk_analysis_action.py,sha256=Glzcq99DAqqOJ2eZYCkUI3R5hA5cZGU0ZuCSinFf2R8,4278
75
- contentctl/objects/risk_event.py,sha256=LnFg0BKnt7rMJvzxZoaFeInKP4w5onvJwOUxMWWDk6w,14303
78
+ contentctl/objects/risk_event.py,sha256=b5Smh3w5Hecmi7E-Ub5DvO8iOPwnVg2ux47u7oemxX4,14041
76
79
  contentctl/objects/risk_object.py,sha256=yY4NmEwEKaRl4sLzCRZb1n8kdpV3HzYbQVQ1ClQWYHw,904
80
+ contentctl/objects/savedsearches_conf.py,sha256=tCyZHqAQ9azgwIyySViY2BdM4To5Cb_GeYEEHPwR4Zc,8604
77
81
  contentctl/objects/security_content_object.py,sha256=j8KNDwSMfZsSIzJucC3NuZo0SlFVpqHfDc6y3-YHjHI,234
78
- contentctl/objects/ssa_detection.py,sha256=ud0T6lq-5XUlmeK8Jzw_aNLe6podVcA1o7THDYvWbik,5934
79
- contentctl/objects/ssa_detection_tags.py,sha256=9aRwbpQHi79NIS9rofjgxDJpw7cWXqG534_kSbvHJh8,5220
80
82
  contentctl/objects/story.py,sha256=FXe11LV19xJTtCgx7DKdvV9cL0gKeryUnE3yjpnDmrU,4957
81
- contentctl/objects/story_tags.py,sha256=qIVCEk3Vr-63tjq3PKapMUUUL6jNCHyp2AqGzDAE-tk,2279
83
+ contentctl/objects/story_tags.py,sha256=cOL8PUzdlFdLPQHc54_-9sdI8nCE1D04oKY7KriOssI,2293
82
84
  contentctl/objects/test_attack_data.py,sha256=9OgErjdPR4S-SJpQePt0uwBLPYHYPtqKDd-auhjz7Uc,430
83
85
  contentctl/objects/test_group.py,sha256=DCtm4ChGYksOwZQVHsioaweOvI37CSlTZJzKvBX-jbY,2586
84
86
  contentctl/objects/threat_object.py,sha256=S8B7RQFfLxN_g7yKPrDTuYhIy9JvQH3YwJ_T5LUZIa4,711
85
87
  contentctl/objects/unit_test.py,sha256=eMFehpHhmZA5WYBqhWUNRF_LpxuLM9VooAxjXeNbrxY,1144
86
88
  contentctl/objects/unit_test_baseline.py,sha256=XHvOm7qLYfqrP6uC5U_pfgw_pf8-S2RojuNmbo6lXlM,227
87
- contentctl/objects/unit_test_old.py,sha256=IfvytHG4ZnUhsvXgdczECZbiwv6YLViYdsk9AqeDBjQ,199
88
89
  contentctl/objects/unit_test_result.py,sha256=POQfvvPpSw-jQzINBz1_IszUMJ4Wbopu8HRS1Qe6P2M,2940
89
- contentctl/objects/unit_test_ssa.py,sha256=RURqXb3e0CuI5nNX8PvFucxatAvMmGSUDngVbqNpoiY,653
90
90
  contentctl/output/api_json_output.py,sha256=n3OTd5z-Vkmsn7ny6QCAar_jSMNuuJfzAQa7xq_9if4,9085
91
91
  contentctl/output/attack_nav_output.py,sha256=95iKV8U9BMMgqh6cCOw1S89Ln73xmJGgJPHTYR0L7hA,2304
92
92
  contentctl/output/attack_nav_writer.py,sha256=64ILZLmNbh2XLmbopgENkeo6t-4SRRG8xZXBmtpNd4g,2219
93
93
  contentctl/output/conf_output.py,sha256=7HcHM9pJLNnan1Kq_7ozvs5iOgfzqdKbO6gwxUZJVnc,9994
94
- contentctl/output/conf_writer.py,sha256=2TaCAPEtU-bMa7A2m7xOxh93PMpzIdhwiHiPLUCeCB4,8281
94
+ contentctl/output/conf_writer.py,sha256=uMxWrdu-4paiTgUGu_FUWMjT-r_IpdZSTUSDZUGC6k8,8541
95
95
  contentctl/output/data_source_writer.py,sha256=ubFjm6XJ4T2d3oqfKwDFasITHeDj3HFmegqVN--5_ME,1635
96
96
  contentctl/output/detection_writer.py,sha256=AzxbssNLmsNIOaYKotew5-ONoyq1cQpKSGy3pe191B0,960
97
97
  contentctl/output/doc_md_output.py,sha256=gf7osH1uSrC6js3D_I72g4uDe9TaB3tsvtqCHi5znp0,3238
98
98
  contentctl/output/jinja_writer.py,sha256=bdiqr9FaXYxth4wZ1A52zTMAS5stHNGpezTkaS5pres,1119
99
99
  contentctl/output/json_writer.py,sha256=Z-iVLnZb8tzYATxbQtXax0dz572lVPFMNVTx-vWbnog,1007
100
- contentctl/output/new_content_yml_output.py,sha256=ktZ9miHluqkw8jD-pn-62bjVp1sQqqQ7B53xy18DHU8,2321
100
+ contentctl/output/new_content_yml_output.py,sha256=KvP0FffQBPznSKqJyRQMtehf4XYEVK5jiPlUwnkekUc,2061
101
101
  contentctl/output/svg_output.py,sha256=T2p4S085MKj5VPZKvo4tWBVOmYme32J9L7kMEBm3SwQ,2751
102
102
  contentctl/output/templates/analyticstories_detections.j2,sha256=MYefoyWAq4b7dth3OlbMWNhFnH3_nnMKaOfw0lMkxT4,917
103
103
  contentctl/output/templates/analyticstories_investigations.j2,sha256=7bwt_6U3dr9hbxOUkp0a1KnRJohNgC7GE1zRg_N_awI,515
@@ -165,8 +165,8 @@ contentctl/templates/detections/web/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
165
165
  contentctl/templates/macros/security_content_ctime.yml,sha256=Gg1YNllHVsX_YB716H1SJLWzxXZEfuJlnsgB2fuyoHU,159
166
166
  contentctl/templates/macros/security_content_summariesonly.yml,sha256=9BYUxAl2E4Nwh8K19F3AJS8Ka7ceO6ZDBjFiO3l3LY0,162
167
167
  contentctl/templates/stories/cobalt_strike.yml,sha256=rlaXxMN-5k8LnKBLPafBoksyMtlmsPMHPJOjTiMiZ-M,3063
168
- contentctl-4.3.3.dist-info/LICENSE.md,sha256=hQWUayRk-pAiOZbZnuy8djmoZkjKBx8MrCFpW-JiOgo,11344
169
- contentctl-4.3.3.dist-info/METADATA,sha256=utrc_z9xKTEuH2CQrudSeb6J4BQZloMvVxkb6y17pvA,20925
170
- contentctl-4.3.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
171
- contentctl-4.3.3.dist-info/entry_points.txt,sha256=5bjZ2NkbQfSwK47uOnA77yCtjgXhvgxnmCQiynRF_-U,57
172
- contentctl-4.3.3.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
- '''