contentctl 5.0.0a0__py3-none-any.whl → 5.0.0a3__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 +88 -55
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
- 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 +163 -124
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +3 -0
- contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
- 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 +78 -50
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +31 -25
- contentctl/actions/validate.py +54 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +12 -13
- 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 -39
- contentctl/input/director.py +69 -37
- contentctl/input/new_content_questions.py +26 -34
- contentctl/input/yml_reader.py +22 -17
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +250 -314
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
- contentctl/objects/alert_action.py +8 -8
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +64 -54
- contentctl/objects/base_test.py +2 -1
- contentctl/objects/base_test_result.py +16 -8
- contentctl/objects/baseline.py +41 -30
- contentctl/objects/baseline_tags.py +29 -22
- contentctl/objects/config.py +772 -560
- contentctl/objects/constants.py +29 -58
- contentctl/objects/correlation_search.py +75 -55
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +13 -13
- contentctl/objects/deployment.py +44 -37
- contentctl/objects/deployment_email.py +1 -1
- contentctl/objects/deployment_notable.py +2 -1
- contentctl/objects/deployment_phantom.py +5 -5
- contentctl/objects/deployment_rba.py +1 -1
- contentctl/objects/deployment_scheduling.py +1 -1
- contentctl/objects/deployment_slack.py +1 -1
- 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 +54 -64
- contentctl/objects/drilldown.py +66 -35
- contentctl/objects/enums.py +61 -43
- 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 +41 -26
- contentctl/objects/investigation_tags.py +29 -17
- contentctl/objects/lookup.py +234 -113
- contentctl/objects/macro.py +55 -38
- 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 +22 -16
- contentctl/objects/rba.py +14 -8
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +27 -20
- 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 +45 -44
- contentctl/objects/story_tags.py +56 -44
- 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 +4 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +22 -22
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +230 -174
- contentctl/output/data_source_writer.py +38 -25
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +20 -8
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/savedsearches_detections.j2 +1 -1
- contentctl/output/templates/transforms.j2 +2 -2
- contentctl/output/yml_writer.py +18 -24
- {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
- contentctl-5.0.0a3.dist-info/RECORD +168 -0
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/observable.py +0 -39
- contentctl-5.0.0a0.dist-info/RECORD +0 -170
- {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
- {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
- {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py
CHANGED
|
@@ -17,10 +17,12 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
17
17
|
# If we are configured to use the persistent container, then check and see if it's already
|
|
18
18
|
# running. If so, just use it without additional configuration.
|
|
19
19
|
try:
|
|
20
|
-
self.container = self.get_docker_client().containers.get(
|
|
20
|
+
self.container = self.get_docker_client().containers.get(
|
|
21
|
+
self.get_name()
|
|
22
|
+
)
|
|
21
23
|
return
|
|
22
24
|
except Exception:
|
|
23
|
-
#We did not find the container running, we will set it up
|
|
25
|
+
# We did not find the container running, we will set it up
|
|
24
26
|
pass
|
|
25
27
|
|
|
26
28
|
self.container = self.make_container()
|
|
@@ -47,9 +49,10 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
47
49
|
raise (Exception(f"Failed to get docker client: {str(e)}"))
|
|
48
50
|
|
|
49
51
|
def check_for_teardown(self):
|
|
50
|
-
|
|
51
52
|
try:
|
|
52
|
-
container: docker.models.containers.Container =
|
|
53
|
+
container: docker.models.containers.Container = (
|
|
54
|
+
self.get_docker_client().containers.get(self.get_name())
|
|
55
|
+
)
|
|
53
56
|
except Exception as e:
|
|
54
57
|
if self.sync_obj.terminate is not True:
|
|
55
58
|
self.pbar.write(
|
|
@@ -57,7 +60,7 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
57
60
|
)
|
|
58
61
|
self.sync_obj.terminate = True
|
|
59
62
|
else:
|
|
60
|
-
if container.status !=
|
|
63
|
+
if container.status != "running":
|
|
61
64
|
self.sync_obj.terminate = True
|
|
62
65
|
self.container = None
|
|
63
66
|
|
|
@@ -90,28 +93,33 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
90
93
|
environment["SPLUNK_PASSWORD"] = self.infrastructure.splunk_app_password
|
|
91
94
|
# Files have already been staged by the time that we call this. Files must only be staged
|
|
92
95
|
# once, not staged by every container
|
|
93
|
-
environment["SPLUNK_APPS_URL"] =
|
|
96
|
+
environment["SPLUNK_APPS_URL"] = (
|
|
97
|
+
self.global_config.getContainerEnvironmentString(stage_file=False)
|
|
98
|
+
)
|
|
94
99
|
if (
|
|
95
100
|
self.global_config.splunk_api_username is not None
|
|
96
101
|
and self.global_config.splunk_api_password is not None
|
|
97
102
|
):
|
|
98
103
|
environment["SPLUNKBASE_USERNAME"] = self.global_config.splunk_api_username
|
|
99
104
|
environment["SPLUNKBASE_PASSWORD"] = self.global_config.splunk_api_password
|
|
100
|
-
|
|
101
|
-
|
|
102
105
|
|
|
103
106
|
def emit_docker_run_equivalent():
|
|
104
|
-
environment_string = " ".join(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
environment_string = " ".join(
|
|
108
|
+
[f'-e "{k}={environment.get(k)}"' for k in environment.keys()]
|
|
109
|
+
)
|
|
110
|
+
print(
|
|
111
|
+
f"\n\ndocker run -d "
|
|
112
|
+
f"-p {self.infrastructure.web_ui_port}:8000 "
|
|
113
|
+
f"-p {self.infrastructure.hec_port}:8088 "
|
|
114
|
+
f"-p {self.infrastructure.api_port}:8089 "
|
|
115
|
+
f"{environment_string} "
|
|
116
|
+
f" --name {self.get_name()} "
|
|
117
|
+
f"--platform linux/amd64 "
|
|
118
|
+
f"{self.global_config.container_settings.full_image_path}\n\n"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# emit_docker_run_equivalent()
|
|
122
|
+
|
|
115
123
|
container = self.get_docker_client().containers.create(
|
|
116
124
|
self.global_config.container_settings.full_image_path,
|
|
117
125
|
ports=ports_dict,
|
|
@@ -119,20 +127,21 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
119
127
|
name=self.get_name(),
|
|
120
128
|
mounts=mounts,
|
|
121
129
|
detach=True,
|
|
122
|
-
platform="linux/amd64"
|
|
130
|
+
platform="linux/amd64",
|
|
123
131
|
)
|
|
124
|
-
|
|
132
|
+
|
|
125
133
|
if self.global_config.enterpriseSecurityInApps():
|
|
126
|
-
#ES sets up https, so make sure it is included in the link
|
|
134
|
+
# ES sets up https, so make sure it is included in the link
|
|
127
135
|
address = f"https://{self.infrastructure.instance_address}:{self.infrastructure.web_ui_port}"
|
|
128
136
|
else:
|
|
129
137
|
address = f"http://{self.infrastructure.instance_address}:{self.infrastructure.web_ui_port}"
|
|
130
|
-
print(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
print(
|
|
139
|
+
f"\nStarted container with the following information:\n"
|
|
140
|
+
f"\tname : [{self.get_name()}]\n"
|
|
141
|
+
f"\taddress : [{address}]\n"
|
|
142
|
+
f"\tusername: [{self.infrastructure.splunk_app_username}]\n"
|
|
143
|
+
f"\tpassword: [{self.infrastructure.splunk_app_password}]\n"
|
|
144
|
+
)
|
|
136
145
|
|
|
137
146
|
return container
|
|
138
147
|
|
|
@@ -141,15 +150,19 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
141
150
|
container: docker.models.containers.Container = (
|
|
142
151
|
self.get_docker_client().containers.get(self.get_name())
|
|
143
152
|
)
|
|
144
|
-
except Exception
|
|
153
|
+
except Exception:
|
|
145
154
|
# Container does not exist, no need to try and remove it
|
|
146
155
|
return
|
|
147
156
|
try:
|
|
148
157
|
# If the user wants to persist the container (or use a previously configured container), then DO NOT remove it.
|
|
149
|
-
# Emit the following message, which they will see on initial setup and teardown at the end of the test.
|
|
158
|
+
# Emit the following message, which they will see on initial setup and teardown at the end of the test.
|
|
150
159
|
if self.global_config.container_settings.leave_running:
|
|
151
|
-
print(
|
|
152
|
-
|
|
160
|
+
print(
|
|
161
|
+
f"\nContainer [{self.get_name()}] has NOT been terminated because 'contentctl_test.yml ---> infrastructure_config ---> persist_and_reuse_container = True'"
|
|
162
|
+
)
|
|
163
|
+
print(
|
|
164
|
+
f"To remove it, please manually run the following at the command line: `docker container rm -fv {self.get_name()}`\n"
|
|
165
|
+
)
|
|
153
166
|
return
|
|
154
167
|
# container was found, so now we try to remove it
|
|
155
168
|
# v also removes volumes linked to the container
|
|
@@ -8,6 +8,7 @@ class TestReportingType(StrEnum):
|
|
|
8
8
|
"""
|
|
9
9
|
5-char identifiers for the type of testing being reported on
|
|
10
10
|
"""
|
|
11
|
+
|
|
11
12
|
# Reporting around general testing setup (e.g. infra, role configuration)
|
|
12
13
|
SETUP = "SETUP"
|
|
13
14
|
|
|
@@ -25,6 +26,7 @@ class TestingStates(StrEnum):
|
|
|
25
26
|
"""
|
|
26
27
|
Defined testing states
|
|
27
28
|
"""
|
|
29
|
+
|
|
28
30
|
BEGINNING_GROUP = "Beginning Test Group"
|
|
29
31
|
BEGINNING_TEST = "Beginning Test"
|
|
30
32
|
DOWNLOADING = "Downloading Data"
|
|
@@ -47,6 +49,7 @@ class FinalTestingStates(StrEnum):
|
|
|
47
49
|
"""
|
|
48
50
|
The possible final states for a test (for pbar reporting)
|
|
49
51
|
"""
|
|
52
|
+
|
|
50
53
|
FAIL = "\x1b[0;30;41m" + "FAIL ".ljust(LONGEST_STATE) + "\x1b[0m"
|
|
51
54
|
ERROR = "\x1b[0;30;41m" + "ERROR".ljust(LONGEST_STATE) + "\x1b[0m"
|
|
52
55
|
PASS = "\x1b[0;30;42m" + "PASS ".ljust(LONGEST_STATE) + "\x1b[0m"
|
|
@@ -64,7 +64,7 @@ class DetectionTestingView(BaseModel, abc.ABC):
|
|
|
64
64
|
try:
|
|
65
65
|
runtime = self.getRuntime()
|
|
66
66
|
time_per_detection = runtime / num_tested
|
|
67
|
-
remaining_time = (num_untested
|
|
67
|
+
remaining_time = (num_untested + 0.5) * time_per_detection
|
|
68
68
|
remaining_time -= datetime.timedelta(
|
|
69
69
|
microseconds=remaining_time.microseconds
|
|
70
70
|
)
|
|
@@ -74,7 +74,14 @@ class DetectionTestingView(BaseModel, abc.ABC):
|
|
|
74
74
|
|
|
75
75
|
def getSummaryObject(
|
|
76
76
|
self,
|
|
77
|
-
test_result_fields: list[str] = [
|
|
77
|
+
test_result_fields: list[str] = [
|
|
78
|
+
"success",
|
|
79
|
+
"message",
|
|
80
|
+
"exception",
|
|
81
|
+
"status",
|
|
82
|
+
"duration",
|
|
83
|
+
"wait_duration",
|
|
84
|
+
],
|
|
78
85
|
test_job_fields: list[str] = ["resultCount", "runDuration"],
|
|
79
86
|
) -> dict[str, dict[str, Any] | list[dict[str, Any]] | str]:
|
|
80
87
|
"""
|
|
@@ -110,11 +117,11 @@ class DetectionTestingView(BaseModel, abc.ABC):
|
|
|
110
117
|
total_skipped += 1
|
|
111
118
|
|
|
112
119
|
# Aggregate production status metrics
|
|
113
|
-
if detection.status == DetectionStatus.production:
|
|
120
|
+
if detection.status == DetectionStatus.production:
|
|
114
121
|
total_production += 1
|
|
115
|
-
elif detection.status == DetectionStatus.experimental:
|
|
122
|
+
elif detection.status == DetectionStatus.experimental:
|
|
116
123
|
total_experimental += 1
|
|
117
|
-
elif detection.status == DetectionStatus.deprecated:
|
|
124
|
+
elif detection.status == DetectionStatus.deprecated:
|
|
118
125
|
total_deprecated += 1
|
|
119
126
|
|
|
120
127
|
# Check if the detection is manual_test
|
|
@@ -128,19 +135,11 @@ class DetectionTestingView(BaseModel, abc.ABC):
|
|
|
128
135
|
tested_detections.append(summary)
|
|
129
136
|
|
|
130
137
|
# Sort tested detections s.t. all failures appear first, then by name
|
|
131
|
-
tested_detections.sort(
|
|
132
|
-
key=lambda x: (
|
|
133
|
-
x["success"],
|
|
134
|
-
x["name"]
|
|
135
|
-
)
|
|
136
|
-
)
|
|
138
|
+
tested_detections.sort(key=lambda x: (x["success"], x["name"]))
|
|
137
139
|
|
|
138
140
|
# Sort skipped detections s.t. detections w/ tests appear before those w/o, then by name
|
|
139
141
|
skipped_detections.sort(
|
|
140
|
-
key=lambda x: (
|
|
141
|
-
0 if len(x["tests"]) > 0 else 1,
|
|
142
|
-
x["name"]
|
|
143
|
-
)
|
|
142
|
+
key=lambda x: (0 if len(x["tests"]) > 0 else 1, x["name"])
|
|
144
143
|
)
|
|
145
144
|
|
|
146
145
|
# TODO (#267): Align test reporting more closely w/ status enums (as it relates to
|
|
@@ -170,9 +169,7 @@ class DetectionTestingView(BaseModel, abc.ABC):
|
|
|
170
169
|
percent_complete = Utils.getPercent(
|
|
171
170
|
len(tested_detections), len(untested_detections), 1
|
|
172
171
|
)
|
|
173
|
-
success_rate = Utils.getPercent(
|
|
174
|
-
total_pass, total_tested_detections, 1
|
|
175
|
-
)
|
|
172
|
+
success_rate = Utils.getPercent(total_pass, total_tested_detections, 1)
|
|
176
173
|
|
|
177
174
|
# TODO (#230): expand testing metrics reported (and make nested)
|
|
178
175
|
# Construct and return the larger results dict
|
|
@@ -3,7 +3,6 @@ from contentctl.actions.detection_testing.views.DetectionTestingView import (
|
|
|
3
3
|
)
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
|
-
import datetime
|
|
7
6
|
import tqdm
|
|
8
7
|
|
|
9
8
|
|
|
@@ -39,15 +38,12 @@ class DetectionTestingViewCLI(DetectionTestingView, arbitrary_types_allowed=True
|
|
|
39
38
|
miniters=0,
|
|
40
39
|
mininterval=0,
|
|
41
40
|
)
|
|
42
|
-
|
|
43
|
-
len(self.sync_obj.outputQueue), len(self.sync_obj.inputQueue)
|
|
44
|
-
)
|
|
41
|
+
self.format_pbar(len(self.sync_obj.outputQueue), len(self.sync_obj.inputQueue))
|
|
45
42
|
|
|
46
43
|
self.showStatus()
|
|
47
44
|
|
|
48
45
|
# TODO (#267): Align test reporting more closely w/ status enums (as it relates to "untested")
|
|
49
46
|
def showStatus(self, interval: int = 1):
|
|
50
|
-
|
|
51
47
|
while True:
|
|
52
48
|
summary = self.getSummaryObject()
|
|
53
49
|
|
|
@@ -13,7 +13,7 @@ class DetectionTestingViewFile(DetectionTestingView):
|
|
|
13
13
|
output_filename: str = OUTPUT_FILENAME
|
|
14
14
|
|
|
15
15
|
def getOutputFilePath(self) -> pathlib.Path:
|
|
16
|
-
folder_path = pathlib.Path(
|
|
16
|
+
folder_path = pathlib.Path(".") / self.output_folder
|
|
17
17
|
output_file = folder_path / self.output_filename
|
|
18
18
|
|
|
19
19
|
return output_file
|
|
@@ -22,7 +22,7 @@ class DetectionTestingViewFile(DetectionTestingView):
|
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
def stop(self):
|
|
25
|
-
folder_path = pathlib.Path(
|
|
25
|
+
folder_path = pathlib.Path(".") / self.output_folder
|
|
26
26
|
output_file = self.getOutputFilePath()
|
|
27
27
|
|
|
28
28
|
folder_path.mkdir(parents=True, exist_ok=True)
|
|
@@ -102,9 +102,7 @@ class SimpleWebServer(ServerAdapter):
|
|
|
102
102
|
class DetectionTestingViewWeb(DetectionTestingView):
|
|
103
103
|
bottleApp: Bottle = Bottle()
|
|
104
104
|
server: SimpleWebServer = SimpleWebServer(host="0.0.0.0", port=DEFAULT_WEB_UI_PORT)
|
|
105
|
-
model_config = ConfigDict(
|
|
106
|
-
arbitrary_types_allowed=True
|
|
107
|
-
)
|
|
105
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
108
106
|
|
|
109
107
|
def setup(self):
|
|
110
108
|
self.bottleApp.route("/", callback=self.showStatus)
|
|
@@ -123,7 +121,6 @@ class DetectionTestingViewWeb(DetectionTestingView):
|
|
|
123
121
|
print(f"Could not open webbrowser for status page: {str(e)}")
|
|
124
122
|
|
|
125
123
|
def stop(self):
|
|
126
|
-
|
|
127
124
|
if self.server.server is None:
|
|
128
125
|
print("Web Server is not running anyway - nothing to shut down")
|
|
129
126
|
return
|
contentctl/actions/doc_gen.py
CHANGED
|
@@ -10,17 +10,21 @@ from contentctl.output.doc_md_output import DocMdOutput
|
|
|
10
10
|
class DocGenInputDto:
|
|
11
11
|
director_input_dto: DirectorInputDto
|
|
12
12
|
|
|
13
|
-
class DocGen:
|
|
14
13
|
|
|
14
|
+
class DocGen:
|
|
15
15
|
def execute(self, input_dto: DocGenInputDto) -> None:
|
|
16
|
-
director_output_dto = DirectorOutputDto([],[],[],[],[],[],[],[],[],[])
|
|
16
|
+
director_output_dto = DirectorOutputDto([], [], [], [], [], [], [], [], [], [])
|
|
17
17
|
director = Director(director_output_dto)
|
|
18
18
|
director.execute(input_dto.director_input_dto)
|
|
19
19
|
|
|
20
20
|
doc_md_output = DocMdOutput()
|
|
21
21
|
doc_md_output.writeObjects(
|
|
22
|
-
[
|
|
23
|
-
|
|
22
|
+
[
|
|
23
|
+
director_output_dto.stories,
|
|
24
|
+
director_output_dto.detections,
|
|
25
|
+
director_output_dto.playbooks,
|
|
26
|
+
],
|
|
27
|
+
os.path.join(input_dto.director_input_dto.input_path, "docs"),
|
|
24
28
|
)
|
|
25
29
|
|
|
26
|
-
print(
|
|
30
|
+
print("Generating Docs of security content successful.")
|
contentctl/actions/initialize.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import shutil
|
|
3
2
|
import os
|
|
4
3
|
import pathlib
|
|
@@ -7,57 +6,70 @@ from contentctl.output.yml_writer import YmlWriter
|
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class Initialize:
|
|
10
|
-
|
|
11
9
|
def execute(self, config: test) -> None:
|
|
12
10
|
# construct a test object from the init object
|
|
13
11
|
# This way we can easily populate a yml with ALL the important
|
|
14
|
-
# fields for validating, building, and testing your app.
|
|
15
|
-
|
|
16
|
-
YmlWriter.writeYmlFile(str(config.path/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
# fields for validating, building, and testing your app.
|
|
13
|
+
|
|
14
|
+
YmlWriter.writeYmlFile(str(config.path / "contentctl.yml"), config.model_dump())
|
|
15
|
+
|
|
16
|
+
# Create the following empty directories:
|
|
17
|
+
for emptyDir in [
|
|
18
|
+
"lookups",
|
|
19
|
+
"baselines",
|
|
20
|
+
"data_sources",
|
|
21
|
+
"docs",
|
|
22
|
+
"reporting",
|
|
23
|
+
"investigations",
|
|
24
|
+
"detections/application",
|
|
25
|
+
"detections/cloud",
|
|
26
|
+
"detections/endpoint",
|
|
27
|
+
"detections/network",
|
|
28
|
+
"detections/web",
|
|
29
|
+
"macros",
|
|
30
|
+
"stories",
|
|
31
|
+
]:
|
|
32
|
+
# Throw an error if this directory already exists
|
|
33
|
+
(config.path / emptyDir).mkdir(exist_ok=False, parents=True)
|
|
25
34
|
|
|
26
35
|
# If this is not a bare config, then populate
|
|
27
36
|
# a small amount of content into the directories
|
|
28
37
|
if not config.bare:
|
|
29
|
-
#copy the contents of all template directories
|
|
38
|
+
# copy the contents of all template directories
|
|
30
39
|
for templateDir, targetDir in [
|
|
31
|
-
(
|
|
32
|
-
(
|
|
33
|
-
(
|
|
34
|
-
(
|
|
40
|
+
("../templates/detections/", "detections"),
|
|
41
|
+
("../templates/data_sources/", "data_sources"),
|
|
42
|
+
("../templates/macros/", "macros"),
|
|
43
|
+
("../templates/stories/", "stories"),
|
|
35
44
|
]:
|
|
36
|
-
source_directory = pathlib.Path(os.path.dirname(__file__))/templateDir
|
|
37
|
-
target_directory = config.path/targetDir
|
|
38
|
-
|
|
39
|
-
# Do not throw an exception if the directory exists. In fact, it was
|
|
45
|
+
source_directory = pathlib.Path(os.path.dirname(__file__)) / templateDir
|
|
46
|
+
target_directory = config.path / targetDir
|
|
47
|
+
|
|
48
|
+
# Do not throw an exception if the directory exists. In fact, it was
|
|
40
49
|
# created above when the structure of the app was created.
|
|
41
50
|
shutil.copytree(source_directory, target_directory, dirs_exist_ok=True)
|
|
42
|
-
|
|
51
|
+
|
|
43
52
|
# The contents of app_template must ALWAYS be copied because it contains
|
|
44
53
|
# several special files.
|
|
45
54
|
# For now, we also copy the deployments because the ability to create custom
|
|
46
55
|
# deployment files is limited with built-in functionality.
|
|
47
56
|
for templateDir, targetDir in [
|
|
48
|
-
(
|
|
49
|
-
(
|
|
57
|
+
("../templates/app_template/", "app_template"),
|
|
58
|
+
("../templates/deployments/", "deployments"),
|
|
50
59
|
]:
|
|
51
|
-
source_directory = pathlib.Path(os.path.dirname(__file__))/templateDir
|
|
52
|
-
target_directory = config.path/targetDir
|
|
53
|
-
#Throw an exception if the target exists
|
|
60
|
+
source_directory = pathlib.Path(os.path.dirname(__file__)) / templateDir
|
|
61
|
+
target_directory = config.path / targetDir
|
|
62
|
+
# Throw an exception if the target exists
|
|
54
63
|
shutil.copytree(source_directory, target_directory, dirs_exist_ok=False)
|
|
55
64
|
|
|
56
65
|
# Create a README.md file. Note that this is the README.md for the repository, not the
|
|
57
66
|
# one which will actually be packaged into the app. That is located in the app_template folder.
|
|
58
|
-
shutil.copyfile(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"Please run 'contentctl new --type {detection,story}' to create new content")
|
|
67
|
+
shutil.copyfile(
|
|
68
|
+
pathlib.Path(os.path.dirname(__file__)) / "../templates/README.md",
|
|
69
|
+
"README.md",
|
|
70
|
+
)
|
|
63
71
|
|
|
72
|
+
print(
|
|
73
|
+
f"The app '{config.app.title}' has been initialized. "
|
|
74
|
+
"Please run 'contentctl new --type {detection,story}' to create new content"
|
|
75
|
+
)
|