contentctl 4.4.7__py3-none-any.whl → 5.0.0__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/__init__.py +1 -1
- contentctl/actions/build.py +102 -57
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
- contentctl/actions/detection_testing/GitService.py +134 -76
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +9 -6
- contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +155 -108
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +33 -28
- contentctl/actions/validate.py +55 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +124 -90
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -53
- contentctl/input/director.py +68 -36
- contentctl/input/new_content_questions.py +27 -35
- contentctl/input/yml_reader.py +28 -18
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
- contentctl/objects/alert_action.py +10 -9
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +65 -54
- contentctl/objects/base_test.py +5 -3
- contentctl/objects/base_test_result.py +19 -11
- contentctl/objects/baseline.py +62 -30
- contentctl/objects/baseline_tags.py +30 -24
- contentctl/objects/config.py +790 -597
- contentctl/objects/constants.py +33 -56
- contentctl/objects/correlation_search.py +150 -136
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +16 -17
- contentctl/objects/deployment.py +43 -44
- contentctl/objects/deployment_email.py +3 -2
- contentctl/objects/deployment_notable.py +4 -2
- contentctl/objects/deployment_phantom.py +7 -6
- contentctl/objects/deployment_rba.py +3 -2
- contentctl/objects/deployment_scheduling.py +3 -2
- contentctl/objects/deployment_slack.py +3 -2
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +58 -103
- contentctl/objects/drilldown.py +66 -34
- contentctl/objects/enums.py +81 -100
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +59 -36
- contentctl/objects/investigation_tags.py +30 -19
- contentctl/objects/lookup.py +304 -101
- contentctl/objects/macro.py +55 -39
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +23 -13
- contentctl/objects/rba.py +96 -0
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +110 -160
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +54 -49
- contentctl/objects/story_tags.py +56 -45
- contentctl/objects/test_attack_data.py +2 -1
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +5 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +233 -220
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +235 -172
- contentctl/output/conf_writer.py +201 -125
- contentctl/output/data_source_writer.py +38 -26
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +21 -11
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/collections.j2 +1 -1
- contentctl/output/templates/doc_detections.j2 +0 -5
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -2
- contentctl/output/templates/savedsearches_detections.j2 +10 -11
- contentctl/output/templates/savedsearches_investigations.j2 +2 -2
- contentctl/output/templates/transforms.j2 +6 -8
- contentctl/output/yml_writer.py +29 -20
- contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
- contentctl/templates/stories/cobalt_strike.yml +1 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
- contentctl-5.0.0.dist-info/RECORD +168 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/event_source.py +0 -11
- contentctl/objects/observable.py +0 -37
- contentctl/output/detection_writer.py +0 -28
- contentctl/output/new_content_yml_output.py +0 -56
- contentctl/output/yml_output.py +0 -66
- contentctl-4.4.7.dist-info/RECORD +0 -173
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -12,6 +12,7 @@ class ManualTest(BaseTest):
|
|
|
12
12
|
"""
|
|
13
13
|
A manual test for a detection
|
|
14
14
|
"""
|
|
15
|
+
|
|
15
16
|
# The test type (manual)
|
|
16
17
|
test_type: TestType = Field(default=TestType.MANUAL)
|
|
17
18
|
|
|
@@ -26,7 +27,6 @@ class ManualTest(BaseTest):
|
|
|
26
27
|
Skip the test by setting its result status
|
|
27
28
|
:param message: the reason for skipping
|
|
28
29
|
"""
|
|
29
|
-
self.result = ManualTestResult(
|
|
30
|
-
message=message,
|
|
31
|
-
status=TestResultStatus.SKIP
|
|
30
|
+
self.result = ManualTestResult( # type: ignore
|
|
31
|
+
message=message, status=TestResultStatus.SKIP
|
|
32
32
|
)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from pydantic import BaseModel, Field, ConfigDict, HttpUrl, field_validator
|
|
3
|
-
from typing import List
|
|
3
|
+
from typing import List
|
|
4
4
|
from enum import StrEnum
|
|
5
5
|
import datetime
|
|
6
6
|
from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class MitreTactics(StrEnum):
|
|
9
10
|
RECONNAISSANCE = "Reconnaissance"
|
|
10
11
|
RESOURCE_DEVELOPMENT = "Resource Development"
|
|
@@ -31,16 +32,17 @@ class AttackGroupMatrix(StrEnum):
|
|
|
31
32
|
class AttackGroupType(StrEnum):
|
|
32
33
|
intrusion_set = "intrusion-set"
|
|
33
34
|
|
|
35
|
+
|
|
34
36
|
class MitreExternalReference(BaseModel):
|
|
35
|
-
model_config = ConfigDict(extra=
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
36
38
|
source_name: str
|
|
37
|
-
external_id: None | str = None
|
|
39
|
+
external_id: None | str = None
|
|
38
40
|
url: None | HttpUrl = None
|
|
39
41
|
description: None | str = None
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
class MitreAttackGroup(BaseModel):
|
|
43
|
-
model_config = ConfigDict(extra=
|
|
45
|
+
model_config = ConfigDict(extra="forbid")
|
|
44
46
|
contributors: list[str] = []
|
|
45
47
|
created: datetime.datetime
|
|
46
48
|
created_by_ref: str
|
|
@@ -53,45 +55,44 @@ class MitreAttackGroup(BaseModel):
|
|
|
53
55
|
matrix: list[AttackGroupMatrix]
|
|
54
56
|
mitre_attack_spec_version: None | str
|
|
55
57
|
mitre_version: str
|
|
56
|
-
#assume that if the deprecated field is not present, then the group is not deprecated
|
|
58
|
+
# assume that if the deprecated field is not present, then the group is not deprecated
|
|
57
59
|
mitre_deprecated: bool
|
|
58
60
|
modified: datetime.datetime
|
|
59
61
|
modified_by_ref: str
|
|
60
62
|
object_marking_refs: list[str]
|
|
61
63
|
type: AttackGroupType
|
|
62
64
|
url: HttpUrl
|
|
63
|
-
|
|
64
65
|
|
|
65
66
|
@field_validator("mitre_deprecated", mode="before")
|
|
66
|
-
def standardize_mitre_deprecated(cls, mitre_deprecated:bool | None) -> bool:
|
|
67
|
-
|
|
67
|
+
def standardize_mitre_deprecated(cls, mitre_deprecated: bool | None) -> bool:
|
|
68
|
+
"""
|
|
68
69
|
For some reason, the API will return either a bool for mitre_deprecated OR
|
|
69
70
|
None. We simplify our typing by converting None to False, and assuming that
|
|
70
71
|
if deprecated is None, then the group is not deprecated.
|
|
71
|
-
|
|
72
|
+
"""
|
|
72
73
|
if mitre_deprecated is None:
|
|
73
74
|
return False
|
|
74
75
|
return mitre_deprecated
|
|
75
76
|
|
|
76
77
|
@field_validator("contributors", mode="before")
|
|
77
|
-
def standardize_contributors(cls, contributors:list[str] | None) -> list[str]:
|
|
78
|
-
|
|
78
|
+
def standardize_contributors(cls, contributors: list[str] | None) -> list[str]:
|
|
79
|
+
"""
|
|
79
80
|
For some reason, the API will return either a list of strings for contributors OR
|
|
80
81
|
None. We simplify our typing by converting None to an empty list.
|
|
81
|
-
|
|
82
|
+
"""
|
|
82
83
|
if contributors is None:
|
|
83
84
|
return []
|
|
84
85
|
return contributors
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
|
|
87
88
|
class MitreAttackEnrichment(BaseModel):
|
|
88
|
-
ConfigDict(
|
|
89
|
+
ConfigDict(extra="forbid")
|
|
89
90
|
mitre_attack_id: MITRE_ATTACK_ID_TYPE = Field(...)
|
|
90
91
|
mitre_attack_technique: str = Field(...)
|
|
91
92
|
mitre_attack_tactics: List[MitreTactics] = Field(...)
|
|
92
93
|
mitre_attack_groups: List[str] = Field(...)
|
|
93
|
-
#Exclude this field from serialization - it is very large and not useful in JSON objects
|
|
94
|
+
# Exclude this field from serialization - it is very large and not useful in JSON objects
|
|
94
95
|
mitre_attack_group_objects: list[MitreAttackGroup] = Field(..., exclude=True)
|
|
96
|
+
|
|
95
97
|
def __hash__(self) -> int:
|
|
96
98
|
return id(self)
|
|
97
|
-
|
|
@@ -14,6 +14,7 @@ class NotableAction(BaseModel):
|
|
|
14
14
|
:param security_domain: the domain associated with the notable action and related rule (detection/search)
|
|
15
15
|
:param severity: severity (e.g. "high") associated with the notable action and related rule (detection/search)
|
|
16
16
|
"""
|
|
17
|
+
|
|
17
18
|
rule_name: str
|
|
18
19
|
rule_description: str
|
|
19
20
|
security_domain: str
|
|
@@ -32,5 +33,5 @@ class NotableAction(BaseModel):
|
|
|
32
33
|
rule_name=dict_["action.notable.param.rule_title"],
|
|
33
34
|
rule_description=dict_["action.notable.param.rule_description"],
|
|
34
35
|
security_domain=dict_["action.notable.param.security_domain"],
|
|
35
|
-
severity=dict_["action.notable.param.severity"]
|
|
36
|
+
severity=dict_["action.notable.param.severity"],
|
|
36
37
|
)
|
|
@@ -13,9 +13,7 @@ class NotableEvent(BaseModel):
|
|
|
13
13
|
|
|
14
14
|
# Allowing fields that aren't explicitly defined to be passed since some of the risk event's
|
|
15
15
|
# fields vary depending on the SPL which generated them
|
|
16
|
-
model_config = ConfigDict(
|
|
17
|
-
extra='allow'
|
|
18
|
-
)
|
|
16
|
+
model_config = ConfigDict(extra="allow")
|
|
19
17
|
|
|
20
18
|
def validate_against_detection(self, detection: Detection) -> None:
|
|
21
19
|
raise NotImplementedError()
|
contentctl/objects/playbook.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Self
|
|
3
3
|
from pydantic import model_validator, Field, FilePath
|
|
4
4
|
|
|
5
5
|
|
|
@@ -10,57 +10,59 @@ from contentctl.objects.enums import PlaybookType
|
|
|
10
10
|
|
|
11
11
|
class Playbook(SecurityContentObject):
|
|
12
12
|
type: PlaybookType = Field(...)
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
# Override the type definition for filePath.
|
|
15
15
|
# This MUST be backed by a file and cannot be None
|
|
16
16
|
file_path: FilePath
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
how_to_implement: str = Field(min_length=4)
|
|
19
19
|
playbook: str = Field(min_length=4)
|
|
20
|
-
app_list: list[str] = Field(...,min_length=0)
|
|
20
|
+
app_list: list[str] = Field(..., min_length=0)
|
|
21
21
|
tags: PlaybookTag = Field(...)
|
|
22
|
-
|
|
23
22
|
|
|
24
|
-
|
|
25
23
|
@model_validator(mode="after")
|
|
26
|
-
def ensureJsonAndPyFilesExist(self)->Self:
|
|
24
|
+
def ensureJsonAndPyFilesExist(self) -> Self:
|
|
27
25
|
json_file_path = self.file_path.with_suffix(".json")
|
|
28
26
|
python_file_path = self.file_path.with_suffix(".py")
|
|
29
|
-
missing:list[str] = []
|
|
27
|
+
missing: list[str] = []
|
|
30
28
|
if not json_file_path.is_file():
|
|
31
|
-
missing.append(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
missing.append(
|
|
30
|
+
f"Playbook file named '{self.file_path.name}' MUST "
|
|
31
|
+
f"have a .json file named '{json_file_path.name}', "
|
|
32
|
+
"but it does not exist"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
35
|
if not python_file_path.is_file():
|
|
36
|
-
missing.append(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
missing.append(
|
|
37
|
+
f"Playbook file named '{self.file_path.name}' MUST "
|
|
38
|
+
f"have a .py file named '{python_file_path.name}', "
|
|
39
|
+
"but it does not exist"
|
|
40
|
+
)
|
|
41
|
+
|
|
41
42
|
if len(missing) == 0:
|
|
42
43
|
return self
|
|
43
44
|
else:
|
|
44
|
-
missing_files_string =
|
|
45
|
+
missing_files_string = "\n - ".join(missing)
|
|
45
46
|
raise ValueError(f"Playbook files missing:\n -{missing_files_string}")
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
#Override playbook file name checking FOR NOW
|
|
48
|
+
# Override playbook file name checking FOR NOW
|
|
49
49
|
@model_validator(mode="after")
|
|
50
|
-
def ensureFileNameMatchesSearchName(self)->Self:
|
|
51
|
-
file_name =
|
|
52
|
-
.replace(
|
|
53
|
-
.replace(
|
|
54
|
-
.replace(
|
|
55
|
-
.replace(
|
|
56
|
-
.lower()
|
|
57
|
-
|
|
58
|
-
|
|
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}")
|
|
50
|
+
def ensureFileNameMatchesSearchName(self) -> Self:
|
|
51
|
+
file_name = (
|
|
52
|
+
self.name.replace(" ", "_")
|
|
53
|
+
.replace("-", "_")
|
|
54
|
+
.replace(".", "_")
|
|
55
|
+
.replace("/", "_")
|
|
56
|
+
.lower()
|
|
57
|
+
+ ".yml"
|
|
58
|
+
)
|
|
63
59
|
|
|
64
|
-
|
|
60
|
+
# allow different capitalization FOR NOW in playbook file names
|
|
61
|
+
if self.file_path is not None and file_name != self.file_path.name.lower():
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"The file name MUST be based off the content 'name' field:\n"
|
|
64
|
+
f"\t- Expected File Name: {file_name}\n"
|
|
65
|
+
f"\t- Actual File Name : {self.file_path.name}"
|
|
66
|
+
)
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
return self
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import
|
|
3
|
-
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
4
4
|
import enum
|
|
5
5
|
from contentctl.objects.detection import Detection
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class PlaybookProduct(str,enum.Enum):
|
|
8
|
+
class PlaybookProduct(str, enum.Enum):
|
|
9
9
|
SPLUNK_SOAR = "Splunk SOAR"
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
class PlaybookUseCase(str, enum.Enum):
|
|
12
13
|
PHISHING = "Phishing"
|
|
13
14
|
ENDPOINT = "Endpoint"
|
|
14
15
|
ENRICHMENT = "Enrichment"
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PlaybookType(str, enum.Enum):
|
|
17
19
|
INPUT = "Input"
|
|
18
20
|
AUTOMATION = "Automation"
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
|
|
23
|
+
class VpeType(str, enum.Enum):
|
|
21
24
|
MODERN = "Modern"
|
|
22
25
|
CLASSIC = "Classic"
|
|
23
|
-
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DefendTechnique(str, enum.Enum):
|
|
24
29
|
D3_AL = "D3-AL"
|
|
25
30
|
D3_DNSDL = "D3-DNSDL"
|
|
26
31
|
D3_DA = "D3-DA"
|
|
@@ -35,16 +40,21 @@ class DefendTechnique(str,enum.Enum):
|
|
|
35
40
|
D3_FHRA = "D3-FHRA"
|
|
36
41
|
D3_SRA = "D3-SRA"
|
|
37
42
|
D3_RUAA = "D3-RUAA"
|
|
43
|
+
|
|
44
|
+
|
|
38
45
|
class PlaybookTag(BaseModel):
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
39
47
|
analytic_story: Optional[list] = None
|
|
40
48
|
detections: Optional[list] = None
|
|
41
|
-
platform_tags: list[str] = Field(...,min_length=0)
|
|
49
|
+
platform_tags: list[str] = Field(..., min_length=0)
|
|
42
50
|
playbook_type: PlaybookType = Field(...)
|
|
43
51
|
vpe_type: VpeType = Field(...)
|
|
44
52
|
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)
|
|
53
|
+
product: list[PlaybookProduct] = Field([], min_length=0)
|
|
54
|
+
use_cases: list[PlaybookUseCase] = Field([], min_length=0)
|
|
47
55
|
defend_technique_id: Optional[List[DefendTechnique]] = None
|
|
48
|
-
|
|
56
|
+
|
|
57
|
+
labels: list[str] = []
|
|
58
|
+
playbook_outputs: list[str] = []
|
|
59
|
+
|
|
49
60
|
detection_objects: list[Detection] = []
|
|
50
|
-
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from pydantic import BaseModel, computed_field, Field
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import Set, Annotated
|
|
5
|
+
from contentctl.objects.enums import RiskSeverity
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RiskScoreValue_Type = Annotated[int, Field(ge=1, le=100)]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RiskObjectType(str, Enum):
|
|
12
|
+
SYSTEM = "system"
|
|
13
|
+
USER = "user"
|
|
14
|
+
OTHER = "other"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ThreatObjectType(str, Enum):
|
|
18
|
+
CERTIFICATE_COMMON_NAME = "certificate_common_name"
|
|
19
|
+
CERTIFICATE_ORGANIZATION = "certificate_organization"
|
|
20
|
+
CERTIFICATE_SERIAL = "certificate_serial"
|
|
21
|
+
CERTIFICATE_UNIT = "certificate_unit"
|
|
22
|
+
COMMAND = "command"
|
|
23
|
+
DOMAIN = "domain"
|
|
24
|
+
EMAIL_ADDRESS = "email_address"
|
|
25
|
+
EMAIL_SUBJECT = "email_subject"
|
|
26
|
+
FILE_HASH = "file_hash"
|
|
27
|
+
FILE_NAME = "file_name"
|
|
28
|
+
FILE_PATH = "file_path"
|
|
29
|
+
HTTP_USER_AGENT = "http_user_agent"
|
|
30
|
+
IP_ADDRESS = "ip_address"
|
|
31
|
+
PROCESS = "process"
|
|
32
|
+
PROCESS_NAME = "process_name"
|
|
33
|
+
PARENT_PROCESS = "parent_process"
|
|
34
|
+
PARENT_PROCESS_NAME = "parent_process_name"
|
|
35
|
+
PROCESS_HASH = "process_hash"
|
|
36
|
+
REGISTRY_PATH = "registry_path"
|
|
37
|
+
REGISTRY_VALUE_NAME = "registry_value_name"
|
|
38
|
+
REGISTRY_VALUE_TEXT = "registry_value_text"
|
|
39
|
+
SERVICE = "service"
|
|
40
|
+
SIGNATURE = "signature"
|
|
41
|
+
SYSTEM = "system"
|
|
42
|
+
TLS_HASH = "tls_hash"
|
|
43
|
+
URL = "url"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RiskObject(BaseModel):
|
|
47
|
+
field: str
|
|
48
|
+
type: RiskObjectType
|
|
49
|
+
score: RiskScoreValue_Type
|
|
50
|
+
|
|
51
|
+
def __hash__(self):
|
|
52
|
+
return hash((self.field, self.type, self.score))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ThreatObject(BaseModel):
|
|
56
|
+
field: str
|
|
57
|
+
type: ThreatObjectType
|
|
58
|
+
|
|
59
|
+
def __hash__(self):
|
|
60
|
+
return hash((self.field, self.type))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RBAObject(BaseModel, ABC):
|
|
64
|
+
message: str
|
|
65
|
+
risk_objects: Annotated[Set[RiskObject], Field(min_length=1)]
|
|
66
|
+
threat_objects: Set[ThreatObject]
|
|
67
|
+
|
|
68
|
+
@computed_field
|
|
69
|
+
@property
|
|
70
|
+
def risk_score(self) -> RiskScoreValue_Type:
|
|
71
|
+
# First get the maximum score associated with
|
|
72
|
+
# a risk object. If there are no objects, then
|
|
73
|
+
# we should throw an exception.
|
|
74
|
+
if len(self.risk_objects) == 0:
|
|
75
|
+
raise Exception(
|
|
76
|
+
"There must be at least one Risk Object present to get Severity."
|
|
77
|
+
)
|
|
78
|
+
return max([risk_object.score for risk_object in self.risk_objects])
|
|
79
|
+
|
|
80
|
+
@computed_field
|
|
81
|
+
@property
|
|
82
|
+
def severity(self) -> RiskSeverity:
|
|
83
|
+
if 0 <= self.risk_score <= 20:
|
|
84
|
+
return RiskSeverity.INFORMATIONAL
|
|
85
|
+
elif 20 < self.risk_score <= 40:
|
|
86
|
+
return RiskSeverity.LOW
|
|
87
|
+
elif 40 < self.risk_score <= 60:
|
|
88
|
+
return RiskSeverity.MEDIUM
|
|
89
|
+
elif 60 < self.risk_score <= 80:
|
|
90
|
+
return RiskSeverity.HIGH
|
|
91
|
+
elif 80 < self.risk_score <= 100:
|
|
92
|
+
return RiskSeverity.CRITICAL
|
|
93
|
+
else:
|
|
94
|
+
raise Exception(
|
|
95
|
+
f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}"
|
|
96
|
+
)
|
|
@@ -18,6 +18,7 @@ class RiskAnalysisAction(BaseModel):
|
|
|
18
18
|
:param message: the message associated w/ the risk event (NOTE: may contain macros of the form
|
|
19
19
|
$...$ which should be replaced with real values in the resulting risk events)
|
|
20
20
|
"""
|
|
21
|
+
|
|
21
22
|
risk_objects: list[RiskObject]
|
|
22
23
|
message: str
|
|
23
24
|
|
|
@@ -80,21 +81,24 @@ class RiskAnalysisAction(BaseModel):
|
|
|
80
81
|
# TODO (#231): add validation ensuring at least 1 risk objects
|
|
81
82
|
for entry in object_dicts:
|
|
82
83
|
if "risk_object_field" in entry:
|
|
83
|
-
risk_objects.append(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
risk_objects.append(
|
|
85
|
+
RiskObject(
|
|
86
|
+
field=entry["risk_object_field"],
|
|
87
|
+
type=entry["risk_object_type"],
|
|
88
|
+
score=int(entry["risk_score"]),
|
|
89
|
+
)
|
|
90
|
+
)
|
|
88
91
|
elif "threat_object_field" in entry:
|
|
89
|
-
threat_objects.append(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
threat_objects.append(
|
|
93
|
+
ThreatObject(
|
|
94
|
+
field=entry["threat_object_field"],
|
|
95
|
+
type=entry["threat_object_type"],
|
|
96
|
+
)
|
|
97
|
+
)
|
|
93
98
|
else:
|
|
94
99
|
raise ValueError(
|
|
95
100
|
f"Unexpected object within 'action.risk.param._risk': {entry}"
|
|
96
101
|
)
|
|
97
102
|
return cls(
|
|
98
|
-
risk_objects=risk_objects,
|
|
99
|
-
message=dict_["action.risk.param._risk_message"]
|
|
103
|
+
risk_objects=risk_objects, message=dict_["action.risk.param._risk_message"]
|
|
100
104
|
)
|