contentctl 3.6.0__py3-none-any.whl → 4.0.2__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/build.py +89 -0
- contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
- contentctl/actions/detection_testing/GitService.py +148 -230
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
- contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
- contentctl/actions/doc_gen.py +1 -1
- contentctl/actions/initialize.py +28 -65
- contentctl/actions/inspect.py +260 -0
- contentctl/actions/new_content.py +106 -13
- contentctl/actions/release_notes.py +168 -144
- contentctl/actions/reporting.py +24 -13
- contentctl/actions/test.py +39 -20
- contentctl/actions/validate.py +25 -48
- contentctl/contentctl.py +196 -754
- contentctl/enrichments/attack_enrichment.py +69 -19
- contentctl/enrichments/cve_enrichment.py +28 -13
- contentctl/helper/link_validator.py +24 -26
- contentctl/helper/utils.py +7 -3
- contentctl/input/director.py +139 -201
- contentctl/input/new_content_questions.py +63 -61
- contentctl/input/sigma_converter.py +1 -2
- contentctl/input/ssa_detection_builder.py +16 -7
- contentctl/input/yml_reader.py +4 -3
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
- contentctl/objects/alert_action.py +40 -0
- contentctl/objects/atomic.py +212 -0
- contentctl/objects/baseline.py +44 -43
- contentctl/objects/baseline_tags.py +69 -20
- contentctl/objects/config.py +857 -125
- contentctl/objects/constants.py +0 -1
- contentctl/objects/correlation_search.py +1 -1
- contentctl/objects/data_source.py +2 -4
- contentctl/objects/deployment.py +61 -21
- contentctl/objects/deployment_email.py +2 -2
- contentctl/objects/deployment_notable.py +4 -4
- contentctl/objects/deployment_phantom.py +2 -2
- contentctl/objects/deployment_rba.py +3 -4
- contentctl/objects/deployment_scheduling.py +2 -3
- contentctl/objects/deployment_slack.py +2 -2
- contentctl/objects/detection.py +1 -5
- contentctl/objects/detection_tags.py +210 -119
- contentctl/objects/enums.py +312 -24
- contentctl/objects/integration_test.py +1 -1
- contentctl/objects/integration_test_result.py +0 -2
- contentctl/objects/investigation.py +62 -53
- contentctl/objects/investigation_tags.py +30 -6
- contentctl/objects/lookup.py +80 -31
- contentctl/objects/macro.py +29 -45
- contentctl/objects/mitre_attack_enrichment.py +29 -5
- contentctl/objects/observable.py +3 -7
- contentctl/objects/playbook.py +60 -30
- contentctl/objects/playbook_tags.py +45 -8
- contentctl/objects/security_content_object.py +1 -5
- contentctl/objects/ssa_detection.py +8 -4
- contentctl/objects/ssa_detection_tags.py +19 -26
- contentctl/objects/story.py +142 -44
- contentctl/objects/story_tags.py +46 -33
- contentctl/objects/unit_test.py +7 -2
- contentctl/objects/unit_test_attack_data.py +10 -19
- contentctl/objects/unit_test_baseline.py +1 -1
- contentctl/objects/unit_test_old.py +4 -3
- contentctl/objects/unit_test_result.py +5 -3
- contentctl/objects/unit_test_ssa.py +31 -0
- contentctl/output/api_json_output.py +202 -130
- contentctl/output/attack_nav_output.py +20 -9
- contentctl/output/attack_nav_writer.py +3 -3
- contentctl/output/ba_yml_output.py +3 -3
- contentctl/output/conf_output.py +125 -391
- contentctl/output/conf_writer.py +169 -31
- contentctl/output/jinja_writer.py +2 -2
- contentctl/output/json_writer.py +17 -5
- contentctl/output/new_content_yml_output.py +8 -7
- contentctl/output/svg_output.py +17 -27
- contentctl/output/templates/analyticstories_detections.j2 +8 -4
- contentctl/output/templates/analyticstories_investigations.j2 +1 -1
- contentctl/output/templates/analyticstories_stories.j2 +6 -6
- contentctl/output/templates/app.conf.j2 +2 -2
- contentctl/output/templates/app.manifest.j2 +2 -2
- contentctl/output/templates/detection_coverage.j2 +6 -8
- contentctl/output/templates/doc_detection_page.j2 +2 -2
- contentctl/output/templates/doc_detections.j2 +2 -2
- contentctl/output/templates/doc_stories.j2 +1 -1
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/header.j2 +2 -1
- contentctl/output/templates/macros.j2 +6 -10
- contentctl/output/templates/savedsearches_baselines.j2 +5 -5
- contentctl/output/templates/savedsearches_detections.j2 +36 -33
- contentctl/output/templates/savedsearches_investigations.j2 +4 -4
- contentctl/output/templates/transforms.j2 +4 -4
- contentctl/output/yml_writer.py +2 -2
- contentctl/templates/app_template/README.md +7 -0
- contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
- contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
- contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
- contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
- contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
- contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
- contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
- contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
- contentctl/templates/stories/cobalt_strike.yml +0 -1
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
- contentctl-4.0.2.dist-info/RECORD +168 -0
- contentctl/actions/detection_testing/DataManipulation.py +0 -149
- contentctl/actions/generate.py +0 -91
- contentctl/helper/config_handler.py +0 -75
- contentctl/input/baseline_builder.py +0 -66
- contentctl/input/basic_builder.py +0 -58
- contentctl/input/detection_builder.py +0 -370
- contentctl/input/investigation_builder.py +0 -42
- contentctl/input/new_content_generator.py +0 -95
- contentctl/input/playbook_builder.py +0 -68
- contentctl/input/story_builder.py +0 -106
- contentctl/objects/app.py +0 -214
- contentctl/objects/repo_config.py +0 -163
- contentctl/objects/test_config.py +0 -630
- contentctl/output/templates/macros_detections.j2 +0 -7
- contentctl/output/templates/splunk_app/README.md +0 -7
- contentctl-3.6.0.dist-info/RECORD +0 -176
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
contentctl/output/conf_writer.py
CHANGED
|
@@ -1,67 +1,205 @@
|
|
|
1
|
+
from typing import Any
|
|
1
2
|
import datetime
|
|
3
|
+
import re
|
|
2
4
|
import os
|
|
5
|
+
import json
|
|
6
|
+
import configparser
|
|
3
7
|
from xmlrpc.client import APPLICATION_ERROR
|
|
4
8
|
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
5
9
|
import pathlib
|
|
6
10
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
7
|
-
from contentctl.objects.config import
|
|
11
|
+
from contentctl.objects.config import build
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
8
13
|
|
|
9
14
|
class ConfWriter():
|
|
10
15
|
|
|
11
16
|
@staticmethod
|
|
12
|
-
def
|
|
13
|
-
|
|
17
|
+
def custom_jinja2_enrichment_filter(string:str, object:SecurityContentObject):
|
|
18
|
+
substitutions = re.findall(r"%[^%]*%", string)
|
|
19
|
+
updated_string = string
|
|
20
|
+
for sub in substitutions:
|
|
21
|
+
sub_without_percents = sub.replace("%","")
|
|
22
|
+
if hasattr(object, sub_without_percents):
|
|
23
|
+
updated_string = updated_string.replace(sub, str(getattr(object, sub_without_percents)))
|
|
24
|
+
elif hasattr(object,'tags') and hasattr(object.tags, sub_without_percents):
|
|
25
|
+
updated_string = updated_string.replace(sub, str(getattr(object.tags, sub_without_percents)))
|
|
26
|
+
else:
|
|
27
|
+
raise Exception(f"Unable to find field {sub} in object {object.name}")
|
|
28
|
+
|
|
29
|
+
return updated_string
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def escapeNewlines(obj:Any):
|
|
33
|
+
# Ensure that any newlines that occur in a string are escaped with a \.
|
|
34
|
+
# Failing to do so will result in an improperly formatted conf files that
|
|
35
|
+
# cannot be parsed
|
|
36
|
+
if isinstance(obj,str):
|
|
37
|
+
return obj.replace(f"\n"," \\\n")
|
|
38
|
+
else:
|
|
39
|
+
return obj
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def writeConfFileHeader(app_output_path:pathlib.Path, config: build) -> pathlib.Path:
|
|
44
|
+
output = ConfWriter.writeFileHeader(app_output_path, config)
|
|
45
|
+
|
|
46
|
+
output_path = config.getPackageDirectoryPath()/app_output_path
|
|
47
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
with open(output_path, 'w') as f:
|
|
49
|
+
output = output.encode('utf-8', 'ignore').decode('utf-8')
|
|
50
|
+
f.write(output)
|
|
51
|
+
|
|
52
|
+
#Ensure that the conf file we just generated/update is syntactically valid
|
|
53
|
+
ConfWriter.validateConfFile(output_path)
|
|
54
|
+
return output_path
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def writeManifestFile(app_output_path:pathlib.Path, template_name : str, config: build, objects : list) -> pathlib.Path:
|
|
58
|
+
j2_env = ConfWriter.getJ2Environment()
|
|
59
|
+
template = j2_env.get_template(template_name)
|
|
60
|
+
|
|
61
|
+
output = template.render(objects=objects, APP_NAME=config.app.label, currentDate=datetime.datetime.now(datetime.UTC).date().isoformat())
|
|
62
|
+
|
|
63
|
+
output_path = config.getPackageDirectoryPath()/app_output_path
|
|
64
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
with open(output_path, 'w') as f:
|
|
66
|
+
output = output.encode('utf-8', 'ignore').decode('utf-8')
|
|
67
|
+
f.write(output)
|
|
68
|
+
return output_path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def writeFileHeader(app_output_path:pathlib.Path, config: build) -> str:
|
|
73
|
+
#Do not output microseconds or +00:000 at the end of the datetime string
|
|
74
|
+
utc_time = datetime.datetime.now(datetime.UTC).replace(microsecond=0,tzinfo=None).isoformat()
|
|
75
|
+
|
|
14
76
|
j2_env = Environment(
|
|
15
77
|
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
|
|
16
78
|
trim_blocks=True)
|
|
17
79
|
|
|
18
80
|
template = j2_env.get_template('header.j2')
|
|
19
|
-
output = template.render(time=utc_time, author=' - '.join([config.
|
|
81
|
+
output = template.render(time=utc_time, author=' - '.join([config.app.author_name,config.app.author_company]), author_email=config.app.author_email)
|
|
82
|
+
|
|
83
|
+
return output
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def writeXmlFile(app_output_path:pathlib.Path, template_name : str, config: build, objects : list) -> None:
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
j2_env = ConfWriter.getJ2Environment()
|
|
92
|
+
template = j2_env.get_template(template_name)
|
|
93
|
+
|
|
94
|
+
output = template.render(objects=objects, APP_NAME=config.app.label)
|
|
95
|
+
|
|
96
|
+
output_path = config.getPackageDirectoryPath()/app_output_path
|
|
20
97
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
-
with open(output_path, '
|
|
22
|
-
output = output.encode('
|
|
98
|
+
with open(output_path, 'a') as f:
|
|
99
|
+
output = output.encode('utf-8', 'ignore').decode('utf-8')
|
|
23
100
|
f.write(output)
|
|
101
|
+
|
|
102
|
+
#Ensure that the conf file we just generated/update is syntactically valid
|
|
103
|
+
ConfWriter.validateXmlFile(output_path)
|
|
104
|
+
|
|
105
|
+
|
|
24
106
|
|
|
25
107
|
|
|
26
108
|
@staticmethod
|
|
27
|
-
def
|
|
109
|
+
def writeXmlFileHeader(app_output_path:pathlib.Path, config: build) -> None:
|
|
110
|
+
output = ConfWriter.writeFileHeader(app_output_path, config)
|
|
111
|
+
output_with_xml_comment = f"<!--\n{output}-->\n"
|
|
112
|
+
|
|
113
|
+
output_path = config.getPackageDirectoryPath()/app_output_path
|
|
28
114
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
115
|
with open(output_path, 'w') as f:
|
|
30
|
-
|
|
116
|
+
output_with_xml_comment = output_with_xml_comment.encode('utf-8', 'ignore').decode('utf-8')
|
|
117
|
+
f.write(output_with_xml_comment)
|
|
118
|
+
|
|
119
|
+
# We INTENTIONALLY do not validate the comment we wrote to the header. This is because right now,
|
|
120
|
+
# the file is an empty XML document (besides the commented header). This means that it will FAIL validation
|
|
31
121
|
|
|
32
122
|
|
|
33
123
|
@staticmethod
|
|
34
|
-
def
|
|
35
|
-
def custom_jinja2_enrichment_filter(string, object):
|
|
36
|
-
customized_string = string
|
|
37
|
-
|
|
38
|
-
for key in dir(object):
|
|
39
|
-
if type(key) is not str:
|
|
40
|
-
key = key.decode()
|
|
41
|
-
if not key.startswith('__') and not key == "_abc_impl" and not callable(getattr(object, key)):
|
|
42
|
-
if hasattr(object, key):
|
|
43
|
-
customized_string = customized_string.replace("%" + key + "%", str(getattr(object, key)))
|
|
44
|
-
|
|
45
|
-
for key in dir(object.tags):
|
|
46
|
-
if type(key) is not str:
|
|
47
|
-
key = key.decode()
|
|
48
|
-
if not key.startswith('__') and not key == "_abc_impl" and not callable(getattr(object.tags, key)):
|
|
49
|
-
if hasattr(object.tags, key):
|
|
50
|
-
customized_string = customized_string.replace("%" + key + "%", str(getattr(object.tags, key)))
|
|
51
|
-
|
|
52
|
-
return customized_string
|
|
53
|
-
|
|
124
|
+
def getJ2Environment()->Environment:
|
|
54
125
|
j2_env = Environment(
|
|
55
126
|
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
|
|
56
127
|
trim_blocks=True,
|
|
57
128
|
undefined=StrictUndefined)
|
|
129
|
+
j2_env.globals.update(objectListToNameList=SecurityContentObject.objectListToNameList)
|
|
130
|
+
|
|
58
131
|
|
|
132
|
+
j2_env.filters['custom_jinja2_enrichment_filter'] = ConfWriter.custom_jinja2_enrichment_filter
|
|
133
|
+
j2_env.filters['escapeNewlines'] = ConfWriter.escapeNewlines
|
|
134
|
+
return j2_env
|
|
59
135
|
|
|
60
|
-
|
|
136
|
+
@staticmethod
|
|
137
|
+
def writeConfFile(app_output_path:pathlib.Path, template_name : str, config: build, objects : list) -> pathlib.Path:
|
|
138
|
+
output_path = config.getPackageDirectoryPath()/app_output_path
|
|
139
|
+
j2_env = ConfWriter.getJ2Environment()
|
|
140
|
+
|
|
61
141
|
template = j2_env.get_template(template_name)
|
|
62
|
-
output = template.render(objects=objects, APP_NAME=config.
|
|
142
|
+
output = template.render(objects=objects, APP_NAME=config.app.label)
|
|
143
|
+
|
|
63
144
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
64
145
|
with open(output_path, 'a') as f:
|
|
65
|
-
output = output.encode('
|
|
146
|
+
output = output.encode('utf-8', 'ignore').decode('utf-8')
|
|
66
147
|
f.write(output)
|
|
148
|
+
return output_path
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def validateConfFile(path:pathlib.Path):
|
|
153
|
+
"""Ensure that the conf file is valid. We will do this by reading back
|
|
154
|
+
the conf using RawConfigParser to ensure that it does not throw any parsing errors.
|
|
155
|
+
This is particularly relevant because newlines contained in string fields may
|
|
156
|
+
break the formatting of the conf file if they have been incorrectly escaped with
|
|
157
|
+
the 'ConfWriter.escapeNewlines()' function.
|
|
158
|
+
|
|
159
|
+
If a conf file failes validation, we will throw an exception
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
path (pathlib.Path): path to the conf file to validate
|
|
163
|
+
"""
|
|
164
|
+
return
|
|
165
|
+
if path.suffix != ".conf":
|
|
166
|
+
#there may be some other files built, so just ignore them
|
|
167
|
+
return
|
|
168
|
+
try:
|
|
169
|
+
_ = configparser.RawConfigParser().read(path)
|
|
170
|
+
except Exception as e:
|
|
171
|
+
raise Exception(f"Failed to validate .conf file {str(path)}: {str(e)}")
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def validateXmlFile(path:pathlib.Path):
|
|
175
|
+
"""Ensure that the XML file is valid XML.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
path (pathlib.Path): path to the xml file to validate
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
with open(path, 'r') as xmlFile:
|
|
183
|
+
_ = ET.fromstring(xmlFile.read())
|
|
184
|
+
except Exception as e:
|
|
185
|
+
raise Exception(f"Failed to validate .xml file {str(path)}: {str(e)}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def validateManifestFile(path:pathlib.Path):
|
|
190
|
+
"""Ensure that the Manifest file is valid JSON.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
path (pathlib.Path): path to the manifest JSON file to validate
|
|
194
|
+
"""
|
|
195
|
+
return
|
|
196
|
+
try:
|
|
197
|
+
with open(path, 'r') as manifestFile:
|
|
198
|
+
_ = json.load(manifestFile)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
raise Exception(f"Failed to validate .manifest file {str(path)} (Note that .manifest files should contain only valid JSON-formatted data): {str(e)}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
67
205
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
2
|
+
from typing import Any
|
|
3
3
|
from jinja2 import Environment, FileSystemLoader
|
|
4
4
|
|
|
5
5
|
|
|
@@ -20,7 +20,7 @@ class JinjaWriter:
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@staticmethod
|
|
23
|
-
def writeObject(template_name : str, output_path : str, object
|
|
23
|
+
def writeObject(template_name : str, output_path : str, object: dict[str,Any]) -> None:
|
|
24
24
|
|
|
25
25
|
j2_env = Environment(
|
|
26
26
|
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
|
contentctl/output/json_writer.py
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import json
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import SecurityContentObject_Abstract
|
|
3
|
+
from typing import List
|
|
4
|
+
from io import TextIOWrapper
|
|
4
5
|
class JsonWriter():
|
|
5
6
|
|
|
6
7
|
@staticmethod
|
|
7
|
-
def writeJsonObject(file_path : str,
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
def writeJsonObject(file_path : str, object_name: str, objs: List[dict],readable_output=False) -> None:
|
|
9
|
+
try:
|
|
10
|
+
with open(file_path, 'w') as outfile:
|
|
11
|
+
if readable_output:
|
|
12
|
+
# At the cost of slightly larger filesize, improve the redability significantly
|
|
13
|
+
# by sorting and indenting keys/values
|
|
14
|
+
sorted_objs = sorted(objs, key=lambda o: o['name'])
|
|
15
|
+
json.dump({object_name:sorted_objs}, outfile, ensure_ascii=False, indent=2)
|
|
16
|
+
else:
|
|
17
|
+
json.dump({object_name:objs}, outfile, ensure_ascii=False)
|
|
18
|
+
|
|
19
|
+
except Exception as e:
|
|
20
|
+
raise Exception(f"Error serializing object to Json File '{file_path}': {str(e)}")
|
|
21
|
+
|
|
@@ -2,17 +2,18 @@ import os
|
|
|
2
2
|
import pathlib
|
|
3
3
|
from contentctl.objects.enums import SecurityContentType
|
|
4
4
|
from contentctl.output.yml_writer import YmlWriter
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import pathlib
|
|
6
|
+
from contentctl.objects.config import NewContentType
|
|
7
7
|
class NewContentYmlOutput():
|
|
8
|
-
output_path:
|
|
8
|
+
output_path: pathlib.Path
|
|
9
9
|
|
|
10
|
-
def __init__(self, output_path:
|
|
10
|
+
def __init__(self, output_path:pathlib.Path):
|
|
11
11
|
self.output_path = output_path
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def writeObjectNewContent(self, object: dict, subdirectory_name: str, type:
|
|
15
|
-
if type ==
|
|
14
|
+
def writeObjectNewContent(self, object: dict, subdirectory_name: str, type: NewContentType) -> None:
|
|
15
|
+
if type == NewContentType.detection:
|
|
16
|
+
|
|
16
17
|
file_path = os.path.join(self.output_path, 'detections', subdirectory_name, self.convertNameToFileName(object['name'], object['tags']['product']))
|
|
17
18
|
output_folder = pathlib.Path(self.output_path)/'detections'/subdirectory_name
|
|
18
19
|
#make sure the output folder exists for this detection
|
|
@@ -21,7 +22,7 @@ class NewContentYmlOutput():
|
|
|
21
22
|
YmlWriter.writeYmlFile(file_path, object)
|
|
22
23
|
print("Successfully created detection " + file_path)
|
|
23
24
|
|
|
24
|
-
elif type ==
|
|
25
|
+
elif type == NewContentType.story:
|
|
25
26
|
file_path = os.path.join(self.output_path, 'stories', self.convertNameToFileName(object['name'], object['tags']['product']))
|
|
26
27
|
YmlWriter.writeYmlFile(file_path, object)
|
|
27
28
|
print("Successfully created story " + file_path)
|
contentctl/output/svg_output.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pathlib
|
|
3
|
+
from typing import List, Any
|
|
3
4
|
|
|
4
5
|
from contentctl.objects.enums import SecurityContentType
|
|
5
6
|
from contentctl.output.jinja_writer import JinjaWriter
|
|
6
|
-
from contentctl.objects.config import Config
|
|
7
7
|
from contentctl.objects.enums import DetectionStatus
|
|
8
|
+
from contentctl.objects.detection import Detection
|
|
8
9
|
class SvgOutput():
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
def get_badge_dict(self, name:str, total_detections:List[Detection], these_detections:List[Detection])->dict[str,Any]:
|
|
13
|
+
obj:dict[str,Any] = {}
|
|
12
14
|
obj['name'] = name
|
|
15
|
+
|
|
13
16
|
if name == "Production":
|
|
14
17
|
obj['color'] = "Green"
|
|
15
18
|
elif name == "Detections":
|
|
@@ -26,40 +29,27 @@ class SvgOutput():
|
|
|
26
29
|
obj['coverage'] = len(these_detections) / obj['count']
|
|
27
30
|
obj['coverage'] = "{:.0%}".format(obj['coverage'])
|
|
28
31
|
return obj
|
|
29
|
-
|
|
30
|
-
def writeObjects(self,
|
|
32
|
+
|
|
33
|
+
def writeObjects(self, detections: List[Detection], output_path: pathlib.Path, type: SecurityContentType = None) -> None:
|
|
31
34
|
|
|
32
|
-
detections_tmp = objects
|
|
33
|
-
|
|
34
|
-
output_path = pathlib.Path(path)
|
|
35
|
-
|
|
36
|
-
production_detections = []
|
|
37
|
-
deprecated_detections = []
|
|
38
|
-
experimental_detections = []
|
|
39
|
-
obj = dict()
|
|
40
35
|
|
|
41
|
-
for detection in detections_tmp:
|
|
42
|
-
if detection.status == DetectionStatus.production.value:
|
|
43
|
-
production_detections.append(detection)
|
|
44
|
-
if detection.status == DetectionStatus.deprecated.value:
|
|
45
|
-
deprecated_detections.append(detection)
|
|
46
|
-
elif detection.status == DetectionStatus.experimental.value:
|
|
47
|
-
experimental_detections.append(detection)
|
|
48
36
|
|
|
37
|
+
total_dict:dict[str,Any] = self.get_badge_dict("Detections", detections, detections)
|
|
38
|
+
production_dict:dict[str,Any] = self.get_badge_dict("% Production", detections, [detection for detection in detections if detection.status == DetectionStatus.production.value])
|
|
39
|
+
#deprecated_dict = self.get_badge_dict("Deprecated", detections, [detection for detection in detections if detection.status == DetectionStatus.deprecated])
|
|
40
|
+
#experimental_dict = self.get_badge_dict("Experimental", detections, [detection for detection in detections if detection.status == DetectionStatus.experimental])
|
|
49
41
|
|
|
50
|
-
total_detections = production_detections + deprecated_detections + experimental_detections
|
|
51
|
-
total_dict = self.get_badge_dict("Detections", total_detections, production_detections)
|
|
52
|
-
production_dict = self.get_badge_dict("Production", total_detections, production_detections)
|
|
53
|
-
deprecated_dict = self.get_badge_dict("Deprecated", total_detections, deprecated_detections)
|
|
54
|
-
experimental_dict = self.get_badge_dict("Experimental", total_detections, experimental_detections)
|
|
55
42
|
|
|
56
43
|
|
|
57
|
-
|
|
44
|
+
|
|
45
|
+
#Total number of detections
|
|
46
|
+
JinjaWriter.writeObject('detection_count.j2', output_path /'detection_count.svg', total_dict)
|
|
58
47
|
#JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'production_count.svg'), production_dict)
|
|
59
48
|
#JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'deprecated_count.svg'), deprecated_dict)
|
|
60
49
|
#JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'experimental_count.svg'), experimental_dict)
|
|
61
50
|
|
|
62
|
-
|
|
51
|
+
#Percentage of detections that are production
|
|
52
|
+
JinjaWriter.writeObject('detection_coverage.j2', output_path/'detection_coverage.svg', production_dict)
|
|
63
53
|
#JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), deprecated_dict)
|
|
64
54
|
#JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), experimental_dict)
|
|
65
55
|
|
|
@@ -5,17 +5,21 @@
|
|
|
5
5
|
{% if (detection.type == 'TTP' or detection.type == 'Anomaly' or detection.type == 'Hunting' or detection.type == 'Correlation') %}
|
|
6
6
|
[savedsearch://{{APP_NAME}} - {{ detection.name }} - Rule]
|
|
7
7
|
type = detection
|
|
8
|
-
asset_type = {{ detection.tags.asset_type }}
|
|
8
|
+
asset_type = {{ detection.tags.asset_type.value }}
|
|
9
9
|
confidence = medium
|
|
10
|
-
explanation = {{ detection.description }}
|
|
10
|
+
explanation = {{ detection.description | escapeNewlines() }}
|
|
11
11
|
{% if detection.how_to_implement is defined %}
|
|
12
|
-
how_to_implement = {{ detection.how_to_implement }}
|
|
12
|
+
how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
|
|
13
13
|
{% else %}
|
|
14
14
|
how_to_implement = none
|
|
15
15
|
{% endif %}
|
|
16
16
|
annotations = {{ detection.mappings | tojson }}
|
|
17
|
-
known_false_positives = {{ detection.known_false_positives }}
|
|
17
|
+
known_false_positives = {{ detection.known_false_positives | escapeNewlines() }}
|
|
18
|
+
{% if detection.providing_technologies | length > 0 %}
|
|
18
19
|
providing_technologies = {{ detection.providing_technologies | tojson }}
|
|
20
|
+
{% else %}
|
|
21
|
+
providing_technologies = null
|
|
22
|
+
{% endif %}
|
|
19
23
|
|
|
20
24
|
{% endif %}
|
|
21
25
|
{% endfor %}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
type = investigation
|
|
8
8
|
explanation = none
|
|
9
9
|
{% if detection.how_to_implement is defined %}
|
|
10
|
-
how_to_implement = {{ detection.how_to_implement }}
|
|
10
|
+
how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
|
|
11
11
|
{% else %}
|
|
12
12
|
how_to_implement = none
|
|
13
13
|
{% endif %}
|
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
{% for story in objects %}
|
|
6
6
|
[analytic_story://{{ story.name }}]
|
|
7
|
-
category = {{ story.tags.
|
|
7
|
+
category = {{ story.tags.getCategory_conf() }}
|
|
8
8
|
last_updated = {{ story.date }}
|
|
9
9
|
version = {{ story.version }}
|
|
10
|
-
references = {{ story.
|
|
11
|
-
maintainers = [{"company": "{{ story.author_company }}", "email": "
|
|
10
|
+
references = {{ story.getReferencesListForJson() | tojson }}
|
|
11
|
+
maintainers = [{"company": "{{ story.author_company }}", "email": "{{ story.author_email }}", "name": "{{ story.author_name }}"}]
|
|
12
12
|
spec_version = 3
|
|
13
|
-
searches = {{
|
|
14
|
-
description = {{ story.description }}
|
|
13
|
+
searches = {{ story.storyAndInvestigationNamesWithApp(APP_NAME) | tojson }}
|
|
14
|
+
description = {{ story.description | escapeNewlines() }}
|
|
15
15
|
{% if story.narrative is defined %}
|
|
16
|
-
narrative = {{ story.narrative }}
|
|
16
|
+
narrative = {{ story.narrative | escapeNewlines() }}
|
|
17
17
|
{% endif %}
|
|
18
18
|
|
|
19
19
|
{% endfor %}
|
|
@@ -21,14 +21,14 @@ reload.es_investigations = simple
|
|
|
21
21
|
[launcher]
|
|
22
22
|
author = {{ objects[0].author_company }}
|
|
23
23
|
version = {{ objects[0].version }}
|
|
24
|
-
description = {{ objects[0].description }}
|
|
24
|
+
description = {{ objects[0].description | escapeNewlines() }}
|
|
25
25
|
|
|
26
26
|
[ui]
|
|
27
27
|
is_visible = true
|
|
28
28
|
label = {{ objects[0].title }}
|
|
29
29
|
|
|
30
30
|
[package]
|
|
31
|
-
id = {{ objects[0].
|
|
31
|
+
id = {{ objects[0].appid }}
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"title": "{{ objects[0].title }}",
|
|
5
5
|
"id": {
|
|
6
6
|
"group": null,
|
|
7
|
-
"name": "{{ objects[0].
|
|
7
|
+
"name": "{{ objects[0].appid }}",
|
|
8
8
|
"version": "{{ objects[0].version }}"
|
|
9
9
|
},
|
|
10
10
|
"author": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"company": "{{ objects[0].author_company }}"
|
|
15
15
|
}
|
|
16
16
|
],
|
|
17
|
-
"releaseDate":
|
|
17
|
+
"releaseDate": "{{ currentDate }}",
|
|
18
18
|
"description": "{{ objects[0].description }}",
|
|
19
19
|
"classification": {
|
|
20
20
|
"intendedAudience": null,
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="130" height="20">
|
|
3
3
|
<linearGradient id="a" x2="0" y2="100%">
|
|
4
4
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
|
5
5
|
<stop offset="2" stop-opacity=".1"/>
|
|
6
6
|
</linearGradient>
|
|
7
7
|
|
|
8
|
-
<rect rx="3" width="
|
|
9
|
-
<rect rx="3" x="
|
|
10
|
-
|
|
11
|
-
<path fill="#4c1" d="M58 0h4v20h-4z"/>
|
|
8
|
+
<rect rx="3" width="90" height="20" fill="#555"/> <!-- Comment -->
|
|
9
|
+
<rect rx="3" x="90" width="40" height="20" fill="#4c1"/>
|
|
12
10
|
|
|
13
11
|
<rect rx="3" width="100" height="20" fill="url(#a)"/>
|
|
14
|
-
<g fill="#fff" text-anchor="
|
|
15
|
-
<text x="
|
|
16
|
-
<text x="
|
|
12
|
+
<g fill="#fff" text-anchor="left" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
|
|
13
|
+
<text x="5" y="14">% Production</text>
|
|
14
|
+
<text x="100" y="14">{{object.coverage}}</text>
|
|
17
15
|
</g>
|
|
18
16
|
</svg>
|
|
@@ -12,8 +12,8 @@ sidebar:
|
|
|
12
12
|
| -------------- | --------------- | --------------- |
|
|
13
13
|
{%- for detection in objects -%}
|
|
14
14
|
{% if detection.tags.mitre_attack_enrichments %}
|
|
15
|
-
| [{{ detection.name }}](/{{ detection.
|
|
15
|
+
| [{{ detection.name }}](/{{ detection.getSource() }}/{{ detection.name | lower | replace(' ', '_') }}/) | {% for attack in detection.tags.mitre_attack_enrichments -%} [{{ attack.mitre_attack_technique }}](/tags/#{{ attack.mitre_attack_technique | lower | replace(" ", "-") }}){% if not loop.last -%}, {% endif -%}{%- endfor %} | [{{ detection.type }}](https://github.com/splunk/security_content/wiki/Detection-Analytic-Types) |
|
|
16
16
|
{%- else %}
|
|
17
|
-
| [{{ detection.name }}](/{{ detection.
|
|
17
|
+
| [{{ detection.name }}](/{{ detection.getSource() }}/{{ detection.name | lower | replace(' ', '_') }}/) | None | [{{ detection.type }}](https://github.com/splunk/security_content/wiki/Detection-Analytic-Types) |
|
|
18
18
|
{%- endif -%}
|
|
19
19
|
{%- endfor -%}
|
|
@@ -5,7 +5,7 @@ excerpt: "{% if object.tags.mitre_attack_enrichments %}{% for attack in object.t
|
|
|
5
5
|
{% if not loop.last -%}, {% endif -%}
|
|
6
6
|
{% endfor %}{% endif -%}"
|
|
7
7
|
categories:
|
|
8
|
-
- {{object.
|
|
8
|
+
- {{object.getSource()|capitalize}}
|
|
9
9
|
last_modified_at: {{object.date}}
|
|
10
10
|
toc: true
|
|
11
11
|
toc_label: ""
|
|
@@ -207,4 +207,4 @@ Alternatively you can replay a dataset into a [Splunk Attack Range](https://gith
|
|
|
207
207
|
{% endfor %}
|
|
208
208
|
{% endif %}
|
|
209
209
|
|
|
210
|
-
[*source*](https://github.com/splunk/security_content/tree/develop/detections/{% if object.experimental is sameas true -%}experimental/{%- endif -%}{{object.
|
|
210
|
+
[*source*](https://github.com/splunk/security_content/tree/develop/detections/{% if object.experimental is sameas true -%}experimental/{%- endif -%}{{object.getSource()}}/{{ object.name | lower | replace (" ", "_") | replace("-", "_") }}.yml) \| *version*: **{{object.version}}**
|
|
@@ -37,7 +37,7 @@ tags:
|
|
|
37
37
|
| ----------- | ----------- |--------------|
|
|
38
38
|
{%- if object.detections %}
|
|
39
39
|
{%- for detection in object.detections %}
|
|
40
|
-
| [{{ detection.name }}](/{{ detection.
|
|
40
|
+
| [{{ detection.name }}](/{{ detection.getSource() }}/{{ detection.name | lower | replace(' ', '_') }}/) | {% if detection.tags.mitre_attack_enrichments %}{% for attack in detection.tags.mitre_attack_enrichments -%}[{{ attack.mitre_attack_technique }}](/tags/#{{ attack.mitre_attack_technique | lower | replace(" ", "-") }}){% if not loop.last %}, {% endif %}{%- endfor %}{% else %}None{%- endif -%} | {{ detection.type }} |
|
|
41
41
|
{%- endfor %}
|
|
42
42
|
{%- endif %}
|
|
43
43
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{% for response_task in objects %}
|
|
3
3
|
[panel://workbench_panel_{{ response_task.lowercase_name }}___response_task]
|
|
4
4
|
label = {{ response_task.name }}
|
|
5
|
-
description = {{ response_task.description }}
|
|
5
|
+
description = {{ response_task.description | escapeNewlines() }}
|
|
6
6
|
disabled = 0
|
|
7
7
|
tokens = {\
|
|
8
8
|
{% for token in response_task.inputs %}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
{% for story in objects %}
|
|
3
3
|
[panel_group://workbench_panel_group_{{ story.lowercase_name}}]
|
|
4
4
|
label = {{ story.name }}
|
|
5
|
-
description = {{ story.description }}
|
|
5
|
+
description = {{ story.description | escapeNewlines() }}
|
|
6
6
|
disabled = 0
|
|
7
7
|
|
|
8
8
|
{% if story.workbench_panels is defined %}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
{% for macro in objects %}
|
|
3
|
-
[{{ macro.name }}{% if macro.arguments
|
|
4
|
-
{% if macro.arguments
|
|
5
|
-
args = {% for arg in macro.arguments %}{{ arg }}{{ ", " if not loop.last }}
|
|
6
|
-
|
|
7
|
-
{% endif %}
|
|
8
|
-
{% if macro.definition is not none %}
|
|
9
|
-
definition = {{ macro.definition }}
|
|
10
|
-
{% else %}
|
|
11
|
-
definition =
|
|
3
|
+
[{{ macro.name }}{% if macro.arguments | length > 0 %}({{ macro.arguments|length }}){% endif %}]
|
|
4
|
+
{% if macro.arguments | length > 0 %}
|
|
5
|
+
args = {% for arg in macro.arguments %}{{ arg }}{{ ", " if not loop.last }}{% endfor %}
|
|
6
|
+
|
|
12
7
|
{% endif %}
|
|
13
|
-
|
|
8
|
+
definition = {{ macro.definition | escapeNewlines() }}
|
|
9
|
+
description = {{ macro.description | escapeNewlines() }}
|
|
14
10
|
|
|
15
11
|
{% endfor %}
|
|
@@ -9,11 +9,11 @@ action.escu = 0
|
|
|
9
9
|
action.escu.enabled = 1
|
|
10
10
|
action.escu.search_type = support
|
|
11
11
|
action.escu.full_search_name = {{APP_NAME}} - {{ detection.name }}
|
|
12
|
-
description = {{ detection.description }}
|
|
12
|
+
description = {{ detection.description | escapeNewlines() }}
|
|
13
13
|
action.escu.creation_date = {{ detection.date }}
|
|
14
14
|
action.escu.modification_date = {{ detection.date }}
|
|
15
15
|
{% if detection.tags.analytic_story is defined %}
|
|
16
|
-
action.escu.analytic_story = {{ detection.tags.analytic_story | tojson }}
|
|
16
|
+
action.escu.analytic_story = {{ objectListToNameList(detection.tags.analytic_story) | tojson }}
|
|
17
17
|
{% else %}
|
|
18
18
|
action.escu.analytic_story = []
|
|
19
19
|
{% endif %}
|
|
@@ -30,9 +30,9 @@ action.escu.providing_technologies = {{ detection.providing_technologies | tojso
|
|
|
30
30
|
{% else %}
|
|
31
31
|
action.escu.providing_technologies = []
|
|
32
32
|
{% endif %}
|
|
33
|
-
action.escu.eli5 = {{ detection.description }}
|
|
33
|
+
action.escu.eli5 = {{ detection.description | escapeNewlines() }}
|
|
34
34
|
{% if detection.how_to_implement is defined %}
|
|
35
|
-
action.escu.how_to_implement = {{ detection.how_to_implement }}
|
|
35
|
+
action.escu.how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
|
|
36
36
|
{% else %}
|
|
37
37
|
action.escu.how_to_implement = none
|
|
38
38
|
{% endif %}
|
|
@@ -42,7 +42,7 @@ disabled = false
|
|
|
42
42
|
disabled = true
|
|
43
43
|
{% endif %}
|
|
44
44
|
is_visible = false
|
|
45
|
-
search = {{ detection.search }}
|
|
45
|
+
search = {{ detection.search | escapeNewlines() }}
|
|
46
46
|
|
|
47
47
|
{% endif %}
|
|
48
48
|
{% endfor %}
|