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
|
@@ -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
|
-
|
|
7
|
+
|
|
6
8
|
from contentctl.input.new_content_questions import NewContentQuestions
|
|
7
|
-
from contentctl.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from
|
|
11
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
82
|
-
del answers[
|
|
83
|
-
answers[
|
|
84
|
-
answers[
|
|
85
|
-
answers[
|
|
86
|
-
answers[
|
|
87
|
-
|
|
88
|
-
answers[
|
|
89
|
-
answers[
|
|
90
|
-
answers[
|
|
91
|
-
answers[
|
|
92
|
-
answers[
|
|
93
|
-
|
|
94
|
-
answers[
|
|
95
|
-
answers[
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|