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
contentctl/actions/test.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from typing import List
|
|
3
3
|
|
|
4
|
-
from contentctl.objects.config import test_common
|
|
5
|
-
from contentctl.objects.enums import DetectionTestingMode, DetectionStatus, AnalyticsType
|
|
4
|
+
from contentctl.objects.config import test_common, Selected, Changes
|
|
6
5
|
from contentctl.objects.detection import Detection
|
|
7
6
|
|
|
8
|
-
from contentctl.input.director import DirectorOutputDto
|
|
9
7
|
|
|
10
8
|
from contentctl.actions.detection_testing.DetectionTestingManager import (
|
|
11
9
|
DetectionTestingManager,
|
|
@@ -41,7 +39,7 @@ MAXIMUM_CONFIGURATION_TIME_SECONDS = 600
|
|
|
41
39
|
class TestInputDto:
|
|
42
40
|
detections: List[Detection]
|
|
43
41
|
config: test_common
|
|
44
|
-
|
|
42
|
+
|
|
45
43
|
|
|
46
44
|
class Test:
|
|
47
45
|
def filter_tests(self, input_dto: TestInputDto) -> None:
|
|
@@ -52,7 +50,7 @@ class Test:
|
|
|
52
50
|
Args:
|
|
53
51
|
input_dto (TestInputDto): A configuration of the test and all of the
|
|
54
52
|
tests to be run.
|
|
55
|
-
"""
|
|
53
|
+
"""
|
|
56
54
|
|
|
57
55
|
if not input_dto.config.enable_integration_testing:
|
|
58
56
|
# Skip all integraiton tests if integration testing is not enabled:
|
|
@@ -61,7 +59,6 @@ class Test:
|
|
|
61
59
|
if isinstance(test, IntegrationTest):
|
|
62
60
|
test.skip("TEST SKIPPED: Skipping all integration tests")
|
|
63
61
|
|
|
64
|
-
|
|
65
62
|
def execute(self, input_dto: TestInputDto) -> bool:
|
|
66
63
|
output_dto = DetectionTestingManagerOutputDto()
|
|
67
64
|
|
|
@@ -78,10 +75,9 @@ class Test:
|
|
|
78
75
|
input_dto=manager_input_dto, output_dto=output_dto
|
|
79
76
|
)
|
|
80
77
|
|
|
81
|
-
mode = input_dto.config.getModeName()
|
|
82
78
|
if len(input_dto.detections) == 0:
|
|
83
79
|
print(
|
|
84
|
-
f"With Detection Testing Mode '{mode}', there were [0] detections found to test."
|
|
80
|
+
f"With Detection Testing Mode '{input_dto.config.mode.mode_name}', there were [0] detections found to test."
|
|
85
81
|
"\nAs such, we will quit immediately."
|
|
86
82
|
)
|
|
87
83
|
# Directly call stop so that the summary.yml will be generated. Of course it will not
|
|
@@ -89,10 +85,21 @@ class Test:
|
|
|
89
85
|
# detections were tested.
|
|
90
86
|
file.stop()
|
|
91
87
|
else:
|
|
92
|
-
print(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
print(
|
|
89
|
+
f"MODE: [{input_dto.config.mode.mode_name}] - Test [{len(input_dto.detections)}] detections"
|
|
90
|
+
)
|
|
91
|
+
if isinstance(input_dto.config.mode, Selected) or isinstance(
|
|
92
|
+
input_dto.config.mode, Changes
|
|
93
|
+
):
|
|
94
|
+
files_string = "\n- ".join(
|
|
95
|
+
[
|
|
96
|
+
str(
|
|
97
|
+
pathlib.Path(detection.file_path).relative_to(
|
|
98
|
+
input_dto.config.path
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
for detection in input_dto.detections
|
|
102
|
+
]
|
|
96
103
|
)
|
|
97
104
|
print(f"Detections:\n- {files_string}")
|
|
98
105
|
|
|
@@ -103,43 +110,41 @@ class Test:
|
|
|
103
110
|
summary_results = file.getSummaryObject()
|
|
104
111
|
summary = summary_results.get("summary", {})
|
|
105
112
|
|
|
106
|
-
print(f"Test Summary (mode: {summary.get('mode','Error')})")
|
|
107
|
-
print(f"\tSuccess : {summary.get('success',False)}")
|
|
108
|
-
print(
|
|
109
|
-
f"\tSuccess Rate : {summary.get('success_rate','ERROR')}"
|
|
110
|
-
)
|
|
113
|
+
print(f"Test Summary (mode: {summary.get('mode', 'Error')})")
|
|
114
|
+
print(f"\tSuccess : {summary.get('success', False)}")
|
|
111
115
|
print(
|
|
112
|
-
f"\
|
|
116
|
+
f"\tSuccess Rate : {summary.get('success_rate', 'ERROR')}"
|
|
113
117
|
)
|
|
114
118
|
print(
|
|
115
|
-
f"\tTotal
|
|
119
|
+
f"\tTotal Detections : {summary.get('total_detections', 'ERROR')}"
|
|
116
120
|
)
|
|
117
121
|
print(
|
|
118
|
-
f"\
|
|
122
|
+
f"\tTotal Tested Detections : {summary.get('total_tested_detections', 'ERROR')}"
|
|
119
123
|
)
|
|
120
124
|
print(
|
|
121
|
-
f"\t
|
|
125
|
+
f"\t Passed Detections : {summary.get('total_pass', 'ERROR')}"
|
|
122
126
|
)
|
|
123
127
|
print(
|
|
124
|
-
f"\
|
|
128
|
+
f"\t Failed Detections : {summary.get('total_fail', 'ERROR')}"
|
|
125
129
|
)
|
|
126
130
|
print(
|
|
127
|
-
"\
|
|
131
|
+
f"\tSkipped Detections : {summary.get('total_skipped', 'ERROR')}"
|
|
128
132
|
)
|
|
133
|
+
print("\tProduction Status :")
|
|
129
134
|
print(
|
|
130
|
-
f"\t Production Detections : {summary.get('total_production','ERROR')}"
|
|
135
|
+
f"\t Production Detections : {summary.get('total_production', 'ERROR')}"
|
|
131
136
|
)
|
|
132
137
|
print(
|
|
133
|
-
f"\t Experimental Detections : {summary.get('total_experimental','ERROR')}"
|
|
138
|
+
f"\t Experimental Detections : {summary.get('total_experimental', 'ERROR')}"
|
|
134
139
|
)
|
|
135
140
|
print(
|
|
136
|
-
f"\t Deprecated Detections : {summary.get('total_deprecated','ERROR')}"
|
|
141
|
+
f"\t Deprecated Detections : {summary.get('total_deprecated', 'ERROR')}"
|
|
137
142
|
)
|
|
138
143
|
print(
|
|
139
|
-
f"\tManually Tested Detections : {summary.get('total_manual','ERROR')}"
|
|
144
|
+
f"\tManually Tested Detections : {summary.get('total_manual', 'ERROR')}"
|
|
140
145
|
)
|
|
141
146
|
print(
|
|
142
|
-
f"\tUntested Detections : {summary.get('total_untested','ERROR')}"
|
|
147
|
+
f"\tUntested Detections : {summary.get('total_untested', 'ERROR')}"
|
|
143
148
|
)
|
|
144
149
|
print(f"\tTest Results File : {file.getOutputFilePath()}")
|
|
145
150
|
print(
|
contentctl/actions/validate.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import pathlib
|
|
3
2
|
|
|
4
3
|
from contentctl.input.director import Director, DirectorOutputDto
|
|
@@ -6,6 +5,7 @@ from contentctl.objects.config import validate
|
|
|
6
5
|
from contentctl.enrichments.attack_enrichment import AttackEnrichment
|
|
7
6
|
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
8
7
|
from contentctl.objects.atomic import AtomicEnrichment
|
|
8
|
+
from contentctl.objects.lookup import FileBackedLookup
|
|
9
9
|
from contentctl.helper.utils import Utils
|
|
10
10
|
from contentctl.objects.data_source import DataSource
|
|
11
11
|
from contentctl.helper.splunk_app import SplunkApp
|
|
@@ -26,7 +26,7 @@ class Validate:
|
|
|
26
26
|
[],
|
|
27
27
|
[],
|
|
28
28
|
[],
|
|
29
|
-
[]
|
|
29
|
+
[],
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
director = Director(director_output_dto)
|
|
@@ -34,51 +34,69 @@ class Validate:
|
|
|
34
34
|
self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto)
|
|
35
35
|
if input_dto.data_source_TA_validation:
|
|
36
36
|
self.validate_latest_TA_information(director_output_dto.data_sources)
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
return director_output_dto
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
def ensure_no_orphaned_files_in_lookups(
|
|
41
|
+
self, repo_path: pathlib.Path, director_output_dto: DirectorOutputDto
|
|
42
|
+
):
|
|
42
43
|
"""
|
|
43
44
|
This function ensures that only files which are relevant to lookups are included in the lookups folder.
|
|
44
45
|
This means that a file must be either:
|
|
45
46
|
1. A lookup YML (.yml)
|
|
46
47
|
2. A lookup CSV (.csv) which is referenced by a YML
|
|
47
48
|
3. A lookup MLMODEL (.mlmodel) which is referenced by a YML.
|
|
48
|
-
|
|
49
|
+
|
|
49
50
|
All other files, includes CSV and MLMODEL files which are NOT
|
|
50
51
|
referenced by a YML, will generate an exception from this function.
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
Args:
|
|
53
54
|
repo_path (pathlib.Path): path to the root of the app
|
|
54
55
|
director_output_dto (DirectorOutputDto): director object with all constructed content
|
|
55
56
|
|
|
56
57
|
Raises:
|
|
57
|
-
Exception: An Exception will be raised if there are any non .yml, .csv, or .mlmodel
|
|
58
|
-
files in this directory. Additionally, an exception will be raised if there
|
|
59
|
-
exists one or more .csv or .mlmodel files that are not referenced by at least 1
|
|
60
|
-
detection .yml file in this directory.
|
|
58
|
+
Exception: An Exception will be raised if there are any non .yml, .csv, or .mlmodel
|
|
59
|
+
files in this directory. Additionally, an exception will be raised if there
|
|
60
|
+
exists one or more .csv or .mlmodel files that are not referenced by at least 1
|
|
61
|
+
detection .yml file in this directory.
|
|
61
62
|
This avoids having additional, unused files in this directory that may be copied into
|
|
62
63
|
the app when it is built (which can cause appinspect errors or larger app size.)
|
|
63
|
-
"""
|
|
64
|
-
lookupsDirectory = repo_path/"lookups"
|
|
65
|
-
|
|
64
|
+
"""
|
|
65
|
+
lookupsDirectory = repo_path / "lookups"
|
|
66
|
+
|
|
66
67
|
# Get all of the files referneced by Lookups
|
|
67
|
-
usedLookupFiles:list[pathlib.Path] = [
|
|
68
|
+
usedLookupFiles: list[pathlib.Path] = [
|
|
69
|
+
lookup.filename
|
|
70
|
+
for lookup in director_output_dto.lookups
|
|
71
|
+
if isinstance(lookup, FileBackedLookup)
|
|
72
|
+
] + [
|
|
73
|
+
lookup.file_path
|
|
74
|
+
for lookup in director_output_dto.lookups
|
|
75
|
+
if lookup.file_path is not None
|
|
76
|
+
]
|
|
68
77
|
|
|
69
78
|
# Get all of the mlmodel and csv files in the lookups directory
|
|
70
|
-
csvAndMlmodelFiles
|
|
71
|
-
|
|
79
|
+
csvAndMlmodelFiles = Utils.get_security_content_files_from_directory(
|
|
80
|
+
lookupsDirectory,
|
|
81
|
+
allowedFileExtensions=[".yml", ".csv", ".mlmodel"],
|
|
82
|
+
fileExtensionsToReturn=[".csv", ".mlmodel"],
|
|
83
|
+
)
|
|
84
|
+
|
|
72
85
|
# Generate an exception of any csv or mlmodel files exist but are not used
|
|
73
|
-
unusedLookupFiles:list[pathlib.Path] = [
|
|
86
|
+
unusedLookupFiles: list[pathlib.Path] = [
|
|
87
|
+
testFile
|
|
88
|
+
for testFile in csvAndMlmodelFiles
|
|
89
|
+
if testFile not in usedLookupFiles
|
|
90
|
+
]
|
|
74
91
|
if len(unusedLookupFiles) > 0:
|
|
75
|
-
raise Exception(
|
|
92
|
+
raise Exception(
|
|
93
|
+
f"The following .csv or .mlmodel files exist in '{lookupsDirectory}', but are not referenced by a lookup file: {[str(path) for path in unusedLookupFiles]}"
|
|
94
|
+
)
|
|
76
95
|
return
|
|
77
|
-
|
|
78
96
|
|
|
79
97
|
def validate_latest_TA_information(self, data_sources: list[DataSource]) -> None:
|
|
80
98
|
validated_TAs: list[tuple[str, str]] = []
|
|
81
|
-
errors:list[str] = []
|
|
99
|
+
errors: list[str] = []
|
|
82
100
|
print("----------------------")
|
|
83
101
|
print("Validating latest TA:")
|
|
84
102
|
print("----------------------")
|
|
@@ -89,22 +107,25 @@ class Validate:
|
|
|
89
107
|
continue
|
|
90
108
|
if supported_TA.url is not None:
|
|
91
109
|
validated_TAs.append(ta_identifier)
|
|
92
|
-
uid = int(str(supported_TA.url).rstrip(
|
|
110
|
+
uid = int(str(supported_TA.url).rstrip("/").split("/")[-1])
|
|
93
111
|
try:
|
|
94
112
|
splunk_app = SplunkApp(app_uid=uid)
|
|
95
113
|
if splunk_app.latest_version != supported_TA.version:
|
|
96
|
-
errors.append(
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
errors.append(
|
|
115
|
+
f"Version mismatch in '{data_source.file_path}' supported TA '{supported_TA.name}'"
|
|
116
|
+
f"\n Latest version on Splunkbase : {splunk_app.latest_version}"
|
|
117
|
+
f"\n Version specified in data source: {supported_TA.version}"
|
|
118
|
+
)
|
|
99
119
|
except Exception as e:
|
|
100
|
-
errors.append(
|
|
101
|
-
|
|
120
|
+
errors.append(
|
|
121
|
+
f"Error processing checking version of TA {supported_TA.name}: {str(e)}"
|
|
122
|
+
)
|
|
123
|
+
|
|
102
124
|
if len(errors) > 0:
|
|
103
|
-
errorString =
|
|
104
|
-
raise Exception(
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
errorString = "\n\n".join(errors)
|
|
126
|
+
raise Exception(
|
|
127
|
+
f"[{len(errors)}] or more TA versions are out of date or have other errors."
|
|
128
|
+
f"Please update the following data sources with the latest versions of "
|
|
129
|
+
f"their supported tas:\n\n{errorString}"
|
|
130
|
+
)
|
|
107
131
|
print("All TA versions are up to date.")
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
contentctl/api.py
CHANGED
|
@@ -5,42 +5,48 @@ from contentctl.objects.config import test_common, test, test_servers
|
|
|
5
5
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
6
6
|
from contentctl.input.director import DirectorOutputDto
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
|
|
9
|
+
def config_from_file(
|
|
10
|
+
path: Path = Path("contentctl.yml"),
|
|
11
|
+
config: dict[str, Any] = {},
|
|
12
|
+
configType: Type[Union[test, test_servers]] = test,
|
|
13
|
+
) -> test_common:
|
|
11
14
|
"""
|
|
12
15
|
Fetch a configuration object that can be used for a number of different contentctl
|
|
13
|
-
operations including validate, build, inspect, test, and test_servers. A file will
|
|
16
|
+
operations including validate, build, inspect, test, and test_servers. A file will
|
|
14
17
|
be used as the basis for constructing the configuration.
|
|
15
18
|
|
|
16
19
|
Args:
|
|
17
|
-
path (Path, optional): Relative or absolute path to a contentctl config file.
|
|
20
|
+
path (Path, optional): Relative or absolute path to a contentctl config file.
|
|
18
21
|
Defaults to Path("contentctl.yml"), which is the default name and location (in the current directory)
|
|
19
22
|
of the configuration files which are automatically generated for contentctl.
|
|
20
23
|
config (dict[], optional): Dictionary of values to override values read from the YML
|
|
21
24
|
path passed as the first argument. Defaults to {}, an empty dict meaning that nothing
|
|
22
|
-
will be overwritten
|
|
23
|
-
configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
|
|
25
|
+
will be overwritten
|
|
26
|
+
configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
|
|
24
27
|
This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
|
|
25
28
|
Returns:
|
|
26
29
|
test_common: Returns a complete contentctl test_common configuration. Note that this configuration
|
|
27
30
|
will have all applicable field for validate and build as well, but can also be used for easily
|
|
28
|
-
construction a test or test_servers object.
|
|
29
|
-
"""
|
|
31
|
+
construction a test or test_servers object.
|
|
32
|
+
"""
|
|
30
33
|
|
|
31
34
|
try:
|
|
32
35
|
yml_dict = YmlReader.load_file(path, add_fields=False)
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
|
|
35
37
|
except Exception as e:
|
|
36
|
-
raise Exception(
|
|
37
|
-
|
|
38
|
+
raise Exception(
|
|
39
|
+
f"Failed to load contentctl configuration from file '{path}': {str(e)}"
|
|
40
|
+
)
|
|
41
|
+
|
|
38
42
|
# Apply settings that have been overridden from the ones in the file
|
|
39
43
|
try:
|
|
40
44
|
yml_dict.update(config)
|
|
41
45
|
except Exception as e:
|
|
42
|
-
raise Exception(
|
|
43
|
-
|
|
46
|
+
raise Exception(
|
|
47
|
+
f"Failed updating dictionary of values read from file '{path}'"
|
|
48
|
+
f" with the dictionary of arguments passed: {str(e)}"
|
|
49
|
+
)
|
|
44
50
|
|
|
45
51
|
# The function below will throw its own descriptive exception if it fails
|
|
46
52
|
configObject = config_from_dict(yml_dict, configType=configType)
|
|
@@ -48,13 +54,12 @@ def config_from_file(path:Path=Path("contentctl.yml"), config: dict[str,Any]={},
|
|
|
48
54
|
return configObject
|
|
49
55
|
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
configType:Type[Union[test,test_servers]]=test)->test_common:
|
|
57
|
+
def config_from_dict(
|
|
58
|
+
config: dict[str, Any] = {}, configType: Type[Union[test, test_servers]] = test
|
|
59
|
+
) -> test_common:
|
|
55
60
|
"""
|
|
56
61
|
Fetch a configuration object that can be used for a number of different contentctl
|
|
57
|
-
operations including validate, build, inspect, test, and test_servers. A dict will
|
|
62
|
+
operations including validate, build, inspect, test, and test_servers. A dict will
|
|
58
63
|
be used as the basis for constructing the configuration.
|
|
59
64
|
|
|
60
65
|
Args:
|
|
@@ -63,29 +68,30 @@ def config_from_dict(config: dict[str,Any]={},
|
|
|
63
68
|
values. Note that based on default values in the contentctl/objects/config.py
|
|
64
69
|
file, this may raise an exception. If so, please set appropriate default values
|
|
65
70
|
in the file above or supply those values via this argument.
|
|
66
|
-
configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
|
|
71
|
+
configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
|
|
67
72
|
This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
|
|
68
73
|
Returns:
|
|
69
74
|
test_common: Returns a complete contentctl test_common configuration. Note that this configuration
|
|
70
75
|
will have all applicable field for validate and build as well, but can also be used for easily
|
|
71
|
-
construction a test or test_servers object.
|
|
72
|
-
"""
|
|
76
|
+
construction a test or test_servers object.
|
|
77
|
+
"""
|
|
73
78
|
try:
|
|
74
79
|
test_object = configType.model_validate(config)
|
|
75
80
|
except Exception as e:
|
|
76
81
|
raise Exception(f"Failed to load contentctl configuration from dict:\n{str(e)}")
|
|
77
|
-
|
|
82
|
+
|
|
78
83
|
return test_object
|
|
79
84
|
|
|
80
85
|
|
|
81
|
-
def update_config(
|
|
82
|
-
|
|
86
|
+
def update_config(
|
|
87
|
+
config: Union[test, test_servers], **key_value_updates: dict[str, Any]
|
|
88
|
+
) -> test_common:
|
|
83
89
|
"""Update any relevant keys in a config file with the specified values.
|
|
84
90
|
Full validation will be performed after this update and descriptive errors
|
|
85
91
|
will be produced
|
|
86
92
|
|
|
87
93
|
Args:
|
|
88
|
-
config (test_common): A previously-constructed test_common object. This can be
|
|
94
|
+
config (test_common): A previously-constructed test_common object. This can be
|
|
89
95
|
build using the configFromDict or configFromFile functions.
|
|
90
96
|
key_value_updates (kwargs, optional): Additional keyword/argument pairs to update
|
|
91
97
|
arbitrary fields in the configuration.
|
|
@@ -101,37 +107,40 @@ def update_config(config:Union[test,test_servers], **key_value_updates:dict[str,
|
|
|
101
107
|
|
|
102
108
|
# Force validation of assignment since doing so via arbitrary dict can be error prone
|
|
103
109
|
# Also, ensure that we do not try to add fields that are not part of the model
|
|
104
|
-
config_copy.model_config.update({
|
|
110
|
+
config_copy.model_config.update({"validate_assignment": True, "extra": "forbid"})
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
|
|
108
112
|
# Collect any errors that may occur
|
|
109
|
-
errors:list[Exception] = []
|
|
110
|
-
|
|
111
|
-
# We need to do this one by one because the extra:forbid argument does not appear to
|
|
113
|
+
errors: list[Exception] = []
|
|
114
|
+
|
|
115
|
+
# We need to do this one by one because the extra:forbid argument does not appear to
|
|
112
116
|
# be respected at this time.
|
|
113
117
|
for key, value in key_value_updates.items():
|
|
114
118
|
try:
|
|
115
|
-
setattr(config_copy,key,value)
|
|
119
|
+
setattr(config_copy, key, value)
|
|
116
120
|
except Exception as e:
|
|
117
121
|
errors.append(e)
|
|
118
122
|
if len(errors) > 0:
|
|
119
|
-
errors_string =
|
|
123
|
+
errors_string = "\n".join([str(e) for e in errors])
|
|
120
124
|
raise Exception(f"Error(s) updaitng configuration:\n{errors_string}")
|
|
121
|
-
|
|
125
|
+
|
|
122
126
|
return config_copy
|
|
123
|
-
|
|
124
127
|
|
|
125
128
|
|
|
126
|
-
def content_to_dict(director:DirectorOutputDto)->dict[str,list[dict[str,Any]]]:
|
|
127
|
-
output_dict:dict[str,list[dict[str,Any]]] = {}
|
|
128
|
-
for contentType in [
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
def content_to_dict(director: DirectorOutputDto) -> dict[str, list[dict[str, Any]]]:
|
|
130
|
+
output_dict: dict[str, list[dict[str, Any]]] = {}
|
|
131
|
+
for contentType in [
|
|
132
|
+
"detections",
|
|
133
|
+
"stories",
|
|
134
|
+
"baselines",
|
|
135
|
+
"investigations",
|
|
136
|
+
"playbooks",
|
|
137
|
+
"macros",
|
|
138
|
+
"lookups",
|
|
139
|
+
"deployments",
|
|
140
|
+
]:
|
|
131
141
|
output_dict[contentType] = []
|
|
132
|
-
t:list[SecurityContentObject] = getattr(director,contentType)
|
|
133
|
-
|
|
142
|
+
t: list[SecurityContentObject] = getattr(director, contentType)
|
|
143
|
+
|
|
134
144
|
for item in t:
|
|
135
145
|
output_dict[contentType].append(item.model_dump())
|
|
136
146
|
return output_dict
|
|
137
|
-
|