contentctl 4.0.5__tar.gz → 4.1.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.0.5 → contentctl-4.1.0}/PKG-INFO +7 -8
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/inspect.py +1 -1
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/new_content.py +6 -3
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/validate.py +1 -0
- contentctl-4.1.0/contentctl/api.py +137 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/contentctl.py +28 -24
- contentctl-4.1.0/contentctl/enrichments/cve_enrichment.py +65 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/director.py +72 -72
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +77 -13
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/baseline.py +0 -1
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/config.py +4 -8
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/detection_tags.py +1 -1
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/macro.py +8 -7
- contentctl-4.1.0/contentctl/output/yml_writer.py +49 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/pyproject.toml +7 -8
- contentctl-4.0.5/contentctl/actions/apav_deploy.py +0 -98
- contentctl-4.0.5/contentctl/actions/api_deploy.py +0 -151
- contentctl-4.0.5/contentctl/enrichments/cve_enrichment.py +0 -100
- contentctl-4.0.5/contentctl/output/yml_writer.py +0 -11
- contentctl-4.0.5/contentctl/templates/app_template/default/distsearch.conf +0 -5
- {contentctl-4.0.5 → contentctl-4.1.0}/LICENSE.md +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/README.md +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/__init__.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/build.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/convert.py +0 -0
- /contentctl-4.0.5/contentctl/actions/acs_deploy.py → /contentctl-4.1.0/contentctl/actions/deploy_acs.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/GitService.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/doc_gen.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/initialize.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/initialize_old.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/release_notes.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/reporting.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/actions/test.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/enrichments/attack_enrichment.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/helper/link_validator.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/helper/logger.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/helper/utils.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/backend_splunk_ba.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/new_content_questions.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/sigma_converter.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/ssa_detection_builder.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/input/yml_reader.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/alert_action.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/atomic.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/base_test.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/base_test_result.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/baseline_tags.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/constants.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/correlation_search.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/data_source.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_email.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_notable.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_phantom.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_rba.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_scheduling.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/deployment_slack.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/detection.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/enums.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/integration_test.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/integration_test_result.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/investigation.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/investigation_tags.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/lookup.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/notable_action.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/observable.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/playbook.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/playbook_tags.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/risk_analysis_action.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/risk_object.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/security_content_object.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/ssa_detection.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/ssa_detection_tags.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/story.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/story_tags.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/test_group.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/threat_object.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_attack_data.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_baseline.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_old.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_result.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/objects/unit_test_ssa.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/api_json_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/attack_nav_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/attack_nav_writer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/ba_yml_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/conf_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/conf_writer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/detection_writer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/doc_md_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/finding_report_writer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/jinja_writer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/json_writer.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/new_content_yml_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/svg_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/app.conf.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/app.manifest.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/collections.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/content-version.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/detection_count.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_detections.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_stories.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/finding_report.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/header.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/macros.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/panel.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/transforms.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/output/yml_output.py +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/README +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_default.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/README.md +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/app.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/commands.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/content-version.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/usage_searches.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/datamodels_cim.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/datamodels_custom.conf +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/application/.gitkeep +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/network/.gitkeep +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/detections/web/.gitkeep +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
- {contentctl-4.0.5 → contentctl-4.1.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.0
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: Splunk Content Control Tool
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Author: STRT
|
|
@@ -10,25 +10,24 @@ 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
|
-
Requires-Dist: Jinja2 (>=3.1.
|
|
13
|
+
Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
|
|
14
14
|
Requires-Dist: PyYAML (>=6.0.1,<7.0.0)
|
|
15
15
|
Requires-Dist: attackcti (>=0.3.7,<0.4.0)
|
|
16
16
|
Requires-Dist: bottle (>=0.12.25,<0.13.0)
|
|
17
17
|
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
|
18
18
|
Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
|
19
19
|
Requires-Dist: pycvesearch (>=1.2,<2.0)
|
|
20
|
-
Requires-Dist: pydantic (>=2.
|
|
20
|
+
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
21
21
|
Requires-Dist: pygit2 (>=1.14.1,<2.0.0)
|
|
22
|
-
Requires-Dist: pysigma (>=0.
|
|
23
|
-
Requires-Dist: pysigma-backend-splunk (>=1.0
|
|
22
|
+
Requires-Dist: pysigma (>=0.11.5,<0.12.0)
|
|
23
|
+
Requires-Dist: pysigma-backend-splunk (>=1.1.0,<2.0.0)
|
|
24
24
|
Requires-Dist: questionary (>=2.0.1,<3.0.0)
|
|
25
25
|
Requires-Dist: requests (>=2.32.2,<2.33.0)
|
|
26
26
|
Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
|
|
27
|
-
Requires-Dist: setuptools (>=69.5.1,<
|
|
27
|
+
Requires-Dist: setuptools (>=69.5.1,<71.0.0)
|
|
28
28
|
Requires-Dist: splunk-sdk (>=2.0.1,<3.0.0)
|
|
29
|
-
Requires-Dist: tqdm (>=4.66.
|
|
29
|
+
Requires-Dist: tqdm (>=4.66.4,<5.0.0)
|
|
30
30
|
Requires-Dist: tyro (>=0.8.3,<0.9.0)
|
|
31
|
-
Requires-Dist: validators (>=0.22.0,<0.23.0)
|
|
32
31
|
Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
|
|
33
32
|
Description-Content-Type: text/markdown
|
|
34
33
|
|
|
@@ -61,7 +61,7 @@ class Inspect:
|
|
|
61
61
|
if not package_path.is_file():
|
|
62
62
|
raise Exception(f"Cannot run Appinspect API on App '{config.app.title}' - "
|
|
63
63
|
f"no package exists as expected path '{package_path}'.\nAre you "
|
|
64
|
-
"trying to 'contentctl
|
|
64
|
+
"trying to 'contentctl deploy_acs' the package BEFORE running 'contentctl build'?")
|
|
65
65
|
|
|
66
66
|
files = {
|
|
67
67
|
"app_package": open(package_path,"rb"),
|
|
@@ -25,7 +25,8 @@ class NewContent:
|
|
|
25
25
|
answers['date'] = datetime.today().strftime('%Y-%m-%d')
|
|
26
26
|
answers['author'] = answers['detection_author']
|
|
27
27
|
del answers['detection_author']
|
|
28
|
-
answers['
|
|
28
|
+
answers['data_sources'] = answers['data_source']
|
|
29
|
+
del answers['data_source']
|
|
29
30
|
answers['type'] = answers['detection_type']
|
|
30
31
|
del answers['detection_type']
|
|
31
32
|
answers['status'] = "production" #start everything as production since that's what we INTEND the content to become
|
|
@@ -49,6 +50,7 @@ class NewContent:
|
|
|
49
50
|
answers['tags']['required_fields'] = ['UPDATE']
|
|
50
51
|
answers['tags']['risk_score'] = 'UPDATE (impact * confidence)/100'
|
|
51
52
|
answers['tags']['security_domain'] = answers['security_domain']
|
|
53
|
+
del answers["security_domain"]
|
|
52
54
|
answers['tags']['cve'] = ['UPDATE WITH CVE(S) IF APPLICABLE']
|
|
53
55
|
|
|
54
56
|
#generate the tests section
|
|
@@ -64,6 +66,7 @@ class NewContent:
|
|
|
64
66
|
]
|
|
65
67
|
}
|
|
66
68
|
]
|
|
69
|
+
del answers["mitre_attack_ids"]
|
|
67
70
|
return answers
|
|
68
71
|
|
|
69
72
|
def buildStory(self)->dict[str,Any]:
|
|
@@ -111,12 +114,12 @@ class NewContent:
|
|
|
111
114
|
#make sure the output folder exists for this detection
|
|
112
115
|
output_folder.mkdir(exist_ok=True)
|
|
113
116
|
|
|
114
|
-
YmlWriter.
|
|
117
|
+
YmlWriter.writeDetection(file_path, object)
|
|
115
118
|
print("Successfully created detection " + file_path)
|
|
116
119
|
|
|
117
120
|
elif type == NewContentType.story:
|
|
118
121
|
file_path = os.path.join(self.output_path, 'stories', self.convertNameToFileName(object['name'], object['tags']['product']))
|
|
119
|
-
YmlWriter.
|
|
122
|
+
YmlWriter.writeStory(file_path, object)
|
|
120
123
|
print("Successfully created story " + file_path)
|
|
121
124
|
|
|
122
125
|
else:
|
|
@@ -23,6 +23,7 @@ class Validate:
|
|
|
23
23
|
director_output_dto = DirectorOutputDto(AtomicTest.getAtomicTestsFromArtRepo(repo_path=input_dto.getAtomicRedTeamRepoPath(),
|
|
24
24
|
enabled=input_dto.enrichments),
|
|
25
25
|
AttackEnrichment.getAttackEnrichment(input_dto),
|
|
26
|
+
CveEnrichment.getCveEnrichment(input_dto),
|
|
26
27
|
[],[],[],[],[],[],[],[],[])
|
|
27
28
|
|
|
28
29
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Union, Type
|
|
3
|
+
from contentctl.input.yml_reader import YmlReader
|
|
4
|
+
from contentctl.objects.config import test_common, test, test_servers
|
|
5
|
+
from contentctl.objects.security_content_object import SecurityContentObject
|
|
6
|
+
from contentctl.input.director import DirectorOutputDto
|
|
7
|
+
|
|
8
|
+
def config_from_file(path:Path=Path("contentctl.yml"), config: dict[str,Any]={},
|
|
9
|
+
configType:Type[Union[test,test_servers]]=test)->test_common:
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
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
|
|
14
|
+
be used as the basis for constructing the configuration.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
path (Path, optional): Relative or absolute path to a contentctl config file.
|
|
18
|
+
Defaults to Path("contentctl.yml"), which is the default name and location (in the current directory)
|
|
19
|
+
of the configuration files which are automatically generated for contentctl.
|
|
20
|
+
config (dict[], optional): Dictionary of values to override values read from the YML
|
|
21
|
+
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.
|
|
24
|
+
This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
|
|
25
|
+
Returns:
|
|
26
|
+
test_common: Returns a complete contentctl test_common configuration. Note that this configuration
|
|
27
|
+
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
|
+
"""
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
yml_dict = YmlReader.load_file(path, add_fields=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
except Exception as e:
|
|
36
|
+
raise Exception(f"Failed to load contentctl configuration from file '{path}': {str(e)}")
|
|
37
|
+
|
|
38
|
+
# Apply settings that have been overridden from the ones in the file
|
|
39
|
+
try:
|
|
40
|
+
yml_dict.update(config)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
raise Exception(f"Failed updating dictionary of values read from file '{path}'"
|
|
43
|
+
f" with the dictionary of arguments passed: {str(e)}")
|
|
44
|
+
|
|
45
|
+
# The function below will throw its own descriptive exception if it fails
|
|
46
|
+
configObject = config_from_dict(yml_dict, configType=configType)
|
|
47
|
+
|
|
48
|
+
return configObject
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def config_from_dict(config: dict[str,Any]={},
|
|
54
|
+
configType:Type[Union[test,test_servers]]=test)->test_common:
|
|
55
|
+
"""
|
|
56
|
+
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
|
|
58
|
+
be used as the basis for constructing the configuration.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
config (dict[str,Any],Optional): If a dictionary is not explicitly passed, then
|
|
62
|
+
an empty dict will be used to create a configuration, if possible, from default
|
|
63
|
+
values. Note that based on default values in the contentctl/objects/config.py
|
|
64
|
+
file, this may raise an exception. If so, please set appropriate default values
|
|
65
|
+
in the file above or supply those values via this argument.
|
|
66
|
+
configType (Type[Union[test,test_servers]], optional): The Config Class to instantiate.
|
|
67
|
+
This may be a test or test_servers object. Note that this is NOT an instance of the class. Defaults to test.
|
|
68
|
+
Returns:
|
|
69
|
+
test_common: Returns a complete contentctl test_common configuration. Note that this configuration
|
|
70
|
+
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
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
test_object = configType.model_validate(config)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
raise Exception(f"Failed to load contentctl configuration from dict:\n{str(e)}")
|
|
77
|
+
|
|
78
|
+
return test_object
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def update_config(config:Union[test,test_servers], **key_value_updates:dict[str,Any])->test_common:
|
|
82
|
+
|
|
83
|
+
"""Update any relevant keys in a config file with the specified values.
|
|
84
|
+
Full validation will be performed after this update and descriptive errors
|
|
85
|
+
will be produced
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
config (test_common): A previously-constructed test_common object. This can be
|
|
89
|
+
build using the configFromDict or configFromFile functions.
|
|
90
|
+
key_value_updates (kwargs, optional): Additional keyword/argument pairs to update
|
|
91
|
+
arbitrary fields in the configuration.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
test_common: A validated object which has had the relevant fields updated.
|
|
95
|
+
Note that descriptive Exceptions will be generated if updated values are either
|
|
96
|
+
invalid (have the wrong type, or disallowed values) or you attempt to update
|
|
97
|
+
fields that do not exist
|
|
98
|
+
"""
|
|
99
|
+
# Create a copy so we don't change the underlying model
|
|
100
|
+
config_copy = config.model_copy(deep=True)
|
|
101
|
+
|
|
102
|
+
# Force validation of assignment since doing so via arbitrary dict can be error prone
|
|
103
|
+
# Also, ensure that we do not try to add fields that are not part of the model
|
|
104
|
+
config_copy.model_config.update({'validate_assignment': True, 'extra': 'forbid'})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# 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
|
|
112
|
+
# be respected at this time.
|
|
113
|
+
for key, value in key_value_updates.items():
|
|
114
|
+
try:
|
|
115
|
+
setattr(config_copy,key,value)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
errors.append(e)
|
|
118
|
+
if len(errors) > 0:
|
|
119
|
+
errors_string = '\n'.join([str(e) for e in errors])
|
|
120
|
+
raise Exception(f"Error(s) updaitng configuration:\n{errors_string}")
|
|
121
|
+
|
|
122
|
+
return config_copy
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
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 ['detections','stories','baselines','investigations',
|
|
129
|
+
'playbooks','macros','lookups','deployments','ssa_detections']:
|
|
130
|
+
|
|
131
|
+
output_dict[contentType] = []
|
|
132
|
+
t:list[SecurityContentObject] = getattr(director,contentType)
|
|
133
|
+
|
|
134
|
+
for item in t:
|
|
135
|
+
output_dict[contentType].append(item.model_dump())
|
|
136
|
+
return output_dict
|
|
137
|
+
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import traceback
|
|
2
|
+
import sys
|
|
3
|
+
import warnings
|
|
4
|
+
import pathlib
|
|
2
5
|
import tyro
|
|
3
|
-
|
|
6
|
+
|
|
7
|
+
from contentctl.actions.initialize import Initialize
|
|
8
|
+
from contentctl.objects.config import init, validate, build, new, deploy_acs, test, test_servers, inspect, report, test_common, release_notes
|
|
4
9
|
from contentctl.actions.validate import Validate
|
|
5
10
|
from contentctl.actions.new_content import NewContent
|
|
6
11
|
from contentctl.actions.detection_testing.GitService import GitService
|
|
@@ -9,14 +14,10 @@ from contentctl.actions.build import (
|
|
|
9
14
|
DirectorOutputDto,
|
|
10
15
|
Build,
|
|
11
16
|
)
|
|
12
|
-
|
|
13
17
|
from contentctl.actions.test import Test
|
|
14
18
|
from contentctl.actions.test import TestInputDto
|
|
15
19
|
from contentctl.actions.reporting import ReportingInputDto, Reporting
|
|
16
20
|
from contentctl.actions.inspect import Inspect
|
|
17
|
-
import sys
|
|
18
|
-
import warnings
|
|
19
|
-
import pathlib
|
|
20
21
|
from contentctl.input.yml_reader import YmlReader
|
|
21
22
|
from contentctl.actions.release_notes import ReleaseNotes
|
|
22
23
|
|
|
@@ -95,13 +96,14 @@ def new_func(config:new):
|
|
|
95
96
|
|
|
96
97
|
def deploy_acs_func(config:deploy_acs):
|
|
97
98
|
#This is a bit challenging to get to work with the default values.
|
|
98
|
-
raise Exception("deploy acs not yet implemented")
|
|
99
|
-
|
|
100
|
-
def deploy_rest_func(config:deploy_rest):
|
|
101
|
-
raise Exception("deploy rest not yet implemented")
|
|
102
|
-
|
|
99
|
+
raise Exception("deploy acs not yet implemented")
|
|
103
100
|
|
|
104
101
|
def test_common_func(config:test_common):
|
|
102
|
+
if type(config) == test:
|
|
103
|
+
#construct the container Infrastructure objects
|
|
104
|
+
config.getContainerInfrastructureObjects()
|
|
105
|
+
#otherwise, they have already been passed as servers
|
|
106
|
+
|
|
105
107
|
director_output_dto = build_func(config)
|
|
106
108
|
gitServer = GitService(director=director_output_dto,config=config)
|
|
107
109
|
detections_to_test = gitServer.getContent()
|
|
@@ -175,15 +177,14 @@ def main():
|
|
|
175
177
|
"test":test.model_validate(config_obj),
|
|
176
178
|
"test_servers":test_servers.model_construct(**t.__dict__),
|
|
177
179
|
"release_notes": release_notes.model_construct(**config_obj),
|
|
178
|
-
"deploy_acs": deploy_acs.model_construct(**t.__dict__)
|
|
179
|
-
#"deploy_rest":deploy_rest()
|
|
180
|
+
"deploy_acs": deploy_acs.model_construct(**t.__dict__)
|
|
180
181
|
}
|
|
181
182
|
)
|
|
182
183
|
|
|
183
184
|
|
|
184
185
|
|
|
185
186
|
|
|
186
|
-
|
|
187
|
+
config = None
|
|
187
188
|
try:
|
|
188
189
|
# Since some model(s) were constructed and not model_validated, we have to catch
|
|
189
190
|
# warnings again when creating the cli
|
|
@@ -209,20 +210,23 @@ def main():
|
|
|
209
210
|
elif type(config) == deploy_acs:
|
|
210
211
|
updated_config = deploy_acs.model_validate(config)
|
|
211
212
|
deploy_acs_func(updated_config)
|
|
212
|
-
elif type(config) == deploy_rest:
|
|
213
|
-
deploy_rest_func(config)
|
|
214
213
|
elif type(config) == test or type(config) == test_servers:
|
|
215
|
-
if type(config) == test:
|
|
216
|
-
#construct the container Infrastructure objects
|
|
217
|
-
config.getContainerInfrastructureObjects()
|
|
218
|
-
#otherwise, they have already been passed as servers
|
|
219
214
|
test_common_func(config)
|
|
220
215
|
else:
|
|
221
216
|
raise Exception(f"Unknown command line type '{type(config).__name__}'")
|
|
222
217
|
except Exception as e:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
218
|
+
if config is None:
|
|
219
|
+
print("There was a serious issue where the config file could not be created.\n"
|
|
220
|
+
"The entire stack trace is provided below (please include it if filing a bug report).\n")
|
|
221
|
+
traceback.print_exc()
|
|
222
|
+
elif config.verbose:
|
|
223
|
+
print("Verbose error logging is ENABLED.\n"
|
|
224
|
+
"The entire stack trace has been provided below (please include it if filing a bug report):\n")
|
|
225
|
+
traceback.print_exc()
|
|
226
|
+
else:
|
|
227
|
+
print("Verbose error logging is DISABLED.\n"
|
|
228
|
+
"Please use the --verbose command line argument if you need more context for your error or file a bug report.")
|
|
229
|
+
print(e)
|
|
230
|
+
|
|
227
231
|
sys.exit(1)
|
|
228
232
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pycvesearch import CVESearch
|
|
3
|
+
import functools
|
|
4
|
+
import os
|
|
5
|
+
import shelve
|
|
6
|
+
import time
|
|
7
|
+
from typing import Annotated, Any, Union, TYPE_CHECKING
|
|
8
|
+
from pydantic import BaseModel,Field, computed_field
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from requests.exceptions import ReadTimeout
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from contentctl.objects.config import validate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CVESSEARCH_API_URL = 'https://cve.circl.lu'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CveEnrichmentObj(BaseModel):
|
|
21
|
+
id:Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"]
|
|
22
|
+
cvss:Annotated[Decimal, Field(ge=.1, le=10, decimal_places=1)]
|
|
23
|
+
summary:str
|
|
24
|
+
|
|
25
|
+
@computed_field
|
|
26
|
+
@property
|
|
27
|
+
def url(self)->str:
|
|
28
|
+
BASE_NVD_URL = "https://nvd.nist.gov/vuln/detail/"
|
|
29
|
+
return f"{BASE_NVD_URL}{self.id}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CveEnrichment(BaseModel):
|
|
33
|
+
use_enrichment: bool = True
|
|
34
|
+
cve_api_obj: Union[CVESearch,None] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Config:
|
|
38
|
+
# Arbitrary_types are allowed to let us use the CVESearch Object
|
|
39
|
+
arbitrary_types_allowed = True
|
|
40
|
+
frozen = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def getCveEnrichment(config:validate, timeout_seconds:int=10, force_disable_enrichment:bool=True)->CveEnrichment:
|
|
45
|
+
if force_disable_enrichment:
|
|
46
|
+
return CveEnrichment(use_enrichment=False, cve_api_obj=None)
|
|
47
|
+
|
|
48
|
+
if config.enrichments:
|
|
49
|
+
try:
|
|
50
|
+
cve_api_obj = CVESearch(CVESSEARCH_API_URL, timeout=timeout_seconds)
|
|
51
|
+
return CveEnrichment(use_enrichment=True, cve_api_obj=cve_api_obj)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise Exception(f"Error setting CVE_SEARCH API to: {CVESSEARCH_API_URL}: {str(e)}")
|
|
54
|
+
|
|
55
|
+
return CveEnrichment(use_enrichment=False, cve_api_obj=None)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def enrich_cve(self, cve_id:str, raise_exception_on_failure:bool=True)->CveEnrichmentObj:
|
|
59
|
+
|
|
60
|
+
if not self.use_enrichment:
|
|
61
|
+
return CveEnrichmentObj(id=cve_id,cvss=Decimal(5.0),summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME")
|
|
62
|
+
else:
|
|
63
|
+
print("WARNING - Dynamic enrichment not supported at this time.")
|
|
64
|
+
return CveEnrichmentObj(id=cve_id,cvss=Decimal(5.0),summary="SUMMARY NOT AVAILABLE! ONLY THE LINK WILL BE USED AT THIS TIME")
|
|
65
|
+
# Depending on needs, we may add dynamic enrichment functionality back to the tool
|
|
@@ -5,9 +5,8 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from pydantic import ValidationError
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
from contentctl.input.yml_reader import YmlReader
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
|
|
9
|
+
|
|
11
10
|
from contentctl.objects.detection import Detection
|
|
12
11
|
from contentctl.objects.story import Story
|
|
13
12
|
|
|
@@ -28,29 +27,69 @@ from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
|
28
27
|
from contentctl.objects.config import validate
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
@dataclass()
|
|
30
|
+
@dataclass
|
|
33
31
|
class DirectorOutputDto:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
32
|
+
# Atomic Tests are first because parsing them
|
|
33
|
+
# is far quicker than attack_enrichment
|
|
34
|
+
atomic_tests: Union[list[AtomicTest],None]
|
|
35
|
+
attack_enrichment: AttackEnrichment
|
|
36
|
+
cve_enrichment: CveEnrichment
|
|
37
|
+
detections: list[Detection]
|
|
38
|
+
stories: list[Story]
|
|
39
|
+
baselines: list[Baseline]
|
|
40
|
+
investigations: list[Investigation]
|
|
41
|
+
playbooks: list[Playbook]
|
|
42
|
+
macros: list[Macro]
|
|
43
|
+
lookups: list[Lookup]
|
|
44
|
+
deployments: list[Deployment]
|
|
45
|
+
ssa_detections: list[SSADetection]
|
|
46
|
+
|
|
47
|
+
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
|
|
48
|
+
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
def addContentToDictMappings(self, content: SecurityContentObject):
|
|
51
|
+
content_name = content.name
|
|
52
|
+
if isinstance(content, SSADetection):
|
|
53
|
+
# Since SSA detections may have the same name as ESCU detection,
|
|
54
|
+
# for this function we prepend 'SSA ' to the name.
|
|
55
|
+
content_name = f"SSA {content_name}"
|
|
56
|
+
if content_name in self.name_to_content_map:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"Duplicate name '{content_name}' with paths:\n"
|
|
59
|
+
f" - {content.file_path}\n"
|
|
60
|
+
f" - {self.name_to_content_map[content_name].file_path}"
|
|
61
|
+
)
|
|
62
|
+
elif content.id in self.uuid_to_content_map:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Duplicate id '{content.id}' with paths:\n"
|
|
65
|
+
f" - {content.file_path}\n"
|
|
66
|
+
f" - {self.name_to_content_map[content_name].file_path}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if isinstance(content, Lookup):
|
|
70
|
+
self.lookups.append(content)
|
|
71
|
+
elif isinstance(content, Macro):
|
|
72
|
+
self.macros.append(content)
|
|
73
|
+
elif isinstance(content, Deployment):
|
|
74
|
+
self.deployments.append(content)
|
|
75
|
+
elif isinstance(content, Playbook):
|
|
76
|
+
self.playbooks.append(content)
|
|
77
|
+
elif isinstance(content, Baseline):
|
|
78
|
+
self.baselines.append(content)
|
|
79
|
+
elif isinstance(content, Investigation):
|
|
80
|
+
self.investigations.append(content)
|
|
81
|
+
elif isinstance(content, Story):
|
|
82
|
+
self.stories.append(content)
|
|
83
|
+
elif isinstance(content, Detection):
|
|
84
|
+
self.detections.append(content)
|
|
85
|
+
elif isinstance(content, SSADetection):
|
|
86
|
+
self.ssa_detections.append(content)
|
|
87
|
+
else:
|
|
88
|
+
raise Exception(f"Unknown security content type: {type(content)}")
|
|
52
89
|
|
|
53
90
|
|
|
91
|
+
self.name_to_content_map[content_name] = content
|
|
92
|
+
self.uuid_to_content_map[content.id] = content
|
|
54
93
|
|
|
55
94
|
|
|
56
95
|
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
|
|
@@ -60,13 +99,6 @@ from contentctl.objects.enums import DetectionStatus
|
|
|
60
99
|
from contentctl.helper.utils import Utils
|
|
61
100
|
|
|
62
101
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
102
|
class Director():
|
|
71
103
|
input_dto: validate
|
|
72
104
|
output_dto: DirectorOutputDto
|
|
@@ -77,27 +109,7 @@ class Director():
|
|
|
77
109
|
def __init__(self, output_dto: DirectorOutputDto) -> None:
|
|
78
110
|
self.output_dto = output_dto
|
|
79
111
|
self.ssa_detection_builder = SSADetectionBuilder()
|
|
80
|
-
|
|
81
|
-
def addContentToDictMappings(self, content:SecurityContentObject):
|
|
82
|
-
content_name = content.name
|
|
83
|
-
if isinstance(content,SSADetection):
|
|
84
|
-
# Since SSA detections may have the same name as ESCU detection,
|
|
85
|
-
# for this function we prepend 'SSA ' to the name.
|
|
86
|
-
content_name = f"SSA {content_name}"
|
|
87
|
-
if content_name in self.output_dto.name_to_content_map:
|
|
88
|
-
raise ValueError(f"Duplicate name '{content_name}' with paths:\n"
|
|
89
|
-
f" - {content.file_path}\n"
|
|
90
|
-
f" - {self.output_dto.name_to_content_map[content_name].file_path}")
|
|
91
|
-
elif content.id in self.output_dto.uuid_to_content_map:
|
|
92
|
-
raise ValueError(f"Duplicate id '{content.id}' with paths:\n"
|
|
93
|
-
f" - {content.file_path}\n"
|
|
94
|
-
f" - {self.output_dto.name_to_content_map[content_name].file_path}")
|
|
95
|
-
|
|
96
|
-
self.output_dto.name_to_content_map[content_name] = content
|
|
97
|
-
self.output_dto.uuid_to_content_map[content.id] = content
|
|
98
|
-
|
|
99
112
|
|
|
100
|
-
|
|
101
113
|
def execute(self, input_dto: validate) -> None:
|
|
102
114
|
self.input_dto = input_dto
|
|
103
115
|
|
|
@@ -146,50 +158,41 @@ class Director():
|
|
|
146
158
|
|
|
147
159
|
if contentType == SecurityContentType.lookups:
|
|
148
160
|
lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
|
|
149
|
-
self.output_dto.
|
|
150
|
-
self.addContentToDictMappings(lookup)
|
|
161
|
+
self.output_dto.addContentToDictMappings(lookup)
|
|
151
162
|
|
|
152
163
|
elif contentType == SecurityContentType.macros:
|
|
153
164
|
macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
154
|
-
self.output_dto.
|
|
155
|
-
self.addContentToDictMappings(macro)
|
|
165
|
+
self.output_dto.addContentToDictMappings(macro)
|
|
156
166
|
|
|
157
167
|
elif contentType == SecurityContentType.deployments:
|
|
158
168
|
deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
159
|
-
self.output_dto.
|
|
160
|
-
self.addContentToDictMappings(deployment)
|
|
169
|
+
self.output_dto.addContentToDictMappings(deployment)
|
|
161
170
|
|
|
162
171
|
elif contentType == SecurityContentType.playbooks:
|
|
163
172
|
playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
164
|
-
self.output_dto.
|
|
165
|
-
self.addContentToDictMappings(playbook)
|
|
173
|
+
self.output_dto.addContentToDictMappings(playbook)
|
|
166
174
|
|
|
167
175
|
elif contentType == SecurityContentType.baselines:
|
|
168
176
|
baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
169
|
-
self.output_dto.
|
|
170
|
-
self.addContentToDictMappings(baseline)
|
|
177
|
+
self.output_dto.addContentToDictMappings(baseline)
|
|
171
178
|
|
|
172
179
|
elif contentType == SecurityContentType.investigations:
|
|
173
180
|
investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
174
|
-
self.output_dto.
|
|
175
|
-
self.addContentToDictMappings(investigation)
|
|
181
|
+
self.output_dto.addContentToDictMappings(investigation)
|
|
176
182
|
|
|
177
183
|
elif contentType == SecurityContentType.stories:
|
|
178
184
|
story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
179
|
-
self.output_dto.
|
|
180
|
-
self.addContentToDictMappings(story)
|
|
185
|
+
self.output_dto.addContentToDictMappings(story)
|
|
181
186
|
|
|
182
187
|
elif contentType == SecurityContentType.detections:
|
|
183
|
-
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
184
|
-
self.output_dto.
|
|
185
|
-
self.addContentToDictMappings(detection)
|
|
188
|
+
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
|
|
189
|
+
self.output_dto.addContentToDictMappings(detection)
|
|
186
190
|
|
|
187
191
|
elif contentType == SecurityContentType.ssa_detections:
|
|
188
192
|
self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
|
|
189
193
|
ssa_detection = self.ssa_detection_builder.getObject()
|
|
190
194
|
if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
|
|
191
|
-
self.output_dto.
|
|
192
|
-
self.addContentToDictMappings(ssa_detection)
|
|
195
|
+
self.output_dto.addContentToDictMappings(ssa_detection)
|
|
193
196
|
|
|
194
197
|
else:
|
|
195
198
|
raise Exception(f"Unsupported type: [{contentType}]")
|
|
@@ -228,6 +231,3 @@ class Director():
|
|
|
228
231
|
builder.addMappings()
|
|
229
232
|
builder.addUnitTest()
|
|
230
233
|
builder.addRBA()
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|