contentctl 4.3.5__tar.gz → 4.4.0__tar.gz
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-4.3.5 → contentctl-4.4.0}/PKG-INFO +3 -2
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/build.py +1 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/GitService.py +10 -10
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +68 -38
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +5 -1
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +10 -8
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/inspect.py +6 -4
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/new_content.py +10 -2
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/validate.py +2 -1
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/enrichments/cve_enrichment.py +6 -7
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/input/director.py +14 -12
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/input/new_content_questions.py +9 -42
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +147 -7
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -9
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/base_test_result.py +7 -7
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/baseline.py +12 -18
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/baseline_tags.py +2 -5
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/config.py +12 -7
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/constants.py +30 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/correlation_search.py +79 -114
- contentctl-4.4.0/contentctl/objects/dashboard.py +100 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/deployment.py +20 -5
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/detection_tags.py +22 -20
- contentctl-4.4.0/contentctl/objects/drilldown.py +70 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/enums.py +26 -22
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/investigation.py +23 -15
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/investigation_tags.py +4 -3
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/lookup.py +8 -1
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/macro.py +16 -7
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/notable_event.py +6 -5
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/risk_analysis_action.py +4 -4
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/risk_event.py +8 -7
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/story.py +4 -16
- contentctl-4.4.0/contentctl/objects/throttling.py +46 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/conf_output.py +4 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/conf_writer.py +20 -3
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl-4.4.0/contentctl/output/templates/analyticstories_investigations.j2 +21 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/analyticstories_stories.j2 +1 -1
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/savedsearches_baselines.j2 +2 -3
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/savedsearches_detections.j2 +12 -7
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/savedsearches_investigations.j2 +3 -4
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +10 -1
- {contentctl-4.3.5 → contentctl-4.4.0}/pyproject.toml +2 -2
- contentctl-4.3.5/contentctl/output/templates/analyticstories_investigations.j2 +0 -21
- contentctl-4.3.5/contentctl/output/templates/finding_report.j2 +0 -30
- {contentctl-4.3.5 → contentctl-4.4.0}/LICENSE.md +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/README.md +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/__init__.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/deploy_acs.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/doc_gen.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/initialize.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/initialize_old.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/release_notes.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/reporting.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/actions/test.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/api.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/contentctl.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/enrichments/attack_enrichment.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/helper/link_validator.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/helper/logger.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/helper/splunk_app.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/helper/utils.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/input/yml_reader.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/alert_action.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/annotated_types.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/atomic.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/base_test.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/data_source.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/deployment_email.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/deployment_notable.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/deployment_phantom.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/deployment_rba.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/deployment_scheduling.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/deployment_slack.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/detection.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/detection_metadata.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/detection_stanza.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/errors.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/event_source.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/integration_test.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/integration_test_result.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/manual_test.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/manual_test_result.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/notable_action.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/observable.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/playbook.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/playbook_tags.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/risk_object.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/savedsearches_conf.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/security_content_object.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/story_tags.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/test_attack_data.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/test_group.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/threat_object.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/unit_test.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/unit_test_baseline.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/objects/unit_test_result.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/api_json_output.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/attack_nav_output.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/attack_nav_writer.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/data_source_writer.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/detection_writer.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/doc_md_output.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/jinja_writer.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/json_writer.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/new_content_yml_output.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/svg_output.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/app.conf.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/app.manifest.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/collections.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/content-version.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/detection_count.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_detections.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_stories.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/header.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/macros.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/panel.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/transforms.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/yml_output.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/output/yml_writer.py +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/README.md +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_default.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/README.md +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/app.conf +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/commands.conf +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/content-version.conf +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/datamodels_cim.conf +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/datamodels_custom.conf +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/detections/application/.gitkeep +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/detections/network/.gitkeep +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/detections/web/.gitkeep +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
- {contentctl-4.3.5 → contentctl-4.4.0}/contentctl/templates/stories/cobalt_strike.yml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: contentctl
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: Splunk Content Control Tool
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Author: STRT
|
|
@@ -10,6 +10,7 @@ Classifier: License :: Other/Proprietary License
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
14
|
Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
|
|
14
15
|
Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
|
|
15
16
|
Requires-Dist: attackcti (>=0.4.0,<0.5.0)
|
|
@@ -26,7 +27,7 @@ Requires-Dist: setuptools (>=69.5.1,<76.0.0)
|
|
|
26
27
|
Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
|
|
27
28
|
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
28
29
|
Requires-Dist: tyro (>=0.8.3,<0.9.0)
|
|
29
|
-
Requires-Dist: xmltodict (>=0.13
|
|
30
|
+
Requires-Dist: xmltodict (>=0.13,<0.15)
|
|
30
31
|
Description-Content-Type: text/markdown
|
|
31
32
|
|
|
32
33
|
|
|
@@ -50,6 +50,7 @@ class Build:
|
|
|
50
50
|
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.investigations, SecurityContentType.investigations))
|
|
51
51
|
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.lookups, SecurityContentType.lookups))
|
|
52
52
|
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.macros, SecurityContentType.macros))
|
|
53
|
+
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.dashboards, SecurityContentType.dashboards))
|
|
53
54
|
updated_conf_files.update(conf_output.writeAppConf())
|
|
54
55
|
|
|
55
56
|
#Ensure that the conf file we just generated/update is syntactically valid
|
|
@@ -67,9 +67,9 @@ class GitService(BaseModel):
|
|
|
67
67
|
|
|
68
68
|
#Make a filename to content map
|
|
69
69
|
filepath_to_content_map = { obj.file_path:obj for (_,obj) in self.director.name_to_content_map.items()}
|
|
70
|
-
updated_detections:
|
|
71
|
-
updated_macros:
|
|
72
|
-
updated_lookups:
|
|
70
|
+
updated_detections:set[Detection] = set()
|
|
71
|
+
updated_macros:set[Macro] = set()
|
|
72
|
+
updated_lookups:set[Lookup] = set()
|
|
73
73
|
|
|
74
74
|
for diff in all_diffs:
|
|
75
75
|
if type(diff) == pygit2.Patch:
|
|
@@ -80,14 +80,14 @@ class GitService(BaseModel):
|
|
|
80
80
|
if decoded_path.is_relative_to(self.config.path/"detections") and decoded_path.suffix == ".yml":
|
|
81
81
|
detectionObject = filepath_to_content_map.get(decoded_path, None)
|
|
82
82
|
if isinstance(detectionObject, Detection):
|
|
83
|
-
updated_detections.
|
|
83
|
+
updated_detections.add(detectionObject)
|
|
84
84
|
else:
|
|
85
85
|
raise Exception(f"Error getting detection object for file {str(decoded_path)}")
|
|
86
86
|
|
|
87
87
|
elif decoded_path.is_relative_to(self.config.path/"macros") and decoded_path.suffix == ".yml":
|
|
88
88
|
macroObject = filepath_to_content_map.get(decoded_path, None)
|
|
89
89
|
if isinstance(macroObject, Macro):
|
|
90
|
-
updated_macros.
|
|
90
|
+
updated_macros.add(macroObject)
|
|
91
91
|
else:
|
|
92
92
|
raise Exception(f"Error getting macro object for file {str(decoded_path)}")
|
|
93
93
|
|
|
@@ -98,7 +98,7 @@ class GitService(BaseModel):
|
|
|
98
98
|
updatedLookup = filepath_to_content_map.get(decoded_path, None)
|
|
99
99
|
if not isinstance(updatedLookup,Lookup):
|
|
100
100
|
raise Exception(f"Expected {decoded_path} to be type {type(Lookup)}, but instead if was {(type(updatedLookup))}")
|
|
101
|
-
updated_lookups.
|
|
101
|
+
updated_lookups.add(updatedLookup)
|
|
102
102
|
|
|
103
103
|
elif decoded_path.suffix == ".csv":
|
|
104
104
|
# If the CSV was updated, we want to make sure that we
|
|
@@ -125,7 +125,7 @@ class GitService(BaseModel):
|
|
|
125
125
|
if updatedLookup is not None and updatedLookup not in updated_lookups:
|
|
126
126
|
# It is possible that both the CSV and YML have been modified for the same lookup,
|
|
127
127
|
# and we do not want to add it twice.
|
|
128
|
-
updated_lookups.
|
|
128
|
+
updated_lookups.add(updatedLookup)
|
|
129
129
|
|
|
130
130
|
else:
|
|
131
131
|
pass
|
|
@@ -136,7 +136,7 @@ class GitService(BaseModel):
|
|
|
136
136
|
|
|
137
137
|
# If a detection has at least one dependency on changed content,
|
|
138
138
|
# then we must test it again
|
|
139
|
-
changed_macros_and_lookups = updated_macros
|
|
139
|
+
changed_macros_and_lookups:set[SecurityContentObject] = updated_macros.union(updated_lookups)
|
|
140
140
|
|
|
141
141
|
for detection in self.director.detections:
|
|
142
142
|
if detection in updated_detections:
|
|
@@ -146,14 +146,14 @@ class GitService(BaseModel):
|
|
|
146
146
|
|
|
147
147
|
for obj in changed_macros_and_lookups:
|
|
148
148
|
if obj in detection.get_content_dependencies():
|
|
149
|
-
updated_detections.
|
|
149
|
+
updated_detections.add(detection)
|
|
150
150
|
break
|
|
151
151
|
|
|
152
152
|
#Print out the names of all modified/new content
|
|
153
153
|
modifiedAndNewContentString = "\n - ".join(sorted([d.name for d in updated_detections]))
|
|
154
154
|
|
|
155
155
|
print(f"[{len(updated_detections)}] Pieces of modifed and new content (this may include experimental/deprecated/manual_test content):\n - {modifiedAndNewContentString}")
|
|
156
|
-
return updated_detections
|
|
156
|
+
return sorted(list(updated_detections))
|
|
157
157
|
|
|
158
158
|
def getSelected(self, detectionFilenames: List[FilePath]) -> List[Detection]:
|
|
159
159
|
filepath_to_content_map: dict[FilePath, SecurityContentObject] = {
|
|
@@ -13,7 +13,7 @@ from sys import stdout
|
|
|
13
13
|
from shutil import copyfile
|
|
14
14
|
from typing import Union, Optional
|
|
15
15
|
|
|
16
|
-
from pydantic import BaseModel, PrivateAttr, Field, dataclasses
|
|
16
|
+
from pydantic import ConfigDict, BaseModel, PrivateAttr, Field, dataclasses
|
|
17
17
|
import requests # type: ignore
|
|
18
18
|
import splunklib.client as client # type: ignore
|
|
19
19
|
from splunklib.binding import HTTPError # type: ignore
|
|
@@ -48,9 +48,9 @@ class SetupTestGroupResults(BaseModel):
|
|
|
48
48
|
success: bool = True
|
|
49
49
|
duration: float = 0
|
|
50
50
|
start_time: float
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
model_config = ConfigDict(
|
|
52
|
+
arbitrary_types_allowed=True
|
|
53
|
+
)
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class CleanupTestGroupResults(BaseModel):
|
|
@@ -68,6 +68,15 @@ class CannotRunBaselineException(Exception):
|
|
|
68
68
|
# exception
|
|
69
69
|
pass
|
|
70
70
|
|
|
71
|
+
class ReplayIndexDoesNotExistOnServer(Exception):
|
|
72
|
+
'''
|
|
73
|
+
In order to replay data files into the Splunk Server
|
|
74
|
+
for testing, they must be replayed into an index that
|
|
75
|
+
exists. If that index does not exist, this error will
|
|
76
|
+
be generated and raised before we try to do anything else
|
|
77
|
+
with that Data File.
|
|
78
|
+
'''
|
|
79
|
+
pass
|
|
71
80
|
|
|
72
81
|
@dataclasses.dataclass(frozen=False)
|
|
73
82
|
class DetectionTestingManagerOutputDto():
|
|
@@ -75,7 +84,7 @@ class DetectionTestingManagerOutputDto():
|
|
|
75
84
|
outputQueue: list[Detection] = Field(default_factory=list)
|
|
76
85
|
currentTestingQueue: dict[str, Union[Detection, None]] = Field(default_factory=dict)
|
|
77
86
|
start_time: Union[datetime.datetime, None] = None
|
|
78
|
-
replay_index: str = "
|
|
87
|
+
replay_index: str = "contentctl_testing_index"
|
|
79
88
|
replay_host: str = "CONTENTCTL_HOST"
|
|
80
89
|
timeout_seconds: int = 60
|
|
81
90
|
terminate: bool = False
|
|
@@ -88,12 +97,13 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
88
97
|
sync_obj: DetectionTestingManagerOutputDto
|
|
89
98
|
hec_token: str = ""
|
|
90
99
|
hec_channel: str = ""
|
|
100
|
+
all_indexes_on_server: list[str] = []
|
|
91
101
|
_conn: client.Service = PrivateAttr()
|
|
92
102
|
pbar: tqdm.tqdm = None
|
|
93
103
|
start_time: Optional[float] = None
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
model_config = ConfigDict(
|
|
105
|
+
arbitrary_types_allowed=True
|
|
106
|
+
)
|
|
97
107
|
|
|
98
108
|
def __init__(self, **data):
|
|
99
109
|
super().__init__(**data)
|
|
@@ -131,6 +141,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
131
141
|
(self.get_conn, "Waiting for App Installation"),
|
|
132
142
|
(self.configure_conf_file_datamodels, "Configuring Datamodels"),
|
|
133
143
|
(self.create_replay_index, f"Create index '{self.sync_obj.replay_index}'"),
|
|
144
|
+
(self.get_all_indexes, "Getting all indexes from server"),
|
|
134
145
|
(self.configure_imported_roles, "Configuring Roles"),
|
|
135
146
|
(self.configure_delete_indexes, "Configuring Indexes"),
|
|
136
147
|
(self.configure_hec, "Configuring HEC"),
|
|
@@ -169,12 +180,11 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
169
180
|
pass
|
|
170
181
|
|
|
171
182
|
try:
|
|
172
|
-
|
|
173
183
|
res = self.get_conn().inputs.create(
|
|
174
184
|
name="DETECTION_TESTING_HEC",
|
|
175
185
|
kind="http",
|
|
176
186
|
index=self.sync_obj.replay_index,
|
|
177
|
-
indexes=
|
|
187
|
+
indexes=",".join(self.all_indexes_on_server), # This allows the HEC to write to all indexes
|
|
178
188
|
useACK=True,
|
|
179
189
|
)
|
|
180
190
|
self.hec_token = str(res.token)
|
|
@@ -183,6 +193,23 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
183
193
|
except Exception as e:
|
|
184
194
|
raise (Exception(f"Failure creating HEC Endpoint: {str(e)}"))
|
|
185
195
|
|
|
196
|
+
def get_all_indexes(self) -> None:
|
|
197
|
+
"""
|
|
198
|
+
Retrieve a list of all indexes in the Splunk instance
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
# We do not include the replay index because by
|
|
202
|
+
# the time we get to this function, it has already
|
|
203
|
+
# been created on the server.
|
|
204
|
+
indexes = []
|
|
205
|
+
res = self.get_conn().indexes
|
|
206
|
+
for index in res.list():
|
|
207
|
+
indexes.append(index.name)
|
|
208
|
+
# Retrieve all available indexes on the splunk instance
|
|
209
|
+
self.all_indexes_on_server = indexes
|
|
210
|
+
except Exception as e:
|
|
211
|
+
raise (Exception(f"Failure getting indexes: {str(e)}"))
|
|
212
|
+
|
|
186
213
|
def get_conn(self) -> client.Service:
|
|
187
214
|
try:
|
|
188
215
|
if not self._conn:
|
|
@@ -265,39 +292,41 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
265
292
|
self,
|
|
266
293
|
imported_roles: list[str] = ["user", "power", "can_delete"],
|
|
267
294
|
enterprise_security_roles: list[str] = ["ess_admin", "ess_analyst", "ess_user"],
|
|
268
|
-
|
|
269
|
-
):
|
|
270
|
-
indexes.append(self.sync_obj.replay_index)
|
|
271
|
-
indexes_encoded = ";".join(indexes)
|
|
295
|
+
):
|
|
272
296
|
try:
|
|
297
|
+
# Set which roles should be configured. For Enterprise Security/Integration Testing,
|
|
298
|
+
# we must add some extra foles.
|
|
299
|
+
if self.global_config.enable_integration_testing:
|
|
300
|
+
roles = imported_roles + enterprise_security_roles
|
|
301
|
+
else:
|
|
302
|
+
roles = imported_roles
|
|
303
|
+
|
|
273
304
|
self.get_conn().roles.post(
|
|
274
305
|
self.infrastructure.splunk_app_username,
|
|
275
|
-
imported_roles=
|
|
276
|
-
srchIndexesAllowed=
|
|
306
|
+
imported_roles=roles,
|
|
307
|
+
srchIndexesAllowed=";".join(self.all_indexes_on_server),
|
|
277
308
|
srchIndexesDefault=self.sync_obj.replay_index,
|
|
278
309
|
)
|
|
279
310
|
return
|
|
280
311
|
except Exception as e:
|
|
281
312
|
self.pbar.write(
|
|
282
|
-
f"
|
|
313
|
+
f"The following role(s) do not exist:'{enterprise_security_roles}: {str(e)}"
|
|
283
314
|
)
|
|
284
315
|
|
|
285
316
|
self.get_conn().roles.post(
|
|
286
317
|
self.infrastructure.splunk_app_username,
|
|
287
318
|
imported_roles=imported_roles,
|
|
288
|
-
srchIndexesAllowed=
|
|
319
|
+
srchIndexesAllowed=";".join(self.all_indexes_on_server),
|
|
289
320
|
srchIndexesDefault=self.sync_obj.replay_index,
|
|
290
321
|
)
|
|
291
322
|
|
|
292
|
-
def configure_delete_indexes(self
|
|
293
|
-
indexes.append(self.sync_obj.replay_index)
|
|
323
|
+
def configure_delete_indexes(self):
|
|
294
324
|
endpoint = "/services/properties/authorize/default/deleteIndexesAllowed"
|
|
295
|
-
indexes_encoded = ";".join(indexes)
|
|
296
325
|
try:
|
|
297
|
-
self.get_conn().post(endpoint, value=
|
|
326
|
+
self.get_conn().post(endpoint, value=";".join(self.all_indexes_on_server))
|
|
298
327
|
except Exception as e:
|
|
299
328
|
self.pbar.write(
|
|
300
|
-
f"Error configuring deleteIndexesAllowed with '{
|
|
329
|
+
f"Error configuring deleteIndexesAllowed with '{self.all_indexes_on_server}': [{str(e)}]"
|
|
301
330
|
)
|
|
302
331
|
|
|
303
332
|
def wait_for_conf_file(self, app_name: str, conf_file_name: str):
|
|
@@ -646,8 +675,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
646
675
|
# Set the mode and timeframe, if required
|
|
647
676
|
kwargs = {"exec_mode": "blocking"}
|
|
648
677
|
|
|
649
|
-
|
|
650
|
-
|
|
651
678
|
# Set earliest_time and latest_time appropriately if FORCE_ALL_TIME is False
|
|
652
679
|
if not FORCE_ALL_TIME:
|
|
653
680
|
if test.earliest_time is not None:
|
|
@@ -1027,8 +1054,8 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1027
1054
|
# Get the start time and compute the timeout
|
|
1028
1055
|
search_start_time = time.time()
|
|
1029
1056
|
search_stop_time = time.time() + self.sync_obj.timeout_seconds
|
|
1030
|
-
|
|
1031
|
-
# Make a copy of the search string since we may
|
|
1057
|
+
|
|
1058
|
+
# Make a copy of the search string since we may
|
|
1032
1059
|
# need to make some small changes to it below
|
|
1033
1060
|
search = detection.search
|
|
1034
1061
|
|
|
@@ -1080,8 +1107,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1080
1107
|
# Initialize the collection of fields that are empty that shouldn't be
|
|
1081
1108
|
present_threat_objects: set[str] = set()
|
|
1082
1109
|
empty_fields: set[str] = set()
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
1110
|
|
|
1086
1111
|
# Filter out any messages in the results
|
|
1087
1112
|
for result in results:
|
|
@@ -1111,7 +1136,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1111
1136
|
# not populated and we should throw an error. This can happen if there is a typo
|
|
1112
1137
|
# on a field. In this case, the field will appear but will not contain any values
|
|
1113
1138
|
current_empty_fields: set[str] = set()
|
|
1114
|
-
|
|
1139
|
+
|
|
1115
1140
|
for field in observable_fields_set:
|
|
1116
1141
|
if result.get(field, 'null') == 'null':
|
|
1117
1142
|
if field in risk_object_fields_set:
|
|
@@ -1131,9 +1156,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1131
1156
|
if field in threat_object_fields_set:
|
|
1132
1157
|
present_threat_objects.add(field)
|
|
1133
1158
|
continue
|
|
1134
|
-
|
|
1135
1159
|
|
|
1136
|
-
|
|
1137
1160
|
# If everything succeeded up until now, and no empty fields are found in the
|
|
1138
1161
|
# current result, then the search was a success
|
|
1139
1162
|
if len(current_empty_fields) == 0:
|
|
@@ -1147,8 +1170,7 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1147
1170
|
|
|
1148
1171
|
else:
|
|
1149
1172
|
empty_fields = empty_fields.union(current_empty_fields)
|
|
1150
|
-
|
|
1151
|
-
|
|
1173
|
+
|
|
1152
1174
|
missing_threat_objects = threat_object_fields_set - present_threat_objects
|
|
1153
1175
|
# Report a failure if there were empty fields in a threat object in all results
|
|
1154
1176
|
if len(missing_threat_objects) > 0:
|
|
@@ -1164,7 +1186,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1164
1186
|
duration=time.time() - search_start_time,
|
|
1165
1187
|
)
|
|
1166
1188
|
return
|
|
1167
|
-
|
|
1168
1189
|
|
|
1169
1190
|
test.result.set_job_content(
|
|
1170
1191
|
job.content,
|
|
@@ -1225,9 +1246,19 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1225
1246
|
test_group: TestGroup,
|
|
1226
1247
|
test_group_start_time: float,
|
|
1227
1248
|
):
|
|
1228
|
-
|
|
1229
|
-
|
|
1249
|
+
# Before attempting to replay the file, ensure that the index we want
|
|
1250
|
+
# to replay into actuall exists. If not, we should throw a detailed
|
|
1251
|
+
# exception that can easily be interpreted by the user.
|
|
1252
|
+
if attack_data_file.custom_index is not None and \
|
|
1253
|
+
attack_data_file.custom_index not in self.all_indexes_on_server:
|
|
1254
|
+
raise ReplayIndexDoesNotExistOnServer(
|
|
1255
|
+
f"Unable to replay data file {attack_data_file.data} "
|
|
1256
|
+
f"into index '{attack_data_file.custom_index}'. "
|
|
1257
|
+
"The index does not exist on the Splunk Server. "
|
|
1258
|
+
f"The only valid indexes on the server are {self.all_indexes_on_server}"
|
|
1259
|
+
)
|
|
1230
1260
|
|
|
1261
|
+
tempfile = mktemp(dir=tmp_dir)
|
|
1231
1262
|
if not (str(attack_data_file.data).startswith("http://") or
|
|
1232
1263
|
str(attack_data_file.data).startswith("https://")) :
|
|
1233
1264
|
if pathlib.Path(str(attack_data_file.data)).is_file():
|
|
@@ -1272,7 +1303,6 @@ class DetectionTestingInfrastructure(BaseModel, abc.ABC):
|
|
|
1272
1303
|
)
|
|
1273
1304
|
)
|
|
1274
1305
|
|
|
1275
|
-
|
|
1276
1306
|
# Upload the data
|
|
1277
1307
|
self.format_pbar_string(
|
|
1278
1308
|
TestReportingType.GROUP,
|
|
@@ -49,13 +49,17 @@ class DetectionTestingInfrastructureContainer(DetectionTestingInfrastructure):
|
|
|
49
49
|
def check_for_teardown(self):
|
|
50
50
|
|
|
51
51
|
try:
|
|
52
|
-
self.get_docker_client().containers.get(self.get_name())
|
|
52
|
+
container: docker.models.containers.Container = self.get_docker_client().containers.get(self.get_name())
|
|
53
53
|
except Exception as e:
|
|
54
54
|
if self.sync_obj.terminate is not True:
|
|
55
55
|
self.pbar.write(
|
|
56
56
|
f"Error: could not get container [{self.get_name()}]: {str(e)}"
|
|
57
57
|
)
|
|
58
58
|
self.sync_obj.terminate = True
|
|
59
|
+
else:
|
|
60
|
+
if container.status != 'running':
|
|
61
|
+
self.sync_obj.terminate = True
|
|
62
|
+
self.container = None
|
|
59
63
|
|
|
60
64
|
if self.sync_obj.terminate:
|
|
61
65
|
self.finish()
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
from
|
|
2
|
-
from contentctl.actions.detection_testing.views.DetectionTestingView import (
|
|
3
|
-
DetectionTestingView,
|
|
4
|
-
)
|
|
1
|
+
from threading import Thread
|
|
5
2
|
|
|
3
|
+
from bottle import template, Bottle, ServerAdapter
|
|
6
4
|
from wsgiref.simple_server import make_server, WSGIRequestHandler
|
|
7
5
|
import jinja2
|
|
8
6
|
import webbrowser
|
|
9
|
-
from
|
|
7
|
+
from pydantic import ConfigDict
|
|
8
|
+
|
|
9
|
+
from contentctl.actions.detection_testing.views.DetectionTestingView import (
|
|
10
|
+
DetectionTestingView,
|
|
11
|
+
)
|
|
10
12
|
|
|
11
13
|
DEFAULT_WEB_UI_PORT = 7999
|
|
12
14
|
|
|
@@ -100,9 +102,9 @@ class SimpleWebServer(ServerAdapter):
|
|
|
100
102
|
class DetectionTestingViewWeb(DetectionTestingView):
|
|
101
103
|
bottleApp: Bottle = Bottle()
|
|
102
104
|
server: SimpleWebServer = SimpleWebServer(host="0.0.0.0", port=DEFAULT_WEB_UI_PORT)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
model_config = ConfigDict(
|
|
106
|
+
arbitrary_types_allowed=True
|
|
107
|
+
)
|
|
106
108
|
|
|
107
109
|
def setup(self):
|
|
108
110
|
self.bottleApp.route("/", callback=self.showStatus)
|
|
@@ -297,9 +297,11 @@ class Inspect:
|
|
|
297
297
|
validation_errors[rule_name] = []
|
|
298
298
|
# No detections should be removed from build to build
|
|
299
299
|
if rule_name not in current_build_conf.detection_stanzas:
|
|
300
|
-
|
|
300
|
+
if config.suppress_missing_content_exceptions:
|
|
301
|
+
print(f"[SUPPRESSED] {DetectionMissingError(rule_name=rule_name).long_message}")
|
|
302
|
+
else:
|
|
303
|
+
validation_errors[rule_name].append(DetectionMissingError(rule_name=rule_name))
|
|
301
304
|
continue
|
|
302
|
-
|
|
303
305
|
# Pull out the individual stanza for readability
|
|
304
306
|
previous_stanza = previous_build_conf.detection_stanzas[rule_name]
|
|
305
307
|
current_stanza = current_build_conf.detection_stanzas[rule_name]
|
|
@@ -335,7 +337,7 @@ class Inspect:
|
|
|
335
337
|
)
|
|
336
338
|
|
|
337
339
|
# Convert our dict mapping to a flat list of errors for use in reporting
|
|
338
|
-
validation_error_list = [x for inner_list in validation_errors.values() for x in inner_list]
|
|
340
|
+
validation_error_list = [x for inner_list in validation_errors.values() for x in inner_list]
|
|
339
341
|
|
|
340
342
|
# Report failure/success
|
|
341
343
|
print("\nDetection Metadata Validation:")
|
|
@@ -355,4 +357,4 @@ class Inspect:
|
|
|
355
357
|
raise ExceptionGroup(
|
|
356
358
|
"Validation errors when comparing detection stanzas in current and previous build:",
|
|
357
359
|
validation_error_list
|
|
358
|
-
)
|
|
360
|
+
)
|
|
@@ -16,7 +16,11 @@ class NewContent:
|
|
|
16
16
|
|
|
17
17
|
def buildDetection(self)->dict[str,Any]:
|
|
18
18
|
questions = NewContentQuestions.get_questions_detection()
|
|
19
|
-
answers = questionary.prompt(
|
|
19
|
+
answers: dict[str,str] = questionary.prompt(
|
|
20
|
+
questions,
|
|
21
|
+
kbi_msg="User did not answer all of the prompt questions. Exiting...")
|
|
22
|
+
if not answers:
|
|
23
|
+
raise ValueError("User didn't answer one or more questions!")
|
|
20
24
|
answers.update(answers)
|
|
21
25
|
answers['name'] = answers['detection_name']
|
|
22
26
|
del answers['detection_name']
|
|
@@ -70,7 +74,11 @@ class NewContent:
|
|
|
70
74
|
|
|
71
75
|
def buildStory(self)->dict[str,Any]:
|
|
72
76
|
questions = NewContentQuestions.get_questions_story()
|
|
73
|
-
answers = questionary.prompt(
|
|
77
|
+
answers = questionary.prompt(
|
|
78
|
+
questions,
|
|
79
|
+
kbi_msg="User did not answer all of the prompt questions. Exiting...")
|
|
80
|
+
if not answers:
|
|
81
|
+
raise ValueError("User didn't answer one or more questions!")
|
|
74
82
|
answers['name'] = answers['story_name']
|
|
75
83
|
del answers['story_name']
|
|
76
84
|
answers['id'] = str(uuid.uuid4())
|
|
@@ -12,7 +12,7 @@ from contentctl.helper.splunk_app import SplunkApp
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Validate:
|
|
15
|
-
def execute(self, input_dto: validate) -> DirectorOutputDto:
|
|
15
|
+
def execute(self, input_dto: validate) -> DirectorOutputDto:
|
|
16
16
|
director_output_dto = DirectorOutputDto(
|
|
17
17
|
AtomicEnrichment.getAtomicEnrichment(input_dto),
|
|
18
18
|
AttackEnrichment.getAttackEnrichment(input_dto),
|
|
@@ -26,6 +26,7 @@ class Validate:
|
|
|
26
26
|
[],
|
|
27
27
|
[],
|
|
28
28
|
[],
|
|
29
|
+
[]
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
director = Director(director_output_dto)
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
import shelve
|
|
6
6
|
import time
|
|
7
7
|
from typing import Annotated, Any, Union, TYPE_CHECKING
|
|
8
|
-
from pydantic import BaseModel,Field, computed_field
|
|
8
|
+
from pydantic import ConfigDict, BaseModel,Field, computed_field
|
|
9
9
|
from decimal import Decimal
|
|
10
10
|
from requests.exceptions import ReadTimeout
|
|
11
11
|
from contentctl.objects.annotated_types import CVE_TYPE
|
|
@@ -32,13 +32,12 @@ class CveEnrichmentObj(BaseModel):
|
|
|
32
32
|
class CveEnrichment(BaseModel):
|
|
33
33
|
use_enrichment: bool = True
|
|
34
34
|
cve_api_obj: Union[CVESearch,None] = None
|
|
35
|
-
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
arbitrary_types_allowed
|
|
40
|
-
frozen
|
|
41
|
-
|
|
36
|
+
# Arbitrary_types are allowed to let us use the CVESearch Object
|
|
37
|
+
model_config = ConfigDict(
|
|
38
|
+
arbitrary_types_allowed=True,
|
|
39
|
+
frozen=True
|
|
40
|
+
)
|
|
42
41
|
|
|
43
42
|
@staticmethod
|
|
44
43
|
def getCveEnrichment(config:validate, timeout_seconds:int=10, force_disable_enrichment:bool=True)->CveEnrichment:
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
-
import
|
|
4
|
-
from typing import Union
|
|
3
|
+
from pathlib import Path
|
|
5
4
|
from dataclasses import dataclass, field
|
|
6
5
|
from pydantic import ValidationError
|
|
7
6
|
from uuid import UUID
|
|
8
7
|
from contentctl.input.yml_reader import YmlReader
|
|
9
8
|
|
|
10
|
-
|
|
11
9
|
from contentctl.objects.detection import Detection
|
|
12
10
|
from contentctl.objects.story import Story
|
|
13
11
|
|
|
14
|
-
from contentctl.objects.enums import SecurityContentProduct
|
|
15
12
|
from contentctl.objects.baseline import Baseline
|
|
16
13
|
from contentctl.objects.investigation import Investigation
|
|
17
14
|
from contentctl.objects.playbook import Playbook
|
|
@@ -21,20 +18,15 @@ from contentctl.objects.lookup import Lookup
|
|
|
21
18
|
from contentctl.objects.atomic import AtomicEnrichment
|
|
22
19
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
23
20
|
from contentctl.objects.data_source import DataSource
|
|
24
|
-
from contentctl.objects.
|
|
25
|
-
|
|
21
|
+
from contentctl.objects.dashboard import Dashboard
|
|
26
22
|
from contentctl.enrichments.attack_enrichment import AttackEnrichment
|
|
27
23
|
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
28
24
|
|
|
29
25
|
from contentctl.objects.config import validate
|
|
30
26
|
from contentctl.objects.enums import SecurityContentType
|
|
31
|
-
|
|
32
|
-
from contentctl.objects.enums import DetectionStatus
|
|
33
27
|
from contentctl.helper.utils import Utils
|
|
34
28
|
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
30
|
@dataclass
|
|
39
31
|
class DirectorOutputDto:
|
|
40
32
|
# Atomic Tests are first because parsing them
|
|
@@ -50,6 +42,8 @@ class DirectorOutputDto:
|
|
|
50
42
|
macros: list[Macro]
|
|
51
43
|
lookups: list[Lookup]
|
|
52
44
|
deployments: list[Deployment]
|
|
45
|
+
dashboards: list[Dashboard]
|
|
46
|
+
|
|
53
47
|
data_sources: list[DataSource]
|
|
54
48
|
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
|
|
55
49
|
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
|
|
@@ -88,6 +82,9 @@ class DirectorOutputDto:
|
|
|
88
82
|
self.stories.append(content)
|
|
89
83
|
elif isinstance(content, Detection):
|
|
90
84
|
self.detections.append(content)
|
|
85
|
+
elif isinstance(content, Dashboard):
|
|
86
|
+
self.dashboards.append(content)
|
|
87
|
+
|
|
91
88
|
elif isinstance(content, DataSource):
|
|
92
89
|
self.data_sources.append(content)
|
|
93
90
|
else:
|
|
@@ -115,7 +112,7 @@ class Director():
|
|
|
115
112
|
self.createSecurityContent(SecurityContentType.data_sources)
|
|
116
113
|
self.createSecurityContent(SecurityContentType.playbooks)
|
|
117
114
|
self.createSecurityContent(SecurityContentType.detections)
|
|
118
|
-
|
|
115
|
+
self.createSecurityContent(SecurityContentType.dashboards)
|
|
119
116
|
|
|
120
117
|
from contentctl.objects.abstract_security_content_objects.detection_abstract import MISSING_SOURCES
|
|
121
118
|
if len(MISSING_SOURCES) > 0:
|
|
@@ -137,6 +134,7 @@ class Director():
|
|
|
137
134
|
SecurityContentType.playbooks,
|
|
138
135
|
SecurityContentType.detections,
|
|
139
136
|
SecurityContentType.data_sources,
|
|
137
|
+
SecurityContentType.dashboards
|
|
140
138
|
]:
|
|
141
139
|
files = Utils.get_all_yml_files_from_directory(
|
|
142
140
|
os.path.join(self.input_dto.path, str(contentType.name))
|
|
@@ -147,7 +145,7 @@ class Director():
|
|
|
147
145
|
else:
|
|
148
146
|
raise (Exception(f"Cannot createSecurityContent for unknown product {contentType}."))
|
|
149
147
|
|
|
150
|
-
validation_errors = []
|
|
148
|
+
validation_errors:list[tuple[Path,ValueError]] = []
|
|
151
149
|
|
|
152
150
|
already_ran = False
|
|
153
151
|
progress_percent = 0
|
|
@@ -189,6 +187,10 @@ class Director():
|
|
|
189
187
|
elif contentType == SecurityContentType.detections:
|
|
190
188
|
detection = Detection.model_validate(modelDict, context={"output_dto":self.output_dto, "app":self.input_dto.app})
|
|
191
189
|
self.output_dto.addContentToDictMappings(detection)
|
|
190
|
+
|
|
191
|
+
elif contentType == SecurityContentType.dashboards:
|
|
192
|
+
dashboard = Dashboard.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
193
|
+
self.output_dto.addContentToDictMappings(dashboard)
|
|
192
194
|
|
|
193
195
|
elif contentType == SecurityContentType.data_sources:
|
|
194
196
|
data_source = DataSource.model_validate(
|