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.
Files changed (123) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +102 -57
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
  5. contentctl/actions/detection_testing/GitService.py +134 -76
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +9 -6
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
  11. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
  12. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
  13. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
  14. contentctl/actions/doc_gen.py +9 -5
  15. contentctl/actions/initialize.py +45 -33
  16. contentctl/actions/inspect.py +118 -61
  17. contentctl/actions/new_content.py +155 -108
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +33 -28
  21. contentctl/actions/validate.py +55 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +124 -90
  24. contentctl/enrichments/attack_enrichment.py +112 -72
  25. contentctl/enrichments/cve_enrichment.py +34 -28
  26. contentctl/enrichments/splunk_app_enrichment.py +38 -36
  27. contentctl/helper/link_validator.py +101 -78
  28. contentctl/helper/splunk_app.py +69 -41
  29. contentctl/helper/utils.py +58 -53
  30. contentctl/input/director.py +68 -36
  31. contentctl/input/new_content_questions.py +27 -35
  32. contentctl/input/yml_reader.py +28 -18
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
  35. contentctl/objects/alert_action.py +10 -9
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +65 -54
  38. contentctl/objects/base_test.py +5 -3
  39. contentctl/objects/base_test_result.py +19 -11
  40. contentctl/objects/baseline.py +62 -30
  41. contentctl/objects/baseline_tags.py +30 -24
  42. contentctl/objects/config.py +790 -597
  43. contentctl/objects/constants.py +33 -56
  44. contentctl/objects/correlation_search.py +150 -136
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +16 -17
  47. contentctl/objects/deployment.py +43 -44
  48. contentctl/objects/deployment_email.py +3 -2
  49. contentctl/objects/deployment_notable.py +4 -2
  50. contentctl/objects/deployment_phantom.py +7 -6
  51. contentctl/objects/deployment_rba.py +3 -2
  52. contentctl/objects/deployment_scheduling.py +3 -2
  53. contentctl/objects/deployment_slack.py +3 -2
  54. contentctl/objects/detection.py +5 -2
  55. contentctl/objects/detection_metadata.py +1 -0
  56. contentctl/objects/detection_stanza.py +7 -2
  57. contentctl/objects/detection_tags.py +58 -103
  58. contentctl/objects/drilldown.py +66 -34
  59. contentctl/objects/enums.py +81 -100
  60. contentctl/objects/errors.py +16 -24
  61. contentctl/objects/integration_test.py +3 -3
  62. contentctl/objects/integration_test_result.py +1 -0
  63. contentctl/objects/investigation.py +59 -36
  64. contentctl/objects/investigation_tags.py +30 -19
  65. contentctl/objects/lookup.py +304 -101
  66. contentctl/objects/macro.py +55 -39
  67. contentctl/objects/manual_test.py +3 -3
  68. contentctl/objects/manual_test_result.py +1 -0
  69. contentctl/objects/mitre_attack_enrichment.py +17 -16
  70. contentctl/objects/notable_action.py +2 -1
  71. contentctl/objects/notable_event.py +1 -3
  72. contentctl/objects/playbook.py +37 -35
  73. contentctl/objects/playbook_tags.py +23 -13
  74. contentctl/objects/rba.py +96 -0
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +110 -160
  77. contentctl/objects/risk_object.py +1 -0
  78. contentctl/objects/savedsearches_conf.py +9 -7
  79. contentctl/objects/security_content_object.py +5 -2
  80. contentctl/objects/story.py +54 -49
  81. contentctl/objects/story_tags.py +56 -45
  82. contentctl/objects/test_attack_data.py +2 -1
  83. contentctl/objects/test_group.py +5 -2
  84. contentctl/objects/threat_object.py +1 -0
  85. contentctl/objects/throttling.py +27 -18
  86. contentctl/objects/unit_test.py +3 -4
  87. contentctl/objects/unit_test_baseline.py +5 -5
  88. contentctl/objects/unit_test_result.py +6 -6
  89. contentctl/output/api_json_output.py +233 -220
  90. contentctl/output/attack_nav_output.py +21 -21
  91. contentctl/output/attack_nav_writer.py +29 -37
  92. contentctl/output/conf_output.py +235 -172
  93. contentctl/output/conf_writer.py +201 -125
  94. contentctl/output/data_source_writer.py +38 -26
  95. contentctl/output/doc_md_output.py +53 -27
  96. contentctl/output/jinja_writer.py +19 -15
  97. contentctl/output/json_writer.py +21 -11
  98. contentctl/output/svg_output.py +56 -38
  99. contentctl/output/templates/analyticstories_detections.j2 +2 -2
  100. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  101. contentctl/output/templates/collections.j2 +1 -1
  102. contentctl/output/templates/doc_detections.j2 +0 -5
  103. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  104. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  105. contentctl/output/templates/savedsearches_baselines.j2 +2 -2
  106. contentctl/output/templates/savedsearches_detections.j2 +10 -11
  107. contentctl/output/templates/savedsearches_investigations.j2 +2 -2
  108. contentctl/output/templates/transforms.j2 +6 -8
  109. contentctl/output/yml_writer.py +29 -20
  110. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  111. contentctl/templates/stories/cobalt_strike.yml +1 -0
  112. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
  113. contentctl-5.0.0.dist-info/RECORD +168 -0
  114. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
  115. contentctl/actions/initialize_old.py +0 -245
  116. contentctl/objects/event_source.py +0 -11
  117. contentctl/objects/observable.py +0 -37
  118. contentctl/output/detection_writer.py +0 -28
  119. contentctl/output/new_content_yml_output.py +0 -56
  120. contentctl/output/yml_output.py +0 -66
  121. contentctl-4.4.7.dist-info/RECORD +0 -173
  122. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
  123. {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 : str, output_path : str, objects : list) -> None:
10
-
8
+ def writeObjectsList(template_name: str, output_path: str, objects: list) -> None:
11
9
  j2_env = Environment(
12
- loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
13
- trim_blocks=False)
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, 'w') as f:
18
- output = output.encode('ascii', 'ignore').decode('ascii')
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(template_name : str, output_path : str, object: dict[str,Any]) -> None:
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(os.path.join(os.path.dirname(__file__), 'templates')),
27
- trim_blocks=False)
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, 'w') as f:
32
- output = output.encode('ascii', 'ignore').decode('ascii')
33
- f.write(output)
35
+ with open(output_path, "w") as f:
36
+ output = output.encode("ascii", "ignore").decode("ascii")
37
+ f.write(output)
@@ -1,21 +1,31 @@
1
1
  import json
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
5
- class JsonWriter():
2
+ from typing import Any
6
3
 
4
+
5
+ class JsonWriter:
7
6
  @staticmethod
8
- def writeJsonObject(file_path : str, object_name: str, objs: List[dict],readable_output=False) -> None:
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, 'w') as outfile:
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['name'])
15
- json.dump({object_name:sorted_objs}, outfile, ensure_ascii=False, indent=2)
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
- raise Exception(f"Error serializing object to Json File '{file_path}': {str(e)}")
21
-
29
+ raise Exception(
30
+ f"Error serializing object to Json File '{file_path}': {str(e)}"
31
+ )
@@ -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
- def get_badge_dict(self, name:str, total_detections:List[Detection], these_detections:List[Detection])->dict[str,Any]:
13
- obj:dict[str,Any] = {}
14
- obj['name'] = name
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['color'] = "Green"
21
+ obj["color"] = "Green"
18
22
  elif name == "Detections":
19
- obj['color'] = "Green"
23
+ obj["color"] = "Green"
20
24
  elif name == "Experimental":
21
- obj['color'] = "Yellow"
25
+ obj["color"] = "Yellow"
22
26
  elif name == "Deprecated":
23
- obj['color'] = "Red"
27
+ obj["color"] = "Red"
24
28
 
25
- obj['count'] = len(total_detections)
26
- if obj['count'] == 0:
27
- obj['coverage'] = "NaN"
29
+ obj["count"] = len(total_detections)
30
+ if obj["count"] == 0:
31
+ obj["coverage"] = "NaN"
28
32
  else:
29
- obj['coverage'] = len(these_detections) / obj['count']
30
- obj['coverage'] = "{:.0%}".format(obj['coverage'])
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.value }}
8
+ asset_type = {{ detection.tags.asset_type }}
9
9
  confidence = medium
10
- explanation = {{ (detection.explanation if detection.explanation else detection.description) | escapeNewlines() }}
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.description | escapeNewlines() }}
14
+ description = {{ story.status_aware_description | escapeNewlines() }}
15
15
  {% if story.narrative is defined %}
16
16
  narrative = {{ story.narrative | escapeNewlines() }}
17
17
  {% endif %}
@@ -1,6 +1,6 @@
1
1
 
2
2
  {% for lookup in objects %}
3
- {% if lookup.collection is defined and lookup.collection != None %}
3
+ {% if lookup.collection is defined %}
4
4
  [{{ lookup.name }}]
5
5
  enforceTypes = false
6
6
  replicate = false
@@ -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.description | escapeNewlines() }}
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.description | escapeNewlines() }}
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.description | escapeNewlines() }}
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.description | escapeNewlines() }}
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
- {% if detection.status == "deprecated" %}
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.description | escapeNewlines() }}
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.tags.message | escapeNewlines() }}
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.value }}
74
- action.notable.param.severity = {{ detection.tags.severity.value }}
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.description | escapeNewlines() }}
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.description | escapeNewlines() }}
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.filename is defined and lookup.filename != None %}
5
- filename = {{ lookup.filename.name }}
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
- {% endif %}
19
- {% if lookup.match_type is defined and lookup.match_type != None %}
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.fields_list is defined and lookup.fields_list != None %}
29
- fields_list = {{ lookup.fields_list }}
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 }}
@@ -1,17 +1,28 @@
1
-
2
1
  import yaml
3
2
  from typing import Any
3
+ from enum import StrEnum, IntEnum
4
4
 
5
- class YmlWriter:
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
- with open(file_path, 'w') as outfile:
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['data_sources']
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['name'] = obj['name']
38
- output['id'] = obj['id']
39
- output['version'] = obj['version']
40
- output['date'] = obj['date']
41
- output['author'] = obj['author']
42
- output['description'] = obj['description']
43
- output['narrative'] = obj['narrative']
44
- output['references'] = obj['references']
45
- output['tags'] = obj['tags']
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
1
+ Metadata-Version: 2.3
2
2
  Name: contentctl
3
- Version: 4.4.7
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.13
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.8.3,<0.9.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