contentctl 5.1.0__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.1.0 → contentctl-5.2.0}/PKG-INFO +1 -1
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +1 -1
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/detection_abstract.py +28 -1
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/data_source.py +2 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/lookup.py +16 -3
- {contentctl-5.1.0 → 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.1.0 → contentctl-5.2.0}/contentctl/output/templates/transforms.j2 +2 -2
- {contentctl-5.1.0 → contentctl-5.2.0}/pyproject.toml +2 -2
- contentctl-5.1.0/contentctl/output/attack_nav_writer.py +0 -67
- {contentctl-5.1.0 → contentctl-5.2.0}/LICENSE.md +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/README.md +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/__init__.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/build.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/deploy_acs.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/DetectionTestingManager.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/GitService.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/generate_detection_coverage_badge.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureServer.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/progress_bar.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingView.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/doc_gen.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/initialize.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/inspect.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/new_content.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/release_notes.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/reporting.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/test.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/actions/validate.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/api.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/contentctl.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/enrichments/attack_enrichment.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/enrichments/cve_enrichment.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/enrichments/splunk_app_enrichment.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/link_validator.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/logger.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/splunk_app.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/helper/utils.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/input/director.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/input/new_content_questions.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/input/yml_reader.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/alert_action.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/annotated_types.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/atomic.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/base_test.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/base_test_result.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/baseline.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/baseline_tags.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/config.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/constants.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/correlation_search.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/dashboard.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_email.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_notable.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_phantom.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_rba.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_scheduling.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/deployment_slack.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection_metadata.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection_stanza.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/detection_tags.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/drilldown.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/enums.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/errors.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/integration_test.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/integration_test_result.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/investigation.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/investigation_tags.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/macro.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/manual_test.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/manual_test_result.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/mitre_attack_enrichment.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/notable_action.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/notable_event.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/playbook.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/playbook_tags.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/rba.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/risk_analysis_action.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/risk_event.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/risk_object.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/savedsearches_conf.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/security_content_object.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/story.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/story_tags.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/test_attack_data.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/test_group.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/threat_object.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/throttling.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/unit_test.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/unit_test_baseline.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/objects/unit_test_result.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/api_json_output.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/conf_output.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/conf_writer.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/data_source_writer.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/doc_md_output.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/jinja_writer.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/json_writer.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/svg_output.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_detections.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_investigations.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_stories.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/app.conf.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/app.manifest.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/collections.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/content-version.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/detection_count.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/detection_coverage.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_detection_page.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_detections.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_navigation_pages.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_playbooks_page.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_stories.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/doc_story_page.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_investigations.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_stories.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/header.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/macros.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/panel.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_baselines.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_detections.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_investigations.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/server.conf.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/workflow_actions.j2 +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/yml_writer.py +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/README.md +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_default.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_story_detail.txt +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_summary.txt +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_usage_dashboard.txt +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README.md +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/analytic_stories.conf +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/commands.conf +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/nav/default.xml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/escu_summary.xml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/data/ui/views/feedback.xml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/use_case_library.conf +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/metadata/default.meta +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon.png +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt.png +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt_2x.png +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon_2x.png +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/data_sources/sysmon_eventid_1.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/datamodels_cim.conf +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/datamodels_custom.conf +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_anomaly.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_baseline.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_correlation.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_hunting.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/deployments/escu_default_configuration_ttp.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/application/.gitkeep +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/cloud/.gitkeep +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/network/.gitkeep +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/detections/web/.gitkeep +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/macros/security_content_ctime.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/macros/security_content_summariesonly.yml +0 -0
- {contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/stories/cobalt_strike.yml +0 -0
|
@@ -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
|
|
|
@@ -474,7 +474,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
474
474
|
"name": lookup.name,
|
|
475
475
|
"description": lookup.description,
|
|
476
476
|
"filename": lookup.filename.name,
|
|
477
|
-
"default_match":
|
|
477
|
+
"default_match": lookup.default_match,
|
|
478
478
|
"case_sensitive_match": "true"
|
|
479
479
|
if lookup.case_sensitive_match
|
|
480
480
|
else "false",
|
|
@@ -1055,3 +1055,30 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
1055
1055
|
# Return the summary
|
|
1056
1056
|
|
|
1057
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,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "contentctl"
|
|
3
3
|
|
|
4
|
-
version = "5.
|
|
4
|
+
version = "5.2.0"
|
|
5
5
|
|
|
6
6
|
description = "Splunk Content Control Tool"
|
|
7
7
|
authors = ["STRT <research@splunk.com>"]
|
|
@@ -33,7 +33,7 @@ gitpython = "^3.1.43"
|
|
|
33
33
|
setuptools = ">=69.5.1,<76.0.0"
|
|
34
34
|
|
|
35
35
|
[tool.poetry.group.dev.dependencies]
|
|
36
|
-
ruff = "^0.9.
|
|
36
|
+
ruff = "^0.9.10"
|
|
37
37
|
|
|
38
38
|
[build-system]
|
|
39
39
|
requires = ["poetry-core>=1.0.0"]
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from typing import Union, List
|
|
3
|
-
import pathlib
|
|
4
|
-
|
|
5
|
-
VERSION = "4.3"
|
|
6
|
-
NAME = "Detection Coverage"
|
|
7
|
-
DESCRIPTION = "security_content detection coverage"
|
|
8
|
-
DOMAIN = "mitre-enterprise"
|
|
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 = 0
|
|
18
|
-
for technique_id in mitre_techniques.keys():
|
|
19
|
-
if mitre_techniques[technique_id]["score"] > max_count:
|
|
20
|
-
max_count = mitre_techniques[technique_id]["score"]
|
|
21
|
-
|
|
22
|
-
layer_json = {
|
|
23
|
-
"version": VERSION,
|
|
24
|
-
"name": NAME,
|
|
25
|
-
"description": DESCRIPTION,
|
|
26
|
-
"domain": DOMAIN,
|
|
27
|
-
"techniques": [],
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
layer_json["gradient"] = {
|
|
31
|
-
"colors": ["#ffffff", "#66b1ff", "#096ed7"],
|
|
32
|
-
"minValue": 0,
|
|
33
|
-
"maxValue": max_count,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
layer_json["filters"] = {
|
|
37
|
-
"platforms": [
|
|
38
|
-
"Windows",
|
|
39
|
-
"Linux",
|
|
40
|
-
"macOS",
|
|
41
|
-
"AWS",
|
|
42
|
-
"GCP",
|
|
43
|
-
"Azure",
|
|
44
|
-
"Office 365",
|
|
45
|
-
"SaaS",
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
layer_json["legendItems"] = [
|
|
50
|
-
{"label": "NO available detections", "color": "#ffffff"},
|
|
51
|
-
{"label": "Some detections available", "color": "#66b1ff"},
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
layer_json["showTacticRowBackground"] = True
|
|
55
|
-
layer_json["tacticRowBackground"] = "#dddddd"
|
|
56
|
-
layer_json["sorting"] = 3
|
|
57
|
-
|
|
58
|
-
for technique_id in mitre_techniques.keys():
|
|
59
|
-
layer_technique = {
|
|
60
|
-
"techniqueID": technique_id,
|
|
61
|
-
"score": mitre_techniques[technique_id]["score"],
|
|
62
|
-
"comment": "\n\n".join(mitre_techniques[technique_id]["file_paths"]),
|
|
63
|
-
}
|
|
64
|
-
layer_json["techniques"].append(layer_technique)
|
|
65
|
-
|
|
66
|
-
with open(output_path, "w") as outfile:
|
|
67
|
-
json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_detections.j2
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_investigations.j2
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/analyticstories_stories.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/es_investigations_stories.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_baselines.j2
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_detections.j2
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/output/templates/savedsearches_investigations.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/README/essoc_summary.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/default/commands.conf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/lookups/mitre_enrichment.csv
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/metadata/default.meta
RENAMED
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt.png
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIconAlt_2x.png
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/app_template/static/appIcon_2x.png
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/data_sources/sysmon_eventid_1.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/macros/security_content_ctime.yml
RENAMED
|
File without changes
|
{contentctl-5.1.0 → contentctl-5.2.0}/contentctl/templates/macros/security_content_summariesonly.yml
RENAMED
|
File without changes
|
|
File without changes
|