contentctl 4.4.7__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contentctl/__init__.py +1 -1
- contentctl/actions/build.py +102 -57
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
- contentctl/actions/detection_testing/GitService.py +134 -76
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +9 -6
- contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +155 -108
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +33 -28
- contentctl/actions/validate.py +55 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +124 -90
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -53
- contentctl/input/director.py +68 -36
- contentctl/input/new_content_questions.py +27 -35
- contentctl/input/yml_reader.py +28 -18
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
- contentctl/objects/alert_action.py +10 -9
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +65 -54
- contentctl/objects/base_test.py +5 -3
- contentctl/objects/base_test_result.py +19 -11
- contentctl/objects/baseline.py +62 -30
- contentctl/objects/baseline_tags.py +30 -24
- contentctl/objects/config.py +790 -597
- contentctl/objects/constants.py +33 -56
- contentctl/objects/correlation_search.py +150 -136
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +16 -17
- contentctl/objects/deployment.py +43 -44
- contentctl/objects/deployment_email.py +3 -2
- contentctl/objects/deployment_notable.py +4 -2
- contentctl/objects/deployment_phantom.py +7 -6
- contentctl/objects/deployment_rba.py +3 -2
- contentctl/objects/deployment_scheduling.py +3 -2
- contentctl/objects/deployment_slack.py +3 -2
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +58 -103
- contentctl/objects/drilldown.py +66 -34
- contentctl/objects/enums.py +81 -100
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +59 -36
- contentctl/objects/investigation_tags.py +30 -19
- contentctl/objects/lookup.py +304 -101
- contentctl/objects/macro.py +55 -39
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +23 -13
- contentctl/objects/rba.py +96 -0
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +110 -160
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +54 -49
- contentctl/objects/story_tags.py +56 -45
- contentctl/objects/test_attack_data.py +2 -1
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +5 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +233 -220
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +235 -172
- contentctl/output/conf_writer.py +201 -125
- contentctl/output/data_source_writer.py +38 -26
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +21 -11
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/collections.j2 +1 -1
- contentctl/output/templates/doc_detections.j2 +0 -5
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -2
- contentctl/output/templates/savedsearches_detections.j2 +10 -11
- contentctl/output/templates/savedsearches_investigations.j2 +2 -2
- contentctl/output/templates/transforms.j2 +6 -8
- contentctl/output/yml_writer.py +29 -20
- contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
- contentctl/templates/stories/cobalt_strike.yml +1 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
- contentctl-5.0.0.dist-info/RECORD +168 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/event_source.py +0 -11
- contentctl/objects/observable.py +0 -37
- contentctl/output/detection_writer.py +0 -28
- contentctl/output/new_content_yml_output.py +0 -56
- contentctl/output/yml_output.py +0 -66
- contentctl-4.4.7.dist-info/RECORD +0 -173
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -4,30 +4,34 @@ from jinja2 import Environment, FileSystemLoader
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class JinjaWriter:
|
|
7
|
-
|
|
8
7
|
@staticmethod
|
|
9
|
-
def writeObjectsList(template_name
|
|
10
|
-
|
|
8
|
+
def writeObjectsList(template_name: str, output_path: str, objects: list) -> None:
|
|
11
9
|
j2_env = Environment(
|
|
12
|
-
loader=FileSystemLoader(
|
|
13
|
-
|
|
10
|
+
loader=FileSystemLoader(
|
|
11
|
+
os.path.join(os.path.dirname(__file__), "templates")
|
|
12
|
+
),
|
|
13
|
+
trim_blocks=False,
|
|
14
|
+
)
|
|
14
15
|
|
|
15
16
|
template = j2_env.get_template(template_name)
|
|
16
17
|
output = template.render(objects=objects)
|
|
17
|
-
with open(output_path,
|
|
18
|
-
output = output.encode(
|
|
18
|
+
with open(output_path, "w") as f:
|
|
19
|
+
output = output.encode("ascii", "ignore").decode("ascii")
|
|
19
20
|
f.write(output)
|
|
20
21
|
|
|
21
|
-
|
|
22
22
|
@staticmethod
|
|
23
|
-
def writeObject(
|
|
24
|
-
|
|
23
|
+
def writeObject(
|
|
24
|
+
template_name: str, output_path: str, object: dict[str, Any]
|
|
25
|
+
) -> None:
|
|
25
26
|
j2_env = Environment(
|
|
26
|
-
loader=FileSystemLoader(
|
|
27
|
-
|
|
27
|
+
loader=FileSystemLoader(
|
|
28
|
+
os.path.join(os.path.dirname(__file__), "templates")
|
|
29
|
+
),
|
|
30
|
+
trim_blocks=False,
|
|
31
|
+
)
|
|
28
32
|
|
|
29
33
|
template = j2_env.get_template(template_name)
|
|
30
34
|
output = template.render(object=object)
|
|
31
|
-
with open(output_path,
|
|
32
|
-
output = output.encode(
|
|
33
|
-
f.write(output)
|
|
35
|
+
with open(output_path, "w") as f:
|
|
36
|
+
output = output.encode("ascii", "ignore").decode("ascii")
|
|
37
|
+
f.write(output)
|
contentctl/output/json_writer.py
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from
|
|
3
|
-
from typing import List
|
|
4
|
-
from io import TextIOWrapper
|
|
5
|
-
class JsonWriter():
|
|
2
|
+
from typing import Any
|
|
6
3
|
|
|
4
|
+
|
|
5
|
+
class JsonWriter:
|
|
7
6
|
@staticmethod
|
|
8
|
-
def writeJsonObject(
|
|
7
|
+
def writeJsonObject(
|
|
8
|
+
file_path: str,
|
|
9
|
+
object_name: str,
|
|
10
|
+
objs: list[dict[str, Any]],
|
|
11
|
+
readable_output: bool = True,
|
|
12
|
+
) -> None:
|
|
9
13
|
try:
|
|
10
|
-
with open(file_path,
|
|
14
|
+
with open(file_path, "w") as outfile:
|
|
11
15
|
if readable_output:
|
|
12
16
|
# At the cost of slightly larger filesize, improve the redability significantly
|
|
13
17
|
# by sorting and indenting keys/values
|
|
14
|
-
sorted_objs = sorted(objs, key=lambda o: o[
|
|
15
|
-
json.dump(
|
|
18
|
+
sorted_objs = sorted(objs, key=lambda o: o["name"])
|
|
19
|
+
json.dump(
|
|
20
|
+
{object_name: sorted_objs},
|
|
21
|
+
outfile,
|
|
22
|
+
ensure_ascii=False,
|
|
23
|
+
indent=2,
|
|
24
|
+
)
|
|
16
25
|
else:
|
|
17
|
-
json.dump({object_name:objs}, outfile, ensure_ascii=False)
|
|
26
|
+
json.dump({object_name: objs}, outfile, ensure_ascii=False)
|
|
18
27
|
|
|
19
28
|
except Exception as e:
|
|
20
|
-
|
|
21
|
-
|
|
29
|
+
raise Exception(
|
|
30
|
+
f"Error serializing object to Json File '{file_path}': {str(e)}"
|
|
31
|
+
)
|
contentctl/output/svg_output.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
1
|
import pathlib
|
|
3
2
|
from typing import List, Any
|
|
4
3
|
|
|
@@ -6,50 +5,69 @@ from contentctl.objects.enums import SecurityContentType
|
|
|
6
5
|
from contentctl.output.jinja_writer import JinjaWriter
|
|
7
6
|
from contentctl.objects.enums import DetectionStatus
|
|
8
7
|
from contentctl.objects.detection import Detection
|
|
9
|
-
class SvgOutput():
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
|
|
10
|
+
class SvgOutput:
|
|
11
|
+
def get_badge_dict(
|
|
12
|
+
self,
|
|
13
|
+
name: str,
|
|
14
|
+
total_detections: List[Detection],
|
|
15
|
+
these_detections: List[Detection],
|
|
16
|
+
) -> dict[str, Any]:
|
|
17
|
+
obj: dict[str, Any] = {}
|
|
18
|
+
obj["name"] = name
|
|
15
19
|
|
|
16
20
|
if name == "Production":
|
|
17
|
-
obj[
|
|
21
|
+
obj["color"] = "Green"
|
|
18
22
|
elif name == "Detections":
|
|
19
|
-
obj[
|
|
23
|
+
obj["color"] = "Green"
|
|
20
24
|
elif name == "Experimental":
|
|
21
|
-
obj[
|
|
25
|
+
obj["color"] = "Yellow"
|
|
22
26
|
elif name == "Deprecated":
|
|
23
|
-
obj[
|
|
27
|
+
obj["color"] = "Red"
|
|
24
28
|
|
|
25
|
-
obj[
|
|
26
|
-
if obj[
|
|
27
|
-
obj[
|
|
29
|
+
obj["count"] = len(total_detections)
|
|
30
|
+
if obj["count"] == 0:
|
|
31
|
+
obj["coverage"] = "NaN"
|
|
28
32
|
else:
|
|
29
|
-
obj[
|
|
30
|
-
obj[
|
|
33
|
+
obj["coverage"] = len(these_detections) / obj["count"]
|
|
34
|
+
obj["coverage"] = "{:.0%}".format(obj["coverage"])
|
|
31
35
|
return obj
|
|
32
|
-
|
|
33
|
-
def writeObjects(self, detections: List[Detection], output_path: pathlib.Path, type: SecurityContentType = None) -> None:
|
|
34
|
-
|
|
35
|
-
|
|
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])
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#Total number of detections
|
|
46
|
-
JinjaWriter.writeObject('detection_count.j2', output_path /'detection_count.svg', total_dict)
|
|
47
|
-
#JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'production_count.svg'), production_dict)
|
|
48
|
-
#JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'deprecated_count.svg'), deprecated_dict)
|
|
49
|
-
#JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'experimental_count.svg'), experimental_dict)
|
|
50
|
-
|
|
51
|
-
#Percentage of detections that are production
|
|
52
|
-
JinjaWriter.writeObject('detection_coverage.j2', output_path/'detection_coverage.svg', production_dict)
|
|
53
|
-
#JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), deprecated_dict)
|
|
54
|
-
#JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), experimental_dict)
|
|
55
36
|
|
|
37
|
+
def writeObjects(
|
|
38
|
+
self,
|
|
39
|
+
detections: List[Detection],
|
|
40
|
+
output_path: pathlib.Path,
|
|
41
|
+
type: SecurityContentType = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
total_dict: dict[str, Any] = self.get_badge_dict(
|
|
44
|
+
"Detections", detections, detections
|
|
45
|
+
)
|
|
46
|
+
production_dict: dict[str, Any] = self.get_badge_dict(
|
|
47
|
+
"% Production",
|
|
48
|
+
detections,
|
|
49
|
+
[
|
|
50
|
+
detection
|
|
51
|
+
for detection in detections
|
|
52
|
+
if detection.status == DetectionStatus.production
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
# deprecated_dict = self.get_badge_dict("Deprecated", detections, [detection for detection in detections if detection.status == DetectionStatus.deprecated])
|
|
56
|
+
# experimental_dict = self.get_badge_dict("Experimental", detections, [detection for detection in detections if detection.status == DetectionStatus.experimental])
|
|
57
|
+
|
|
58
|
+
# Total number of detections
|
|
59
|
+
JinjaWriter.writeObject(
|
|
60
|
+
"detection_count.j2", output_path / "detection_count.svg", total_dict
|
|
61
|
+
)
|
|
62
|
+
# JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'production_count.svg'), production_dict)
|
|
63
|
+
# JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'deprecated_count.svg'), deprecated_dict)
|
|
64
|
+
# JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'experimental_count.svg'), experimental_dict)
|
|
65
|
+
|
|
66
|
+
# Percentage of detections that are production
|
|
67
|
+
JinjaWriter.writeObject(
|
|
68
|
+
"detection_coverage.j2",
|
|
69
|
+
output_path / "detection_coverage.svg",
|
|
70
|
+
production_dict,
|
|
71
|
+
)
|
|
72
|
+
# JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), deprecated_dict)
|
|
73
|
+
# JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), experimental_dict)
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
{% if (detection.type == 'TTP' or detection.type == 'Anomaly' or detection.type == 'Hunting' or detection.type == 'Correlation') %}
|
|
6
6
|
[savedsearch://{{ detection.get_conf_stanza_name(app) }}]
|
|
7
7
|
type = detection
|
|
8
|
-
asset_type = {{ detection.tags.asset_type
|
|
8
|
+
asset_type = {{ detection.tags.asset_type }}
|
|
9
9
|
confidence = medium
|
|
10
|
-
explanation = {{
|
|
10
|
+
explanation = {{ detection.status_aware_description | escapeNewlines() }}
|
|
11
11
|
{% if detection.how_to_implement is defined %}
|
|
12
12
|
how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
|
|
13
13
|
{% else %}
|
|
@@ -11,7 +11,7 @@ references = {{ story.getReferencesListForJson() | tojson }}
|
|
|
11
11
|
maintainers = [{"company": "{{ story.author_company }}", "email": "{{ story.author_email }}", "name": "{{ story.author_name }}"}]
|
|
12
12
|
spec_version = 3
|
|
13
13
|
searches = {{ story.storyAndInvestigationNamesWithApp(app) | tojson }}
|
|
14
|
-
description = {{ story.
|
|
14
|
+
description = {{ story.status_aware_description | escapeNewlines() }}
|
|
15
15
|
{% if story.narrative is defined %}
|
|
16
16
|
narrative = {{ story.narrative | escapeNewlines() }}
|
|
17
17
|
{% endif %}
|
|
@@ -162,11 +162,6 @@ The SPL above uses the following Lookups:
|
|
|
162
162
|
{% endfor %}
|
|
163
163
|
{% endif -%}
|
|
164
164
|
|
|
165
|
-
#### Required field
|
|
166
|
-
{% for field in object.tags.required_fields -%}
|
|
167
|
-
* {{ field }}
|
|
168
|
-
{% endfor %}
|
|
169
|
-
|
|
170
165
|
#### How To Implement
|
|
171
166
|
{{ object.how_to_implement}}
|
|
172
167
|
|
|
@@ -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.
|
|
5
|
+
description = {{ response_task.status_aware_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.
|
|
5
|
+
description = {{ story.status_aware_description | escapeNewlines() }}
|
|
6
6
|
disabled = 0
|
|
7
7
|
|
|
8
8
|
{% if story.workbench_panels is defined %}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
action.escu = 0
|
|
9
9
|
action.escu.enabled = 1
|
|
10
10
|
action.escu.search_type = support
|
|
11
|
-
description = {{ detection.
|
|
11
|
+
description = {{ detection.status_aware_description | escapeNewlines() }}
|
|
12
12
|
action.escu.creation_date = {{ detection.date }}
|
|
13
13
|
action.escu.modification_date = {{ detection.date }}
|
|
14
14
|
{% if detection.tags.analytic_story is defined %}
|
|
@@ -29,7 +29,7 @@ action.escu.providing_technologies = {{ detection.providing_technologies | tojso
|
|
|
29
29
|
{% else %}
|
|
30
30
|
action.escu.providing_technologies = []
|
|
31
31
|
{% endif %}
|
|
32
|
-
action.escu.eli5 = {{ detection.
|
|
32
|
+
action.escu.eli5 = {{ detection.status_aware_description | escapeNewlines() }}
|
|
33
33
|
{% if detection.how_to_implement is defined %}
|
|
34
34
|
action.escu.how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
|
|
35
35
|
{% else %}
|
|
@@ -5,16 +5,10 @@
|
|
|
5
5
|
[{{ detection.get_conf_stanza_name(app) }}]
|
|
6
6
|
action.escu = 0
|
|
7
7
|
action.escu.enabled = 1
|
|
8
|
-
{
|
|
9
|
-
description = **WARNING**, this detection has been marked **DEPRECATED** by the Splunk Threat Research Team. This means that it will no longer be maintained or supported. If you have any questions feel free to email us at: research@splunk.com. {{ detection.description | escapeNewlines() }}
|
|
10
|
-
{% elif detection.status == "experimental" %}
|
|
11
|
-
description = **WARNING**, this detection is marked **EXPERIMENTAL** by the Splunk Threat Research Team. This means that the detection has been manually tested but we do not have the associated attack data to perform automated testing or cannot share this attack dataset due to its sensitive nature. If you have any questions feel free to email us at: research@splunk.com. {{ detection.description | escapeNewlines() }}
|
|
12
|
-
{% else %}
|
|
13
|
-
description = {{ detection.description | escapeNewlines() }}
|
|
14
|
-
{% endif %}
|
|
8
|
+
description = {{ detection.status_aware_description | escapeNewlines() }}
|
|
15
9
|
action.escu.mappings = {{ detection.mappings | tojson }}
|
|
16
10
|
action.escu.data_models = {{ detection.datamodel | tojson }}
|
|
17
|
-
action.escu.eli5 = {{ detection.
|
|
11
|
+
action.escu.eli5 = {{ detection.status_aware_description | escapeNewlines() }}
|
|
18
12
|
{% if detection.how_to_implement %}
|
|
19
13
|
action.escu.how_to_implement = {{ detection.how_to_implement | escapeNewlines() }}
|
|
20
14
|
{% else %}
|
|
@@ -44,7 +38,7 @@ action.escu.providing_technologies = null
|
|
|
44
38
|
action.escu.analytic_story = {{ objectListToNameList(detection.tags.analytic_story) | tojson }}
|
|
45
39
|
{% if detection.deployment.alert_action.rba.enabled%}
|
|
46
40
|
action.risk = 1
|
|
47
|
-
action.risk.param._risk_message = {{ detection.
|
|
41
|
+
action.risk.param._risk_message = {{ detection.rba.message | escapeNewlines() }}
|
|
48
42
|
action.risk.param._risk = {{ detection.risk | tojson }}
|
|
49
43
|
action.risk.param._risk_score = 0
|
|
50
44
|
action.risk.param.verbose = 0
|
|
@@ -70,8 +64,13 @@ action.notable.param.nes_fields = {{ detection.nes_fields }}
|
|
|
70
64
|
{% endif %}
|
|
71
65
|
action.notable.param.rule_description = {{ detection.deployment.alert_action.notable.rule_description | custom_jinja2_enrichment_filter(detection) | escapeNewlines()}}
|
|
72
66
|
action.notable.param.rule_title = {% if detection.type | lower == "correlation" %}RBA: {{ detection.deployment.alert_action.notable.rule_title | custom_jinja2_enrichment_filter(detection) }}{% else %}{{ detection.deployment.alert_action.notable.rule_title | custom_jinja2_enrichment_filter(detection) }}{% endif +%}
|
|
73
|
-
action.notable.param.security_domain = {{ detection.tags.security_domain
|
|
74
|
-
|
|
67
|
+
action.notable.param.security_domain = {{ detection.tags.security_domain }}
|
|
68
|
+
{% if detection.rba %}
|
|
69
|
+
action.notable.param.severity = {{ detection.rba.severity }}
|
|
70
|
+
{% else %}
|
|
71
|
+
{# Correlations do not have detection.rba defined, but should get a default severity #}
|
|
72
|
+
action.notable.param.severity = high
|
|
73
|
+
{% endif %}
|
|
75
74
|
{% endif %}
|
|
76
75
|
{% if detection.deployment.alert_action.email %}
|
|
77
76
|
action.email.subject.alert = {{ detection.deployment.alert_action.email.subject | custom_jinja2_enrichment_filter(detection) | escapeNewlines() }}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
action.escu = 0
|
|
10
10
|
action.escu.enabled = 1
|
|
11
11
|
action.escu.search_type = investigative
|
|
12
|
-
description = {{ detection.
|
|
12
|
+
description = {{ detection.status_aware_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 %}
|
|
@@ -21,7 +21,7 @@ action.escu.earliest_time_offset = 3600
|
|
|
21
21
|
action.escu.latest_time_offset = 86400
|
|
22
22
|
action.escu.providing_technologies = []
|
|
23
23
|
action.escu.data_models = {{ detection.datamodel | tojson }}
|
|
24
|
-
action.escu.eli5 = {{ detection.
|
|
24
|
+
action.escu.eli5 = {{ detection.status_aware_description | escapeNewlines() }}
|
|
25
25
|
action.escu.how_to_implement = none
|
|
26
26
|
action.escu.known_false_positives = None at this time
|
|
27
27
|
disabled = true
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
{% for lookup in objects %}
|
|
3
3
|
[{{ lookup.name }}]
|
|
4
|
-
{% if lookup.
|
|
5
|
-
filename = {{ lookup.
|
|
4
|
+
{% if lookup.app_filename is defined and lookup.app_filename != None %}
|
|
5
|
+
filename = {{ lookup.app_filename.name }}
|
|
6
6
|
{% else %}
|
|
7
7
|
collection = {{ lookup.collection }}
|
|
8
8
|
external_type = kvstore
|
|
@@ -13,11 +13,9 @@ default_match = {{ lookup.default_match | lower }}
|
|
|
13
13
|
{% if lookup.case_sensitive_match is defined and lookup.case_sensitive_match != None %}
|
|
14
14
|
case_sensitive_match = {{ lookup.case_sensitive_match | lower }}
|
|
15
15
|
{% endif %}
|
|
16
|
-
{% if lookup.description is defined and lookup.description != None %}
|
|
17
16
|
# description = {{ lookup.description | escapeNewlines() }}
|
|
18
|
-
{%
|
|
19
|
-
|
|
20
|
-
match_type = {{ lookup.match_type }}
|
|
17
|
+
{% if lookup.match_type | length > 0 %}
|
|
18
|
+
match_type = {{ lookup.match_type_to_conf_format }}
|
|
21
19
|
{% endif %}
|
|
22
20
|
{% if lookup.max_matches is defined and lookup.max_matches != None %}
|
|
23
21
|
max_matches = {{ lookup.max_matches }}
|
|
@@ -25,8 +23,8 @@ max_matches = {{ lookup.max_matches }}
|
|
|
25
23
|
{% if lookup.min_matches is defined and lookup.min_matches != None %}
|
|
26
24
|
min_matches = {{ lookup.min_matches }}
|
|
27
25
|
{% endif %}
|
|
28
|
-
{% if lookup.
|
|
29
|
-
fields_list = {{ lookup.
|
|
26
|
+
{% if lookup.fields_to_fields_list_conf_format is defined %}
|
|
27
|
+
fields_list = {{ lookup.fields_to_fields_list_conf_format }}
|
|
30
28
|
{% endif %}
|
|
31
29
|
{% if lookup.filter is defined and lookup.filter != None %}
|
|
32
30
|
filter = {{ lookup.filter }}
|
contentctl/output/yml_writer.py
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
|
-
|
|
2
1
|
import yaml
|
|
3
2
|
from typing import Any
|
|
3
|
+
from enum import StrEnum, IntEnum
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# Set the following so that we can write StrEnum and IntEnum
|
|
6
|
+
# to files. Otherwise, we will get the following errors when trying
|
|
7
|
+
# to write to files:
|
|
8
|
+
# yaml.representer.RepresenterError: ('cannot represent an object',.....
|
|
9
|
+
yaml.SafeDumper.add_multi_representer(
|
|
10
|
+
StrEnum, yaml.representer.SafeRepresenter.represent_str
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
yaml.SafeDumper.add_multi_representer(
|
|
14
|
+
IntEnum, yaml.representer.SafeRepresenter.represent_int
|
|
15
|
+
)
|
|
6
16
|
|
|
7
|
-
@staticmethod
|
|
8
|
-
def writeYmlFile(file_path : str, obj : dict[Any,Any]) -> None:
|
|
9
17
|
|
|
10
|
-
|
|
18
|
+
class YmlWriter:
|
|
19
|
+
@staticmethod
|
|
20
|
+
def writeYmlFile(file_path: str, obj: dict[Any, Any]) -> None:
|
|
21
|
+
with open(file_path, "w") as outfile:
|
|
11
22
|
yaml.safe_dump(obj, outfile, default_flow_style=False, sort_keys=False)
|
|
12
23
|
|
|
13
24
|
@staticmethod
|
|
14
|
-
def writeDetection(file_path: str, obj: dict[Any,Any]) -> None:
|
|
25
|
+
def writeDetection(file_path: str, obj: dict[Any, Any]) -> None:
|
|
15
26
|
output = dict()
|
|
16
27
|
output["name"] = obj["name"]
|
|
17
28
|
output["id"] = obj["id"]
|
|
@@ -20,7 +31,7 @@ class YmlWriter:
|
|
|
20
31
|
output["author"] = obj["author"]
|
|
21
32
|
output["type"] = obj["type"]
|
|
22
33
|
output["status"] = obj["status"]
|
|
23
|
-
output["data_source"] = obj[
|
|
34
|
+
output["data_source"] = obj["data_sources"]
|
|
24
35
|
output["description"] = obj["description"]
|
|
25
36
|
output["search"] = obj["search"]
|
|
26
37
|
output["how_to_implement"] = obj["how_to_implement"]
|
|
@@ -30,20 +41,18 @@ class YmlWriter:
|
|
|
30
41
|
output["tests"] = obj["tags"]
|
|
31
42
|
|
|
32
43
|
YmlWriter.writeYmlFile(file_path=file_path, obj=output)
|
|
33
|
-
|
|
44
|
+
|
|
34
45
|
@staticmethod
|
|
35
|
-
def writeStory(file_path: str, obj: dict[Any,Any]) -> None:
|
|
46
|
+
def writeStory(file_path: str, obj: dict[Any, Any]) -> None:
|
|
36
47
|
output = dict()
|
|
37
|
-
output[
|
|
38
|
-
output[
|
|
39
|
-
output[
|
|
40
|
-
output[
|
|
41
|
-
output[
|
|
42
|
-
output[
|
|
43
|
-
output[
|
|
44
|
-
output[
|
|
45
|
-
output[
|
|
48
|
+
output["name"] = obj["name"]
|
|
49
|
+
output["id"] = obj["id"]
|
|
50
|
+
output["version"] = obj["version"]
|
|
51
|
+
output["date"] = obj["date"]
|
|
52
|
+
output["author"] = obj["author"]
|
|
53
|
+
output["description"] = obj["description"]
|
|
54
|
+
output["narrative"] = obj["narrative"]
|
|
55
|
+
output["references"] = obj["references"]
|
|
56
|
+
output["tags"] = obj["tags"]
|
|
46
57
|
|
|
47
58
|
YmlWriter.writeYmlFile(file_path=file_path, obj=output)
|
|
48
|
-
|
|
49
|
-
|
|
@@ -38,51 +38,33 @@ drilldown_searches:
|
|
|
38
38
|
search: '| from datamodel Risk.All_Risk | search normalized_risk_object IN ($user$, $dest$) starthoursago=168 endhoursago=1 | stats count min(_time) as firstTime max(_time) as lastTime values(search_name) as "Search Name" values(risk_message) as "Risk Message" values(analyticstories) as "Analytic Stories" values(annotations._all) as "Annotations" values(annotations.mitre_attack.mitre_tactic) as "ATT&CK Tactics" by normalized_risk_object | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`'
|
|
39
39
|
earliest_offset: $info_min_time$
|
|
40
40
|
latest_offset: $info_max_time$
|
|
41
|
+
rba:
|
|
42
|
+
message: An instance of $parent_process_name$ spawning $process_name$ was identified
|
|
43
|
+
on endpoint $dest$ by user $user$. This behavior is indicative of suspicious loading
|
|
44
|
+
of 7zip.
|
|
45
|
+
risk_objects:
|
|
46
|
+
- field: user
|
|
47
|
+
type: user
|
|
48
|
+
score: 56
|
|
49
|
+
- field: dest
|
|
50
|
+
type: system
|
|
51
|
+
score: 60
|
|
52
|
+
threat_objects:
|
|
53
|
+
- field: parent_process_name
|
|
54
|
+
type: parent_process_name
|
|
55
|
+
- field: process_name
|
|
56
|
+
type: process_name
|
|
41
57
|
tags:
|
|
42
58
|
analytic_story:
|
|
43
59
|
- Cobalt Strike
|
|
44
60
|
asset_type: Endpoint
|
|
45
|
-
confidence: 80
|
|
46
|
-
impact: 80
|
|
47
|
-
message: An instance of $parent_process_name$ spawning $process_name$ was identified
|
|
48
|
-
on endpoint $dest$ by user $user$. This behavior is indicative of suspicious loading
|
|
49
|
-
of 7zip.
|
|
50
61
|
mitre_attack_id:
|
|
51
62
|
- T1560.001
|
|
52
63
|
- T1560
|
|
53
|
-
observable:
|
|
54
|
-
- name: user
|
|
55
|
-
type: User
|
|
56
|
-
role:
|
|
57
|
-
- Victim
|
|
58
|
-
- name: dest
|
|
59
|
-
type: Hostname
|
|
60
|
-
role:
|
|
61
|
-
- Victim
|
|
62
|
-
- name: parent_process_name
|
|
63
|
-
type: Process
|
|
64
|
-
role:
|
|
65
|
-
- Attacker
|
|
66
|
-
- name: process_name
|
|
67
|
-
type: Process
|
|
68
|
-
role:
|
|
69
|
-
- Attacker
|
|
70
64
|
product:
|
|
71
65
|
- Splunk Enterprise
|
|
72
66
|
- Splunk Enterprise Security
|
|
73
67
|
- Splunk Cloud
|
|
74
|
-
required_fields:
|
|
75
|
-
- _time
|
|
76
|
-
- Processes.process_name
|
|
77
|
-
- Processes.process
|
|
78
|
-
- Processes.dest
|
|
79
|
-
- Processes.user
|
|
80
|
-
- Processes.parent_process_name
|
|
81
|
-
- Processes.process_name
|
|
82
|
-
- Processes.parent_process
|
|
83
|
-
- Processes.process_id
|
|
84
|
-
- Processes.parent_process_id
|
|
85
|
-
risk_score: 64
|
|
86
68
|
security_domain: endpoint
|
|
87
69
|
tests:
|
|
88
70
|
- name: True Positive Test
|
|
@@ -3,6 +3,7 @@ id: bcfd17e8-5461-400a-80a2-3b7d1459220c
|
|
|
3
3
|
version: 1
|
|
4
4
|
date: '2021-02-16'
|
|
5
5
|
author: Michael Haag, Splunk
|
|
6
|
+
status: production
|
|
6
7
|
description: Cobalt Strike is threat emulation software. Red teams and penetration
|
|
7
8
|
testers use Cobalt Strike to demonstrate the risk of a breach and evaluate mature
|
|
8
9
|
security programs. Most recently, Cobalt Strike has become the choice tool by threat
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: contentctl
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: Splunk Content Control Tool
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Author: STRT
|
|
7
7
|
Author-email: research@splunk.com
|
|
8
|
-
Requires-Python: >=3.11,<3.
|
|
8
|
+
Requires-Python: >=3.11,<3.14
|
|
9
9
|
Classifier: License :: Other/Proprietary License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
14
|
Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
|
|
14
15
|
Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
|
|
15
16
|
Requires-Dist: attackcti (>=0.4.0,<0.5.0)
|
|
@@ -25,7 +26,7 @@ Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
|
|
|
25
26
|
Requires-Dist: setuptools (>=69.5.1,<76.0.0)
|
|
26
27
|
Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
|
|
27
28
|
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
28
|
-
Requires-Dist: tyro (>=0.
|
|
29
|
+
Requires-Dist: tyro (>=0.9.2,<0.10.0)
|
|
29
30
|
Requires-Dist: xmltodict (>=0.13,<0.15)
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
|