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.
Files changed (123) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +102 -57
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
  5. contentctl/actions/detection_testing/GitService.py +134 -76
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +9 -6
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
  11. contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
  12. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
  13. contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
  14. contentctl/actions/doc_gen.py +9 -5
  15. contentctl/actions/initialize.py +45 -33
  16. contentctl/actions/inspect.py +118 -61
  17. contentctl/actions/new_content.py +155 -108
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +33 -28
  21. contentctl/actions/validate.py +55 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +124 -90
  24. contentctl/enrichments/attack_enrichment.py +112 -72
  25. contentctl/enrichments/cve_enrichment.py +34 -28
  26. contentctl/enrichments/splunk_app_enrichment.py +38 -36
  27. contentctl/helper/link_validator.py +101 -78
  28. contentctl/helper/splunk_app.py +69 -41
  29. contentctl/helper/utils.py +58 -53
  30. contentctl/input/director.py +68 -36
  31. contentctl/input/new_content_questions.py +27 -35
  32. contentctl/input/yml_reader.py +28 -18
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
  35. contentctl/objects/alert_action.py +10 -9
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +65 -54
  38. contentctl/objects/base_test.py +5 -3
  39. contentctl/objects/base_test_result.py +19 -11
  40. contentctl/objects/baseline.py +62 -30
  41. contentctl/objects/baseline_tags.py +30 -24
  42. contentctl/objects/config.py +790 -597
  43. contentctl/objects/constants.py +33 -56
  44. contentctl/objects/correlation_search.py +150 -136
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +16 -17
  47. contentctl/objects/deployment.py +43 -44
  48. contentctl/objects/deployment_email.py +3 -2
  49. contentctl/objects/deployment_notable.py +4 -2
  50. contentctl/objects/deployment_phantom.py +7 -6
  51. contentctl/objects/deployment_rba.py +3 -2
  52. contentctl/objects/deployment_scheduling.py +3 -2
  53. contentctl/objects/deployment_slack.py +3 -2
  54. contentctl/objects/detection.py +5 -2
  55. contentctl/objects/detection_metadata.py +1 -0
  56. contentctl/objects/detection_stanza.py +7 -2
  57. contentctl/objects/detection_tags.py +58 -103
  58. contentctl/objects/drilldown.py +66 -34
  59. contentctl/objects/enums.py +81 -100
  60. contentctl/objects/errors.py +16 -24
  61. contentctl/objects/integration_test.py +3 -3
  62. contentctl/objects/integration_test_result.py +1 -0
  63. contentctl/objects/investigation.py +59 -36
  64. contentctl/objects/investigation_tags.py +30 -19
  65. contentctl/objects/lookup.py +304 -101
  66. contentctl/objects/macro.py +55 -39
  67. contentctl/objects/manual_test.py +3 -3
  68. contentctl/objects/manual_test_result.py +1 -0
  69. contentctl/objects/mitre_attack_enrichment.py +17 -16
  70. contentctl/objects/notable_action.py +2 -1
  71. contentctl/objects/notable_event.py +1 -3
  72. contentctl/objects/playbook.py +37 -35
  73. contentctl/objects/playbook_tags.py +23 -13
  74. contentctl/objects/rba.py +96 -0
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +110 -160
  77. contentctl/objects/risk_object.py +1 -0
  78. contentctl/objects/savedsearches_conf.py +9 -7
  79. contentctl/objects/security_content_object.py +5 -2
  80. contentctl/objects/story.py +54 -49
  81. contentctl/objects/story_tags.py +56 -45
  82. contentctl/objects/test_attack_data.py +2 -1
  83. contentctl/objects/test_group.py +5 -2
  84. contentctl/objects/threat_object.py +1 -0
  85. contentctl/objects/throttling.py +27 -18
  86. contentctl/objects/unit_test.py +3 -4
  87. contentctl/objects/unit_test_baseline.py +5 -5
  88. contentctl/objects/unit_test_result.py +6 -6
  89. contentctl/output/api_json_output.py +233 -220
  90. contentctl/output/attack_nav_output.py +21 -21
  91. contentctl/output/attack_nav_writer.py +29 -37
  92. contentctl/output/conf_output.py +235 -172
  93. contentctl/output/conf_writer.py +201 -125
  94. contentctl/output/data_source_writer.py +38 -26
  95. contentctl/output/doc_md_output.py +53 -27
  96. contentctl/output/jinja_writer.py +19 -15
  97. contentctl/output/json_writer.py +21 -11
  98. contentctl/output/svg_output.py +56 -38
  99. contentctl/output/templates/analyticstories_detections.j2 +2 -2
  100. contentctl/output/templates/analyticstories_stories.j2 +1 -1
  101. contentctl/output/templates/collections.j2 +1 -1
  102. contentctl/output/templates/doc_detections.j2 +0 -5
  103. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  104. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  105. contentctl/output/templates/savedsearches_baselines.j2 +2 -2
  106. contentctl/output/templates/savedsearches_detections.j2 +10 -11
  107. contentctl/output/templates/savedsearches_investigations.j2 +2 -2
  108. contentctl/output/templates/transforms.j2 +6 -8
  109. contentctl/output/yml_writer.py +29 -20
  110. contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
  111. contentctl/templates/stories/cobalt_strike.yml +1 -0
  112. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
  113. contentctl-5.0.0.dist-info/RECORD +168 -0
  114. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
  115. contentctl/actions/initialize_old.py +0 -245
  116. contentctl/objects/event_source.py +0 -11
  117. contentctl/objects/observable.py +0 -37
  118. contentctl/output/detection_writer.py +0 -28
  119. contentctl/output/new_content_yml_output.py +0 -56
  120. contentctl/output/yml_output.py +0 -66
  121. contentctl-4.4.7.dist-info/RECORD +0 -173
  122. {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
  123. {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( # type: ignore
30
- message=message,
31
- status=TestResultStatus.SKIP
30
+ self.result = ManualTestResult( # type: ignore
31
+ message=message, status=TestResultStatus.SKIP
32
32
  )
@@ -5,4 +5,5 @@ class ManualTestResult(BaseTestResult):
5
5
  """
6
6
  A manual test result
7
7
  """
8
+
8
9
  pass
@@ -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, Annotated
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='forbid')
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='forbid')
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
- # TODO (#266): disable the use_enum_values configuration
87
+
87
88
  class MitreAttackEnrichment(BaseModel):
88
- ConfigDict(use_enum_values=True)
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()
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING,Self
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(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
-
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(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
-
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 = '\n - '.join(missing)
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 = 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}")
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
- return self
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 TYPE_CHECKING, Optional, List
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
- class PlaybookUseCase(str,enum.Enum):
11
+
12
+ class PlaybookUseCase(str, enum.Enum):
12
13
  PHISHING = "Phishing"
13
14
  ENDPOINT = "Endpoint"
14
15
  ENRICHMENT = "Enrichment"
15
-
16
- class PlaybookType(str,enum.Enum):
16
+
17
+
18
+ class PlaybookType(str, enum.Enum):
17
19
  INPUT = "Input"
18
20
  AUTOMATION = "Automation"
19
21
 
20
- class VpeType(str,enum.Enum):
22
+
23
+ class VpeType(str, enum.Enum):
21
24
  MODERN = "Modern"
22
25
  CLASSIC = "Classic"
23
- class DefendTechnique(str,enum.Enum):
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(RiskObject(
84
- field=entry["risk_object_field"],
85
- type=entry["risk_object_type"],
86
- score=int(entry["risk_score"])
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(ThreatObject(
90
- field=entry["threat_object_field"],
91
- type=entry["threat_object_type"]
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
  )