contentctl 5.0.0a2__py3-none-any.whl → 5.0.0a3__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 (106) hide show
  1. contentctl/__init__.py +1 -1
  2. contentctl/actions/build.py +88 -55
  3. contentctl/actions/deploy_acs.py +29 -24
  4. contentctl/actions/detection_testing/DetectionTestingManager.py +66 -41
  5. contentctl/actions/detection_testing/GitService.py +2 -4
  6. contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
  7. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +163 -124
  8. contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
  9. contentctl/actions/detection_testing/progress_bar.py +3 -0
  10. contentctl/actions/detection_testing/views/DetectionTestingView.py +15 -18
  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 +78 -50
  18. contentctl/actions/release_notes.py +276 -146
  19. contentctl/actions/reporting.py +23 -19
  20. contentctl/actions/test.py +31 -25
  21. contentctl/actions/validate.py +54 -34
  22. contentctl/api.py +54 -45
  23. contentctl/contentctl.py +10 -10
  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 -39
  30. contentctl/input/director.py +69 -37
  31. contentctl/input/new_content_questions.py +26 -34
  32. contentctl/input/yml_reader.py +22 -17
  33. contentctl/objects/abstract_security_content_objects/detection_abstract.py +250 -314
  34. contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +58 -36
  35. contentctl/objects/alert_action.py +8 -8
  36. contentctl/objects/annotated_types.py +1 -1
  37. contentctl/objects/atomic.py +64 -54
  38. contentctl/objects/base_test.py +2 -1
  39. contentctl/objects/base_test_result.py +16 -8
  40. contentctl/objects/baseline.py +41 -30
  41. contentctl/objects/baseline_tags.py +29 -22
  42. contentctl/objects/config.py +1 -1
  43. contentctl/objects/constants.py +29 -58
  44. contentctl/objects/correlation_search.py +75 -55
  45. contentctl/objects/dashboard.py +55 -41
  46. contentctl/objects/data_source.py +13 -13
  47. contentctl/objects/deployment.py +44 -37
  48. contentctl/objects/deployment_email.py +1 -1
  49. contentctl/objects/deployment_notable.py +2 -1
  50. contentctl/objects/deployment_phantom.py +5 -5
  51. contentctl/objects/deployment_rba.py +1 -1
  52. contentctl/objects/deployment_scheduling.py +1 -1
  53. contentctl/objects/deployment_slack.py +1 -1
  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 +54 -64
  58. contentctl/objects/drilldown.py +66 -35
  59. contentctl/objects/enums.py +61 -43
  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 +41 -26
  64. contentctl/objects/investigation_tags.py +29 -17
  65. contentctl/objects/lookup.py +234 -113
  66. contentctl/objects/macro.py +55 -38
  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 +22 -16
  74. contentctl/objects/rba.py +14 -8
  75. contentctl/objects/risk_analysis_action.py +15 -11
  76. contentctl/objects/risk_event.py +27 -20
  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 +45 -44
  81. contentctl/objects/story_tags.py +56 -44
  82. contentctl/objects/test_group.py +5 -2
  83. contentctl/objects/threat_object.py +1 -0
  84. contentctl/objects/throttling.py +27 -18
  85. contentctl/objects/unit_test.py +3 -4
  86. contentctl/objects/unit_test_baseline.py +4 -5
  87. contentctl/objects/unit_test_result.py +6 -6
  88. contentctl/output/api_json_output.py +22 -22
  89. contentctl/output/attack_nav_output.py +21 -21
  90. contentctl/output/attack_nav_writer.py +29 -37
  91. contentctl/output/conf_output.py +230 -174
  92. contentctl/output/data_source_writer.py +38 -25
  93. contentctl/output/doc_md_output.py +53 -27
  94. contentctl/output/jinja_writer.py +19 -15
  95. contentctl/output/json_writer.py +20 -8
  96. contentctl/output/svg_output.py +56 -38
  97. contentctl/output/templates/transforms.j2 +2 -2
  98. contentctl/output/yml_writer.py +18 -24
  99. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
  100. contentctl-5.0.0a3.dist-info/RECORD +168 -0
  101. contentctl/actions/initialize_old.py +0 -245
  102. contentctl/objects/observable.py +0 -39
  103. contentctl-5.0.0a2.dist-info/RECORD +0 -170
  104. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
  105. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
  106. {contentctl-5.0.0a2.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,16 @@
1
-
2
1
  from __future__ import annotations
3
- from typing import Annotated, List,Any, TYPE_CHECKING
2
+ from typing import Annotated, List, Any, TYPE_CHECKING
3
+
4
4
  if TYPE_CHECKING:
5
5
  from contentctl.input.director import DirectorOutputDto
6
6
 
7
- from pydantic import field_validator, ValidationInfo, Field, model_serializer, computed_field
7
+ from pydantic import (
8
+ field_validator,
9
+ ValidationInfo,
10
+ Field,
11
+ model_serializer,
12
+ computed_field,
13
+ )
8
14
  from contentctl.objects.deployment import Deployment
9
15
  from contentctl.objects.security_content_object import SecurityContentObject
10
16
  from contentctl.objects.enums import DataModel
@@ -13,11 +19,15 @@ from contentctl.objects.baseline_tags import BaselineTags
13
19
  from contentctl.objects.config import CustomApp
14
20
 
15
21
  from contentctl.objects.lookup import Lookup
16
- from contentctl.objects.constants import CONTENTCTL_MAX_SEARCH_NAME_LENGTH,CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE
22
+ from contentctl.objects.constants import (
23
+ CONTENTCTL_MAX_SEARCH_NAME_LENGTH,
24
+ CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE,
25
+ )
26
+
17
27
 
18
28
  class Baseline(SecurityContentObject):
19
- name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
20
- type: Annotated[str,Field(pattern="^Baseline$")] = Field(...)
29
+ name: str = Field(..., max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
30
+ type: Annotated[str, Field(pattern="^Baseline$")] = Field(...)
21
31
  search: str = Field(..., min_length=4)
22
32
  how_to_implement: str = Field(..., min_length=4)
23
33
  known_false_positives: str = Field(..., min_length=4)
@@ -26,30 +36,31 @@ class Baseline(SecurityContentObject):
26
36
  # enrichment
27
37
  deployment: Deployment = Field({})
28
38
 
29
-
30
- @field_validator('lookups', mode="before")
39
+ @field_validator("lookups", mode="before")
31
40
  @classmethod
32
- def getBaselineLookups(cls, v:list[str], info:ValidationInfo) -> list[Lookup]:
33
- '''
41
+ def getBaselineLookups(cls, v: list[str], info: ValidationInfo) -> list[Lookup]:
42
+ """
34
43
  This function has been copied and renamed from the Detection_Abstract class
35
- '''
36
- director:DirectorOutputDto = info.context.get("output_dto",None)
37
- search: str | None = info.data.get("search",None)
44
+ """
45
+ director: DirectorOutputDto = info.context.get("output_dto", None)
46
+ search: str | None = info.data.get("search", None)
38
47
  if search is None:
39
48
  raise ValueError("Search was None - is this file missing the search field?")
40
-
49
+
41
50
  lookups = Lookup.get_lookups(search, director)
42
51
  return lookups
43
52
 
44
- def get_conf_stanza_name(self, app:CustomApp)->str:
45
- stanza_name = CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
53
+ def get_conf_stanza_name(self, app: CustomApp) -> str:
54
+ stanza_name = CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE.format(
55
+ app_label=app.label, detection_name=self.name
56
+ )
46
57
  self.check_conf_stanza_max_length(stanza_name)
47
58
  return stanza_name
48
59
 
49
60
  @field_validator("deployment", mode="before")
50
- def getDeployment(cls, v:Any, info:ValidationInfo)->Deployment:
51
- return Deployment.getDeployment(v,info)
52
-
61
+ def getDeployment(cls, v: Any, info: ValidationInfo) -> Deployment:
62
+ return Deployment.getDeployment(v, info)
63
+
53
64
  @computed_field
54
65
  @property
55
66
  def datamodel(self) -> List[DataModel]:
@@ -57,21 +68,21 @@ class Baseline(SecurityContentObject):
57
68
 
58
69
  @model_serializer
59
70
  def serialize_model(self):
60
- #Call serializer for parent
71
+ # Call serializer for parent
61
72
  super_fields = super().serialize_model()
62
-
63
- #All fields custom to this model
64
- model= {
73
+
74
+ # All fields custom to this model
75
+ model = {
65
76
  "tags": self.tags.model_dump(),
66
77
  "type": self.type,
67
78
  "search": self.search,
68
- "how_to_implement":self.how_to_implement,
69
- "known_false_positives":self.known_false_positives,
79
+ "how_to_implement": self.how_to_implement,
80
+ "known_false_positives": self.known_false_positives,
70
81
  "datamodel": self.datamodel,
71
82
  }
72
-
73
- #Combine fields from this model with fields from parent
83
+
84
+ # Combine fields from this model with fields from parent
74
85
  super_fields.update(model)
75
-
76
- #return the model
77
- return super_fields
86
+
87
+ # return the model
88
+ return super_fields
@@ -1,5 +1,12 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, Field, field_validator, ValidationInfo, model_serializer, ConfigDict
2
+ from pydantic import (
3
+ BaseModel,
4
+ Field,
5
+ field_validator,
6
+ ValidationInfo,
7
+ model_serializer,
8
+ ConfigDict,
9
+ )
3
10
  from typing import List, Any, Union
4
11
 
5
12
  from contentctl.objects.story import Story
@@ -8,35 +15,35 @@ from contentctl.objects.enums import SecurityContentProductName
8
15
  from contentctl.objects.enums import SecurityDomain
9
16
 
10
17
 
11
-
12
-
13
-
14
18
  class BaselineTags(BaseModel):
15
19
  model_config = ConfigDict(extra="forbid")
16
20
  analytic_story: list[Story] = Field(...)
17
- #deployment: Deployment = Field('SET_IN_GET_DEPLOYMENT_FUNCTION')
21
+ # deployment: Deployment = Field('SET_IN_GET_DEPLOYMENT_FUNCTION')
18
22
  # TODO (#223): can we remove str from the possible types here?
19
- detections: List[Union[Detection,str]] = Field(...)
20
- product: List[SecurityContentProductName] = Field(...,min_length=1)
23
+ detections: List[Union[Detection, str]] = Field(...)
24
+ product: List[SecurityContentProductName] = Field(..., min_length=1)
21
25
  security_domain: SecurityDomain = Field(...)
22
26
 
27
+ @field_validator("analytic_story", mode="before")
28
+ def getStories(cls, v: Any, info: ValidationInfo) -> List[Story]:
29
+ return Story.mapNamesToSecurityContentObjects(
30
+ v, info.context.get("output_dto", None)
31
+ )
23
32
 
24
- @field_validator("analytic_story",mode="before")
25
- def getStories(cls, v:Any, info:ValidationInfo)->List[Story]:
26
- return Story.mapNamesToSecurityContentObjects(v, info.context.get("output_dto",None))
27
-
28
-
29
33
  @model_serializer
30
- def serialize_model(self):
31
- #All fields custom to this model
32
- model= {
34
+ def serialize_model(self):
35
+ # All fields custom to this model
36
+ model = {
33
37
  "analytic_story": [story.name for story in self.analytic_story],
34
- "detections": [detection.name for detection in self.detections if isinstance(detection,Detection)],
38
+ "detections": [
39
+ detection.name
40
+ for detection in self.detections
41
+ if isinstance(detection, Detection)
42
+ ],
35
43
  "product": self.product,
36
- "security_domain":self.security_domain,
37
- "deployments": None
44
+ "security_domain": self.security_domain,
45
+ "deployments": None,
38
46
  }
39
-
40
-
41
- #return the model
42
- return model
47
+
48
+ # return the model
49
+ return model
@@ -1162,7 +1162,7 @@ class release_notes(Config_Base):
1162
1162
  p = self.path / "dist"
1163
1163
  try:
1164
1164
  p.mkdir(exist_ok=True, parents=True)
1165
- except Exception:
1165
+ except Exception as e:
1166
1166
  raise Exception(
1167
1167
  f"Error making the directory '{p}' to hold release_notes: {str(e)}"
1168
1168
  )
@@ -15,7 +15,7 @@ ATTACK_TACTICS_KILLCHAIN_MAPPING = {
15
15
  "Collection": "Exploitation",
16
16
  "Command And Control": "Command and Control",
17
17
  "Exfiltration": "Actions on Objectives",
18
- "Impact": "Actions on Objectives"
18
+ "Impact": "Actions on Objectives",
19
19
  }
20
20
 
21
21
  SES_CONTEXT_MAPPING = {
@@ -65,7 +65,7 @@ SES_CONTEXT_MAPPING = {
65
65
  "Other:Policy Violation": 82,
66
66
  "Other:Threat Intelligence": 83,
67
67
  "Other:Flight Risk": 84,
68
- "Other:Removable Storage": 85
68
+ "Other:Removable Storage": 85,
69
69
  }
70
70
 
71
71
  SES_KILL_CHAIN_MAPPINGS = {
@@ -76,49 +76,9 @@ SES_KILL_CHAIN_MAPPINGS = {
76
76
  "Exploitation": 4,
77
77
  "Installation": 5,
78
78
  "Command and Control": 6,
79
- "Actions on Objectives": 7
79
+ "Actions on Objectives": 7,
80
80
  }
81
81
 
82
- # TODO (cmcginley): @ljstella should this be removed? also referenced in new_content.py
83
- SES_OBSERVABLE_ROLE_MAPPING = {
84
- "Other": -1,
85
- "Unknown": 0,
86
- "Actor": 1,
87
- "Target": 2,
88
- "Attacker": 3,
89
- "Victim": 4,
90
- "Parent Process": 5,
91
- "Child Process": 6,
92
- "Known Bad": 7,
93
- "Data Loss": 8,
94
- "Observer": 9
95
- }
96
-
97
- # TODO (cmcginley): @ljstella should this be removed? also referenced in new_content.py
98
- SES_OBSERVABLE_TYPE_MAPPING = {
99
- "Unknown": 0,
100
- "Hostname": 1,
101
- "IP Address": 2,
102
- "MAC Address": 3,
103
- "User Name": 4,
104
- "Email Address": 5,
105
- "URL String": 6,
106
- "File Name": 7,
107
- "File Hash": 8,
108
- "Process Name": 9,
109
- "Resource UID": 10,
110
- "Endpoint": 20,
111
- "User": 21,
112
- "Email": 22,
113
- "Uniform Resource Locator": 23,
114
- "File": 24,
115
- "Process": 25,
116
- "Geo Location": 26,
117
- "Container": 27,
118
- "Registry Key": 28,
119
- "Registry Value": 29,
120
- "Other": 99
121
- }
122
82
 
123
83
  SES_ATTACK_TACTICS_ID_MAPPING = {
124
84
  "Reconnaissance": "TA0043",
@@ -134,24 +94,19 @@ SES_ATTACK_TACTICS_ID_MAPPING = {
134
94
  "Collection": "TA0009",
135
95
  "Command_and_Control": "TA0011",
136
96
  "Exfiltration": "TA0010",
137
- "Impact": "TA0040"
97
+ "Impact": "TA0040",
138
98
  }
139
99
 
140
- # TODO (cmcginley): is this just for the transition testing?
141
- RBA_OBSERVABLE_ROLE_MAPPING = {
142
- "Attacker": 0,
143
- "Victim": 1
144
- }
145
100
 
146
101
  # The relative path to the directory where any apps/packages will be downloaded
147
102
  DOWNLOADS_DIRECTORY = "downloads"
148
103
 
149
104
  # Maximum length of the name field for a search.
150
- # This number is derived from a limitation that exists in
105
+ # This number is derived from a limitation that exists in
151
106
  # ESCU where a search cannot be edited, due to validation
152
107
  # errors, if its name is longer than 99 characters.
153
108
  # When an saved search is cloned in Enterprise Security User Interface,
154
- # it is wrapped in the following:
109
+ # it is wrapped in the following:
155
110
  # {Detection.tags.security_domain} - {SEARCH_STANZA_NAME} - Rule
156
111
  # Similarly, when we generate the search stanza name in contentctl, it
157
112
  # is app.label - detection.name - Rule
@@ -160,16 +115,32 @@ DOWNLOADS_DIRECTORY = "downloads"
160
115
  # or in ESCU:
161
116
  # ESCU - {detection.name} - Rule,
162
117
  # this gives us a maximum length below.
163
- # When an ESCU search is cloned, it will
118
+ # When an ESCU search is cloned, it will
164
119
  # have a full name like (the following is NOT a typo):
165
120
  # Endpoint - ESCU - Name of Search From YML File - Rule - Rule
166
121
  # The math below accounts for all these caveats
167
122
  ES_MAX_STANZA_LENGTH = 99
168
- CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name} - Rule"
123
+ CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE = (
124
+ "{app_label} - {detection_name} - Rule"
125
+ )
169
126
  CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name}"
170
- CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name} - Response Task"
127
+ CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE = (
128
+ "{app_label} - {detection_name} - Response Task"
129
+ )
171
130
 
172
- ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE = "{security_domain_value} - {search_name} - Rule"
173
- SECURITY_DOMAIN_MAX_LENGTH = max([len(SecurityDomain[value]) for value in SecurityDomain._member_map_])
174
- CONTENTCTL_MAX_STANZA_LENGTH = ES_MAX_STANZA_LENGTH - len(ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(security_domain_value="X"*SECURITY_DOMAIN_MAX_LENGTH,search_name=""))
175
- CONTENTCTL_MAX_SEARCH_NAME_LENGTH = CONTENTCTL_MAX_STANZA_LENGTH - len(CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE.format(app_label="ESCU", detection_name=""))
131
+ ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE = (
132
+ "{security_domain_value} - {search_name} - Rule"
133
+ )
134
+ SECURITY_DOMAIN_MAX_LENGTH = max(
135
+ [len(SecurityDomain[value]) for value in SecurityDomain._member_map_]
136
+ )
137
+ CONTENTCTL_MAX_STANZA_LENGTH = ES_MAX_STANZA_LENGTH - len(
138
+ ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(
139
+ security_domain_value="X" * SECURITY_DOMAIN_MAX_LENGTH, search_name=""
140
+ )
141
+ )
142
+ CONTENTCTL_MAX_SEARCH_NAME_LENGTH = CONTENTCTL_MAX_STANZA_LENGTH - len(
143
+ CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE.format(
144
+ app_label="ESCU", detection_name=""
145
+ )
146
+ )