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.
Files changed (142) hide show
  1. contentctl/actions/build.py +89 -0
  2. contentctl/actions/detection_testing/DetectionTestingManager.py +48 -49
  3. contentctl/actions/detection_testing/GitService.py +148 -230
  4. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +14 -24
  5. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +43 -17
  6. contentctl/actions/detection_testing/views/DetectionTestingView.py +3 -2
  7. contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +0 -8
  8. contentctl/actions/doc_gen.py +1 -1
  9. contentctl/actions/initialize.py +28 -65
  10. contentctl/actions/inspect.py +260 -0
  11. contentctl/actions/new_content.py +106 -13
  12. contentctl/actions/release_notes.py +168 -144
  13. contentctl/actions/reporting.py +24 -13
  14. contentctl/actions/test.py +39 -20
  15. contentctl/actions/validate.py +25 -48
  16. contentctl/contentctl.py +196 -754
  17. contentctl/enrichments/attack_enrichment.py +69 -19
  18. contentctl/enrichments/cve_enrichment.py +28 -13
  19. contentctl/helper/link_validator.py +24 -26
  20. contentctl/helper/utils.py +7 -3
  21. contentctl/input/director.py +139 -201
  22. contentctl/input/new_content_questions.py +63 -61
  23. contentctl/input/sigma_converter.py +1 -2
  24. contentctl/input/ssa_detection_builder.py +16 -7
  25. contentctl/input/yml_reader.py +4 -3
  26. contentctl/objects/abstract_security_content_objects/detection_abstract.py +487 -154
  27. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +155 -51
  28. contentctl/objects/alert_action.py +40 -0
  29. contentctl/objects/atomic.py +212 -0
  30. contentctl/objects/baseline.py +44 -43
  31. contentctl/objects/baseline_tags.py +69 -20
  32. contentctl/objects/config.py +857 -125
  33. contentctl/objects/constants.py +0 -1
  34. contentctl/objects/correlation_search.py +1 -1
  35. contentctl/objects/data_source.py +2 -4
  36. contentctl/objects/deployment.py +61 -21
  37. contentctl/objects/deployment_email.py +2 -2
  38. contentctl/objects/deployment_notable.py +4 -4
  39. contentctl/objects/deployment_phantom.py +2 -2
  40. contentctl/objects/deployment_rba.py +3 -4
  41. contentctl/objects/deployment_scheduling.py +2 -3
  42. contentctl/objects/deployment_slack.py +2 -2
  43. contentctl/objects/detection.py +1 -5
  44. contentctl/objects/detection_tags.py +210 -119
  45. contentctl/objects/enums.py +312 -24
  46. contentctl/objects/integration_test.py +1 -1
  47. contentctl/objects/integration_test_result.py +0 -2
  48. contentctl/objects/investigation.py +62 -53
  49. contentctl/objects/investigation_tags.py +30 -6
  50. contentctl/objects/lookup.py +80 -31
  51. contentctl/objects/macro.py +29 -45
  52. contentctl/objects/mitre_attack_enrichment.py +29 -5
  53. contentctl/objects/observable.py +3 -7
  54. contentctl/objects/playbook.py +60 -30
  55. contentctl/objects/playbook_tags.py +45 -8
  56. contentctl/objects/security_content_object.py +1 -5
  57. contentctl/objects/ssa_detection.py +8 -4
  58. contentctl/objects/ssa_detection_tags.py +19 -26
  59. contentctl/objects/story.py +142 -44
  60. contentctl/objects/story_tags.py +46 -33
  61. contentctl/objects/unit_test.py +7 -2
  62. contentctl/objects/unit_test_attack_data.py +10 -19
  63. contentctl/objects/unit_test_baseline.py +1 -1
  64. contentctl/objects/unit_test_old.py +4 -3
  65. contentctl/objects/unit_test_result.py +5 -3
  66. contentctl/objects/unit_test_ssa.py +31 -0
  67. contentctl/output/api_json_output.py +202 -130
  68. contentctl/output/attack_nav_output.py +20 -9
  69. contentctl/output/attack_nav_writer.py +3 -3
  70. contentctl/output/ba_yml_output.py +3 -3
  71. contentctl/output/conf_output.py +125 -391
  72. contentctl/output/conf_writer.py +169 -31
  73. contentctl/output/jinja_writer.py +2 -2
  74. contentctl/output/json_writer.py +17 -5
  75. contentctl/output/new_content_yml_output.py +8 -7
  76. contentctl/output/svg_output.py +17 -27
  77. contentctl/output/templates/analyticstories_detections.j2 +8 -4
  78. contentctl/output/templates/analyticstories_investigations.j2 +1 -1
  79. contentctl/output/templates/analyticstories_stories.j2 +6 -6
  80. contentctl/output/templates/app.conf.j2 +2 -2
  81. contentctl/output/templates/app.manifest.j2 +2 -2
  82. contentctl/output/templates/detection_coverage.j2 +6 -8
  83. contentctl/output/templates/doc_detection_page.j2 +2 -2
  84. contentctl/output/templates/doc_detections.j2 +2 -2
  85. contentctl/output/templates/doc_stories.j2 +1 -1
  86. contentctl/output/templates/es_investigations_investigations.j2 +1 -1
  87. contentctl/output/templates/es_investigations_stories.j2 +1 -1
  88. contentctl/output/templates/header.j2 +2 -1
  89. contentctl/output/templates/macros.j2 +6 -10
  90. contentctl/output/templates/savedsearches_baselines.j2 +5 -5
  91. contentctl/output/templates/savedsearches_detections.j2 +36 -33
  92. contentctl/output/templates/savedsearches_investigations.j2 +4 -4
  93. contentctl/output/templates/transforms.j2 +4 -4
  94. contentctl/output/yml_writer.py +2 -2
  95. contentctl/templates/app_template/README.md +7 -0
  96. contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/nav/default.xml +1 -0
  97. contentctl/templates/app_template/lookups/mitre_enrichment.csv +638 -0
  98. contentctl/templates/deployments/{00_default_anomaly.yml → escu_default_configuration_anomaly.yml} +1 -2
  99. contentctl/templates/deployments/{00_default_baseline.yml → escu_default_configuration_baseline.yml} +1 -2
  100. contentctl/templates/deployments/{00_default_correlation.yml → escu_default_configuration_correlation.yml} +2 -2
  101. contentctl/templates/deployments/{00_default_hunting.yml → escu_default_configuration_hunting.yml} +2 -2
  102. contentctl/templates/deployments/{00_default_ttp.yml → escu_default_configuration_ttp.yml} +1 -2
  103. contentctl/templates/detections/anomalous_usage_of_7zip.yml +0 -1
  104. contentctl/templates/stories/cobalt_strike.yml +0 -1
  105. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/METADATA +36 -15
  106. contentctl-4.0.2.dist-info/RECORD +168 -0
  107. contentctl/actions/detection_testing/DataManipulation.py +0 -149
  108. contentctl/actions/generate.py +0 -91
  109. contentctl/helper/config_handler.py +0 -75
  110. contentctl/input/baseline_builder.py +0 -66
  111. contentctl/input/basic_builder.py +0 -58
  112. contentctl/input/detection_builder.py +0 -370
  113. contentctl/input/investigation_builder.py +0 -42
  114. contentctl/input/new_content_generator.py +0 -95
  115. contentctl/input/playbook_builder.py +0 -68
  116. contentctl/input/story_builder.py +0 -106
  117. contentctl/objects/app.py +0 -214
  118. contentctl/objects/repo_config.py +0 -163
  119. contentctl/objects/test_config.py +0 -630
  120. contentctl/output/templates/macros_detections.j2 +0 -7
  121. contentctl/output/templates/splunk_app/README.md +0 -7
  122. contentctl-3.6.0.dist-info/RECORD +0 -176
  123. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_story_detail.txt +0 -0
  124. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_summary.txt +0 -0
  125. /contentctl/{output/templates/splunk_app → templates/app_template}/README/essoc_usage_dashboard.txt +0 -0
  126. /contentctl/{output/templates/splunk_app → templates/app_template}/default/analytic_stories.conf +0 -0
  127. /contentctl/{output/templates/splunk_app → templates/app_template}/default/app.conf +0 -0
  128. /contentctl/{output/templates/splunk_app → templates/app_template}/default/commands.conf +0 -0
  129. /contentctl/{output/templates/splunk_app → templates/app_template}/default/content-version.conf +0 -0
  130. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/escu_summary.xml +0 -0
  131. /contentctl/{output/templates/splunk_app → templates/app_template}/default/data/ui/views/feedback.xml +0 -0
  132. /contentctl/{output/templates/splunk_app → templates/app_template}/default/distsearch.conf +0 -0
  133. /contentctl/{output/templates/splunk_app → templates/app_template}/default/usage_searches.conf +0 -0
  134. /contentctl/{output/templates/splunk_app → templates/app_template}/default/use_case_library.conf +0 -0
  135. /contentctl/{output/templates/splunk_app → templates/app_template}/metadata/default.meta +0 -0
  136. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon.png +0 -0
  137. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt.png +0 -0
  138. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIconAlt_2x.png +0 -0
  139. /contentctl/{output/templates/splunk_app → templates/app_template}/static/appIcon_2x.png +0 -0
  140. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/LICENSE.md +0 -0
  141. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/WHEEL +0 -0
  142. {contentctl-3.6.0.dist-info → contentctl-4.0.2.dist-info}/entry_points.txt +0 -0
@@ -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, const=True)
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
@@ -1,10 +1,8 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
1
3
 
2
4
 
3
5
 
4
- from pydantic import BaseModel, validator, ValidationError
5
- from dataclasses import dataclass
6
-
7
-
8
6
  class DataSource(BaseModel):
9
7
  name: str
10
8
  id: str
@@ -1,30 +1,70 @@
1
-
2
- import uuid
3
- import string
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.deployment_email import DeploymentEmail
11
- from contentctl.objects.deployment_notable import DeploymentNotable
12
- from contentctl.objects.deployment_rba import DeploymentRBA
13
- from contentctl.objects.deployment_slack import DeploymentSlack
14
- from contentctl.objects.deployment_phantom import DeploymentPhantom
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 = None
24
- email: DeploymentEmail = None
25
- notable: DeploymentNotable = None
26
- rba: DeploymentRBA = None
27
- slack: DeploymentSlack = None
28
- phantom: DeploymentPhantom = None
29
- tags: dict = None
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,5 +1,5 @@
1
-
2
- from pydantic import BaseModel, validator, ValidationError
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
3
3
 
4
4
 
5
5
  class DeploymentEmail(BaseModel):
@@ -1,8 +1,8 @@
1
-
2
- from pydantic import BaseModel, validator, ValidationError
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: list
8
+ nes_fields: List[str]
@@ -1,5 +1,5 @@
1
-
2
- from pydantic import BaseModel, validator, ValidationError
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
3
3
 
4
4
 
5
5
  class DeploymentPhantom(BaseModel):
@@ -1,7 +1,6 @@
1
-
2
-
3
- from pydantic import BaseModel, validator, ValidationError
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
4
3
 
5
4
 
6
5
  class DeploymentRBA(BaseModel):
7
- enabled: str
6
+ enabled: bool = False
@@ -1,6 +1,5 @@
1
-
2
-
3
- from pydantic import BaseModel, validator, ValidationError
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
4
3
 
5
4
 
6
5
  class DeploymentScheduling(BaseModel):
@@ -1,5 +1,5 @@
1
-
2
- from pydantic import BaseModel, validator, ValidationError
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
3
3
 
4
4
 
5
5
  class DeploymentSlack(BaseModel):
@@ -1,10 +1,6 @@
1
- from typing import Union
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 re
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
- name: str
11
- analytic_story: list
12
- asset_type: str
13
- automated_detection_testing: str = None
14
- cis20: list = None
15
- confidence: str
16
- impact: int
17
- kill_chain_phases: list = None
18
- mitre_attack_id: list = None
19
- nist: list = None
20
- observable: list[Observable] = []
21
- message: str
22
- product: list
23
- required_fields: list
24
- risk_score: int
25
- security_domain: str
26
- risk_severity: str = None
27
- cve: list = None
28
- supported_tas: list = None
29
- atomic_guid: list = None
30
- drilldown_search: str = None
31
- manual_test: str = None
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: list[MitreAttackEnrichment] = []
36
- confidence_id: int = None
37
- impact_id: int = None
38
- context_ids: list = None
39
- risk_level_id: int = None
40
- risk_level: str = None
41
- observable_str: str = None
42
- evidence_str: str = None
43
- kill_chain_phases_id: list = None
44
- research_site_url: str = None
45
- event_schema: str = None
46
- mappings: list = None
47
- annotations: dict = None
48
-
49
-
50
- @validator('cis20')
51
- def tags_cis20(cls, v, values):
52
- pattern = '^CIS ([0-9]|1[0-9]|20)$' #DO NOT match leading zeroes and ensure no extra characters before or after the string
53
- for value in v:
54
- if not re.match(pattern, value):
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
- @validator('nist')
59
- def tags_nist(cls, v, values):
60
- # Sourced Courtest of NIST: https://www.nist.gov/system/files/documents/cyberframework/cybersecurity-framework-021214.pdf (Page 19)
61
- IDENTIFY = [f'ID.{category}' for category in ["AM", "BE", "GV", "RA", "RM"] ]
62
- PROTECT = [f'PR.{category}' for category in ["AC", "AT", "DS", "IP", "MA", "PT"]]
63
- DETECT = [f'DE.{category}' for category in ["AE", "CM", "DP"] ]
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 v
96
-
97
- @validator('kill_chain_phases')
98
- def tags_kill_chain_phases(cls, v, values):
99
- valid_kill_chain_phases = SES_KILL_CHAIN_MAPPINGS.keys()
100
- for value in v:
101
- if value not in valid_kill_chain_phases:
102
- raise ValueError('kill chain phase not valid for ' + values["name"] + '. valid options are ' + str(valid_kill_chain_phases))
103
- return v
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