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/lookup.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
from typing import Tuple
|
|
2
|
+
from pydantic import field_validator, ValidationInfo, model_validator, FilePath, model_serializer
|
|
3
|
+
from typing import TYPE_CHECKING, Optional, Any, Union
|
|
5
4
|
import re
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from contentctl.input.director import DirectorOutputDto
|
|
7
|
+
from contentctl.objects.config import validate
|
|
6
8
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
7
|
-
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
LOOKUPS_TO_IGNORE = set(["outputlookup"])
|
|
10
12
|
LOOKUPS_TO_IGNORE.add("ut_shannon_lookup") #In the URL toolbox app which is recommended for ESCU
|
|
@@ -18,39 +20,86 @@ LOOKUPS_TO_IGNORE.add("=")
|
|
|
18
20
|
LOOKUPS_TO_IGNORE.add("other_lookups")
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
class Lookup(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
class Lookup(SecurityContentObject):
|
|
24
|
+
|
|
25
|
+
collection: Optional[str] = None
|
|
26
|
+
fields_list: Optional[str] = None
|
|
27
|
+
filename: Optional[FilePath] = None
|
|
28
|
+
default_match: Optional[bool] = None
|
|
29
|
+
match_type: Optional[str] = None
|
|
30
|
+
min_matches: Optional[int] = None
|
|
31
|
+
case_sensitive_match: Optional[bool] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@model_serializer
|
|
35
|
+
def serialize_model(self):
|
|
36
|
+
#Call parent serializer
|
|
37
|
+
super_fields = super().serialize_model()
|
|
38
|
+
|
|
39
|
+
#All fields custom to this model
|
|
40
|
+
model= {
|
|
41
|
+
"filename": self.filename.name if self.filename is not None else None,
|
|
42
|
+
"default_match": "true" if self.default_match is True else "false",
|
|
43
|
+
"match_type": self.match_type,
|
|
44
|
+
"min_matches": self.min_matches,
|
|
45
|
+
"case_sensitive_match": "true" if self.case_sensitive_match is True else "false",
|
|
46
|
+
"collection": self.collection,
|
|
47
|
+
"fields_list": self.fields_list
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#return the model
|
|
51
|
+
model.update(super_fields)
|
|
52
|
+
return model
|
|
53
|
+
|
|
54
|
+
@model_validator(mode="before")
|
|
55
|
+
def fix_lookup_path(cls, data:Any, info: ValidationInfo)->Any:
|
|
56
|
+
if data.get("filename"):
|
|
57
|
+
config:validate = info.context.get("config",None)
|
|
58
|
+
if config is not None:
|
|
59
|
+
data["filename"] = config.path / "lookups/" / data["filename"]
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError("config required for constructing lookup filename, but it was not")
|
|
62
|
+
return data
|
|
40
63
|
|
|
64
|
+
@field_validator('filename')
|
|
65
|
+
@classmethod
|
|
66
|
+
def lookup_file_valid(cls, v: Union[FilePath,None], info: ValidationInfo):
|
|
67
|
+
if not v:
|
|
68
|
+
return v
|
|
69
|
+
if not (v.name.endswith(".csv") or v.name.endswith(".mlmodel")):
|
|
70
|
+
raise ValueError(f"All Lookup files must be CSV files and end in .csv. The following file does not: '{v}'")
|
|
41
71
|
|
|
42
|
-
# Allow long names for lookups
|
|
43
|
-
@validator('name',check_fields=False)
|
|
44
|
-
def name_max_length(cls, v):
|
|
45
|
-
#if len(v) > 67:
|
|
46
|
-
# raise ValueError('name is longer then 67 chars: ' + v)
|
|
47
72
|
return v
|
|
73
|
+
|
|
74
|
+
@field_validator('match_type')
|
|
75
|
+
@classmethod
|
|
76
|
+
def match_type_valid(cls, v: Union[str,None], info: ValidationInfo):
|
|
77
|
+
if not v:
|
|
78
|
+
#Match type can be None and that's okay
|
|
79
|
+
return v
|
|
80
|
+
|
|
81
|
+
if not (v.startswith("WILDCARD(") or v.endswith(")")) :
|
|
82
|
+
raise ValueError(f"All match_types must take the format 'WILDCARD(field_name)'. The following file does not: '{v}'")
|
|
83
|
+
return v
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
#Ensure that exactly one of location or filename are defined
|
|
87
|
+
@model_validator(mode='after')
|
|
88
|
+
def ensure_mutually_exclusive_fields(self)->Lookup:
|
|
89
|
+
if self.filename is not None and self.collection is not None:
|
|
90
|
+
raise ValueError("filename and collection cannot be defined in the lookup file. Exactly one must be defined.")
|
|
91
|
+
elif self.filename is None and self.collection is None:
|
|
92
|
+
raise ValueError("Neither filename nor collection were defined in the lookup file. Exactly one must "
|
|
93
|
+
"be defined.")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
return self
|
|
97
|
+
|
|
48
98
|
|
|
49
99
|
@staticmethod
|
|
50
|
-
def get_lookups(text_field: str,
|
|
100
|
+
def get_lookups(text_field: str, director:DirectorOutputDto, ignore_lookups:set[str]=LOOKUPS_TO_IGNORE)->list[Lookup]:
|
|
51
101
|
lookups_to_get = set(re.findall(r'[^output]lookup (?:update=true)?(?:append=t)?\s*([^\s]*)', text_field))
|
|
52
102
|
lookups_to_ignore = set([lookup for lookup in lookups_to_get if any(to_ignore in lookups_to_get for to_ignore in ignore_lookups)])
|
|
53
103
|
lookups_to_get -= lookups_to_ignore
|
|
54
|
-
|
|
55
|
-
return found_lookups, missing_lookups
|
|
104
|
+
return Lookup.mapNamesToSecurityContentObjects(list(lookups_to_get), director)
|
|
56
105
|
|
contentctl/objects/macro.py
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# Used so that we can have a staticmethod that takes the class
|
|
2
2
|
# type Macro as an argument
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING, List
|
|
4
5
|
import re
|
|
5
|
-
from pydantic import
|
|
6
|
-
|
|
6
|
+
from pydantic import Field, model_serializer
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from contentctl.input.director import DirectorOutputDto
|
|
7
9
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
8
|
-
|
|
9
|
-
from typing import Tuple
|
|
10
|
+
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
MACROS_TO_IGNORE = set(["_filter", "drop_dm_object_name"])
|
|
@@ -17,32 +18,32 @@ MACROS_TO_IGNORE.add("cim_corporate_web_domain_search")
|
|
|
17
18
|
MACROS_TO_IGNORE.add("prohibited_processes")
|
|
18
19
|
|
|
19
20
|
|
|
20
|
-
class Macro(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
arguments: list = None
|
|
26
|
-
file_path: str = None
|
|
27
|
-
|
|
28
|
-
# Macro can have different punctuatuation in it,
|
|
29
|
-
# so redefine the name validator. For now, jsut
|
|
30
|
-
# allow any characters in the macro
|
|
31
|
-
@validator('name',check_fields=False)
|
|
32
|
-
def name_invalid_chars(cls, v):
|
|
33
|
-
return v
|
|
21
|
+
class Macro(SecurityContentObject):
|
|
22
|
+
definition: str = Field(..., min_length=1)
|
|
23
|
+
arguments: List[str] = Field([])
|
|
24
|
+
|
|
25
|
+
|
|
34
26
|
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# raise ValueError('name is longer then 67 chars: ' + v)
|
|
41
|
-
return v
|
|
28
|
+
@model_serializer
|
|
29
|
+
def serialize_model(self):
|
|
30
|
+
#Call serializer for parent
|
|
31
|
+
super_fields = super().serialize_model()
|
|
42
32
|
|
|
33
|
+
#All fields custom to this model
|
|
34
|
+
model= {
|
|
35
|
+
"definition": self.definition,
|
|
36
|
+
"description": self.description,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#return the model
|
|
40
|
+
model.update(super_fields)
|
|
41
|
+
|
|
42
|
+
return model
|
|
43
43
|
|
|
44
44
|
@staticmethod
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
def get_macros(text_field:str, director:DirectorOutputDto , ignore_macros:set[str]=MACROS_TO_IGNORE)->list[Macro]:
|
|
46
47
|
#Remove any comments, allowing there to be macros (which have a single backtick) inside those comments
|
|
47
48
|
#If a comment ENDS in a macro, for example ```this is a comment with a macro `macro_here````
|
|
48
49
|
#then there is a small edge case where the regex below does not work properly. If that is
|
|
@@ -50,6 +51,7 @@ class Macro(BaseModel):
|
|
|
50
51
|
text_field = re.sub(r"\`\`\`\`", r"` ```", text_field)
|
|
51
52
|
text_field = re.sub(r"\`\`\`.*?\`\`\`", " ", text_field)
|
|
52
53
|
|
|
54
|
+
|
|
53
55
|
macros_to_get = re.findall(r'`([^\s]+)`', text_field)
|
|
54
56
|
#If macros take arguments, stop at the first argument. We just want the name of the macro
|
|
55
57
|
macros_to_get = set([macro[:macro.find('(')] if macro.find('(') != -1 else macro for macro in macros_to_get])
|
|
@@ -57,24 +59,6 @@ class Macro(BaseModel):
|
|
|
57
59
|
macros_to_ignore = set([macro for macro in macros_to_get if any(to_ignore in macro for to_ignore in ignore_macros)])
|
|
58
60
|
#remove the ones that we will ignore
|
|
59
61
|
macros_to_get -= macros_to_ignore
|
|
60
|
-
|
|
61
|
-
return found_macros, missing_macros
|
|
62
|
-
|
|
63
|
-
# found_macros = [macro for macro in all_macros if macro.name in macros_to_get]
|
|
64
|
-
|
|
65
|
-
# missing_macros = macros_to_get - set([macro.name for macro in found_macros])
|
|
66
|
-
# missing_macros_after_ignored_macros = set()
|
|
67
|
-
# for macro in missing_macros:
|
|
68
|
-
# found = False
|
|
69
|
-
# for ignore in ignore_macros:
|
|
70
|
-
# if ignore in macro:
|
|
71
|
-
# found=True
|
|
72
|
-
# break
|
|
73
|
-
# if found is False:
|
|
74
|
-
# missing_macros_after_ignored_macros.add(macro)
|
|
75
|
-
|
|
76
|
-
#return found_macros, missing_macros_after_ignored_macros
|
|
62
|
+
return Macro.mapNamesToSecurityContentObjects(list(macros_to_get), director)
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
|
|
@@ -1,8 +1,32 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
3
|
+
from typing import Set,List,Annotated
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MitreTactics(StrEnum):
|
|
8
|
+
RECONNAISSANCE = "Reconnaissance"
|
|
9
|
+
RESOURCE_DEVELOPMENT = "Resource Development"
|
|
10
|
+
INITIAL_ACCESS = "Initial Access"
|
|
11
|
+
EXECUTION = "Execution"
|
|
12
|
+
PERSISTENCE = "Persistence"
|
|
13
|
+
PRIVILEGE_ESCALATION = "Privilege Escalation"
|
|
14
|
+
DEFENSE_EVASION = "Defense Evasion"
|
|
15
|
+
CREDENTIAL_ACCESS = "Credential Access"
|
|
16
|
+
DISCOVERY = "Discovery"
|
|
17
|
+
LATERAL_MOVEMENT = "Lateral Movement"
|
|
18
|
+
COLLECTION = "Collection"
|
|
19
|
+
COMMAND_AND_CONTROL = "Command And Control"
|
|
20
|
+
EXFILTRATION = "Exfiltration"
|
|
21
|
+
IMPACT = "Impact"
|
|
2
22
|
|
|
3
23
|
|
|
4
24
|
class MitreAttackEnrichment(BaseModel):
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
25
|
+
ConfigDict(use_enum_values=True)
|
|
26
|
+
mitre_attack_id: Annotated[str, Field(pattern="^T\d{4}(.\d{3})?$")] = Field(...)
|
|
27
|
+
mitre_attack_technique: str = Field(...)
|
|
28
|
+
mitre_attack_tactics: List[MitreTactics] = Field(...)
|
|
29
|
+
mitre_attack_groups: List[str] = Field(...)
|
|
30
|
+
|
|
31
|
+
def __hash__(self) -> int:
|
|
32
|
+
return id(self)
|
contentctl/objects/observable.py
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
from typing import Literal
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
from pydantic import BaseModel, validator, ValidationError
|
|
7
|
-
from contentctl.objects.enums import SecurityContentType
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from pydantic import BaseModel, validator
|
|
3
|
+
|
|
8
4
|
from contentctl.objects.constants import *
|
|
9
5
|
|
|
10
6
|
|
contentctl/objects/playbook.py
CHANGED
|
@@ -1,36 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING,Self
|
|
3
|
+
from pydantic import model_validator, Field, FilePath
|
|
1
4
|
|
|
2
|
-
import uuid
|
|
3
|
-
import string
|
|
4
5
|
|
|
5
|
-
from pydantic import BaseModel, validator, ValidationError
|
|
6
|
-
|
|
7
|
-
from contentctl.objects.security_content_object import SecurityContentObject
|
|
8
6
|
from contentctl.objects.playbook_tags import PlaybookTag
|
|
9
|
-
from contentctl.
|
|
10
|
-
from contentctl.objects.enums import
|
|
7
|
+
from contentctl.objects.security_content_object import SecurityContentObject
|
|
8
|
+
from contentctl.objects.enums import PlaybookType
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
class Playbook(SecurityContentObject):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
12
|
+
type: PlaybookType = Field(...)
|
|
13
|
+
|
|
14
|
+
# Override the type definition for filePath.
|
|
15
|
+
# This MUST be backed by a file and cannot be None
|
|
16
|
+
file_path: FilePath
|
|
17
|
+
|
|
18
|
+
how_to_implement: str = Field(min_length=4)
|
|
19
|
+
playbook: str = Field(min_length=4)
|
|
20
|
+
app_list: list[str] = Field(...,min_length=0)
|
|
21
|
+
tags: PlaybookTag = Field(...)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@model_validator(mode="after")
|
|
26
|
+
def ensureJsonAndPyFilesExist(self)->Self:
|
|
27
|
+
json_file_path = self.file_path.with_suffix(".json")
|
|
28
|
+
python_file_path = self.file_path.with_suffix(".py")
|
|
29
|
+
missing:list[str] = []
|
|
30
|
+
if not json_file_path.is_file():
|
|
31
|
+
missing.append(f"Playbook file named '{self.file_path.name}' MUST "\
|
|
32
|
+
f"have a .json file named '{json_file_path.name}', "\
|
|
33
|
+
"but it does not exist")
|
|
34
|
+
|
|
35
|
+
if not python_file_path.is_file():
|
|
36
|
+
missing.append(f"Playbook file named '{self.file_path.name}' MUST "\
|
|
37
|
+
f"have a .py file named '{python_file_path.name}', "\
|
|
38
|
+
"but it does not exist")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if len(missing) == 0:
|
|
42
|
+
return self
|
|
43
|
+
else:
|
|
44
|
+
missing_files_string = '\n - '.join(missing)
|
|
45
|
+
raise ValueError(f"Playbook files missing:\n -{missing_files_string}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
#Override playbook file name checking FOR NOW
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def ensureFileNameMatchesSearchName(self)->Self:
|
|
51
|
+
file_name = self.name \
|
|
52
|
+
.replace(' ', '_') \
|
|
53
|
+
.replace('-','_') \
|
|
54
|
+
.replace('.','_') \
|
|
55
|
+
.replace('/','_') \
|
|
56
|
+
.lower() + ".yml"
|
|
57
|
+
|
|
58
|
+
#allow different capitalization FOR NOW in playbook file names
|
|
59
|
+
if (self.file_path is not None and file_name != self.file_path.name.lower()):
|
|
60
|
+
raise ValueError(f"The file name MUST be based off the content 'name' field:\n"\
|
|
61
|
+
f"\t- Expected File Name: {file_name}\n"\
|
|
62
|
+
f"\t- Actual File Name : {self.file_path.name}")
|
|
63
|
+
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
|
|
@@ -1,13 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Optional, List
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
import enum
|
|
5
|
+
from contentctl.objects.detection import Detection
|
|
1
6
|
|
|
2
|
-
from pydantic import BaseModel, validator, ValidationError
|
|
3
7
|
|
|
8
|
+
class PlaybookProduct(str,enum.Enum):
|
|
9
|
+
SPLUNK_SOAR = "Splunk SOAR"
|
|
4
10
|
|
|
11
|
+
class PlaybookUseCase(str,enum.Enum):
|
|
12
|
+
PHISHING = "Phishing"
|
|
13
|
+
ENDPOINT = "Endpoint"
|
|
14
|
+
ENRICHMENT = "Enrichment"
|
|
15
|
+
|
|
16
|
+
class PlaybookType(str,enum.Enum):
|
|
17
|
+
INPUT = "Input"
|
|
18
|
+
AUTOMATION = "Automation"
|
|
19
|
+
|
|
20
|
+
class VpeType(str,enum.Enum):
|
|
21
|
+
MODERN = "Modern"
|
|
22
|
+
CLASSIC = "Classic"
|
|
23
|
+
class DefendTechnique(str,enum.Enum):
|
|
24
|
+
D3_AL = "D3-AL"
|
|
25
|
+
D3_DNSDL = "D3-DNSDL"
|
|
26
|
+
D3_DA = "D3-DA"
|
|
27
|
+
D3_IAA = "D3-IAA"
|
|
28
|
+
D3_IRA = "D3-IRA"
|
|
29
|
+
D3_OTF = "D3-OTF"
|
|
30
|
+
D3_ER = "D3-ER"
|
|
31
|
+
D3_RE = "D3-RE"
|
|
32
|
+
D3_URA = "D3-URA"
|
|
33
|
+
D3_DNRA = "D3-DNRA"
|
|
34
|
+
D3_IPRA = "D3-IPRA"
|
|
35
|
+
D3_FHRA = "D3-FHRA"
|
|
36
|
+
D3_SRA = "D3-SRA"
|
|
37
|
+
D3_RUAA = "D3-RUAA"
|
|
5
38
|
class PlaybookTag(BaseModel):
|
|
6
|
-
analytic_story: list = None
|
|
7
|
-
detections: list = None
|
|
8
|
-
platform_tags: list =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
playbook_fields: list =
|
|
12
|
-
|
|
39
|
+
analytic_story: Optional[list] = None
|
|
40
|
+
detections: Optional[list] = None
|
|
41
|
+
platform_tags: list[str] = Field(...,min_length=0)
|
|
42
|
+
playbook_type: PlaybookType = Field(...)
|
|
43
|
+
vpe_type: VpeType = Field(...)
|
|
44
|
+
playbook_fields: list[str] = Field([], min_length=0)
|
|
45
|
+
product: list[PlaybookProduct] = Field([],min_length=0)
|
|
46
|
+
use_cases: list[PlaybookUseCase] = Field([],min_length=0)
|
|
47
|
+
defend_technique_id: Optional[List[DefendTechnique]] = None
|
|
48
|
+
|
|
49
|
+
detection_objects: list[Detection] = []
|
|
13
50
|
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import string
|
|
3
|
-
import uuid
|
|
4
|
-
from datetime import datetime
|
|
5
|
-
from pydantic import BaseModel, validator, ValidationError
|
|
1
|
+
from __future__ import annotations
|
|
6
2
|
from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import SecurityContentObject_Abstract
|
|
7
3
|
|
|
8
4
|
class SecurityContentObject(SecurityContentObject_Abstract):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import uuid
|
|
2
3
|
import string
|
|
3
4
|
import requests
|
|
@@ -14,8 +15,8 @@ from contentctl.objects.enums import DataModel
|
|
|
14
15
|
from contentctl.objects.enums import DetectionStatus
|
|
15
16
|
from contentctl.objects.deployment import Deployment
|
|
16
17
|
from contentctl.objects.ssa_detection_tags import SSADetectionTags
|
|
17
|
-
from contentctl.objects.
|
|
18
|
-
from contentctl.objects.
|
|
18
|
+
from contentctl.objects.unit_test_ssa import UnitTestSSA
|
|
19
|
+
from contentctl.objects.unit_test_old import UnitTestOld
|
|
19
20
|
from contentctl.objects.macro import Macro
|
|
20
21
|
from contentctl.objects.lookup import Lookup
|
|
21
22
|
from contentctl.objects.baseline import Baseline
|
|
@@ -40,7 +41,7 @@ class SSADetection(BaseModel):
|
|
|
40
41
|
known_false_positives: str
|
|
41
42
|
references: list
|
|
42
43
|
tags: SSADetectionTags
|
|
43
|
-
tests: list[
|
|
44
|
+
tests: list[UnitTestSSA] = None
|
|
44
45
|
|
|
45
46
|
# enrichments
|
|
46
47
|
annotations: dict = None
|
|
@@ -48,7 +49,7 @@ class SSADetection(BaseModel):
|
|
|
48
49
|
mappings: dict = None
|
|
49
50
|
file_path: str = None
|
|
50
51
|
source: str = None
|
|
51
|
-
test: Union[
|
|
52
|
+
test: Union[UnitTestSSA, dict, UnitTestOld] = None
|
|
52
53
|
runtime: str = None
|
|
53
54
|
internalVersion: int = None
|
|
54
55
|
|
|
@@ -61,6 +62,7 @@ class SSADetection(BaseModel):
|
|
|
61
62
|
class Config:
|
|
62
63
|
use_enum_values = True
|
|
63
64
|
|
|
65
|
+
'''
|
|
64
66
|
@validator("name")
|
|
65
67
|
def name_invalid_chars(cls, v):
|
|
66
68
|
invalidChars = set(string.punctuation.replace("-", ""))
|
|
@@ -150,3 +152,5 @@ class SSADetection(BaseModel):
|
|
|
150
152
|
"At least one test is required for a production or validation detection: " + values["name"]
|
|
151
153
|
)
|
|
152
154
|
return v
|
|
155
|
+
|
|
156
|
+
'''
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import re
|
|
3
|
+
from typing import List
|
|
4
|
+
from pydantic import BaseModel, validator, ValidationError, model_validator, Field
|
|
2
5
|
|
|
3
|
-
from pydantic import BaseModel, validator, ValidationError, root_validator
|
|
4
6
|
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
|
|
5
7
|
from contentctl.objects.constants import *
|
|
6
|
-
|
|
8
|
+
from contentctl.objects.enums import SecurityContentProductName
|
|
7
9
|
|
|
8
10
|
class SSADetectionTags(BaseModel):
|
|
9
11
|
# detection spec
|
|
10
|
-
name: str
|
|
12
|
+
#name: str
|
|
11
13
|
analytic_story: list
|
|
12
14
|
asset_type: str
|
|
13
15
|
automated_detection_testing: str = None
|
|
@@ -19,7 +21,7 @@ class SSADetectionTags(BaseModel):
|
|
|
19
21
|
mitre_attack_id: list = None
|
|
20
22
|
nist: list = None
|
|
21
23
|
observable: list
|
|
22
|
-
product:
|
|
24
|
+
product: List[SecurityContentProductName] = Field(...,min_length=1)
|
|
23
25
|
required_fields: list
|
|
24
26
|
risk_score: int
|
|
25
27
|
security_domain: str
|
|
@@ -77,7 +79,7 @@ class SSADetectionTags(BaseModel):
|
|
|
77
79
|
def tags_confidence(cls, v, values):
|
|
78
80
|
v = int(v)
|
|
79
81
|
if not (v > 0 and v <= 100):
|
|
80
|
-
raise ValueError('confidence score is out of range 1-100
|
|
82
|
+
raise ValueError('confidence score is out of range 1-100.' )
|
|
81
83
|
else:
|
|
82
84
|
return v
|
|
83
85
|
|
|
@@ -85,7 +87,7 @@ class SSADetectionTags(BaseModel):
|
|
|
85
87
|
@validator('impact')
|
|
86
88
|
def tags_impact(cls, v, values):
|
|
87
89
|
if not (v > 0 and v <= 100):
|
|
88
|
-
raise ValueError('impact score is out of range 1-100
|
|
90
|
+
raise ValueError('impact score is out of range 1-100.')
|
|
89
91
|
else:
|
|
90
92
|
return v
|
|
91
93
|
|
|
@@ -94,7 +96,7 @@ class SSADetectionTags(BaseModel):
|
|
|
94
96
|
valid_kill_chain_phases = SES_KILL_CHAIN_MAPPINGS.keys()
|
|
95
97
|
for value in v:
|
|
96
98
|
if value not in valid_kill_chain_phases:
|
|
97
|
-
raise ValueError('kill chain phase not valid
|
|
99
|
+
raise ValueError('kill chain phase not valid. Valid options are ' + str(valid_kill_chain_phases))
|
|
98
100
|
return v
|
|
99
101
|
|
|
100
102
|
@validator('mitre_attack_id')
|
|
@@ -102,20 +104,10 @@ class SSADetectionTags(BaseModel):
|
|
|
102
104
|
pattern = 'T[0-9]{4}'
|
|
103
105
|
for value in v:
|
|
104
106
|
if not re.match(pattern, value):
|
|
105
|
-
raise ValueError('Mitre Attack ID are not following the pattern Txxxx:
|
|
107
|
+
raise ValueError('Mitre Attack ID are not following the pattern Txxxx:' )
|
|
106
108
|
return v
|
|
107
109
|
|
|
108
|
-
@validator('product')
|
|
109
|
-
def tags_product(cls, v, values):
|
|
110
|
-
valid_products = [
|
|
111
|
-
"Splunk Enterprise", "Splunk Enterprise Security", "Splunk Cloud",
|
|
112
|
-
"Splunk Security Analytics for AWS", "Splunk Behavioral Analytics"
|
|
113
|
-
]
|
|
114
110
|
|
|
115
|
-
for value in v:
|
|
116
|
-
if value not in valid_products:
|
|
117
|
-
raise ValueError('product is not valid for ' + values['name'] + '. valid products are ' + str(valid_products))
|
|
118
|
-
return v
|
|
119
111
|
|
|
120
112
|
@validator('risk_score')
|
|
121
113
|
def tags_calculate_risk_score(cls, v, values):
|
|
@@ -125,21 +117,22 @@ class SSADetectionTags(BaseModel):
|
|
|
125
117
|
f"\n Expected risk_score={calculated_risk_score}, found risk_score={int(v)}: {values['name']}")
|
|
126
118
|
return v
|
|
127
119
|
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
|
|
121
|
+
@model_validator(mode="after")
|
|
122
|
+
def tags_observable(self):
|
|
130
123
|
valid_roles = SES_OBSERVABLE_ROLE_MAPPING.keys()
|
|
131
124
|
valid_types = SES_OBSERVABLE_TYPE_MAPPING.keys()
|
|
132
125
|
|
|
133
|
-
for value in
|
|
126
|
+
for value in self.observable:
|
|
134
127
|
if value['type'] in valid_types:
|
|
135
|
-
if 'Splunk Behavioral Analytics' in
|
|
128
|
+
if 'Splunk Behavioral Analytics' in self.product:
|
|
136
129
|
continue
|
|
137
130
|
|
|
138
131
|
if 'role' not in value:
|
|
139
|
-
raise ValueError('Observable role is missing
|
|
132
|
+
raise ValueError('Observable role is missing')
|
|
140
133
|
for role in value['role']:
|
|
141
134
|
if role not in valid_roles:
|
|
142
|
-
raise ValueError('Observable role ' + role + ' not valid
|
|
135
|
+
raise ValueError(f'Observable role ' + role + ' not valid. Valid options are {str(valid_roles)}')
|
|
143
136
|
else:
|
|
144
|
-
raise ValueError('Observable type ' + value['type'] + ' not valid
|
|
145
|
-
return
|
|
137
|
+
raise ValueError(f'Observable type ' + value['type'] + ' not valid. Valid options are {str(valid_types)}')
|
|
138
|
+
return self
|