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
@@ -1,8 +1,8 @@
1
1
  from typing import Union, Any
2
- from enum import Enum
2
+ from enum import StrEnum
3
3
 
4
4
  from pydantic import ConfigDict, BaseModel
5
- from splunklib.data import Record # type: ignore
5
+ from splunklib.data import Record # type: ignore
6
6
 
7
7
  from contentctl.helper.utils import Utils
8
8
 
@@ -10,8 +10,9 @@ from contentctl.helper.utils import Utils
10
10
  # TODO (#267): Align test reporting more closely w/ status enums (as it relates to "untested")
11
11
  # TODO (PEX-432): add status "UNSET" so that we can make sure the result is always of this enum
12
12
  # type; remove mypy ignores associated w/ these typing issues once we do
13
- class TestResultStatus(str, Enum):
13
+ class TestResultStatus(StrEnum):
14
14
  """Enum for test status (e.g. pass/fail)"""
15
+
15
16
  # Test failed (detection did NOT fire appropriately)
16
17
  FAIL = "fail"
17
18
 
@@ -35,6 +36,7 @@ class BaseTestResult(BaseModel):
35
36
  """
36
37
  Base class for test results
37
38
  """
39
+
38
40
  # Message for the result
39
41
  message: Union[None, str] = None
40
42
 
@@ -54,10 +56,7 @@ class BaseTestResult(BaseModel):
54
56
  sid_link: Union[None, str] = None
55
57
 
56
58
  # Needed to allow for embedding of Exceptions in the model
57
- model_config = ConfigDict(
58
- validate_assignment=True,
59
- arbitrary_types_allowed=True
60
- )
59
+ model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)
61
60
 
62
61
  @property
63
62
  def passed(self) -> bool:
@@ -81,7 +80,10 @@ class BaseTestResult(BaseModel):
81
80
  Property returning True if status is FAIL or ERROR; False otherwise (PASS, SKIP)
82
81
  :returns: bool indicating fialure if True
83
82
  """
84
- return self.status == TestResultStatus.FAIL or self.status == TestResultStatus.ERROR
83
+ return (
84
+ self.status == TestResultStatus.FAIL
85
+ or self.status == TestResultStatus.ERROR
86
+ )
85
87
 
86
88
  @property
87
89
  def complete(self) -> bool:
@@ -94,7 +96,13 @@ class BaseTestResult(BaseModel):
94
96
  def get_summary_dict(
95
97
  self,
96
98
  model_fields: list[str] = [
97
- "success", "exception", "message", "sid_link", "status", "duration", "wait_duration"
99
+ "success",
100
+ "exception",
101
+ "message",
102
+ "sid_link",
103
+ "status",
104
+ "duration",
105
+ "wait_duration",
98
106
  ],
99
107
  job_fields: list[str] = ["search", "resultCount", "runDuration"],
100
108
  ) -> dict[str, Any]:
@@ -113,7 +121,7 @@ class BaseTestResult(BaseModel):
113
121
  # Exceptions and enums cannot be serialized, so convert to str
114
122
  if isinstance(getattr(self, field), Exception):
115
123
  summary_dict[field] = str(getattr(self, field))
116
- elif isinstance(getattr(self, field), Enum):
124
+ elif isinstance(getattr(self, field), StrEnum):
117
125
  summary_dict[field] = str(getattr(self, field))
118
126
  else:
119
127
  summary_dict[field] = getattr(self, field)
@@ -125,7 +133,7 @@ class BaseTestResult(BaseModel):
125
133
  # Grab the job content fields required
126
134
  for field in job_fields:
127
135
  if self.job_content is not None:
128
- value: Any = self.job_content.get(field, None) # type: ignore
136
+ value: Any = self.job_content.get(field, None) # type: ignore
129
137
 
130
138
  # convert runDuration to a fixed width string representation of a float
131
139
  if field == "runDuration":
@@ -1,57 +1,89 @@
1
-
2
1
  from __future__ import annotations
3
- from typing import Annotated, Optional, List,Any
4
- from pydantic import field_validator, ValidationInfo, Field, model_serializer
5
- from contentctl.objects.deployment import Deployment
6
- from contentctl.objects.security_content_object import SecurityContentObject
7
- from contentctl.objects.enums import DataModel
8
- from contentctl.objects.baseline_tags import BaselineTags
9
2
 
10
- from contentctl.objects.config import CustomApp
3
+ from typing import TYPE_CHECKING, Annotated, Any, List, Literal
11
4
 
5
+ if TYPE_CHECKING:
6
+ from contentctl.input.director import DirectorOutputDto
7
+
8
+ from pydantic import (
9
+ Field,
10
+ ValidationInfo,
11
+ computed_field,
12
+ field_validator,
13
+ model_serializer,
14
+ )
15
+
16
+ from contentctl.objects.baseline_tags import BaselineTags
17
+ from contentctl.objects.config import CustomApp
18
+ from contentctl.objects.constants import (
19
+ CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE,
20
+ CONTENTCTL_MAX_SEARCH_NAME_LENGTH,
21
+ )
22
+ from contentctl.objects.deployment import Deployment
23
+ from contentctl.objects.enums import DataModel, DetectionStatus
24
+ from contentctl.objects.lookup import Lookup
25
+ from contentctl.objects.security_content_object import SecurityContentObject
12
26
 
13
- from contentctl.objects.constants import CONTENTCTL_MAX_SEARCH_NAME_LENGTH,CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE
14
27
 
15
28
  class Baseline(SecurityContentObject):
16
- name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
17
- type: Annotated[str,Field(pattern="^Baseline$")] = Field(...)
18
- datamodel: Optional[List[DataModel]] = None
29
+ name: str = Field(..., max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
30
+ type: Annotated[str, Field(pattern="^Baseline$")] = Field(...)
19
31
  search: str = Field(..., min_length=4)
20
32
  how_to_implement: str = Field(..., min_length=4)
21
33
  known_false_positives: str = Field(..., min_length=4)
22
34
  tags: BaselineTags = Field(...)
23
-
35
+ lookups: list[Lookup] = Field([], validate_default=True)
24
36
  # enrichment
25
37
  deployment: Deployment = Field({})
26
-
38
+ status: Literal[DetectionStatus.production, DetectionStatus.deprecated]
39
+
40
+ @field_validator("lookups", mode="before")
41
+ @classmethod
42
+ def getBaselineLookups(cls, v: list[str], info: ValidationInfo) -> list[Lookup]:
43
+ """
44
+ This function has been copied and renamed from the Detection_Abstract class
45
+ """
46
+ director: DirectorOutputDto = info.context.get("output_dto", None)
47
+ search: str | None = info.data.get("search", None)
48
+ if search is None:
49
+ raise ValueError("Search was None - is this file missing the search field?")
50
+
51
+ lookups = Lookup.get_lookups(search, director)
52
+ return lookups
27
53
 
28
- def get_conf_stanza_name(self, app:CustomApp)->str:
29
- stanza_name = CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
54
+ def get_conf_stanza_name(self, app: CustomApp) -> str:
55
+ stanza_name = CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE.format(
56
+ app_label=app.label, detection_name=self.name
57
+ )
30
58
  self.check_conf_stanza_max_length(stanza_name)
31
59
  return stanza_name
32
60
 
33
61
  @field_validator("deployment", mode="before")
34
- def getDeployment(cls, v:Any, info:ValidationInfo)->Deployment:
35
- return Deployment.getDeployment(v,info)
36
-
62
+ def getDeployment(cls, v: Any, info: ValidationInfo) -> Deployment:
63
+ return Deployment.getDeployment(v, info)
64
+
65
+ @computed_field
66
+ @property
67
+ def datamodel(self) -> List[DataModel]:
68
+ return [dm for dm in DataModel if dm in self.search]
37
69
 
38
70
  @model_serializer
39
71
  def serialize_model(self):
40
- #Call serializer for parent
72
+ # Call serializer for parent
41
73
  super_fields = super().serialize_model()
42
-
43
- #All fields custom to this model
44
- model= {
74
+
75
+ # All fields custom to this model
76
+ model = {
45
77
  "tags": self.tags.model_dump(),
46
78
  "type": self.type,
47
79
  "search": self.search,
48
- "how_to_implement":self.how_to_implement,
49
- "known_false_positives":self.known_false_positives,
80
+ "how_to_implement": self.how_to_implement,
81
+ "known_false_positives": self.known_false_positives,
50
82
  "datamodel": self.datamodel,
51
83
  }
52
-
53
- #Combine fields from this model with fields from parent
84
+
85
+ # Combine fields from this model with fields from parent
54
86
  super_fields.update(model)
55
-
56
- #return the model
57
- return super_fields
87
+
88
+ # return the model
89
+ return super_fields
@@ -1,5 +1,12 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, Field, field_validator, ValidationInfo, model_serializer
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,36 +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):
19
+ model_config = ConfigDict(extra="forbid")
15
20
  analytic_story: list[Story] = Field(...)
16
- #deployment: Deployment = Field('SET_IN_GET_DEPLOYMENT_FUNCTION')
21
+ # deployment: Deployment = Field('SET_IN_GET_DEPLOYMENT_FUNCTION')
17
22
  # TODO (#223): can we remove str from the possible types here?
18
- detections: List[Union[Detection,str]] = Field(...)
19
- product: List[SecurityContentProductName] = Field(...,min_length=1)
20
- required_fields: List[str] = 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
- "required_fields":self.required_fields,
37
- "security_domain":self.security_domain,
38
- "deployments": None
44
+ "security_domain": self.security_domain,
45
+ "deployments": None,
39
46
  }
40
-
41
-
42
- #return the model
43
- return model
47
+
48
+ # return the model
49
+ return model