contentctl 4.2.5__tar.gz → 4.3.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.2.5 → contentctl-4.3.0}/PKG-INFO +15 -5
- {contentctl-4.2.5 → contentctl-4.3.0}/README.md +14 -2
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/build.py +0 -14
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/validate.py +0 -1
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/input/director.py +9 -44
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +56 -74
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/config.py +0 -2
- {contentctl-4.2.5 → contentctl-4.3.0}/pyproject.toml +1 -4
- contentctl-4.2.5/contentctl/actions/convert.py +0 -25
- contentctl-4.2.5/contentctl/input/backend_splunk_ba.py +0 -144
- contentctl-4.2.5/contentctl/input/sigma_converter.py +0 -436
- contentctl-4.2.5/contentctl/input/ssa_detection_builder.py +0 -169
- contentctl-4.2.5/contentctl/output/ba_yml_output.py +0 -153
- contentctl-4.2.5/contentctl/output/finding_report_writer.py +0 -91
- {contentctl-4.2.5 → contentctl-4.3.0}/LICENSE.md +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/__init__.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/deploy_acs.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/GitService.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/doc_gen.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/initialize.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/initialize_old.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/inspect.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/new_content.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/release_notes.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/reporting.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/actions/test.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/api.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/contentctl.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/enrichments/attack_enrichment.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/enrichments/cve_enrichment.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/helper/link_validator.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/helper/logger.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/helper/splunk_app.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/helper/utils.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/input/new_content_questions.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/input/yml_reader.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/alert_action.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/atomic.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/base_test.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/base_test_result.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/baseline.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/baseline_tags.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/constants.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/correlation_search.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/data_source.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/deployment.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/deployment_email.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/deployment_notable.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/deployment_phantom.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/deployment_rba.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/deployment_scheduling.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/deployment_slack.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/detection.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/detection_tags.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/enums.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/errors.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/event_source.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/integration_test.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/integration_test_result.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/investigation.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/investigation_tags.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/lookup.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/macro.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/notable_action.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/notable_event.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/observable.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/playbook.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/playbook_tags.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/risk_analysis_action.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/risk_event.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/risk_object.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/security_content_object.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/ssa_detection.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/ssa_detection_tags.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/story.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/story_tags.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/test_group.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/threat_object.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/unit_test.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/unit_test_attack_data.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/unit_test_baseline.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/unit_test_old.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/unit_test_result.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/objects/unit_test_ssa.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/api_json_output.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/attack_nav_output.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/attack_nav_writer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/conf_output.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/conf_writer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/data_source_writer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/detection_writer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/doc_md_output.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/jinja_writer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/json_writer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/new_content_yml_output.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/svg_output.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/app.conf.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/app.manifest.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/collections.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/content-version.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/detection_count.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_detections.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_stories.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/finding_report.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/header.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/macros.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/panel.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/transforms.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/yml_output.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/output/yml_writer.py +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/README.md +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_default.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/README.md +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/app.conf +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/commands.conf +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/content-version.conf +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/datamodels_cim.conf +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/datamodels_custom.conf +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/detections/application/.gitkeep +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/detections/network/.gitkeep +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/detections/web/.gitkeep +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
- {contentctl-4.2.5 → contentctl-4.3.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.3.0
|
|
4
4
|
Summary: Splunk Content Control Tool
|
|
5
5
|
License: Apache 2.0
|
|
6
6
|
Author: STRT
|
|
@@ -19,8 +19,6 @@ Requires-Dist: gitpython (>=3.1.43,<4.0.0)
|
|
|
19
19
|
Requires-Dist: pycvesearch (>=1.2,<2.0)
|
|
20
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.11.5,<0.12.0)
|
|
23
|
-
Requires-Dist: pysigma-backend-splunk (>=1.1.0,<2.0.0)
|
|
24
22
|
Requires-Dist: questionary (>=2.0.1,<3.0.0)
|
|
25
23
|
Requires-Dist: requests (>=2.32.2,<2.33.0)
|
|
26
24
|
Requires-Dist: semantic-version (>=2.10.0,<3.0.0)
|
|
@@ -36,8 +34,20 @@ Description-Content-Type: text/markdown
|
|
|
36
34
|
<p align="center">
|
|
37
35
|
<img src="docs/contentctl_logo_white.png" title="In case you're wondering, it's a capybara" alt="contentctl logo" width="250" height="250"></p>
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
# contentctl Quick Start Guide
|
|
38
|
+
If you are already familiar with contentctl, the following common commands may be very useful for basic operations
|
|
39
|
+
|
|
40
|
+
| Operation | Command |
|
|
41
|
+
|-----------|---------|
|
|
42
|
+
| Create a repository | `contentctl init` |
|
|
43
|
+
| Validate Your Content | `contentctl validate` |
|
|
44
|
+
| Validate Your Content, performing MITRE Enrichments | `contentctl validate –-enrichments`|
|
|
45
|
+
| Build Your App | `contentctl build` |
|
|
46
|
+
| Test All the content in your app, pausing so that you can debug a search if it fails | `contentctl test –-post-test-behavior pause_on_failure mode:all` |
|
|
47
|
+
| Test All the content in your app, pausing after every detection to allow debugging | `contentctl test –-post-test-behavior always_pause mode:all` |
|
|
48
|
+
| Test 1 or more specified detections. If you are testing more than one detection, the paths are space-separated. You may also use shell-expanded regexes | `contentctl test –-post-test-behavior always_pause mode:selected --mode.files detections/endpoint/7zip_commandline_to_smb_share_path.yml detections/cloud/aws_multi_factor_authentication_disabled.yml detections/application/okta*` |
|
|
49
|
+
| Diff your current branch with a target_branch and test detections that have been updated. Your current branch **must be DIFFERENT** than the target_branch | `contentctl test –-post-test-behavior always_pause mode:changes –-mode.target_branch develop` |
|
|
50
|
+
| Perform Integration Testing of all content. Note that Enterprise Security MUST be listed as an app in your contentctl.yml folder, otherwise all tests will subsequently fail | `contentctl test –-enable-integration-testing --post-test-behavior never_pause mode:all` |
|
|
41
51
|
|
|
42
52
|
# Introduction
|
|
43
53
|
#### Security Is Hard
|
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="docs/contentctl_logo_white.png" title="In case you're wondering, it's a capybara" alt="contentctl logo" width="250" height="250"></p>
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
# contentctl Quick Start Guide
|
|
7
|
+
If you are already familiar with contentctl, the following common commands may be very useful for basic operations
|
|
8
|
+
|
|
9
|
+
| Operation | Command |
|
|
10
|
+
|-----------|---------|
|
|
11
|
+
| Create a repository | `contentctl init` |
|
|
12
|
+
| Validate Your Content | `contentctl validate` |
|
|
13
|
+
| Validate Your Content, performing MITRE Enrichments | `contentctl validate –-enrichments`|
|
|
14
|
+
| Build Your App | `contentctl build` |
|
|
15
|
+
| Test All the content in your app, pausing so that you can debug a search if it fails | `contentctl test –-post-test-behavior pause_on_failure mode:all` |
|
|
16
|
+
| Test All the content in your app, pausing after every detection to allow debugging | `contentctl test –-post-test-behavior always_pause mode:all` |
|
|
17
|
+
| Test 1 or more specified detections. If you are testing more than one detection, the paths are space-separated. You may also use shell-expanded regexes | `contentctl test –-post-test-behavior always_pause mode:selected --mode.files detections/endpoint/7zip_commandline_to_smb_share_path.yml detections/cloud/aws_multi_factor_authentication_disabled.yml detections/application/okta*` |
|
|
18
|
+
| Diff your current branch with a target_branch and test detections that have been updated. Your current branch **must be DIFFERENT** than the target_branch | `contentctl test –-post-test-behavior always_pause mode:changes –-mode.target_branch develop` |
|
|
19
|
+
| Perform Integration Testing of all content. Note that Enterprise Security MUST be listed as an app in your contentctl.yml folder, otherwise all tests will subsequently fail | `contentctl test –-enable-integration-testing --post-test-behavior never_pause mode:all` |
|
|
8
20
|
|
|
9
21
|
# Introduction
|
|
10
22
|
#### Security Is Hard
|
|
@@ -8,7 +8,6 @@ from contentctl.objects.enums import SecurityContentProduct, SecurityContentType
|
|
|
8
8
|
from contentctl.input.director import Director, DirectorOutputDto
|
|
9
9
|
from contentctl.output.conf_output import ConfOutput
|
|
10
10
|
from contentctl.output.conf_writer import ConfWriter
|
|
11
|
-
from contentctl.output.ba_yml_output import BAYmlOutput
|
|
12
11
|
from contentctl.output.api_json_output import ApiJsonOutput
|
|
13
12
|
from contentctl.output.data_source_writer import DataSourceWriter
|
|
14
13
|
from contentctl.objects.lookup import Lookup
|
|
@@ -86,17 +85,4 @@ class Build:
|
|
|
86
85
|
|
|
87
86
|
print(f"Build of '{input_dto.config.app.title}' API successful to {input_dto.config.getAPIPath()}")
|
|
88
87
|
|
|
89
|
-
if input_dto.config.build_ssa:
|
|
90
|
-
|
|
91
|
-
srs_path = input_dto.config.getSSAPath() / 'srs'
|
|
92
|
-
complex_path = input_dto.config.getSSAPath() / 'complex'
|
|
93
|
-
shutil.rmtree(srs_path, ignore_errors=True)
|
|
94
|
-
shutil.rmtree(complex_path, ignore_errors=True)
|
|
95
|
-
srs_path.mkdir(parents=True)
|
|
96
|
-
complex_path.mkdir(parents=True)
|
|
97
|
-
ba_yml_output = BAYmlOutput()
|
|
98
|
-
ba_yml_output.writeObjects(input_dto.director_output_dto.ssa_detections, str(input_dto.config.getSSAPath()))
|
|
99
|
-
|
|
100
|
-
print(f"Build of 'SSA' successful to {input_dto.config.getSSAPath()}")
|
|
101
|
-
|
|
102
88
|
return input_dto.director_output_dto
|
|
@@ -28,13 +28,11 @@ from contentctl.enrichments.attack_enrichment import AttackEnrichment
|
|
|
28
28
|
from contentctl.enrichments.cve_enrichment import CveEnrichment
|
|
29
29
|
|
|
30
30
|
from contentctl.objects.config import validate
|
|
31
|
-
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
|
|
32
31
|
from contentctl.objects.enums import SecurityContentType
|
|
33
32
|
|
|
34
33
|
from contentctl.objects.enums import DetectionStatus
|
|
35
34
|
from contentctl.helper.utils import Utils
|
|
36
35
|
|
|
37
|
-
from contentctl.input.ssa_detection_builder import SSADetectionBuilder
|
|
38
36
|
from contentctl.objects.enums import SecurityContentType
|
|
39
37
|
|
|
40
38
|
from contentctl.objects.enums import DetectionStatus
|
|
@@ -56,7 +54,6 @@ class DirectorOutputDto:
|
|
|
56
54
|
macros: list[Macro]
|
|
57
55
|
lookups: list[Lookup]
|
|
58
56
|
deployments: list[Deployment]
|
|
59
|
-
ssa_detections: list[SSADetection]
|
|
60
57
|
data_sources: list[DataSource]
|
|
61
58
|
name_to_content_map: dict[str, SecurityContentObject] = field(default_factory=dict)
|
|
62
59
|
uuid_to_content_map: dict[UUID, SecurityContentObject] = field(default_factory=dict)
|
|
@@ -98,8 +95,6 @@ class DirectorOutputDto:
|
|
|
98
95
|
self.stories.append(content)
|
|
99
96
|
elif isinstance(content, Detection):
|
|
100
97
|
self.detections.append(content)
|
|
101
|
-
elif isinstance(content, SSADetection):
|
|
102
|
-
self.ssa_detections.append(content)
|
|
103
98
|
elif isinstance(content, DataSource):
|
|
104
99
|
self.data_sources.append(content)
|
|
105
100
|
else:
|
|
@@ -112,11 +107,9 @@ class DirectorOutputDto:
|
|
|
112
107
|
class Director():
|
|
113
108
|
input_dto: validate
|
|
114
109
|
output_dto: DirectorOutputDto
|
|
115
|
-
ssa_detection_builder: SSADetectionBuilder
|
|
116
110
|
|
|
117
111
|
def __init__(self, output_dto: DirectorOutputDto) -> None:
|
|
118
112
|
self.output_dto = output_dto
|
|
119
|
-
self.ssa_detection_builder = SSADetectionBuilder()
|
|
120
113
|
|
|
121
114
|
def execute(self, input_dto: validate) -> None:
|
|
122
115
|
self.input_dto = input_dto
|
|
@@ -129,7 +122,6 @@ class Director():
|
|
|
129
122
|
self.createSecurityContent(SecurityContentType.data_sources)
|
|
130
123
|
self.createSecurityContent(SecurityContentType.playbooks)
|
|
131
124
|
self.createSecurityContent(SecurityContentType.detections)
|
|
132
|
-
self.createSecurityContent(SecurityContentType.ssa_detections)
|
|
133
125
|
|
|
134
126
|
|
|
135
127
|
from contentctl.objects.abstract_security_content_objects.detection_abstract import MISSING_SOURCES
|
|
@@ -142,12 +134,7 @@ class Director():
|
|
|
142
134
|
print("No missing data_sources!")
|
|
143
135
|
|
|
144
136
|
def createSecurityContent(self, contentType: SecurityContentType) -> None:
|
|
145
|
-
if contentType
|
|
146
|
-
files = Utils.get_all_yml_files_from_directory(
|
|
147
|
-
os.path.join(self.input_dto.path, "ssa_detections")
|
|
148
|
-
)
|
|
149
|
-
security_content_files = [f for f in files if f.name.startswith("ssa___")]
|
|
150
|
-
elif contentType in [
|
|
137
|
+
if contentType in [
|
|
151
138
|
SecurityContentType.deployments,
|
|
152
139
|
SecurityContentType.lookups,
|
|
153
140
|
SecurityContentType.macros,
|
|
@@ -179,43 +166,37 @@ class Director():
|
|
|
179
166
|
modelDict = YmlReader.load_file(file)
|
|
180
167
|
|
|
181
168
|
if contentType == SecurityContentType.lookups:
|
|
182
|
-
lookup = Lookup.model_validate(modelDict,context={"output_dto":self.output_dto, "config":self.input_dto})
|
|
169
|
+
lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
|
|
183
170
|
self.output_dto.addContentToDictMappings(lookup)
|
|
184
171
|
|
|
185
172
|
elif contentType == SecurityContentType.macros:
|
|
186
|
-
macro = Macro.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
173
|
+
macro = Macro.model_validate(modelDict, context={"output_dto":self.output_dto})
|
|
187
174
|
self.output_dto.addContentToDictMappings(macro)
|
|
188
175
|
|
|
189
176
|
elif contentType == SecurityContentType.deployments:
|
|
190
|
-
deployment = Deployment.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
177
|
+
deployment = Deployment.model_validate(modelDict, context={"output_dto":self.output_dto})
|
|
191
178
|
self.output_dto.addContentToDictMappings(deployment)
|
|
192
179
|
|
|
193
180
|
elif contentType == SecurityContentType.playbooks:
|
|
194
|
-
playbook = Playbook.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
181
|
+
playbook = Playbook.model_validate(modelDict, context={"output_dto":self.output_dto})
|
|
195
182
|
self.output_dto.addContentToDictMappings(playbook)
|
|
196
183
|
|
|
197
184
|
elif contentType == SecurityContentType.baselines:
|
|
198
|
-
baseline = Baseline.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
185
|
+
baseline = Baseline.model_validate(modelDict, context={"output_dto":self.output_dto})
|
|
199
186
|
self.output_dto.addContentToDictMappings(baseline)
|
|
200
187
|
|
|
201
188
|
elif contentType == SecurityContentType.investigations:
|
|
202
|
-
investigation = Investigation.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
189
|
+
investigation = Investigation.model_validate(modelDict, context={"output_dto":self.output_dto})
|
|
203
190
|
self.output_dto.addContentToDictMappings(investigation)
|
|
204
191
|
|
|
205
192
|
elif contentType == SecurityContentType.stories:
|
|
206
|
-
story = Story.model_validate(modelDict,context={"output_dto":self.output_dto})
|
|
193
|
+
story = Story.model_validate(modelDict, context={"output_dto":self.output_dto})
|
|
207
194
|
self.output_dto.addContentToDictMappings(story)
|
|
208
195
|
|
|
209
196
|
elif contentType == SecurityContentType.detections:
|
|
210
|
-
detection = Detection.model_validate(modelDict,context={"output_dto":self.output_dto, "app":self.input_dto.app})
|
|
197
|
+
detection = Detection.model_validate(modelDict, context={"output_dto":self.output_dto, "app":self.input_dto.app})
|
|
211
198
|
self.output_dto.addContentToDictMappings(detection)
|
|
212
199
|
|
|
213
|
-
elif contentType == SecurityContentType.ssa_detections:
|
|
214
|
-
self.constructSSADetection(self.ssa_detection_builder, self.output_dto,str(file))
|
|
215
|
-
ssa_detection = self.ssa_detection_builder.getObject()
|
|
216
|
-
if ssa_detection.status in [DetectionStatus.production.value, DetectionStatus.validation.value]:
|
|
217
|
-
self.output_dto.addContentToDictMappings(ssa_detection)
|
|
218
|
-
|
|
219
200
|
elif contentType == SecurityContentType.data_sources:
|
|
220
201
|
data_source = DataSource.model_validate(
|
|
221
202
|
modelDict, context={"output_dto": self.output_dto}
|
|
@@ -262,19 +243,3 @@ class Director():
|
|
|
262
243
|
f"The following {len(validation_errors)} error(s) were found during validation:\n\n{errors_string}\n\nVALIDATION FAILED"
|
|
263
244
|
)
|
|
264
245
|
|
|
265
|
-
def constructSSADetection(
|
|
266
|
-
self,
|
|
267
|
-
builder: SSADetectionBuilder,
|
|
268
|
-
directorOutput: DirectorOutputDto,
|
|
269
|
-
file_path: str,
|
|
270
|
-
) -> None:
|
|
271
|
-
builder.reset()
|
|
272
|
-
builder.setObject(file_path)
|
|
273
|
-
builder.addMitreAttackEnrichmentNew(directorOutput.attack_enrichment)
|
|
274
|
-
builder.addKillChainPhase()
|
|
275
|
-
builder.addCIS()
|
|
276
|
-
builder.addNist()
|
|
277
|
-
builder.addAnnotations()
|
|
278
|
-
builder.addMappings()
|
|
279
|
-
builder.addUnitTest()
|
|
280
|
-
builder.addRBA()
|
|
@@ -46,7 +46,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
46
46
|
status: DetectionStatus = Field(...)
|
|
47
47
|
data_source: list[str] = []
|
|
48
48
|
tags: DetectionTags = Field(...)
|
|
49
|
-
search:
|
|
49
|
+
search: str = Field(...)
|
|
50
50
|
how_to_implement: str = Field(..., min_length=4)
|
|
51
51
|
known_false_positives: str = Field(..., min_length=4)
|
|
52
52
|
|
|
@@ -65,11 +65,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
65
65
|
|
|
66
66
|
@field_validator("search", mode="before")
|
|
67
67
|
@classmethod
|
|
68
|
-
def validate_presence_of_filter_macro(
|
|
69
|
-
cls,
|
|
70
|
-
value: Union[str, dict[str, Any]],
|
|
71
|
-
info: ValidationInfo
|
|
72
|
-
) -> Union[str, dict[str, Any]]:
|
|
68
|
+
def validate_presence_of_filter_macro(cls, value:str, info:ValidationInfo)->str:
|
|
73
69
|
"""
|
|
74
70
|
Validates that, if required to be present, the filter macro is present with the proper name.
|
|
75
71
|
The filter macro MUST be derived from the name of the detection
|
|
@@ -83,12 +79,9 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
83
79
|
|
|
84
80
|
Returns:
|
|
85
81
|
Union[str, dict[str,Any]]: The search, either in sigma or SPL format.
|
|
86
|
-
"""
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# If the search is a dict, then it is in Sigma format so return it
|
|
90
|
-
return value
|
|
91
|
-
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
92
85
|
# Otherwise, the search is SPL.
|
|
93
86
|
|
|
94
87
|
# In the future, we will may add support that makes the inclusion of the
|
|
@@ -155,15 +148,16 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
155
148
|
@computed_field
|
|
156
149
|
@property
|
|
157
150
|
def datamodel(self) -> List[DataModel]:
|
|
158
|
-
if
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
return [dm for dm in DataModel if dm.value in self.search]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
162
155
|
|
|
163
156
|
@computed_field
|
|
164
157
|
@property
|
|
165
158
|
def source(self) -> str:
|
|
166
159
|
return self.file_path.absolute().parent.name
|
|
160
|
+
|
|
167
161
|
|
|
168
162
|
deployment: Deployment = Field({})
|
|
169
163
|
|
|
@@ -249,12 +243,9 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
249
243
|
@computed_field
|
|
250
244
|
@property
|
|
251
245
|
def providing_technologies(self) -> List[ProvidingTechnology]:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
# Dict-formatted searches (sigma) will not have providing technologies
|
|
256
|
-
return []
|
|
257
|
-
|
|
246
|
+
return ProvidingTechnology.getProvidingTechFromSearch(self.search)
|
|
247
|
+
|
|
248
|
+
|
|
258
249
|
@computed_field
|
|
259
250
|
@property
|
|
260
251
|
def risk(self) -> list[dict[str, Any]]:
|
|
@@ -445,18 +436,13 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
445
436
|
|
|
446
437
|
@field_validator('lookups', mode="before")
|
|
447
438
|
@classmethod
|
|
448
|
-
def getDetectionLookups(cls, v:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if not isinstance(search, str):
|
|
456
|
-
# The search was sigma formatted (or failed other validation and was None), so we will
|
|
457
|
-
# not validate macros in it
|
|
458
|
-
return []
|
|
459
|
-
|
|
439
|
+
def getDetectionLookups(cls, v:list[str], info:ValidationInfo) -> list[Lookup]:
|
|
440
|
+
director:DirectorOutputDto = info.context.get("output_dto",None)
|
|
441
|
+
|
|
442
|
+
search:Union[str,None] = info.data.get("search",None)
|
|
443
|
+
if search is None:
|
|
444
|
+
raise ValueError("Search was None - is this file missing the search field?")
|
|
445
|
+
|
|
460
446
|
lookups = Lookup.get_lookups(search, director)
|
|
461
447
|
return lookups
|
|
462
448
|
|
|
@@ -496,11 +482,9 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
496
482
|
|
|
497
483
|
director: DirectorOutputDto = info.context.get("output_dto", None)
|
|
498
484
|
|
|
499
|
-
search: str |
|
|
500
|
-
if
|
|
501
|
-
|
|
502
|
-
# not validate macros in it
|
|
503
|
-
return []
|
|
485
|
+
search: str | None = info.data.get("search", None)
|
|
486
|
+
if search is None:
|
|
487
|
+
raise ValueError("Search was None - is this file missing the search field?")
|
|
504
488
|
|
|
505
489
|
search_name: Union[str, Any] = info.data.get("name", None)
|
|
506
490
|
message = f"Expected 'search_name' to be a string, instead it was [{type(search_name)}]"
|
|
@@ -614,45 +598,43 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
614
598
|
|
|
615
599
|
@model_validator(mode="after")
|
|
616
600
|
def search_observables_exist_validate(self):
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
observable_fields = [ob.name.lower() for ob in self.tags.observable]
|
|
601
|
+
observable_fields = [ob.name.lower() for ob in self.tags.observable]
|
|
620
602
|
|
|
621
|
-
|
|
622
|
-
|
|
603
|
+
# All $field$ fields from the message must appear in the search
|
|
604
|
+
field_match_regex = r"\$([^\s.]*)\$"
|
|
623
605
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
606
|
+
missing_fields: set[str]
|
|
607
|
+
if self.tags.message:
|
|
608
|
+
matches = re.findall(field_match_regex, self.tags.message.lower())
|
|
609
|
+
message_fields = [match.replace("$", "").lower() for match in matches]
|
|
610
|
+
missing_fields = set([field for field in observable_fields if field not in self.search.lower()])
|
|
611
|
+
else:
|
|
612
|
+
message_fields = []
|
|
613
|
+
missing_fields = set()
|
|
614
|
+
|
|
615
|
+
error_messages: list[str] = []
|
|
616
|
+
if len(missing_fields) > 0:
|
|
617
|
+
error_messages.append(
|
|
618
|
+
"The following fields are declared as observables, but do not exist in the "
|
|
619
|
+
f"search: {missing_fields}"
|
|
620
|
+
)
|
|
639
621
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
622
|
+
missing_fields = set([field for field in message_fields if field not in self.search.lower()])
|
|
623
|
+
if len(missing_fields) > 0:
|
|
624
|
+
error_messages.append(
|
|
625
|
+
"The following fields are used as fields in the message, but do not exist in "
|
|
626
|
+
f"the search: {missing_fields}"
|
|
627
|
+
)
|
|
646
628
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
629
|
+
# NOTE: we ignore the type error around self.status because we are using Pydantic's
|
|
630
|
+
# use_enum_values configuration
|
|
631
|
+
# https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.populate_by_name
|
|
632
|
+
if len(error_messages) > 0 and self.status == DetectionStatus.production.value: # type: ignore
|
|
633
|
+
msg = (
|
|
634
|
+
"Use of fields in observables/messages that do not appear in search:\n\t- "
|
|
635
|
+
"\n\t- ".join(error_messages)
|
|
636
|
+
)
|
|
637
|
+
raise ValueError(msg)
|
|
656
638
|
|
|
657
639
|
# Found everything
|
|
658
640
|
return self
|
|
@@ -175,7 +175,6 @@ class validate(Config_Base):
|
|
|
175
175
|
"be avoided for performance reasons.")
|
|
176
176
|
build_app: bool = Field(default=True, description="Should an app be built and output in the build_path?")
|
|
177
177
|
build_api: bool = Field(default=False, description="Should api objects be built and output in the build_path?")
|
|
178
|
-
build_ssa: bool = Field(default=False, description="Should ssa objects be built and output in the build_path?")
|
|
179
178
|
data_source_TA_validation: bool = Field(default=False, description="Validate latest TA information from Splunkbase")
|
|
180
179
|
|
|
181
180
|
def getAtomicRedTeamRepoPath(self, atomic_red_team_repo_name:str = "atomic-red-team"):
|
|
@@ -577,7 +576,6 @@ class test_common(build):
|
|
|
577
576
|
# output to dist. We have already built it!
|
|
578
577
|
self.build_app = False
|
|
579
578
|
self.build_api = False
|
|
580
|
-
self.build_ssa = False
|
|
581
579
|
self.enrichments = False
|
|
582
580
|
|
|
583
581
|
self.enable_integration_testing = True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "contentctl"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.3.0"
|
|
4
4
|
description = "Splunk Content Control Tool"
|
|
5
5
|
authors = ["STRT <research@splunk.com>"]
|
|
6
6
|
license = "Apache 2.0"
|
|
@@ -24,9 +24,6 @@ splunk-sdk = "^2.0.1"
|
|
|
24
24
|
semantic-version = "^2.10.0"
|
|
25
25
|
bottle = "^0.12.25"
|
|
26
26
|
tqdm = "^4.66.4"
|
|
27
|
-
#splunk-appinspect = "^2.36.0"
|
|
28
|
-
pysigma = "^0.11.5"
|
|
29
|
-
pysigma-backend-splunk = "^1.1.0"
|
|
30
27
|
pygit2 = "^1.14.1"
|
|
31
28
|
tyro = "^0.8.3"
|
|
32
29
|
gitpython = "^3.1.43"
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import sys
|
|
3
|
-
import shutil
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
from dataclasses import dataclass
|
|
7
|
-
|
|
8
|
-
from contentctl.input.sigma_converter import *
|
|
9
|
-
from contentctl.output.yml_output import YmlOutput
|
|
10
|
-
|
|
11
|
-
@dataclass(frozen=True)
|
|
12
|
-
class ConvertInputDto:
|
|
13
|
-
sigma_converter_input_dto: SigmaConverterInputDto
|
|
14
|
-
output_path : str
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class Convert:
|
|
18
|
-
|
|
19
|
-
def execute(self, input_dto: ConvertInputDto) -> None:
|
|
20
|
-
sigma_converter_output_dto = SigmaConverterOutputDto([])
|
|
21
|
-
sigma_converter = SigmaConverter(sigma_converter_output_dto)
|
|
22
|
-
sigma_converter.execute(input_dto.sigma_converter_input_dto)
|
|
23
|
-
|
|
24
|
-
yml_output = YmlOutput()
|
|
25
|
-
yml_output.writeDetections(sigma_converter_output_dto.detections, input_dto.output_path)
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from sigma.conversion.state import ConversionState
|
|
3
|
-
from sigma.rule import SigmaRule
|
|
4
|
-
from sigma.conversion.base import TextQueryBackend
|
|
5
|
-
from sigma.conversion.deferred import DeferredTextQueryExpression
|
|
6
|
-
from sigma.conditions import ConditionFieldEqualsValueExpression, ConditionOR, ConditionAND, ConditionNOT, ConditionItem
|
|
7
|
-
from sigma.types import SigmaCompareExpression
|
|
8
|
-
from sigma.exceptions import SigmaFeatureNotSupportedByBackendError
|
|
9
|
-
from sigma.pipelines.splunk.splunk import splunk_sysmon_process_creation_cim_mapping, splunk_windows_registry_cim_mapping, splunk_windows_file_event_cim_mapping
|
|
10
|
-
|
|
11
|
-
from contentctl.objects.ssa_detection import SSADetection
|
|
12
|
-
|
|
13
|
-
from typing import ClassVar, Dict, List, Optional, Pattern, Tuple
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class SplunkBABackend(TextQueryBackend):
|
|
17
|
-
"""Splunk SPL backend."""
|
|
18
|
-
precedence: ClassVar[Tuple[ConditionItem, ConditionItem, ConditionItem]] = (ConditionNOT, ConditionOR, ConditionAND)
|
|
19
|
-
group_expression : ClassVar[str] = "({expr})"
|
|
20
|
-
parenthesize : bool = True
|
|
21
|
-
|
|
22
|
-
or_token : ClassVar[str] = "OR"
|
|
23
|
-
and_token : ClassVar[str] = "AND"
|
|
24
|
-
not_token : ClassVar[str] = "NOT"
|
|
25
|
-
eq_token : ClassVar[str] = "="
|
|
26
|
-
|
|
27
|
-
field_quote: ClassVar[str] = '"'
|
|
28
|
-
field_quote_pattern: ClassVar[Pattern] = re.compile("^[\w.]+$")
|
|
29
|
-
|
|
30
|
-
str_quote : ClassVar[str] = '"'
|
|
31
|
-
escape_char : ClassVar[str] = "\\"
|
|
32
|
-
wildcard_multi : ClassVar[str] = "%"
|
|
33
|
-
wildcard_single : ClassVar[str] = "%"
|
|
34
|
-
add_escaped : ClassVar[str] = "\\"
|
|
35
|
-
|
|
36
|
-
re_expression : ClassVar[str] = "match({field}, /(?i){regex}/)=true"
|
|
37
|
-
re_escape_char : ClassVar[str] = ""
|
|
38
|
-
re_escape : ClassVar[Tuple[str]] = ('"',)
|
|
39
|
-
|
|
40
|
-
cidr_expression : ClassVar[str] = "{value}"
|
|
41
|
-
|
|
42
|
-
compare_op_expression : ClassVar[str] = "{field}{operator}{value}"
|
|
43
|
-
compare_operators : ClassVar[Dict[SigmaCompareExpression.CompareOperators, str]] = {
|
|
44
|
-
SigmaCompareExpression.CompareOperators.LT : "<",
|
|
45
|
-
SigmaCompareExpression.CompareOperators.LTE : "<=",
|
|
46
|
-
SigmaCompareExpression.CompareOperators.GT : ">",
|
|
47
|
-
SigmaCompareExpression.CompareOperators.GTE : ">=",
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
field_null_expression : ClassVar[str] = "{field} IS NOT NULL"
|
|
51
|
-
|
|
52
|
-
convert_or_as_in : ClassVar[bool] = True
|
|
53
|
-
convert_and_as_in : ClassVar[bool] = False
|
|
54
|
-
in_expressions_allow_wildcards : ClassVar[bool] = False
|
|
55
|
-
field_in_list_expression : ClassVar[str] = "{field} {op} ({list})"
|
|
56
|
-
or_in_operator : ClassVar[Optional[str]] = "IN"
|
|
57
|
-
list_separator : ClassVar[str] = ", "
|
|
58
|
-
|
|
59
|
-
unbound_value_str_expression : ClassVar[str] = '{value}'
|
|
60
|
-
unbound_value_num_expression : ClassVar[str] = '{value}'
|
|
61
|
-
unbound_value_re_expression : ClassVar[str] = '{value}'
|
|
62
|
-
|
|
63
|
-
deferred_start : ClassVar[str] = " "
|
|
64
|
-
deferred_separator : ClassVar[str] = " OR "
|
|
65
|
-
deferred_only_query : ClassVar[str] = "*"
|
|
66
|
-
|
|
67
|
-
wildcard_match_expression : ClassVar[Optional[str]] = "{field} LIKE {value}"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def __init__(self, processing_pipeline: Optional["sigma.processing.pipeline.ProcessingPipeline"] = None, collect_errors: bool = False, min_time : str = "-30d", max_time : str = "now", detection : SSADetection = None, field_mapping: dict = None, **kwargs):
|
|
71
|
-
super().__init__(processing_pipeline, collect_errors, **kwargs)
|
|
72
|
-
self.min_time = min_time or "-30d"
|
|
73
|
-
self.max_time = max_time or "now"
|
|
74
|
-
self.detection = detection
|
|
75
|
-
self.field_mapping = field_mapping
|
|
76
|
-
|
|
77
|
-
def finalize_query_data_model(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> str:
|
|
78
|
-
|
|
79
|
-
try:
|
|
80
|
-
fields = state.processing_state["fields"]
|
|
81
|
-
except KeyError:
|
|
82
|
-
raise SigmaFeatureNotSupportedByBackendError("No fields specified by processing pipeline")
|
|
83
|
-
|
|
84
|
-
# fields_input_parsing = ''
|
|
85
|
-
# for count, value in enumerate(fields):
|
|
86
|
-
# fields_input_parsing = fields_input_parsing + value + '=ucast(map_get(input_event, "' + value + '"), "string", null)'
|
|
87
|
-
# if not count == len(fields) - 1:
|
|
88
|
-
# fields_input_parsing = fields_input_parsing + ', '
|
|
89
|
-
|
|
90
|
-
detection_str = """
|
|
91
|
-
$main = from source
|
|
92
|
-
| eval timestamp = time
|
|
93
|
-
| eval metadata_uid = metadata.uid
|
|
94
|
-
""".replace("\n", " ")
|
|
95
|
-
|
|
96
|
-
parsed_fields = []
|
|
97
|
-
|
|
98
|
-
for field in self.field_mapping["mapping"].keys():
|
|
99
|
-
mapped_field = self.field_mapping["mapping"][field]
|
|
100
|
-
parent = 'parent'
|
|
101
|
-
i = 1
|
|
102
|
-
values = mapped_field.split('.')
|
|
103
|
-
for val in values:
|
|
104
|
-
if parent == "parent":
|
|
105
|
-
parent = val
|
|
106
|
-
continue
|
|
107
|
-
else:
|
|
108
|
-
new_val = parent + '_' + val
|
|
109
|
-
if new_val in parsed_fields:
|
|
110
|
-
parent = new_val
|
|
111
|
-
i = i + 1
|
|
112
|
-
continue
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
new_val_equals = new_val + "="
|
|
116
|
-
new_val_IN = new_val + " IN"
|
|
117
|
-
if new_val_equals in query or new_val_IN in query:
|
|
118
|
-
parser_str = '| eval ' + new_val + ' = ' + 'lower(' + parent + '.' + val + ') '
|
|
119
|
-
else:
|
|
120
|
-
parser_str = '| eval ' + new_val + ' = ' + parent + '.' + val + ' '
|
|
121
|
-
detection_str = detection_str + parser_str
|
|
122
|
-
parsed_fields.append(new_val)
|
|
123
|
-
parent = new_val
|
|
124
|
-
i = i + 1
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
### Convert sigma values into lower case
|
|
128
|
-
lower_query = ""
|
|
129
|
-
in_quotes = False
|
|
130
|
-
for char in query:
|
|
131
|
-
if char == '"':
|
|
132
|
-
in_quotes = not in_quotes
|
|
133
|
-
if in_quotes:
|
|
134
|
-
lower_query += char.lower()
|
|
135
|
-
else:
|
|
136
|
-
lower_query += char
|
|
137
|
-
|
|
138
|
-
detection_str = detection_str + "| where " + lower_query
|
|
139
|
-
|
|
140
|
-
detection_str = detection_str.replace("\\\\\\\\", "\\\\")
|
|
141
|
-
return detection_str
|
|
142
|
-
|
|
143
|
-
def finalize_output_data_model(self, queries: List[str]) -> List[str]:
|
|
144
|
-
return queries
|