contentctl 3.6.0__py3-none-any.whl → 4.0.2__py3-none-any.whl
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/actions/build.py +89 -0
- contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
- contentctl/actions/detection_testing/GitService.py +148 -230
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
- contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
- contentctl/actions/doc_gen.py +1 -1
- contentctl/actions/initialize.py +28 -65
- contentctl/actions/inspect.py +260 -0
- contentctl/actions/new_content.py +106 -13
- contentctl/actions/release_notes.py +168 -144
- contentctl/actions/reporting.py +24 -13
- contentctl/actions/test.py +39 -20
- contentctl/actions/validate.py +25 -48
- contentctl/contentctl.py +196 -754
- contentctl/enrichments/attack_enrichment.py +69 -19
- contentctl/enrichments/cve_enrichment.py +28 -13
- contentctl/helper/link_validator.py +24 -26
- contentctl/helper/utils.py +7 -3
- contentctl/input/director.py +139 -201
- contentctl/input/new_content_questions.py +63 -61
- contentctl/input/sigma_converter.py +1 -2
- contentctl/input/ssa_detection_builder.py +16 -7
- contentctl/input/yml_reader.py +4 -3
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
- contentctl/objects/alert_action.py +40 -0
- contentctl/objects/atomic.py +212 -0
- contentctl/objects/baseline.py +44 -43
- contentctl/objects/baseline_tags.py +69 -20
- contentctl/objects/config.py +857 -125
- contentctl/objects/constants.py +0 -1
- contentctl/objects/correlation_search.py +1 -1
- contentctl/objects/data_source.py +2 -4
- contentctl/objects/deployment.py +61 -21
- contentctl/objects/deployment_email.py +2 -2
- contentctl/objects/deployment_notable.py +4 -4
- contentctl/objects/deployment_phantom.py +2 -2
- contentctl/objects/deployment_rba.py +3 -4
- contentctl/objects/deployment_scheduling.py +2 -3
- contentctl/objects/deployment_slack.py +2 -2
- contentctl/objects/detection.py +1 -5
- contentctl/objects/detection_tags.py +210 -119
- contentctl/objects/enums.py +312 -24
- contentctl/objects/integration_test.py +1 -1
- contentctl/objects/integration_test_result.py +0 -2
- contentctl/objects/investigation.py +62 -53
- contentctl/objects/investigation_tags.py +30 -6
- contentctl/objects/lookup.py +80 -31
- contentctl/objects/macro.py +29 -45
- contentctl/objects/mitre_attack_enrichment.py +29 -5
- contentctl/objects/observable.py +3 -7
- contentctl/objects/playbook.py +60 -30
- contentctl/objects/playbook_tags.py +45 -8
- contentctl/objects/security_content_object.py +1 -5
- contentctl/objects/ssa_detection.py +8 -4
- contentctl/objects/ssa_detection_tags.py +19 -26
- contentctl/objects/story.py +142 -44
- contentctl/objects/story_tags.py +46 -33
- contentctl/objects/unit_test.py +7 -2
- contentctl/objects/unit_test_attack_data.py +10 -19
- contentctl/objects/unit_test_baseline.py +1 -1
- contentctl/objects/unit_test_old.py +4 -3
- contentctl/objects/unit_test_result.py +5 -3
- contentctl/objects/unit_test_ssa.py +31 -0
- contentctl/output/api_json_output.py +202 -130
- contentctl/output/attack_nav_output.py +20 -9
- contentctl/output/attack_nav_writer.py +3 -3
- contentctl/output/ba_yml_output.py +3 -3
- contentctl/output/conf_output.py +125 -391
- contentctl/output/conf_writer.py +169 -31
- contentctl/output/jinja_writer.py +2 -2
- contentctl/output/json_writer.py +17 -5
- contentctl/output/new_content_yml_output.py +8 -7
- contentctl/output/svg_output.py +17 -27
- contentctl/output/templates/analyticstories_detections.j2 +8 -4
- contentctl/output/templates/analyticstories_investigations.j2 +1 -1
- contentctl/output/templates/analyticstories_stories.j2 +6 -6
- contentctl/output/templates/app.conf.j2 +2 -2
- contentctl/output/templates/app.manifest.j2 +2 -2
- contentctl/output/templates/detection_coverage.j2 +6 -8
- contentctl/output/templates/doc_detection_page.j2 +2 -2
- contentctl/output/templates/doc_detections.j2 +2 -2
- contentctl/output/templates/doc_stories.j2 +1 -1
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/header.j2 +2 -1
- contentctl/output/templates/macros.j2 +6 -10
- contentctl/output/templates/savedsearches_baselines.j2 +5 -5
- contentctl/output/templates/savedsearches_detections.j2 +36 -33
- contentctl/output/templates/savedsearches_investigations.j2 +4 -4
- contentctl/output/templates/transforms.j2 +4 -4
- contentctl/output/yml_writer.py +2 -2
- contentctl/templates/app_template/README.md +7 -0
- contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
- contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
- contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
- contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
- contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
- contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
- contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
- contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
- contentctl/templates/stories/cobalt_strike.yml +0 -1
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
- contentctl-4.0.2.dist-info/RECORD +168 -0
- contentctl/actions/detection_testing/DataManipulation.py +0 -149
- contentctl/actions/generate.py +0 -91
- contentctl/helper/config_handler.py +0 -75
- contentctl/input/baseline_builder.py +0 -66
- contentctl/input/basic_builder.py +0 -58
- contentctl/input/detection_builder.py +0 -370
- contentctl/input/investigation_builder.py +0 -42
- contentctl/input/new_content_generator.py +0 -95
- contentctl/input/playbook_builder.py +0 -68
- contentctl/input/story_builder.py +0 -106
- contentctl/objects/app.py +0 -214
- contentctl/objects/repo_config.py +0 -163
- contentctl/objects/test_config.py +0 -630
- contentctl/output/templates/macros_detections.j2 +0 -7
- contentctl/output/templates/splunk_app/README.md +0 -7
- contentctl-3.6.0.dist-info/RECORD +0 -176
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
- /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
- {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
contentctl/objects/constants.py
CHANGED
|
@@ -12,7 +12,6 @@ ATTACK_TACTICS_KILLCHAIN_MAPPING = {
|
|
|
12
12
|
"Lateral Movement": "Exploitation",
|
|
13
13
|
"Collection": "Exploitation",
|
|
14
14
|
"Command And Control": "Command and Control",
|
|
15
|
-
"Command And Control": "Command and Control",
|
|
16
15
|
"Exfiltration": "Actions on Objectives",
|
|
17
16
|
"Impact": "Actions on Objectives"
|
|
18
17
|
}
|
|
@@ -220,7 +220,7 @@ class CorrelationSearch(BaseModel):
|
|
|
220
220
|
|
|
221
221
|
# The logger to use (logs all go to a null pipe unless ENABLE_LOGGING is set to True, so as not
|
|
222
222
|
# to conflict w/ tqdm)
|
|
223
|
-
logger: logging.Logger = Field(default_factory=get_logger
|
|
223
|
+
logger: logging.Logger = Field(default_factory=get_logger)
|
|
224
224
|
|
|
225
225
|
# The search name (e.g. "ESCU - Windows Modify Registry EnableLinkedConnections - Rule")
|
|
226
226
|
name: Optional[str] = None
|
contentctl/objects/deployment.py
CHANGED
|
@@ -1,30 +1,70 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, validator, ValidationError
|
|
6
|
-
from datetime import datetime
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pydantic import Field, computed_field, model_validator,ValidationInfo, model_serializer
|
|
3
|
+
from typing import Optional,Any
|
|
7
4
|
|
|
8
5
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
9
6
|
from contentctl.objects.deployment_scheduling import DeploymentScheduling
|
|
10
|
-
from contentctl.objects.
|
|
11
|
-
|
|
12
|
-
from contentctl.objects.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from contentctl.objects.enums import SecurityContentType
|
|
7
|
+
from contentctl.objects.alert_action import AlertAction
|
|
8
|
+
|
|
9
|
+
from contentctl.objects.enums import DeploymentType
|
|
10
|
+
|
|
11
|
+
|
|
16
12
|
class Deployment(SecurityContentObject):
|
|
17
|
-
name: str = "PLACEHOLDER_NAME"
|
|
18
13
|
#id: str = None
|
|
19
14
|
#date: str = None
|
|
20
15
|
#author: str = None
|
|
21
16
|
#description: str = None
|
|
22
17
|
#contentType: SecurityContentType = SecurityContentType.deployments
|
|
23
|
-
scheduling: DeploymentScheduling =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
scheduling: DeploymentScheduling = Field(...)
|
|
19
|
+
alert_action: AlertAction = AlertAction()
|
|
20
|
+
type: DeploymentType = Field(...)
|
|
21
|
+
|
|
22
|
+
#Type was the only tag exposed and should likely be removed/refactored.
|
|
23
|
+
#For transitional reasons, provide this as a computed_field in prep for removal
|
|
24
|
+
@computed_field
|
|
25
|
+
@property
|
|
26
|
+
def tags(self)->dict[str,DeploymentType]:
|
|
27
|
+
return {"type": self.type}
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def getDeployment(v:dict[str,Any], info:ValidationInfo)->Deployment:
|
|
31
|
+
if v != {}:
|
|
32
|
+
# If the user has defined a deployment, then allow it to be validated
|
|
33
|
+
# and override the default deployment info defined in type:Baseline
|
|
34
|
+
v['type'] = DeploymentType.Embedded
|
|
35
|
+
|
|
36
|
+
detection_name = info.data.get("name", None)
|
|
37
|
+
if detection_name is None:
|
|
38
|
+
raise ValueError("Could not create inline deployment - Baseline or Detection lacking 'name' field,")
|
|
39
|
+
|
|
40
|
+
v['name'] = f"{detection_name} - Inline Deployment"
|
|
41
|
+
# This constructs a temporary in-memory deployment,
|
|
42
|
+
# allowing the deployment to be easily defined in the
|
|
43
|
+
# detection on a per detection basis.
|
|
44
|
+
return Deployment.model_validate(v)
|
|
45
|
+
|
|
46
|
+
else:
|
|
47
|
+
return SecurityContentObject.getDeploymentFromType(info.data.get("type",None), info)
|
|
48
|
+
|
|
49
|
+
@model_serializer
|
|
50
|
+
def serialize_model(self):
|
|
51
|
+
#Call serializer for parent
|
|
52
|
+
super_fields = super().serialize_model()
|
|
53
|
+
|
|
54
|
+
#All fields custom to this model
|
|
55
|
+
model= {
|
|
56
|
+
"scheduling": self.scheduling.model_dump(),
|
|
57
|
+
"tags": self.tags
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
#Combine fields from this model with fields from parent
|
|
62
|
+
model.update(super_fields)
|
|
63
|
+
|
|
64
|
+
alert_action_fields = self.alert_action.model_dump()
|
|
65
|
+
model.update(alert_action_fields)
|
|
66
|
+
|
|
67
|
+
del(model['references'])
|
|
68
|
+
|
|
69
|
+
#return the model
|
|
70
|
+
return model
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
from pydantic import BaseModel
|
|
3
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from typing import List
|
|
4
4
|
|
|
5
5
|
class DeploymentNotable(BaseModel):
|
|
6
6
|
rule_description: str
|
|
7
7
|
rule_title: str
|
|
8
|
-
nes_fields:
|
|
8
|
+
nes_fields: List[str]
|
contentctl/objects/detection.py
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
from
|
|
2
|
-
from pydantic import validator
|
|
3
|
-
|
|
1
|
+
from __future__ import annotations
|
|
4
2
|
from contentctl.objects.abstract_security_content_objects.detection_abstract import Detection_Abstract
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
4
|
class Detection(Detection_Abstract):
|
|
9
5
|
# Customization to the Detection Class go here.
|
|
10
6
|
# You may add fields and/or validations
|
|
@@ -1,134 +1,100 @@
|
|
|
1
|
-
import
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional, Annotated, Union
|
|
4
|
+
from pydantic import BaseModel,Field, NonNegativeInt, PositiveInt, computed_field, UUID4, HttpUrl, ConfigDict, field_validator, ValidationInfo, model_serializer, model_validator
|
|
5
|
+
from contentctl.objects.story import Story
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from contentctl.input.director import DirectorOutputDto
|
|
8
|
+
|
|
9
|
+
|
|
2
10
|
|
|
3
|
-
from pydantic import BaseModel, validator, ValidationError, root_validator
|
|
4
11
|
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
|
|
5
12
|
from contentctl.objects.constants import *
|
|
6
13
|
from contentctl.objects.observable import Observable
|
|
14
|
+
from contentctl.objects.enums import Cis18Value, AssetType, SecurityDomain, RiskSeverity, KillChainPhase, NistCategory, RiskLevel, SecurityContentProductName
|
|
15
|
+
from contentctl.objects.atomic import AtomicTest
|
|
16
|
+
|
|
17
|
+
|
|
7
18
|
|
|
8
19
|
class DetectionTags(BaseModel):
|
|
9
20
|
# detection spec
|
|
10
|
-
|
|
11
|
-
analytic_story: list
|
|
12
|
-
asset_type:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
confidence:
|
|
16
|
-
impact:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
model_config = ConfigDict(use_enum_values=True,validate_default=False)
|
|
22
|
+
analytic_story: list[Story] = Field(...)
|
|
23
|
+
asset_type: AssetType = Field(...)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
confidence: NonNegativeInt = Field(...,le=100)
|
|
27
|
+
impact: NonNegativeInt = Field(...,le=100)
|
|
28
|
+
@computed_field
|
|
29
|
+
@property
|
|
30
|
+
def risk_score(self)->int:
|
|
31
|
+
return round((self.confidence * self.impact)/100)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
mitre_attack_id: List[Annotated[str, Field(pattern="^T\d{4}(.\d{3})?$")]] = []
|
|
35
|
+
nist: list[NistCategory] = []
|
|
36
|
+
observable: List[Observable] = []
|
|
37
|
+
message: Optional[str] = Field(...)
|
|
38
|
+
product: list[SecurityContentProductName] = Field(...,min_length=1)
|
|
39
|
+
required_fields: list[str] = Field(min_length=1)
|
|
40
|
+
|
|
41
|
+
security_domain: SecurityDomain = Field(...)
|
|
42
|
+
|
|
43
|
+
@computed_field
|
|
44
|
+
@property
|
|
45
|
+
def risk_severity(self)->RiskSeverity:
|
|
46
|
+
if self.risk_score >= 80:
|
|
47
|
+
return RiskSeverity('high')
|
|
48
|
+
elif (self.risk_score >= 50 and self.risk_score <= 79):
|
|
49
|
+
return RiskSeverity('medium')
|
|
50
|
+
else:
|
|
51
|
+
return RiskSeverity('low')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
cve: List[Annotated[str, "^CVE-[1|2][0-9]{3}-[0-9]+$"]] = []
|
|
56
|
+
atomic_guid: List[AtomicTest] = []
|
|
57
|
+
drilldown_search: Optional[str] = None
|
|
32
58
|
|
|
33
59
|
|
|
34
60
|
# enrichment
|
|
35
|
-
mitre_attack_enrichments:
|
|
36
|
-
confidence_id:
|
|
37
|
-
impact_id:
|
|
38
|
-
context_ids: list = None
|
|
39
|
-
risk_level_id:
|
|
40
|
-
risk_level:
|
|
41
|
-
observable_str: str = None
|
|
42
|
-
evidence_str: str = None
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
raise ValueError(f"CIS control '{value}' is not a valid Control ('CIS 1' -> 'CIS 20'): {values['name']}")
|
|
56
|
-
return v
|
|
61
|
+
mitre_attack_enrichments: List[MitreAttackEnrichment] = Field([],validate_default=True)
|
|
62
|
+
confidence_id: Optional[PositiveInt] = Field(None,ge=1,le=3)
|
|
63
|
+
impact_id: Optional[PositiveInt] = Field(None,ge=1,le=5)
|
|
64
|
+
# context_ids: list = None
|
|
65
|
+
risk_level_id: Optional[NonNegativeInt] = Field(None,le=4)
|
|
66
|
+
risk_level: Optional[RiskLevel] = None
|
|
67
|
+
#observable_str: str = None
|
|
68
|
+
evidence_str: Optional[str] = None
|
|
69
|
+
|
|
70
|
+
@computed_field
|
|
71
|
+
@property
|
|
72
|
+
def kill_chain_phases(self)->list[KillChainPhase]:
|
|
73
|
+
if self.mitre_attack_enrichments is None:
|
|
74
|
+
return []
|
|
75
|
+
phases:set[KillChainPhase] = set()
|
|
76
|
+
for enrichment in self.mitre_attack_enrichments:
|
|
77
|
+
for tactic in enrichment.mitre_attack_tactics:
|
|
78
|
+
phase = KillChainPhase(ATTACK_TACTICS_KILLCHAIN_MAPPING[tactic])
|
|
79
|
+
phases.add(phase)
|
|
80
|
+
return sorted(list(phases))
|
|
57
81
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
RESPOND = [f'RS.{category}' for category in ["RP", "CO", "AN", "MI", "IM"] ]
|
|
65
|
-
RECOVER = [f'RC.{category}' for category in ["RP", "IM", "CO"] ]
|
|
66
|
-
ALL_NIST_CATEGORIES = IDENTIFY + PROTECT + DETECT + RESPOND + RECOVER
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for value in v:
|
|
70
|
-
if not value in ALL_NIST_CATEGORIES:
|
|
71
|
-
raise ValueError(f"NIST Category '{value}' is not a valid category")
|
|
72
|
-
return v
|
|
73
|
-
|
|
74
|
-
@validator('confidence')
|
|
75
|
-
def tags_confidence(cls, v, values):
|
|
76
|
-
v = int(v)
|
|
77
|
-
if not (v > 0 and v <= 100):
|
|
78
|
-
raise ValueError('confidence score is out of range 1-100: ' + values["name"])
|
|
79
|
-
else:
|
|
80
|
-
return v
|
|
81
|
-
|
|
82
|
-
@validator('context_ids')
|
|
83
|
-
def tags_context(cls, v, values):
|
|
84
|
-
context_list = SES_CONTEXT_MAPPING.keys()
|
|
85
|
-
for value in v:
|
|
86
|
-
if value not in context_list:
|
|
87
|
-
raise ValueError('context value not valid for ' + values["name"] + '. valid options are ' + str(context_list) )
|
|
88
|
-
return v
|
|
89
|
-
|
|
90
|
-
@validator('impact')
|
|
91
|
-
def tags_impact(cls, v, values):
|
|
92
|
-
if not (v > 0 and v <= 100):
|
|
93
|
-
raise ValueError('impact score is out of range 1-100: ' + values["name"])
|
|
82
|
+
#enum is intentionally Cis18 even though field is named cis20 for legacy reasons
|
|
83
|
+
@computed_field
|
|
84
|
+
@property
|
|
85
|
+
def cis20(self)->list[Cis18Value]:
|
|
86
|
+
if self.security_domain == SecurityDomain.NETWORK:
|
|
87
|
+
return [Cis18Value.CIS_13]
|
|
94
88
|
else:
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@validator('mitre_attack_id')
|
|
106
|
-
def tags_mitre_attack_id(cls, v, values):
|
|
107
|
-
pattern = 'T[0-9]{4}'
|
|
108
|
-
for value in v:
|
|
109
|
-
if not re.match(pattern, value):
|
|
110
|
-
raise ValueError('Mitre Attack ID are not following the pattern Txxxx: ' + values["name"])
|
|
111
|
-
return v
|
|
112
|
-
|
|
113
|
-
@validator('product')
|
|
114
|
-
def tags_product(cls, v, values):
|
|
115
|
-
valid_products = [
|
|
116
|
-
"Splunk Enterprise", "Splunk Enterprise Security", "Splunk Cloud",
|
|
117
|
-
"Splunk Security Analytics for AWS", "Splunk Behavioral Analytics"
|
|
118
|
-
]
|
|
119
|
-
|
|
120
|
-
for value in v:
|
|
121
|
-
if value not in valid_products:
|
|
122
|
-
raise ValueError('product is not valid for ' + values['name'] + '. valid products are ' + str(valid_products))
|
|
123
|
-
return v
|
|
124
|
-
|
|
125
|
-
@validator('risk_score')
|
|
126
|
-
def tags_calculate_risk_score(cls, v, values):
|
|
127
|
-
calculated_risk_score = round(values['impact'] * values['confidence'] / 100)
|
|
128
|
-
if calculated_risk_score != int(v):
|
|
129
|
-
raise ValueError(f"Risk Score must be calculated as round(confidence * impact / 100)"
|
|
130
|
-
f"\n Expected risk_score={calculated_risk_score}, found risk_score={int(v)}: {values['name']}")
|
|
131
|
-
return v
|
|
89
|
+
return [Cis18Value.CIS_10]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
research_site_url: Optional[HttpUrl] = None
|
|
93
|
+
event_schema: str = "ocsf"
|
|
94
|
+
mappings: Optional[List] = None
|
|
95
|
+
#annotations: Optional[dict] = None
|
|
96
|
+
manual_test: Optional[str] = None
|
|
97
|
+
|
|
132
98
|
|
|
133
99
|
# The following validator is temporarily disabled pending further discussions
|
|
134
100
|
# @validator('message')
|
|
@@ -157,4 +123,129 @@ class DetectionTags(BaseModel):
|
|
|
157
123
|
# raise ValueError(f"The following observables were declared, but are not referenced in the message: {unused_observables}")
|
|
158
124
|
# return v
|
|
159
125
|
|
|
126
|
+
|
|
127
|
+
@model_serializer
|
|
128
|
+
def serialize_model(self):
|
|
129
|
+
#Since this field has no parent, there is no need to call super() serialization function
|
|
130
|
+
return {
|
|
131
|
+
"analytic_story": [story.name for story in self.analytic_story],
|
|
132
|
+
"asset_type": self.asset_type.value,
|
|
133
|
+
"cis20": self.cis20,
|
|
134
|
+
"kill_chain_phases": self.kill_chain_phases,
|
|
135
|
+
"nist": self.nist,
|
|
136
|
+
"observable": self.observable,
|
|
137
|
+
"message": self.message,
|
|
138
|
+
"risk_score": self.risk_score,
|
|
139
|
+
"security_domain": self.security_domain,
|
|
140
|
+
"risk_severity": self.risk_severity,
|
|
141
|
+
"mitre_attack_enrichments": self.mitre_attack_enrichments
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@model_validator(mode="after")
|
|
146
|
+
def addAttackEnrichment(self, info:ValidationInfo):
|
|
147
|
+
if len(self.mitre_attack_enrichments) > 0:
|
|
148
|
+
raise ValueError(f"Error, field 'mitre_attack_enrichment' should be empty and dynamically populated at runtime. Instead, this field contained: {str(v)}")
|
|
149
|
+
|
|
150
|
+
output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
|
|
151
|
+
if output_dto is None:
|
|
152
|
+
raise ValueError("Context not provided to detection.detection_tags model post validator")
|
|
153
|
+
|
|
154
|
+
if output_dto.attack_enrichment.use_enrichment is False:
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
mitre_enrichments = []
|
|
159
|
+
missing_tactics = []
|
|
160
|
+
for mitre_attack_id in self.mitre_attack_id:
|
|
161
|
+
try:
|
|
162
|
+
mitre_enrichments.append(output_dto.attack_enrichment.getEnrichmentByMitreID(mitre_attack_id))
|
|
163
|
+
except Exception as e:
|
|
164
|
+
missing_tactics.append(mitre_attack_id)
|
|
165
|
+
|
|
166
|
+
if len(missing_tactics) > 0:
|
|
167
|
+
raise ValueError(f"Missing Mitre Attack IDs. {missing_tactics} not found.")
|
|
168
|
+
else:
|
|
169
|
+
self.mitre_attack_enrichments = mitre_enrichments
|
|
170
|
+
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
'''
|
|
174
|
+
@field_validator('mitre_attack_enrichments', mode="before")
|
|
175
|
+
@classmethod
|
|
176
|
+
def addAttackEnrichments(cls, v:list[MitreAttackEnrichment], info:ValidationInfo)->list[MitreAttackEnrichment]:
|
|
177
|
+
if len(v) > 0:
|
|
178
|
+
raise ValueError(f"Error, field 'mitre_attack_enrichment' should be empty and dynamically populated at runtime. Instead, this field contained: {str(v)}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
|
|
182
|
+
if output_dto is None:
|
|
183
|
+
raise ValueError("Context not provided to detection.detection_tags.mitre_attack_enrichments")
|
|
184
|
+
|
|
185
|
+
enrichments = []
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
return enrichments
|
|
191
|
+
'''
|
|
192
|
+
|
|
193
|
+
@field_validator('analytic_story',mode="before")
|
|
194
|
+
@classmethod
|
|
195
|
+
def mapStoryNamesToStoryObjects(cls, v:list[str], info:ValidationInfo)->list[Story]:
|
|
196
|
+
return Story.mapNamesToSecurityContentObjects(v, info.context.get("output_dto",None))
|
|
197
|
+
|
|
198
|
+
def getAtomicGuidStringArray(self)->List[str]:
|
|
199
|
+
return [str(atomic_guid.auto_generated_guid) for atomic_guid in self.atomic_guid]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@field_validator('atomic_guid',mode="before")
|
|
203
|
+
@classmethod
|
|
204
|
+
def mapAtomicGuidsToAtomicTests(cls, v:List[UUID4], info:ValidationInfo)->List[AtomicTest]:
|
|
205
|
+
if len(v) == 0:
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
output_dto:Union[DirectorOutputDto,None]= info.context.get("output_dto",None)
|
|
209
|
+
if output_dto is None:
|
|
210
|
+
raise ValueError("Context not provided to detection.detection_tags.atomic_guid validator")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
all_tests:List[AtomicTest]= output_dto.atomic_tests
|
|
214
|
+
|
|
215
|
+
matched_tests:List[AtomicTest] = []
|
|
216
|
+
missing_tests:List[UUID4] = []
|
|
217
|
+
badly_formatted_guids:List[str] = []
|
|
218
|
+
for atomic_guid_str in v:
|
|
219
|
+
try:
|
|
220
|
+
#Ensure that this is a valid UUID
|
|
221
|
+
atomic_guid = uuid.UUID(str(atomic_guid_str))
|
|
222
|
+
except Exception as e:
|
|
223
|
+
#We will not try to load a test for this since it was invalid
|
|
224
|
+
badly_formatted_guids.append(str(atomic_guid_str))
|
|
225
|
+
continue
|
|
226
|
+
try:
|
|
227
|
+
matched_tests.append(AtomicTest.getAtomicByAtomicGuid(atomic_guid,all_tests))
|
|
228
|
+
except Exception as _:
|
|
229
|
+
missing_tests.append(atomic_guid)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if len(missing_tests) > 0:
|
|
235
|
+
missing_tests_string = f"\n\tWARNING: Failed to find [{len(missing_tests)}] Atomic Test(s) with the following atomic_guids (called auto_generated_guid in the ART Repo)."\
|
|
236
|
+
f"\n\tPlease review the output above for potential exception(s) when parsing the Atomic Red Team Repo."\
|
|
237
|
+
f"\n\tVerify that these auto_generated_guid exist and try updating/pulling the repo again.: {[str(guid) for guid in missing_tests]}"
|
|
238
|
+
else:
|
|
239
|
+
missing_tests_string = ""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if len(badly_formatted_guids) > 0:
|
|
243
|
+
if len(badly_formatted_guids) > 0:
|
|
244
|
+
bad_guids_string = f"The following [{len(badly_formatted_guids)}] value(s) are not properly formatted UUIDs: {badly_formatted_guids}\n"
|
|
245
|
+
raise ValueError(f"{bad_guids_string}{missing_tests_string}")
|
|
246
|
+
|
|
247
|
+
elif len(missing_tests) > 0:
|
|
248
|
+
print(missing_tests_string)
|
|
249
|
+
|
|
250
|
+
return matched_tests + [AtomicTest.AtomicTestWhenTestIsMissing(test) for test in missing_tests]
|
|
160
251
|
|