contentctl 5.0.0a2__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 +2 -4
- 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 +10 -10
- 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 +1 -1
- 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/transforms.j2 +2 -2
- contentctl/output/yml_writer.py +18 -24
- {contentctl-5.0.0a2.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.0a2.dist-info/RECORD +0 -170
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
- {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
contentctl/actions/test.py
CHANGED
|
@@ -2,10 +2,8 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import List
|
|
3
3
|
|
|
4
4
|
from contentctl.objects.config import test_common, Selected, Changes
|
|
5
|
-
from contentctl.objects.enums import DetectionTestingMode, DetectionStatus, AnalyticsType
|
|
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
|
|
|
@@ -88,10 +85,21 @@ class Test:
|
|
|
88
85
|
# detections were tested.
|
|
89
86
|
file.stop()
|
|
90
87
|
else:
|
|
91
|
-
print(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
]
|
|
95
103
|
)
|
|
96
104
|
print(f"Detections:\n- {files_string}")
|
|
97
105
|
|
|
@@ -102,43 +110,41 @@ class Test:
|
|
|
102
110
|
summary_results = file.getSummaryObject()
|
|
103
111
|
summary = summary_results.get("summary", {})
|
|
104
112
|
|
|
105
|
-
print(f"Test Summary (mode: {summary.get('mode','Error')})")
|
|
106
|
-
print(f"\tSuccess : {summary.get('success',False)}")
|
|
107
|
-
print(
|
|
108
|
-
f"\tSuccess Rate : {summary.get('success_rate','ERROR')}"
|
|
109
|
-
)
|
|
113
|
+
print(f"Test Summary (mode: {summary.get('mode', 'Error')})")
|
|
114
|
+
print(f"\tSuccess : {summary.get('success', False)}")
|
|
110
115
|
print(
|
|
111
|
-
f"\
|
|
116
|
+
f"\tSuccess Rate : {summary.get('success_rate', 'ERROR')}"
|
|
112
117
|
)
|
|
113
118
|
print(
|
|
114
|
-
f"\tTotal
|
|
119
|
+
f"\tTotal Detections : {summary.get('total_detections', 'ERROR')}"
|
|
115
120
|
)
|
|
116
121
|
print(
|
|
117
|
-
f"\
|
|
122
|
+
f"\tTotal Tested Detections : {summary.get('total_tested_detections', 'ERROR')}"
|
|
118
123
|
)
|
|
119
124
|
print(
|
|
120
|
-
f"\t
|
|
125
|
+
f"\t Passed Detections : {summary.get('total_pass', 'ERROR')}"
|
|
121
126
|
)
|
|
122
127
|
print(
|
|
123
|
-
f"\
|
|
128
|
+
f"\t Failed Detections : {summary.get('total_fail', 'ERROR')}"
|
|
124
129
|
)
|
|
125
130
|
print(
|
|
126
|
-
"\
|
|
131
|
+
f"\tSkipped Detections : {summary.get('total_skipped', 'ERROR')}"
|
|
127
132
|
)
|
|
133
|
+
print("\tProduction Status :")
|
|
128
134
|
print(
|
|
129
|
-
f"\t Production Detections : {summary.get('total_production','ERROR')}"
|
|
135
|
+
f"\t Production Detections : {summary.get('total_production', 'ERROR')}"
|
|
130
136
|
)
|
|
131
137
|
print(
|
|
132
|
-
f"\t Experimental Detections : {summary.get('total_experimental','ERROR')}"
|
|
138
|
+
f"\t Experimental Detections : {summary.get('total_experimental', 'ERROR')}"
|
|
133
139
|
)
|
|
134
140
|
print(
|
|
135
|
-
f"\t Deprecated Detections : {summary.get('total_deprecated','ERROR')}"
|
|
141
|
+
f"\t Deprecated Detections : {summary.get('total_deprecated', 'ERROR')}"
|
|
136
142
|
)
|
|
137
143
|
print(
|
|
138
|
-
f"\tManually Tested Detections : {summary.get('total_manual','ERROR')}"
|
|
144
|
+
f"\tManually Tested Detections : {summary.get('total_manual', 'ERROR')}"
|
|
139
145
|
)
|
|
140
146
|
print(
|
|
141
|
-
f"\tUntested Detections : {summary.get('total_untested','ERROR')}"
|
|
147
|
+
f"\tUntested Detections : {summary.get('total_untested', 'ERROR')}"
|
|
142
148
|
)
|
|
143
149
|
print(f"\tTest Results File : {file.getOutputFilePath()}")
|
|
144
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
|
|
@@ -27,7 +26,7 @@ class Validate:
|
|
|
27
26
|
[],
|
|
28
27
|
[],
|
|
29
28
|
[],
|
|
30
|
-
[]
|
|
29
|
+
[],
|
|
31
30
|
)
|
|
32
31
|
|
|
33
32
|
director = Director(director_output_dto)
|
|
@@ -35,51 +34,69 @@ class Validate:
|
|
|
35
34
|
self.ensure_no_orphaned_files_in_lookups(input_dto.path, director_output_dto)
|
|
36
35
|
if input_dto.data_source_TA_validation:
|
|
37
36
|
self.validate_latest_TA_information(director_output_dto.data_sources)
|
|
38
|
-
|
|
37
|
+
|
|
39
38
|
return director_output_dto
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
def ensure_no_orphaned_files_in_lookups(
|
|
41
|
+
self, repo_path: pathlib.Path, director_output_dto: DirectorOutputDto
|
|
42
|
+
):
|
|
43
43
|
"""
|
|
44
44
|
This function ensures that only files which are relevant to lookups are included in the lookups folder.
|
|
45
45
|
This means that a file must be either:
|
|
46
46
|
1. A lookup YML (.yml)
|
|
47
47
|
2. A lookup CSV (.csv) which is referenced by a YML
|
|
48
48
|
3. A lookup MLMODEL (.mlmodel) which is referenced by a YML.
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
All other files, includes CSV and MLMODEL files which are NOT
|
|
51
51
|
referenced by a YML, will generate an exception from this function.
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
Args:
|
|
54
54
|
repo_path (pathlib.Path): path to the root of the app
|
|
55
55
|
director_output_dto (DirectorOutputDto): director object with all constructed content
|
|
56
56
|
|
|
57
57
|
Raises:
|
|
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.
|
|
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.
|
|
62
62
|
This avoids having additional, unused files in this directory that may be copied into
|
|
63
63
|
the app when it is built (which can cause appinspect errors or larger app size.)
|
|
64
|
-
"""
|
|
65
|
-
lookupsDirectory = repo_path/"lookups"
|
|
66
|
-
|
|
64
|
+
"""
|
|
65
|
+
lookupsDirectory = repo_path / "lookups"
|
|
66
|
+
|
|
67
67
|
# Get all of the files referneced by Lookups
|
|
68
|
-
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
|
+
]
|
|
69
77
|
|
|
70
78
|
# Get all of the mlmodel and csv files in the lookups directory
|
|
71
|
-
csvAndMlmodelFiles
|
|
72
|
-
|
|
79
|
+
csvAndMlmodelFiles = Utils.get_security_content_files_from_directory(
|
|
80
|
+
lookupsDirectory,
|
|
81
|
+
allowedFileExtensions=[".yml", ".csv", ".mlmodel"],
|
|
82
|
+
fileExtensionsToReturn=[".csv", ".mlmodel"],
|
|
83
|
+
)
|
|
84
|
+
|
|
73
85
|
# Generate an exception of any csv or mlmodel files exist but are not used
|
|
74
|
-
unusedLookupFiles:list[pathlib.Path] = [
|
|
86
|
+
unusedLookupFiles: list[pathlib.Path] = [
|
|
87
|
+
testFile
|
|
88
|
+
for testFile in csvAndMlmodelFiles
|
|
89
|
+
if testFile not in usedLookupFiles
|
|
90
|
+
]
|
|
75
91
|
if len(unusedLookupFiles) > 0:
|
|
76
|
-
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
|
+
)
|
|
77
95
|
return
|
|
78
|
-
|
|
79
96
|
|
|
80
97
|
def validate_latest_TA_information(self, data_sources: list[DataSource]) -> None:
|
|
81
98
|
validated_TAs: list[tuple[str, str]] = []
|
|
82
|
-
errors:list[str] = []
|
|
99
|
+
errors: list[str] = []
|
|
83
100
|
print("----------------------")
|
|
84
101
|
print("Validating latest TA:")
|
|
85
102
|
print("----------------------")
|
|
@@ -90,22 +107,25 @@ class Validate:
|
|
|
90
107
|
continue
|
|
91
108
|
if supported_TA.url is not None:
|
|
92
109
|
validated_TAs.append(ta_identifier)
|
|
93
|
-
uid = int(str(supported_TA.url).rstrip(
|
|
110
|
+
uid = int(str(supported_TA.url).rstrip("/").split("/")[-1])
|
|
94
111
|
try:
|
|
95
112
|
splunk_app = SplunkApp(app_uid=uid)
|
|
96
113
|
if splunk_app.latest_version != supported_TA.version:
|
|
97
|
-
errors.append(
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
)
|
|
100
119
|
except Exception as e:
|
|
101
|
-
errors.append(
|
|
102
|
-
|
|
120
|
+
errors.append(
|
|
121
|
+
f"Error processing checking version of TA {supported_TA.name}: {str(e)}"
|
|
122
|
+
)
|
|
123
|
+
|
|
103
124
|
if len(errors) > 0:
|
|
104
|
-
errorString =
|
|
105
|
-
raise Exception(
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
)
|
|
108
131
|
print("All TA versions are up to date.")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
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
|
-
|
contentctl/contentctl.py
CHANGED
|
@@ -113,7 +113,7 @@ def deploy_acs_func(config: deploy_acs):
|
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
def test_common_func(config: test_common):
|
|
116
|
-
if type(config)
|
|
116
|
+
if type(config) is test:
|
|
117
117
|
# construct the container Infrastructure objects
|
|
118
118
|
config.getContainerInfrastructureObjects()
|
|
119
119
|
# otherwise, they have already been passed as servers
|
|
@@ -220,25 +220,25 @@ def main():
|
|
|
220
220
|
with warnings.catch_warnings(action="ignore"):
|
|
221
221
|
config = tyro.cli(models)
|
|
222
222
|
|
|
223
|
-
if type(config)
|
|
223
|
+
if type(config) is init:
|
|
224
224
|
t.__dict__.update(config.__dict__)
|
|
225
225
|
init_func(t)
|
|
226
|
-
elif type(config)
|
|
226
|
+
elif type(config) is validate:
|
|
227
227
|
validate_func(config)
|
|
228
|
-
elif type(config)
|
|
228
|
+
elif type(config) is report:
|
|
229
229
|
report_func(config)
|
|
230
|
-
elif type(config)
|
|
230
|
+
elif type(config) is build:
|
|
231
231
|
build_func(config)
|
|
232
|
-
elif type(config)
|
|
232
|
+
elif type(config) is new:
|
|
233
233
|
new_func(config)
|
|
234
|
-
elif type(config)
|
|
234
|
+
elif type(config) is inspect:
|
|
235
235
|
inspect_func(config)
|
|
236
|
-
elif type(config)
|
|
236
|
+
elif type(config) is release_notes:
|
|
237
237
|
release_notes_func(config)
|
|
238
|
-
elif type(config)
|
|
238
|
+
elif type(config) is deploy_acs:
|
|
239
239
|
updated_config = deploy_acs.model_validate(config)
|
|
240
240
|
deploy_acs_func(updated_config)
|
|
241
|
-
elif type(config)
|
|
241
|
+
elif type(config) is test or type(config) is test_servers:
|
|
242
242
|
test_common_func(config)
|
|
243
243
|
else:
|
|
244
244
|
raise Exception(f"Unknown command line type '{type(config).__name__}'")
|