contentctl 5.0.5__tar.gz → 5.2.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-5.2.0/PKG-INFO +74 -0
- contentctl-5.2.0/README.md +41 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +1 -1
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/contentctl.py +0 -16
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/input/director.py +16 -31
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +32 -25
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/data_source.py +2 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/lookup.py +16 -3
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/attack_nav_output.py +11 -4
- contentctl-5.2.0/contentctl/output/attack_nav_writer.py +83 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/transforms.j2 +2 -2
- {contentctl-5.0.5 → contentctl-5.2.0}/pyproject.toml +3 -2
- contentctl-5.0.5/PKG-INFO +0 -263
- contentctl-5.0.5/README.md +0 -230
- contentctl-5.0.5/contentctl/output/attack_nav_writer.py +0 -67
- {contentctl-5.0.5 → contentctl-5.2.0}/LICENSE.md +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/__init__.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/build.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/deploy_acs.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/GitService.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/doc_gen.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/initialize.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/inspect.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/new_content.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/release_notes.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/reporting.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/test.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/actions/validate.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/api.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/enrichments/attack_enrichment.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/enrichments/cve_enrichment.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/link_validator.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/logger.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/splunk_app.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/helper/utils.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/input/new_content_questions.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/input/yml_reader.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/alert_action.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/annotated_types.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/atomic.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/base_test.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/base_test_result.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/baseline.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/baseline_tags.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/config.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/constants.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/correlation_search.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/dashboard.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_email.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_notable.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_phantom.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_rba.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_scheduling.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/deployment_slack.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection_metadata.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection_stanza.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/detection_tags.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/drilldown.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/enums.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/errors.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/integration_test.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/integration_test_result.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/investigation.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/investigation_tags.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/macro.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/manual_test.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/manual_test_result.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/notable_action.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/notable_event.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/playbook.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/playbook_tags.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/rba.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/risk_analysis_action.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/risk_event.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/risk_object.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/savedsearches_conf.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/security_content_object.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/story.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/story_tags.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/test_attack_data.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/test_group.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/threat_object.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/throttling.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/unit_test.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/unit_test_baseline.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/objects/unit_test_result.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/api_json_output.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/conf_output.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/conf_writer.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/data_source_writer.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/doc_md_output.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/jinja_writer.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/json_writer.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/svg_output.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/app.conf.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/app.manifest.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/collections.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/content-version.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/detection_count.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_detections.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_stories.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/header.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/macros.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/panel.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/server.conf.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/output/yml_writer.py +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/README.md +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_default.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/README.md +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/commands.conf +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/datamodels_cim.conf +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/datamodels_custom.conf +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/application/.gitkeep +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/network/.gitkeep +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/detections/web/.gitkeep +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
- {contentctl-5.0.5 → contentctl-5.2.0}/contentctl/templates/stories/cobalt_strike.yml +0 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: contentctl
|
|
3
|
+
Version: 5.2.0
|
|
4
|
+
Summary: Splunk Content Control Tool
|
|
5
|
+
License: Apache 2.0
|
|
6
|
+
Author: STRT
|
|
7
|
+
Author-email: research@splunk.com
|
|
8
|
+
Requires-Python: >=3.11,<3.14
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: Jinja2 (>=3.1.4,<4.0.0)
|
|
15
|
+
Requires-Dist: PyYAML (>=6.0.2,<7.0.0)
|
|
16
|
+
Requires-Dist: attackcti (>=0.4.0,<0.5.0)
|
|
17
|
+
Requires-Dist: bottle (>=0.12.25,<0.14.0)
|
|
18
|
+
Requires-Dist: docker (>=7.1.0,<8.0.0)
|
|
19
|
+
Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
|
20
|
+
Requires-Dist: pycvesearch (>=1.2,<2.0)
|
|
21
|
+
Requires-Dist: pydantic (>=2.9.2,<2.10.0)
|
|
22
|
+
Requires-Dist: pygit2 (>=1.15.1,<2.0.0)
|
|
23
|
+
Requires-Dist: questionary (>=2.0.1,<3.0.0)
|
|
24
|
+
Requires-Dist: requests (>=2.32.3,<2.33.0)
|
|
25
|
+
Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
|
|
26
|
+
Requires-Dist: setuptools (>=69.5.1,<76.0.0)
|
|
27
|
+
Requires-Dist: splunk-sdk (>=2.0.2,<3.0.0)
|
|
28
|
+
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
29
|
+
Requires-Dist: tyro (>=0.9.2,<0.10.0)
|
|
30
|
+
Requires-Dist: xmltodict (>=0.13,<0.15)
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# contentctl
|
|
34
|
+
<p align="center">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/splunk/contentctl/refs/heads/main/docs/contentctl_logo_white.png" title="In case you're wondering, it's a capybara" alt="the logo for the contentctl project, which depicts a doodled 4 legged animal that is supposed to represent a capybara, with the name of the project below it" width="250" height="250"></p>
|
|
36
|
+
|
|
37
|
+
> [!NOTE]
|
|
38
|
+
> Looking to migrate from an earlier release to the new contentctl v5+ ? Check out our migration guide [here](docs/contentctl_v5_migration_guide.md).
|
|
39
|
+
|
|
40
|
+
## What is contentctl?
|
|
41
|
+
`contentctl` is a tool developed by the Splunk Threat Research Team to help with managing the content living in [splunk/security_content](https://github.com/splunk/security_content) and producing the Enterprise Security Content Update app for Splunk. While its development is largely driven by STRT's needs, it has been somewhat genericized and can be used by customers and partners to package their own content. Simply put, `contentctl` is the workhorse that packages detections, macros, lookups, dashboards into a Splunk app that you can use, and that understands the YAML structure and project layout we've selected to keep development clean.
|
|
42
|
+
|
|
43
|
+
## Quick Start Guide
|
|
44
|
+
Check out our [User Guide](docs/UserGuide.md) to get started!
|
|
45
|
+
|
|
46
|
+
## Content Testing
|
|
47
|
+
Read more about how `contentctl` can help test and validate your content in a real Splunk instance [here](docs/ContentTestingGuide.md).
|
|
48
|
+
|
|
49
|
+
## Sample CICD Workflows
|
|
50
|
+
Already using `contentctl`, or looking to get started with it already configured in GitHub Actions? [Our guide](docs/Sample_CICD_Templates.md) includes workflows to help you build and test your app.
|
|
51
|
+
|
|
52
|
+
## Contribution Guide
|
|
53
|
+
Read [the Contribution Guidelines](CONTRIBUTING.md) for this project before opening a Pull Request.
|
|
54
|
+
|
|
55
|
+
## Ecosystem
|
|
56
|
+
| Project | Description |
|
|
57
|
+
| --------------------- | ------------------------------------------------------- |
|
|
58
|
+
| [Splunk Security Content](https://github.com/splunk/security_content) | Splunk Threat Research Team's Content included in the [Enterprise Security Content Update App (ESCU)](https://splunkbase.splunk.com/app/3449)|
|
|
59
|
+
| [Splunk Attack Range](https://github.com/splunk/attack_range) | Easily deploy a preconfigured Splunk Environment locally or on AWS containing a Splunk Instance, Windows and Linux Machines, and Attacker Tools like Kali Linux. Automatically simulate attacks or run your own|
|
|
60
|
+
| [Splunk Attack Data](https://github.com/splunk/attack_data) | Repository of Attack Simulation Data for writing and Testing Detections| |
|
|
61
|
+
| [Splunk contentctl](https://github.com/splunk/contentctl) | Generate, validate, build, test, and deploy custom Security Content|
|
|
62
|
+
| [SigmaHQ Sigma Rules](https://github.com/SigmaHQ/sigma) | Official Repository for Sigma Rules. These rules are an excellent starting point for new content. |
|
|
63
|
+
| [PurpleSharp Attack Simulation](https://github.com/mvelazc0/PurpleSharp) | Open source adversary simulation tool for Windows Active Directory environments (integrated into Attack Range)|
|
|
64
|
+
| [Red Canary Atomic Red Team](https://github.com/redcanaryco/atomic-red-team) | Library of attack simulations mapped to the MITRE ATT&CK® framework (integrated into Attack Range)|
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
Copyright 2023 Splunk Inc.
|
|
68
|
+
|
|
69
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
|
70
|
+
|
|
71
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
72
|
+
|
|
73
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
74
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# contentctl
|
|
2
|
+
<p align="center">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/splunk/contentctl/refs/heads/main/docs/contentctl_logo_white.png" title="In case you're wondering, it's a capybara" alt="the logo for the contentctl project, which depicts a doodled 4 legged animal that is supposed to represent a capybara, with the name of the project below it" width="250" height="250"></p>
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> Looking to migrate from an earlier release to the new contentctl v5+ ? Check out our migration guide [here](docs/contentctl_v5_migration_guide.md).
|
|
7
|
+
|
|
8
|
+
## What is contentctl?
|
|
9
|
+
`contentctl` is a tool developed by the Splunk Threat Research Team to help with managing the content living in [splunk/security_content](https://github.com/splunk/security_content) and producing the Enterprise Security Content Update app for Splunk. While its development is largely driven by STRT's needs, it has been somewhat genericized and can be used by customers and partners to package their own content. Simply put, `contentctl` is the workhorse that packages detections, macros, lookups, dashboards into a Splunk app that you can use, and that understands the YAML structure and project layout we've selected to keep development clean.
|
|
10
|
+
|
|
11
|
+
## Quick Start Guide
|
|
12
|
+
Check out our [User Guide](docs/UserGuide.md) to get started!
|
|
13
|
+
|
|
14
|
+
## Content Testing
|
|
15
|
+
Read more about how `contentctl` can help test and validate your content in a real Splunk instance [here](docs/ContentTestingGuide.md).
|
|
16
|
+
|
|
17
|
+
## Sample CICD Workflows
|
|
18
|
+
Already using `contentctl`, or looking to get started with it already configured in GitHub Actions? [Our guide](docs/Sample_CICD_Templates.md) includes workflows to help you build and test your app.
|
|
19
|
+
|
|
20
|
+
## Contribution Guide
|
|
21
|
+
Read [the Contribution Guidelines](CONTRIBUTING.md) for this project before opening a Pull Request.
|
|
22
|
+
|
|
23
|
+
## Ecosystem
|
|
24
|
+
| Project | Description |
|
|
25
|
+
| --------------------- | ------------------------------------------------------- |
|
|
26
|
+
| [Splunk Security Content](https://github.com/splunk/security_content) | Splunk Threat Research Team's Content included in the [Enterprise Security Content Update App (ESCU)](https://splunkbase.splunk.com/app/3449)|
|
|
27
|
+
| [Splunk Attack Range](https://github.com/splunk/attack_range) | Easily deploy a preconfigured Splunk Environment locally or on AWS containing a Splunk Instance, Windows and Linux Machines, and Attacker Tools like Kali Linux. Automatically simulate attacks or run your own|
|
|
28
|
+
| [Splunk Attack Data](https://github.com/splunk/attack_data) | Repository of Attack Simulation Data for writing and Testing Detections| |
|
|
29
|
+
| [Splunk contentctl](https://github.com/splunk/contentctl) | Generate, validate, build, test, and deploy custom Security Content|
|
|
30
|
+
| [SigmaHQ Sigma Rules](https://github.com/SigmaHQ/sigma) | Official Repository for Sigma Rules. These rules are an excellent starting point for new content. |
|
|
31
|
+
| [PurpleSharp Attack Simulation](https://github.com/mvelazc0/PurpleSharp) | Open source adversary simulation tool for Windows Active Directory environments (integrated into Attack Range)|
|
|
32
|
+
| [Red Canary Atomic Red Team](https://github.com/redcanaryco/atomic-red-team) | Library of attack simulations mapped to the MITRE ATT&CK® framework (integrated into Attack Range)|
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
Copyright 2023 Splunk Inc.
|
|
36
|
+
|
|
37
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
|
38
|
+
|
|
39
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
40
|
+
|
|
41
|
+
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
@@ -89,7 +89,7 @@ class DetectionTestingManagerOutputDto:
|
|
|
89
89
|
start_time: Union[datetime.datetime, None] = None
|
|
90
90
|
replay_index: str = "contentctl_testing_index"
|
|
91
91
|
replay_host: str = "CONTENTCTL_HOST"
|
|
92
|
-
timeout_seconds: int =
|
|
92
|
+
timeout_seconds: int = 120
|
|
93
93
|
terminate: bool = False
|
|
94
94
|
|
|
95
95
|
|
|
@@ -143,20 +143,6 @@ def test_common_func(config: test_common):
|
|
|
143
143
|
raise Exception("There was at least one unsuccessful test")
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
CONTENTCTL_5_WARNING = """
|
|
147
|
-
*****************************************************************************
|
|
148
|
-
WARNING - THIS IS AN ALPHA BUILD OF CONTENTCTL 5.
|
|
149
|
-
THERE HAVE BEEN NUMEROUS CHANGES IN CONTENTCTL (ESPECIALLY TO YML FORMATS).
|
|
150
|
-
YOU ALMOST CERTAINLY DO NOT WANT TO USE THIS BUILD.
|
|
151
|
-
IF YOU ENCOUNTER ERRORS, PLEASE USE THE LATEST CURRENTYLY SUPPORTED RELEASE:
|
|
152
|
-
|
|
153
|
-
CONTENTCTL==4.4.7
|
|
154
|
-
|
|
155
|
-
YOU HAVE BEEN WARNED!
|
|
156
|
-
*****************************************************************************
|
|
157
|
-
"""
|
|
158
|
-
|
|
159
|
-
|
|
160
146
|
def get_random_compliment():
|
|
161
147
|
compliments = [
|
|
162
148
|
"Your detection rules are like a zero-day shield! 🛡️",
|
|
@@ -187,7 +173,6 @@ class RecognizeCommand:
|
|
|
187
173
|
|
|
188
174
|
|
|
189
175
|
def main():
|
|
190
|
-
print(CONTENTCTL_5_WARNING)
|
|
191
176
|
try:
|
|
192
177
|
configFile = pathlib.Path("contentctl.yml")
|
|
193
178
|
|
|
@@ -299,7 +284,6 @@ def main():
|
|
|
299
284
|
)
|
|
300
285
|
|
|
301
286
|
print(e)
|
|
302
|
-
print(CONTENTCTL_5_WARNING)
|
|
303
287
|
sys.exit(1)
|
|
304
288
|
|
|
305
289
|
|
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
-
from pathlib import Path
|
|
4
3
|
from dataclasses import dataclass, field
|
|
5
|
-
from
|
|
4
|
+
from pathlib import Path
|
|
6
5
|
from uuid import UUID
|
|
7
|
-
from contentctl.input.yml_reader import YmlReader
|
|
8
6
|
|
|
9
|
-
from
|
|
10
|
-
from contentctl.objects.story import Story
|
|
7
|
+
from pydantic import ValidationError
|
|
11
8
|
|
|
12
|
-
from contentctl.objects.baseline import Baseline
|
|
13
|
-
from contentctl.objects.investigation import Investigation
|
|
14
|
-
from contentctl.objects.playbook import Playbook
|
|
15
|
-
from contentctl.objects.deployment import Deployment
|
|
16
|
-
from contentctl.objects.macro import Macro
|
|
17
|
-
from contentctl.objects.lookup import LookupAdapter, Lookup
|
|
18
|
-
from contentctl.objects.atomic import AtomicEnrichment
|
|
19
|
-
from contentctl.objects.security_content_object import SecurityContentObject
|
|
20
|
-
from contentctl.objects.data_source import DataSource
|
|
21
|
-
from contentctl.objects.dashboard import Dashboard
|
|
22
9
|
from contentctl.enrichments.attack_enrichment import AttackEnrichment
|
|
23
10
|
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
24
|
-
|
|
11
|
+
from contentctl.helper.utils import Utils
|
|
12
|
+
from contentctl.input.yml_reader import YmlReader
|
|
13
|
+
from contentctl.objects.atomic import AtomicEnrichment
|
|
14
|
+
from contentctl.objects.baseline import Baseline
|
|
25
15
|
from contentctl.objects.config import validate
|
|
16
|
+
from contentctl.objects.dashboard import Dashboard
|
|
17
|
+
from contentctl.objects.data_source import DataSource
|
|
18
|
+
from contentctl.objects.deployment import Deployment
|
|
19
|
+
from contentctl.objects.detection import Detection
|
|
26
20
|
from contentctl.objects.enums import SecurityContentType
|
|
27
|
-
from contentctl.
|
|
21
|
+
from contentctl.objects.investigation import Investigation
|
|
22
|
+
from contentctl.objects.lookup import Lookup, LookupAdapter
|
|
23
|
+
from contentctl.objects.macro import Macro
|
|
24
|
+
from contentctl.objects.playbook import Playbook
|
|
25
|
+
from contentctl.objects.security_content_object import SecurityContentObject
|
|
26
|
+
from contentctl.objects.story import Story
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
@dataclass
|
|
@@ -113,20 +112,6 @@ class Director:
|
|
|
113
112
|
self.createSecurityContent(SecurityContentType.detections)
|
|
114
113
|
self.createSecurityContent(SecurityContentType.dashboards)
|
|
115
114
|
|
|
116
|
-
from contentctl.objects.abstract_security_content_objects.detection_abstract import (
|
|
117
|
-
MISSING_SOURCES,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
if len(MISSING_SOURCES) > 0:
|
|
121
|
-
missing_sources_string = "\n 🟡 ".join(sorted(list(MISSING_SOURCES)))
|
|
122
|
-
print(
|
|
123
|
-
"WARNING: The following data_sources have been used in detections, but are not yet defined.\n"
|
|
124
|
-
"This is not yet an error since not all data_sources have been defined, but will be convered to an error soon:\n 🟡 "
|
|
125
|
-
f"{missing_sources_string}"
|
|
126
|
-
)
|
|
127
|
-
else:
|
|
128
|
-
print("No missing data_sources!")
|
|
129
|
-
|
|
130
115
|
def createSecurityContent(self, contentType: SecurityContentType) -> None:
|
|
131
116
|
if contentType in [
|
|
132
117
|
SecurityContentType.deployments,
|
|
@@ -51,8 +51,6 @@ from contentctl.objects.security_content_object import SecurityContentObject
|
|
|
51
51
|
from contentctl.objects.test_group import TestGroup
|
|
52
52
|
from contentctl.objects.unit_test import UnitTest
|
|
53
53
|
|
|
54
|
-
MISSING_SOURCES: set[str] = set()
|
|
55
|
-
|
|
56
54
|
# Those AnalyticsTypes that we do not test via contentctl
|
|
57
55
|
SKIPPED_ANALYTICS_TYPES: set[str] = {AnalyticsType.Correlation}
|
|
58
56
|
|
|
@@ -476,7 +474,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
476
474
|
"name": lookup.name,
|
|
477
475
|
"description": lookup.description,
|
|
478
476
|
"filename": lookup.filename.name,
|
|
479
|
-
"default_match":
|
|
477
|
+
"default_match": lookup.default_match,
|
|
480
478
|
"case_sensitive_match": "true"
|
|
481
479
|
if lookup.case_sensitive_match
|
|
482
480
|
else "false",
|
|
@@ -514,7 +512,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
514
512
|
baseline.tags.detections = new_detections
|
|
515
513
|
|
|
516
514
|
# Data source may be defined 1 on each line, OR they may be defined as
|
|
517
|
-
#
|
|
515
|
+
# SOURCE_1 AND ANOTHERSOURCE AND A_THIRD_SOURCE
|
|
518
516
|
# if more than 1 data source is required for a detection (for example, because it includes a join)
|
|
519
517
|
# Parse and update the list to resolve individual names and remove potential duplicates
|
|
520
518
|
updated_data_source_names: set[str] = set()
|
|
@@ -524,27 +522,9 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
524
522
|
updated_data_source_names.update(split_data_sources)
|
|
525
523
|
|
|
526
524
|
sources = sorted(list(updated_data_source_names))
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
for source in sources:
|
|
531
|
-
try:
|
|
532
|
-
matched_data_sources += DataSource.mapNamesToSecurityContentObjects(
|
|
533
|
-
[source], director
|
|
534
|
-
)
|
|
535
|
-
except Exception:
|
|
536
|
-
# We gobble this up and add it to a global set so that we
|
|
537
|
-
# can print it ONCE at the end of the build of datasources.
|
|
538
|
-
# This will be removed later as per the note below
|
|
539
|
-
MISSING_SOURCES.add(source)
|
|
540
|
-
|
|
541
|
-
if len(missing_sources) > 0:
|
|
542
|
-
# This will be changed to ValueError when we have a complete list of data sources
|
|
543
|
-
print(
|
|
544
|
-
"WARNING: The following exception occurred when mapping the data_source field to "
|
|
545
|
-
f"DataSource objects:{missing_sources}"
|
|
546
|
-
)
|
|
547
|
-
|
|
525
|
+
matched_data_sources = DataSource.mapNamesToSecurityContentObjects(
|
|
526
|
+
sources, director
|
|
527
|
+
)
|
|
548
528
|
self.data_source_objects = matched_data_sources
|
|
549
529
|
|
|
550
530
|
for story in self.tags.analytic_story:
|
|
@@ -1075,3 +1055,30 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
1075
1055
|
# Return the summary
|
|
1076
1056
|
|
|
1077
1057
|
return summary_dict
|
|
1058
|
+
|
|
1059
|
+
@model_validator(mode="after")
|
|
1060
|
+
def validate_data_source_output_fields(self):
|
|
1061
|
+
# Skip validation for Hunting and Correlation types, or non-production detections
|
|
1062
|
+
if self.status != DetectionStatus.production or self.type in {
|
|
1063
|
+
AnalyticsType.Hunting,
|
|
1064
|
+
AnalyticsType.Correlation,
|
|
1065
|
+
}:
|
|
1066
|
+
return self
|
|
1067
|
+
|
|
1068
|
+
# Validate that all required output fields are present in the search
|
|
1069
|
+
for data_source in self.data_source_objects:
|
|
1070
|
+
if not data_source.output_fields:
|
|
1071
|
+
continue
|
|
1072
|
+
|
|
1073
|
+
missing_fields = [
|
|
1074
|
+
field for field in data_source.output_fields if field not in self.search
|
|
1075
|
+
]
|
|
1076
|
+
|
|
1077
|
+
if missing_fields:
|
|
1078
|
+
raise ValueError(
|
|
1079
|
+
f"Data source '{data_source.name}' has output fields "
|
|
1080
|
+
f"{missing_fields} that are not present in the search "
|
|
1081
|
+
f"for detection '{self.name}'"
|
|
1082
|
+
)
|
|
1083
|
+
|
|
1084
|
+
return self
|
|
@@ -17,10 +17,12 @@ class DataSource(SecurityContentObject):
|
|
|
17
17
|
source: str = Field(...)
|
|
18
18
|
sourcetype: str = Field(...)
|
|
19
19
|
separator: Optional[str] = None
|
|
20
|
+
separator_value: None | str = None
|
|
20
21
|
configuration: Optional[str] = None
|
|
21
22
|
supported_TA: list[TA] = []
|
|
22
23
|
fields: None | list = None
|
|
23
24
|
field_mappings: None | list = None
|
|
25
|
+
mitre_components: list[str] = []
|
|
24
26
|
convert_to_log_source: None | list = None
|
|
25
27
|
example_log: None | str = None
|
|
26
28
|
output_fields: list[str] = []
|
|
@@ -6,9 +6,10 @@ import pathlib
|
|
|
6
6
|
import re
|
|
7
7
|
from enum import StrEnum, auto
|
|
8
8
|
from functools import cached_property
|
|
9
|
-
from typing import TYPE_CHECKING, Annotated, Any, Literal,
|
|
9
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Self
|
|
10
10
|
|
|
11
11
|
from pydantic import (
|
|
12
|
+
BeforeValidator,
|
|
12
13
|
Field,
|
|
13
14
|
FilePath,
|
|
14
15
|
NonNegativeInt,
|
|
@@ -69,7 +70,19 @@ class Lookup_Type(StrEnum):
|
|
|
69
70
|
|
|
70
71
|
# TODO (#220): Split Lookup into 2 classes
|
|
71
72
|
class Lookup(SecurityContentObject, abc.ABC):
|
|
72
|
-
|
|
73
|
+
# We need to make sure that this is converted to a string because we widely
|
|
74
|
+
# use the string "False" in our lookup content. However, PyYAML reads this
|
|
75
|
+
# as a BOOL and this causes parsing to fail. As such, we will always
|
|
76
|
+
# convert this to a string if it is passed as a bool
|
|
77
|
+
default_match: Annotated[
|
|
78
|
+
str, BeforeValidator(lambda dm: str(dm).lower() if isinstance(dm, bool) else dm)
|
|
79
|
+
] = Field(
|
|
80
|
+
default="",
|
|
81
|
+
description="This field is given a default value of ''"
|
|
82
|
+
"because it is the default value specified in the transforms.conf "
|
|
83
|
+
"docs. Giving it a type of str rather than str | None simplifies "
|
|
84
|
+
"the typing for the field.",
|
|
85
|
+
)
|
|
73
86
|
# Per the documentation for transforms.conf, EXACT should not be specified in this list,
|
|
74
87
|
# so we include only WILDCARD and CIDR
|
|
75
88
|
match_type: list[Annotated[str, Field(pattern=r"(^WILDCARD|CIDR)\(.+\)$")]] = Field(
|
|
@@ -88,7 +101,7 @@ class Lookup(SecurityContentObject, abc.ABC):
|
|
|
88
101
|
|
|
89
102
|
# All fields custom to this model
|
|
90
103
|
model = {
|
|
91
|
-
"default_match":
|
|
104
|
+
"default_match": self.default_match,
|
|
92
105
|
"match_type": self.match_type_to_conf_format,
|
|
93
106
|
"min_matches": self.min_matches,
|
|
94
107
|
"max_matches": self.max_matches,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from typing import List, Union
|
|
2
1
|
import pathlib
|
|
2
|
+
from typing import List, Union
|
|
3
3
|
|
|
4
4
|
from contentctl.objects.detection import Detection
|
|
5
5
|
from contentctl.output.attack_nav_writer import AttackNavWriter
|
|
@@ -10,14 +10,21 @@ class AttackNavOutput:
|
|
|
10
10
|
self, detections: List[Detection], output_path: pathlib.Path
|
|
11
11
|
) -> None:
|
|
12
12
|
techniques: dict[str, dict[str, Union[List[str], int]]] = {}
|
|
13
|
+
|
|
13
14
|
for detection in detections:
|
|
14
15
|
for tactic in detection.tags.mitre_attack_id:
|
|
15
16
|
if tactic not in techniques:
|
|
16
17
|
techniques[tactic] = {"score": 0, "file_paths": []}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
detection_type = detection.source
|
|
20
|
+
detection_id = detection.id
|
|
21
|
+
|
|
22
|
+
# Store all three pieces of information separately
|
|
23
|
+
detection_info = f"{detection_type}|{detection_id}|{detection.name}"
|
|
24
|
+
|
|
25
|
+
techniques[tactic]["score"] = techniques[tactic].get("score", 0) + 1
|
|
26
|
+
if isinstance(techniques[tactic]["file_paths"], list):
|
|
27
|
+
techniques[tactic]["file_paths"].append(detection_info)
|
|
21
28
|
|
|
22
29
|
"""
|
|
23
30
|
for detection in objects:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
from typing import List, Union
|
|
4
|
+
|
|
5
|
+
VERSION = "4.5"
|
|
6
|
+
NAME = "Detection Coverage"
|
|
7
|
+
DESCRIPTION = "Security Content Detection Coverage"
|
|
8
|
+
DOMAIN = "enterprise-attack"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AttackNavWriter:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def writeAttackNavFile(
|
|
14
|
+
mitre_techniques: dict[str, dict[str, Union[List[str], int]]],
|
|
15
|
+
output_path: pathlib.Path,
|
|
16
|
+
) -> None:
|
|
17
|
+
max_count = max(
|
|
18
|
+
(technique["score"] for technique in mitre_techniques.values()), default=0
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
layer_json = {
|
|
22
|
+
"versions": {"attack": "16", "navigator": "5.1.0", "layer": VERSION},
|
|
23
|
+
"name": NAME,
|
|
24
|
+
"description": DESCRIPTION,
|
|
25
|
+
"domain": DOMAIN,
|
|
26
|
+
"techniques": [],
|
|
27
|
+
"gradient": {
|
|
28
|
+
"colors": ["#ffffff", "#66b1ff", "#096ed7"],
|
|
29
|
+
"minValue": 0,
|
|
30
|
+
"maxValue": max_count,
|
|
31
|
+
},
|
|
32
|
+
"filters": {
|
|
33
|
+
"platforms": [
|
|
34
|
+
"Windows",
|
|
35
|
+
"Linux",
|
|
36
|
+
"macOS",
|
|
37
|
+
"Network",
|
|
38
|
+
"AWS",
|
|
39
|
+
"GCP",
|
|
40
|
+
"Azure",
|
|
41
|
+
"Azure AD",
|
|
42
|
+
"Office 365",
|
|
43
|
+
"SaaS",
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"layout": {
|
|
47
|
+
"layout": "side",
|
|
48
|
+
"showName": True,
|
|
49
|
+
"showID": True,
|
|
50
|
+
"showAggregateScores": False,
|
|
51
|
+
},
|
|
52
|
+
"legendItems": [
|
|
53
|
+
{"label": "No detections", "color": "#ffffff"},
|
|
54
|
+
{"label": "Has detections", "color": "#66b1ff"},
|
|
55
|
+
],
|
|
56
|
+
"showTacticRowBackground": True,
|
|
57
|
+
"tacticRowBackground": "#dddddd",
|
|
58
|
+
"selectTechniquesAcrossTactics": True,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for technique_id, data in mitre_techniques.items():
|
|
62
|
+
links = []
|
|
63
|
+
for detection_info in data["file_paths"]:
|
|
64
|
+
# Split the detection info into its components
|
|
65
|
+
detection_type, detection_id, detection_name = detection_info.split("|")
|
|
66
|
+
|
|
67
|
+
# Construct research website URL (without the name)
|
|
68
|
+
research_url = (
|
|
69
|
+
f"https://research.splunk.com/{detection_type}/{detection_id}/"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
links.append({"label": detection_name, "url": research_url})
|
|
73
|
+
|
|
74
|
+
layer_technique = {
|
|
75
|
+
"techniqueID": technique_id,
|
|
76
|
+
"score": data["score"],
|
|
77
|
+
"enabled": True,
|
|
78
|
+
"links": links,
|
|
79
|
+
}
|
|
80
|
+
layer_json["techniques"].append(layer_technique)
|
|
81
|
+
|
|
82
|
+
with open(output_path, "w") as outfile:
|
|
83
|
+
json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
|
|
@@ -7,8 +7,8 @@ filename = {{ lookup.app_filename.name }}
|
|
|
7
7
|
collection = {{ lookup.collection }}
|
|
8
8
|
external_type = kvstore
|
|
9
9
|
{% endif %}
|
|
10
|
-
{% if lookup.default_match
|
|
11
|
-
default_match = {{ lookup.default_match
|
|
10
|
+
{% if lookup.default_match != '' %}
|
|
11
|
+
default_match = {{ lookup.default_match }}
|
|
12
12
|
{% endif %}
|
|
13
13
|
{% if lookup.case_sensitive_match is defined and lookup.case_sensitive_match != None %}
|
|
14
14
|
case_sensitive_match = {{ lookup.case_sensitive_match | lower }}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "contentctl"
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
version = "5.2.0"
|
|
4
5
|
|
|
5
6
|
description = "Splunk Content Control Tool"
|
|
6
7
|
authors = ["STRT <research@splunk.com>"]
|
|
@@ -32,7 +33,7 @@ gitpython = "^3.1.43"
|
|
|
32
33
|
setuptools = ">=69.5.1,<76.0.0"
|
|
33
34
|
|
|
34
35
|
[tool.poetry.group.dev.dependencies]
|
|
35
|
-
ruff = "^0.9.
|
|
36
|
+
ruff = "^0.9.10"
|
|
36
37
|
|
|
37
38
|
[build-system]
|
|
38
39
|
requires = ["poetry-core>=1.0.0"]
|