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
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
-
3
- from pydantic import BaseModel, validator, ValidationError
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
- from contentctl.objects.enums import SecurityContentType
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(BaseModel):
22
- #contentType: SecurityContentType = SecurityContentType.lookups
23
- name: str
24
- description: str
25
- collection: str = None
26
- fields_list: str = None
27
- filename: str = None
28
- default_match: str = None
29
- match_type: str = None
30
- min_matches: int = None
31
- case_sensitive_match: str = None
32
- file_path:str = None
33
-
34
- # Macro can have different punctuatuation in it,
35
- # so redefine the name validator. For now, jsut
36
- # allow any characters in the macro
37
- @validator('name',check_fields=False)
38
- def name_invalid_chars(cls, v):
39
- return v
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, all_lookups: list[Lookup], ignore_lookups:set[str]=LOOKUPS_TO_IGNORE)->Tuple[list[Lookup], set[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
- found_lookups, missing_lookups = SecurityContentObject.get_objects_by_name(lookups_to_get, all_lookups)
55
- return found_lookups, missing_lookups
104
+ return Lookup.mapNamesToSecurityContentObjects(list(lookups_to_get), director)
56
105
 
@@ -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 BaseModel, validator, ValidationError
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
- from contentctl.objects.enums import SecurityContentType
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(BaseModel):
21
- #contentType: SecurityContentType = SecurityContentType.macros
22
- name: str
23
- definition: str
24
- description: str
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
- # Allow long names for macros
37
- @validator('name',check_fields=False)
38
- def name_max_length(cls, v):
39
- #if len(v) > 67:
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
- def get_macros(text_field:str, all_macros: list[Macro], ignore_macros:set[str]=MACROS_TO_IGNORE)->Tuple[list[Macro], set[str]]:
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
- found_macros, missing_macros = SecurityContentObject.get_objects_by_name(macros_to_get, all_macros)
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 pydantic import BaseModel, validator, ValidationError
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
- mitre_attack_id: str
6
- mitre_attack_technique: str
7
- mitre_attack_tactics: list
8
- mitre_attack_groups: list
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)
@@ -1,10 +1,6 @@
1
- import abc
2
- import string
3
- import uuid
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
 
@@ -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.helper.link_validator import LinkValidator
10
- from contentctl.objects.enums import SecurityContentType
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
- #name: str
15
- #id: str
16
- #version: int
17
- #date: str
18
- #author: str
19
- #contentType: SecurityContentType = SecurityContentType.playbooks
20
- type: str
21
- #description: str
22
- how_to_implement: str
23
- playbook: str
24
- check_references: bool = False #Validation is done in order, this field must be defined first
25
- references: list
26
- app_list: list
27
- tags: PlaybookTag
28
-
29
-
30
- @validator('references')
31
- def references_check(cls, v, values):
32
- return LinkValidator.SecurityContentObject_validate_references(v, values)
33
-
34
- @validator('how_to_implement')
35
- def encode_error(cls, v, values, field):
36
- return SecurityContentObject.free_text_field_valid(cls,v,values,field)
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 = None
9
- playbook_fields: list = None
10
- product: list = None
11
- playbook_fields: list = None
12
- detection_objects: list = None
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 abc
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.config import ConfigDetectionConfiguration
18
- from contentctl.objects.unit_test import UnitTest
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[UnitTest] = None
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[UnitTest, dict] = None
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: list
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: ' + values["name"])
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: ' + values["name"])
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 for ' + values["name"] + '. valid options are ' + str(valid_kill_chain_phases))
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: ' + values["name"])
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
- @root_validator
129
- def tags_observable(cls, values):
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 values["observable"]:
126
+ for value in self.observable:
134
127
  if value['type'] in valid_types:
135
- if 'Splunk Behavioral Analytics' in values["product"]:
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 for ' + values["name"])
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 for ' + values["name"] + '. valid options are ' + str(valid_roles))
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 for ' + values["name"] + '. valid options are ' + str(valid_types))
145
- return values
137
+ raise ValueError(f'Observable type ' + value['type'] + ' not valid. Valid options are {str(valid_types)}')
138
+ return self