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
@@ -1,133 +1,180 @@
1
+ import pathlib
2
+ import uuid
3
+ from datetime import datetime
4
+ from typing import Any
1
5
 
2
-
3
- from dataclasses import dataclass
4
6
  import questionary
5
- from typing import Any
7
+
6
8
  from contentctl.input.new_content_questions import NewContentQuestions
7
- from contentctl.output.new_content_yml_output import NewContentYmlOutput
8
- from contentctl.objects.config import new, NewContentType
9
- import uuid
10
- from datetime import datetime
11
- import pathlib
12
- from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import SecurityContentObject_Abstract
9
+ from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
10
+ SecurityContentObject_Abstract,
11
+ )
12
+ from contentctl.objects.config import NewContentType, new
13
+ from contentctl.objects.enums import AssetType
13
14
  from contentctl.output.yml_writer import YmlWriter
14
15
 
16
+
15
17
  class NewContent:
18
+ UPDATE_PREFIX = "__UPDATE__"
19
+
20
+ DEFAULT_DRILLDOWN_DEF = [
21
+ {
22
+ "name": f'View the detection results for - "${UPDATE_PREFIX}FIRST_RISK_OBJECT$" and "${UPDATE_PREFIX}SECOND_RISK_OBJECT$"',
23
+ "search": f'%original_detection_search% | search "${UPDATE_PREFIX}FIRST_RISK_OBJECT = "${UPDATE_PREFIX}FIRST_RISK_OBJECT$" second_observable_type_here = "${UPDATE_PREFIX}SECOND_RISK_OBJECT$"',
24
+ "earliest_offset": "$info_min_time$",
25
+ "latest_offset": "$info_max_time$",
26
+ },
27
+ {
28
+ "name": f'View risk events for the last 7 days for - "${UPDATE_PREFIX}FIRST_RISK_OBJECT$" and "${UPDATE_PREFIX}SECOND_RISK_OBJECT$"',
29
+ "search": f'| from datamodel Risk.All_Risk | search normalized_risk_object IN ("${UPDATE_PREFIX}FIRST_RISK_OBJECT$", "${UPDATE_PREFIX}SECOND_RISK_OBJECT$") starthoursago=168 | 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)`',
30
+ "earliest_offset": "$info_min_time$",
31
+ "latest_offset": "$info_max_time$",
32
+ },
33
+ ]
16
34
 
17
- def buildDetection(self)->dict[str,Any]:
35
+ DEFAULT_RBA = {
36
+ "message": "Risk Message goes here",
37
+ "risk_objects": [{"field": "dest", "type": "system", "score": 10}],
38
+ "threat_objects": [
39
+ {"field": "parent_process_name", "type": "parent_process_name"}
40
+ ],
41
+ }
42
+
43
+ def buildDetection(self) -> tuple[dict[str, Any], str]:
18
44
  questions = NewContentQuestions.get_questions_detection()
19
- answers: dict[str,str] = questionary.prompt(
20
- questions,
21
- kbi_msg="User did not answer all of the prompt questions. Exiting...")
45
+ answers: dict[str, str] = questionary.prompt(
46
+ questions,
47
+ kbi_msg="User did not answer all of the prompt questions. Exiting...",
48
+ )
22
49
  if not answers:
23
50
  raise ValueError("User didn't answer one or more questions!")
24
- answers.update(answers)
25
- answers['name'] = answers['detection_name']
26
- del answers['detection_name']
27
- answers['id'] = str(uuid.uuid4())
28
- answers['version'] = 1
29
- answers['date'] = datetime.today().strftime('%Y-%m-%d')
30
- answers['author'] = answers['detection_author']
31
- del answers['detection_author']
32
- answers['data_source'] = answers['data_source']
33
- answers['type'] = answers['detection_type']
34
- del answers['detection_type']
35
- answers['status'] = "production" #start everything as production since that's what we INTEND the content to become
36
- answers['description'] = 'UPDATE_DESCRIPTION'
37
- file_name = answers['name'].replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower()
38
- answers['search'] = answers['detection_search'] + ' | `' + file_name + '_filter`'
39
- del answers['detection_search']
40
- answers['how_to_implement'] = 'UPDATE_HOW_TO_IMPLEMENT'
41
- answers['known_false_positives'] = 'UPDATE_KNOWN_FALSE_POSITIVES'
42
- answers['references'] = ['REFERENCE']
43
- answers['tags'] = dict()
44
- answers['tags']['analytic_story'] = ['UPDATE_STORY_NAME']
45
- answers['tags']['asset_type'] = 'UPDATE asset_type'
46
- answers['tags']['confidence'] = 'UPDATE value between 1-100'
47
- answers['tags']['impact'] = 'UPDATE value between 1-100'
48
- answers['tags']['message'] = 'UPDATE message'
49
- answers['tags']['mitre_attack_id'] = [x.strip() for x in answers['mitre_attack_ids'].split(',')]
50
- answers['tags']['observable'] = [{'name': 'UPDATE', 'type': 'UPDATE', 'role': ['UPDATE']}]
51
- answers['tags']['product'] = ['Splunk Enterprise','Splunk Enterprise Security','Splunk Cloud']
52
- answers['tags']['required_fields'] = ['UPDATE']
53
- answers['tags']['risk_score'] = 'UPDATE (impact * confidence)/100'
54
- answers['tags']['security_domain'] = answers['security_domain']
55
- del answers["security_domain"]
56
- answers['tags']['cve'] = ['UPDATE WITH CVE(S) IF APPLICABLE']
57
-
58
- #generate the tests section
59
- answers['tests'] = [
60
- {
61
- 'name': "True Positive Test",
62
- 'attack_data': [
63
- {
64
- 'data': "https://github.com/splunk/contentctl/wiki",
65
- "sourcetype": "UPDATE SOURCETYPE",
66
- "source": "UPDATE SOURCE"
67
- }
68
- ]
69
- }
70
- ]
71
- del answers["mitre_attack_ids"]
72
- return answers
73
51
 
74
- def buildStory(self)->dict[str,Any]:
52
+ data_source_field = (
53
+ answers["data_sources"]
54
+ if len(answers["data_sources"]) > 0
55
+ else [f"{NewContent.UPDATE_PREFIX} zero or more data_sources"]
56
+ )
57
+ file_name = (
58
+ answers["detection_name"]
59
+ .replace(" ", "_")
60
+ .replace("-", "_")
61
+ .replace(".", "_")
62
+ .replace("/", "_")
63
+ .lower()
64
+ )
65
+
66
+ # Minimum lenght for a mitre tactic is 5 characters: T1000
67
+ if len(answers["mitre_attack_ids"]) >= 5:
68
+ mitre_attack_ids = [
69
+ x.strip() for x in answers["mitre_attack_ids"].split(",")
70
+ ]
71
+ else:
72
+ # string was too short, so just put a placeholder
73
+ mitre_attack_ids = [
74
+ f"{NewContent.UPDATE_PREFIX} zero or more mitre_attack_ids"
75
+ ]
76
+
77
+ output_file_answers: dict[str, Any] = {
78
+ "name": answers["detection_name"],
79
+ "id": str(uuid.uuid4()),
80
+ "version": 1,
81
+ "date": datetime.today().strftime("%Y-%m-%d"),
82
+ "author": answers["detection_author"],
83
+ "status": "production", # start everything as production since that's what we INTEND the content to become
84
+ "type": answers["detection_type"],
85
+ "description": f"{NewContent.UPDATE_PREFIX} by providing a description of your search",
86
+ "data_source": data_source_field,
87
+ "search": f"{answers['detection_search']} | `{file_name}_filter`",
88
+ "how_to_implement": f"{NewContent.UPDATE_PREFIX} how to implement your search",
89
+ "known_false_positives": f"{NewContent.UPDATE_PREFIX} known false positives for your search",
90
+ "references": [
91
+ f"{NewContent.UPDATE_PREFIX} zero or more http references to provide more information about your search"
92
+ ],
93
+ "drilldown_searches": NewContent.DEFAULT_DRILLDOWN_DEF,
94
+ "rba": NewContent.DEFAULT_RBA,
95
+ "tags": {
96
+ "analytic_story": [
97
+ f"{NewContent.UPDATE_PREFIX} by providing zero or more analytic stories"
98
+ ],
99
+ "asset_type": f"{NewContent.UPDATE_PREFIX} by providing and asset type from {list(AssetType._value2member_map_)}",
100
+ "mitre_attack_id": mitre_attack_ids,
101
+ "product": [
102
+ "Splunk Enterprise",
103
+ "Splunk Enterprise Security",
104
+ "Splunk Cloud",
105
+ ],
106
+ "security_domain": answers["security_domain"],
107
+ "cve": [f"{NewContent.UPDATE_PREFIX} with CVE(s) if applicable"],
108
+ },
109
+ "tests": [
110
+ {
111
+ "name": "True Positive Test",
112
+ "attack_data": [
113
+ {
114
+ "data": f"{NewContent.UPDATE_PREFIX} the data file to replay. Go to https://github.com/splunk/contentctl/wiki for information about the format of this field",
115
+ "sourcetype": f"{NewContent.UPDATE_PREFIX} the sourcetype of your data file.",
116
+ "source": f"{NewContent.UPDATE_PREFIX} the source of your datafile",
117
+ }
118
+ ],
119
+ }
120
+ ],
121
+ }
122
+
123
+ if answers["detection_type"] not in ["TTP", "Anomaly", "Correlation"]:
124
+ del output_file_answers["drilldown_searches"]
125
+
126
+ if answers["detection_type"] not in ["TTP", "Anomaly"]:
127
+ del output_file_answers["rba"]
128
+
129
+ return output_file_answers, answers["detection_kind"]
130
+
131
+ def buildStory(self) -> dict[str, Any]:
75
132
  questions = NewContentQuestions.get_questions_story()
76
133
  answers = questionary.prompt(
77
- questions,
78
- kbi_msg="User did not answer all of the prompt questions. Exiting...")
134
+ questions,
135
+ kbi_msg="User did not answer all of the prompt questions. Exiting...",
136
+ )
79
137
  if not answers:
80
138
  raise ValueError("User didn't answer one or more questions!")
81
- answers['name'] = answers['story_name']
82
- del answers['story_name']
83
- answers['id'] = str(uuid.uuid4())
84
- answers['version'] = 1
85
- answers['date'] = datetime.today().strftime('%Y-%m-%d')
86
- answers['author'] = answers['story_author']
87
- del answers['story_author']
88
- answers['description'] = 'UPDATE_DESCRIPTION'
89
- answers['narrative'] = 'UPDATE_NARRATIVE'
90
- answers['references'] = []
91
- answers['tags'] = dict()
92
- answers['tags']['category'] = answers['category']
93
- del answers['category']
94
- answers['tags']['product'] = ['Splunk Enterprise','Splunk Enterprise Security','Splunk Cloud']
95
- answers['tags']['usecase'] = answers['usecase']
96
- del answers['usecase']
97
- answers['tags']['cve'] = ['UPDATE WITH CVE(S) IF APPLICABLE']
139
+ answers["name"] = answers["story_name"]
140
+ del answers["story_name"]
141
+ answers["id"] = str(uuid.uuid4())
142
+ answers["version"] = 1
143
+ answers["status"] = "production"
144
+ answers["date"] = datetime.today().strftime("%Y-%m-%d")
145
+ answers["author"] = answers["story_author"]
146
+ del answers["story_author"]
147
+ answers["description"] = "UPDATE_DESCRIPTION"
148
+ answers["narrative"] = "UPDATE_NARRATIVE"
149
+ answers["references"] = []
150
+ answers["tags"] = dict()
151
+ answers["tags"]["category"] = answers["category"]
152
+ del answers["category"]
153
+ answers["tags"]["product"] = [
154
+ "Splunk Enterprise",
155
+ "Splunk Enterprise Security",
156
+ "Splunk Cloud",
157
+ ]
158
+ answers["tags"]["usecase"] = answers["usecase"]
159
+ del answers["usecase"]
160
+ answers["tags"]["cve"] = ["UPDATE WITH CVE(S) IF APPLICABLE"]
98
161
  return answers
99
-
100
162
 
101
163
  def execute(self, input_dto: new) -> None:
102
164
  if input_dto.type == NewContentType.detection:
103
- content_dict = self.buildDetection()
104
- subdirectory = pathlib.Path('detections') / content_dict.pop('detection_kind')
165
+ content_dict, detection_kind = self.buildDetection()
166
+ subdirectory = pathlib.Path("detections") / detection_kind
105
167
  elif input_dto.type == NewContentType.story:
106
168
  content_dict = self.buildStory()
107
- subdirectory = pathlib.Path('stories')
169
+ subdirectory = pathlib.Path("stories")
108
170
  else:
109
171
  raise Exception(f"Unsupported new content type: [{input_dto.type}]")
110
172
 
111
- full_output_path = input_dto.path / subdirectory / SecurityContentObject_Abstract.contentNameToFileName(content_dict.get('name'))
173
+ full_output_path = (
174
+ input_dto.path
175
+ / subdirectory
176
+ / SecurityContentObject_Abstract.contentNameToFileName(
177
+ content_dict.get("name")
178
+ )
179
+ )
112
180
  YmlWriter.writeYmlFile(str(full_output_path), content_dict)
113
-
114
-
115
-
116
- def writeObjectNewContent(self, object: dict, subdirectory_name: str, type: NewContentType) -> None:
117
- if type == NewContentType.detection:
118
- file_path = os.path.join(self.output_path, 'detections', subdirectory_name, self.convertNameToFileName(object['name'], object['tags']['product']))
119
- output_folder = pathlib.Path(self.output_path)/'detections'/subdirectory_name
120
- #make sure the output folder exists for this detection
121
- output_folder.mkdir(exist_ok=True)
122
-
123
- YmlWriter.writeDetection(file_path, object)
124
- print("Successfully created detection " + file_path)
125
-
126
- elif type == NewContentType.story:
127
- file_path = os.path.join(self.output_path, 'stories', self.convertNameToFileName(object['name'], object['tags']['product']))
128
- YmlWriter.writeStory(file_path, object)
129
- print("Successfully created story " + file_path)
130
-
131
- else:
132
- raise(Exception(f"Object Must be Story or Detection, but is not: {object}"))
133
-