contentctl 5.0.0a0__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 (107) 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 +134 -76
  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 +12 -13
  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 +772 -560
  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/savedsearches_detections.j2 +1 -1
  98. contentctl/output/templates/transforms.j2 +2 -2
  99. contentctl/output/yml_writer.py +18 -24
  100. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/METADATA +1 -1
  101. contentctl-5.0.0a3.dist-info/RECORD +168 -0
  102. contentctl/actions/initialize_old.py +0 -245
  103. contentctl/objects/observable.py +0 -39
  104. contentctl-5.0.0a0.dist-info/RECORD +0 -170
  105. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/LICENSE.md +0 -0
  106. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/WHEEL +0 -0
  107. {contentctl-5.0.0a0.dist-info → contentctl-5.0.0a3.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING,List
2
+ from typing import TYPE_CHECKING, List
3
3
  from contentctl.objects.story_tags import StoryTags
4
- from pydantic import Field, model_serializer,computed_field, model_validator
4
+ from pydantic import Field, model_serializer, computed_field, model_validator
5
5
  import re
6
+
6
7
  if TYPE_CHECKING:
7
8
  from contentctl.objects.detection import Detection
8
9
  from contentctl.objects.investigation import Investigation
@@ -12,88 +13,90 @@ if TYPE_CHECKING:
12
13
 
13
14
  from contentctl.objects.security_content_object import SecurityContentObject
14
15
 
16
+
15
17
  class Story(SecurityContentObject):
16
18
  narrative: str = Field(...)
17
19
  tags: StoryTags = Field(...)
18
20
 
19
21
  # These are updated when detection and investigation objects are created.
20
22
  # Specifically in the model_post_init functions
21
- detections:List[Detection] = []
23
+ detections: List[Detection] = []
22
24
  investigations: List[Investigation] = []
23
25
  baselines: List[Baseline] = []
24
-
25
-
26
+
26
27
  @computed_field
27
28
  @property
28
- def data_sources(self)-> list[DataSource]:
29
+ def data_sources(self) -> list[DataSource]:
29
30
  # Only add a data_source if it does not already exist in the story
30
- data_source_objects:set[DataSource] = set()
31
+ data_source_objects: set[DataSource] = set()
31
32
  for detection in self.detections:
32
33
  data_source_objects.update(set(detection.data_source_objects))
33
-
34
+
34
35
  return sorted(list(data_source_objects))
35
36
 
37
+ def storyAndInvestigationNamesWithApp(self, app: CustomApp) -> List[str]:
38
+ return [
39
+ detection.get_conf_stanza_name(app) for detection in self.detections
40
+ ] + [
41
+ investigation.get_response_task_name(app)
42
+ for investigation in self.investigations
43
+ ]
36
44
 
37
- def storyAndInvestigationNamesWithApp(self, app:CustomApp)->List[str]:
38
- return [detection.get_conf_stanza_name(app) for detection in self.detections] + \
39
- [investigation.get_response_task_name(app) for investigation in self.investigations]
40
-
41
45
  @model_serializer
42
46
  def serialize_model(self):
43
- #Call serializer for parent
47
+ # Call serializer for parent
44
48
  super_fields = super().serialize_model()
45
-
46
- #All fields custom to this model
47
- model= {
49
+
50
+ # All fields custom to this model
51
+ model = {
48
52
  "narrative": self.narrative,
49
53
  "tags": self.tags.model_dump(),
50
54
  "detection_names": self.detection_names,
51
55
  "investigation_names": self.investigation_names,
52
56
  "baseline_names": self.baseline_names,
53
57
  "author_company": self.author_company,
54
- "author_name":self.author_name
58
+ "author_name": self.author_name,
55
59
  }
56
60
  detections = []
57
61
  for detection in self.detections:
58
62
  new_detection = {
59
- "name":detection.name,
60
- "source":detection.source,
61
- "type":detection.type
63
+ "name": detection.name,
64
+ "source": detection.source,
65
+ "type": detection.type,
62
66
  }
63
67
  if self.tags.mitre_attack_enrichments is not None:
64
- new_detection['tags'] = {"mitre_attack_enrichments": [{"mitre_attack_technique": enrichment.mitre_attack_technique} for enrichment in detection.tags.mitre_attack_enrichments]}
68
+ new_detection["tags"] = {
69
+ "mitre_attack_enrichments": [
70
+ {"mitre_attack_technique": enrichment.mitre_attack_technique}
71
+ for enrichment in detection.tags.mitre_attack_enrichments
72
+ ]
73
+ }
65
74
  detections.append(new_detection)
66
75
 
67
- model['detections'] = detections
68
- #Combine fields from this model with fields from parent
76
+ model["detections"] = detections
77
+ # Combine fields from this model with fields from parent
69
78
  super_fields.update(model)
70
-
71
- #return the model
79
+
80
+ # return the model
72
81
  return super_fields
73
82
 
74
83
  @model_validator(mode="after")
75
84
  def setTagsFields(self):
76
-
77
85
  enrichments = []
78
86
  for detection in self.detections:
79
87
  enrichments.extend(detection.tags.mitre_attack_enrichments)
80
88
  self.tags.mitre_attack_enrichments = list(set(enrichments))
81
89
 
82
-
83
90
  tactics = []
84
91
  for enrichment in self.tags.mitre_attack_enrichments:
85
92
  tactics.extend(enrichment.mitre_attack_tactics)
86
93
  self.tags.mitre_attack_tactics = set(tactics)
87
94
 
88
-
89
-
90
95
  datamodels = []
91
96
  for detection in self.detections:
92
97
  datamodels.extend(detection.datamodel)
93
98
  self.tags.datamodels = set(datamodels)
94
99
 
95
-
96
-
97
100
  kill_chain_phases = []
98
101
  for detection in self.detections:
99
102
  kill_chain_phases.extend(detection.tags.kill_chain_phases)
@@ -101,42 +104,40 @@ class Story(SecurityContentObject):
101
104
 
102
105
  return self
103
106
 
104
-
105
107
  @computed_field
106
108
  @property
107
- def author_name(self)->str:
108
- match_author = re.search(r'^([^,]+)', self.author)
109
+ def author_name(self) -> str:
110
+ match_author = re.search(r"^([^,]+)", self.author)
109
111
  if match_author is None:
110
- return 'no'
112
+ return "no"
111
113
  else:
112
114
  return match_author.group(1)
113
115
 
114
116
  @computed_field
115
117
  @property
116
- def author_company(self)->str:
117
- match_company = re.search(r',\s?(.*)$', self.author)
118
+ def author_company(self) -> str:
119
+ match_company = re.search(r",\s?(.*)$", self.author)
118
120
  if match_company is None:
119
- return 'no'
121
+ return "no"
120
122
  else:
121
123
  return match_company.group(1)
122
124
 
123
125
  @computed_field
124
126
  @property
125
- def author_email(self)->str:
127
+ def author_email(self) -> str:
126
128
  return "-"
127
129
 
128
130
  @computed_field
129
131
  @property
130
- def detection_names(self)->List[str]:
132
+ def detection_names(self) -> List[str]:
131
133
  return [detection.name for detection in self.detections]
132
-
134
+
133
135
  @computed_field
134
136
  @property
135
- def investigation_names(self)->List[str]:
137
+ def investigation_names(self) -> List[str]:
136
138
  return [investigation.name for investigation in self.investigations]
137
139
 
138
140
  @computed_field
139
141
  @property
140
- def baseline_names(self)->List[str]:
142
+ def baseline_names(self) -> List[str]:
141
143
  return [baseline.name for baseline in self.baselines]
142
-
@@ -1,54 +1,66 @@
1
1
  from __future__ import annotations
2
2
  from pydantic import BaseModel, Field, model_serializer, ConfigDict
3
- from typing import List,Set,Optional, Annotated
3
+ from typing import List, Set, Optional
4
4
 
5
5
  from enum import Enum
6
6
 
7
7
  from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
8
- from contentctl.objects.enums import StoryCategory, DataModel, KillChainPhase, SecurityContentProductName
9
- from contentctl.objects.annotated_types import CVE_TYPE,MITRE_ATTACK_ID_TYPE
8
+ from contentctl.objects.enums import (
9
+ StoryCategory,
10
+ DataModel,
11
+ KillChainPhase,
12
+ SecurityContentProductName,
13
+ )
14
+ from contentctl.objects.annotated_types import CVE_TYPE, MITRE_ATTACK_ID_TYPE
10
15
 
11
- class StoryUseCase(str,Enum):
12
- FRAUD_DETECTION = "Fraud Detection"
13
- COMPLIANCE = "Compliance"
14
- APPLICATION_SECURITY = "Application Security"
15
- SECURITY_MONITORING = "Security Monitoring"
16
- ADVANCED_THREAD_DETECTION = "Advanced Threat Detection"
17
- INSIDER_THREAT = "Insider Threat"
18
- OTHER = "Other"
16
+
17
+ class StoryUseCase(str, Enum):
18
+ FRAUD_DETECTION = "Fraud Detection"
19
+ COMPLIANCE = "Compliance"
20
+ APPLICATION_SECURITY = "Application Security"
21
+ SECURITY_MONITORING = "Security Monitoring"
22
+ ADVANCED_THREAD_DETECTION = "Advanced Threat Detection"
23
+ INSIDER_THREAT = "Insider Threat"
24
+ OTHER = "Other"
19
25
 
20
26
 
21
27
  class StoryTags(BaseModel):
22
- model_config = ConfigDict(extra='forbid')
23
- category: List[StoryCategory] = Field(...,min_length=1)
24
- product: List[SecurityContentProductName] = Field(...,min_length=1)
25
- usecase: StoryUseCase = Field(...)
26
-
27
- # enrichment
28
- mitre_attack_enrichments: Optional[List[MitreAttackEnrichment]] = None
29
- mitre_attack_tactics: Optional[Set[MITRE_ATTACK_ID_TYPE]] = None
30
- datamodels: Optional[Set[DataModel]] = None
31
- kill_chain_phases: Optional[Set[KillChainPhase]] = None
32
- cve: List[CVE_TYPE] = []
33
- group: List[str] = Field([], description="A list of groups who leverage the techniques list in this Analytic Story.")
34
-
35
- def getCategory_conf(self) -> str:
36
- #if len(self.category) > 1:
37
- # print("Story with more than 1 category. We can only have 1 category, fix it!")
38
- return list(self.category)[0]
39
-
40
- @model_serializer
41
- def serialize_model(self):
42
- #no super to call
43
- return {
44
- "category": list(self.category),
45
- "product": list(self.product),
46
- "usecase": self.usecase,
47
- "mitre_attack_enrichments": self.mitre_attack_enrichments,
48
- "mitre_attack_tactics": list(self.mitre_attack_tactics) if self.mitre_attack_tactics is not None else None,
49
- "datamodels": list(self.datamodels) if self.datamodels is not None else None,
50
- "kill_chain_phases": list(self.kill_chain_phases) if self.kill_chain_phases is not None else None
51
- }
52
-
53
-
54
-
28
+ model_config = ConfigDict(extra="forbid")
29
+ category: List[StoryCategory] = Field(..., min_length=1)
30
+ product: List[SecurityContentProductName] = Field(..., min_length=1)
31
+ usecase: StoryUseCase = Field(...)
32
+
33
+ # enrichment
34
+ mitre_attack_enrichments: Optional[List[MitreAttackEnrichment]] = None
35
+ mitre_attack_tactics: Optional[Set[MITRE_ATTACK_ID_TYPE]] = None
36
+ datamodels: Optional[Set[DataModel]] = None
37
+ kill_chain_phases: Optional[Set[KillChainPhase]] = None
38
+ cve: List[CVE_TYPE] = []
39
+ group: List[str] = Field(
40
+ [],
41
+ description="A list of groups who leverage the techniques list in this Analytic Story.",
42
+ )
43
+
44
+ def getCategory_conf(self) -> str:
45
+ # if len(self.category) > 1:
46
+ # print("Story with more than 1 category. We can only have 1 category, fix it!")
47
+ return list(self.category)[0]
48
+
49
+ @model_serializer
50
+ def serialize_model(self):
51
+ # no super to call
52
+ return {
53
+ "category": list(self.category),
54
+ "product": list(self.product),
55
+ "usecase": self.usecase,
56
+ "mitre_attack_enrichments": self.mitre_attack_enrichments,
57
+ "mitre_attack_tactics": list(self.mitre_attack_tactics)
58
+ if self.mitre_attack_tactics is not None
59
+ else None,
60
+ "datamodels": list(self.datamodels)
61
+ if self.datamodels is not None
62
+ else None,
63
+ "kill_chain_phases": list(self.kill_chain_phases)
64
+ if self.kill_chain_phases is not None
65
+ else None,
66
+ }
@@ -15,13 +15,16 @@ class TestGroup(BaseModel):
15
15
  :param integration_test: an IntegrationTest
16
16
  :param attack_data: the attack data associated with tests in the TestGroup
17
17
  """
18
+
18
19
  name: str
19
20
  unit_test: UnitTest
20
21
  integration_test: IntegrationTest
21
22
  attack_data: list[TestAttackData]
22
23
 
23
24
  @classmethod
24
- def derive_from_unit_test(cls, unit_test: UnitTest, name_prefix: str) -> "TestGroup":
25
+ def derive_from_unit_test(
26
+ cls, unit_test: UnitTest, name_prefix: str
27
+ ) -> "TestGroup":
25
28
  """
26
29
  Given a UnitTest and a prefix, construct a TestGroup, with in IntegrationTest corresponding to the UnitTest
27
30
  :param unit_test: the UnitTest
@@ -36,7 +39,7 @@ class TestGroup(BaseModel):
36
39
  name=f"{name_prefix}:{unit_test.name}",
37
40
  unit_test=unit_test,
38
41
  integration_test=integration_test,
39
- attack_data=unit_test.attack_data
42
+ attack_data=unit_test.attack_data,
40
43
  )
41
44
 
42
45
  def unit_test_skipped(self) -> bool:
@@ -11,5 +11,6 @@ class ThreatObject(BaseModel):
11
11
  :param field: the name of the field from which the risk object will get it's name
12
12
  :param type_: the type of the risk object (e.g. "system")
13
13
  """
14
+
14
15
  field: str
15
16
  type: str
@@ -2,25 +2,34 @@ from pydantic import BaseModel, Field, field_validator
2
2
  from typing import Annotated
3
3
 
4
4
 
5
- # Alert Suppression/Throttling settings have been taken from
5
+ # Alert Suppression/Throttling settings have been taken from
6
6
  # https://docs.splunk.com/Documentation/Splunk/9.2.2/Admin/Savedsearchesconf
7
7
  class Throttling(BaseModel):
8
- fields: list[str] = Field(..., description="The list of fields to throttle on. These fields MUST occur in the search.", min_length=1)
9
- period: Annotated[str,Field(pattern="^[0-9]+[smh]$")] = Field(..., description="How often the alert should be triggered. "
10
- "This may be specified in seconds, minutes, or hours. "
11
- "For example, if an alert should be triggered once a day,"
12
- " it may be specified in seconds (86400s), minutes (1440m), or hours import (24h).")
13
-
8
+ fields: list[str] = Field(
9
+ ...,
10
+ description="The list of fields to throttle on. These fields MUST occur in the search.",
11
+ min_length=1,
12
+ )
13
+ period: Annotated[str, Field(pattern="^[0-9]+[smh]$")] = Field(
14
+ ...,
15
+ description="How often the alert should be triggered. "
16
+ "This may be specified in seconds, minutes, or hours. "
17
+ "For example, if an alert should be triggered once a day,"
18
+ " it may be specified in seconds (86400s), minutes (1440m), or hours import (24h).",
19
+ )
20
+
14
21
  @field_validator("fields")
15
- def no_spaces_in_fields(cls, v:list[str])->list[str]:
22
+ def no_spaces_in_fields(cls, v: list[str]) -> list[str]:
16
23
  for field in v:
17
- if ' ' in field:
18
- raise ValueError("Spaces are not presently supported in 'alert.suppress.fields' / throttling fields in conf files. "
19
- "The field '{field}' has a space in it. If this is a blocker, please raise this as an issue on the Project.")
24
+ if " " in field:
25
+ raise ValueError(
26
+ "Spaces are not presently supported in 'alert.suppress.fields' / throttling fields in conf files. "
27
+ "The field '{field}' has a space in it. If this is a blocker, please raise this as an issue on the Project."
28
+ )
20
29
  return v
21
30
 
22
- def conf_formatted_fields(self)->str:
23
- '''
31
+ def conf_formatted_fields(self) -> str:
32
+ """
24
33
  TODO:
25
34
  The field alert.suppress.fields is defined as follows:
26
35
  alert.suppress.fields = <comma-delimited-field-list>
@@ -28,19 +37,19 @@ class Throttling(BaseModel):
28
37
  be specified if the digest mode is disabled and suppression is enabled.
29
38
 
30
39
  In order to support fields with spaces in them, we may need to wrap each
31
- field in "".
40
+ field in "".
32
41
  This function returns a properly formatted value, where each field
33
- is wrapped in "" and separated with a comma. For example, the fields
42
+ is wrapped in "" and separated with a comma. For example, the fields
34
43
  ["field1", "field 2", "field3"] would be returned as the string
35
44
 
36
45
  "field1","field 2","field3
37
46
 
38
47
  However, for now, we will error on fields with spaces and simply
39
48
  separate with commas
40
- '''
41
-
49
+ """
50
+
42
51
  return ",".join(self.fields)
43
52
 
44
53
  # The following may be used once we determine proper support
45
54
  # for fields with spaces
46
- #return ",".join([f'"{field}"' for field in self.fields])
55
+ # return ",".join([f'"{field}"' for field in self.fields])
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from pydantic import Field
4
4
 
5
- from contentctl.objects.unit_test_baseline import UnitTestBaseline
6
5
  from contentctl.objects.test_attack_data import TestAttackData
7
6
  from contentctl.objects.unit_test_result import UnitTestResult
8
7
  from contentctl.objects.base_test import BaseTest, TestType
@@ -13,6 +12,7 @@ class UnitTest(BaseTest):
13
12
  """
14
13
  A unit test for a detection
15
14
  """
15
+
16
16
  # contentType: SecurityContentType = SecurityContentType.unit_tests
17
17
 
18
18
  # The test type (unit)
@@ -29,7 +29,6 @@ class UnitTest(BaseTest):
29
29
  Skip the test by setting its result status
30
30
  :param message: the reason for skipping
31
31
  """
32
- self.result = UnitTestResult( # type: ignore
33
- message=message,
34
- status=TestResultStatus.SKIP
32
+ self.result = UnitTestResult( # type: ignore
33
+ message=message, status=TestResultStatus.SKIP
35
34
  )
@@ -1,12 +1,11 @@
1
-
2
-
3
- from pydantic import BaseModel,ConfigDict
1
+ from pydantic import BaseModel, ConfigDict
4
2
  from typing import Union
5
3
 
4
+
6
5
  class UnitTestBaseline(BaseModel):
7
6
  model_config = ConfigDict(extra="forbid")
8
7
  name: str
9
8
  file: str
10
9
  pass_condition: str
11
- earliest_time: Union[str,None] = None
12
- latest_time: Union[str,None] = None
10
+ earliest_time: Union[str, None] = None
11
+ latest_time: Union[str, None] = None
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Union,TYPE_CHECKING
3
+ from typing import Union, TYPE_CHECKING
4
4
  from splunklib.data import Record
5
5
  from contentctl.objects.base_test_result import BaseTestResult, TestResultStatus
6
6
 
@@ -15,7 +15,7 @@ SID_TEMPLATE = "{server}:{web_port}/en-US/app/search/search?sid={sid}"
15
15
 
16
16
  class UnitTestResult(BaseTestResult):
17
17
  missing_observables: list[str] = []
18
-
18
+
19
19
  def set_job_content(
20
20
  self,
21
21
  content: Union[Record, None],
@@ -40,7 +40,7 @@ class UnitTestResult(BaseTestResult):
40
40
  self.exception = exception
41
41
  self.status = status
42
42
  self.job_content = content
43
-
43
+
44
44
  # Set the job content, if given
45
45
  if content is not None:
46
46
  if self.status == TestResultStatus.PASS:
@@ -50,7 +50,7 @@ class UnitTestResult(BaseTestResult):
50
50
  elif self.status == TestResultStatus.ERROR:
51
51
  self.message = "TEST ERROR"
52
52
  elif self.status == TestResultStatus.SKIP:
53
- #A test that was SKIPPED should not have job content since it should not have been run.
53
+ # A test that was SKIPPED should not have job content since it should not have been run.
54
54
  self.message = "TEST SKIPPED"
55
55
 
56
56
  if not config.instance_address.startswith("http://"):
@@ -64,7 +64,7 @@ class UnitTestResult(BaseTestResult):
64
64
  )
65
65
 
66
66
  elif self.status == TestResultStatus.SKIP:
67
- self.message = "TEST SKIPPED"
67
+ self.message = "TEST SKIPPED"
68
68
  pass
69
69
 
70
70
  elif content is None:
@@ -72,7 +72,7 @@ class UnitTestResult(BaseTestResult):
72
72
  if self.exception is not None:
73
73
  self.message = f"EXCEPTION: {str(self.exception)}"
74
74
  else:
75
- self.message = f"ERROR with no more specific message available."
75
+ self.message = "ERROR with no more specific message available."
76
76
  self.sid_link = NO_SID
77
77
 
78
78
  return self.success
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
  from typing import TYPE_CHECKING
3
+
3
4
  if TYPE_CHECKING:
4
5
  from contentctl.objects.detection import Detection
5
6
  from contentctl.objects.lookup import Lookup
@@ -15,12 +16,11 @@ import pathlib
15
16
  from contentctl.output.json_writer import JsonWriter
16
17
 
17
18
 
18
-
19
19
  class ApiJsonOutput:
20
20
  output_path: pathlib.Path
21
21
  app_label: str
22
22
 
23
- def __init__(self, output_path:pathlib.Path, app_label: str):
23
+ def __init__(self, output_path: pathlib.Path, app_label: str):
24
24
  self.output_path = output_path
25
25
  self.app_label = app_label
26
26
 
@@ -53,7 +53,7 @@ class ApiJsonOutput:
53
53
  )
54
54
  for detection in objects
55
55
  ]
56
- #Only a subset of macro fields are required:
56
+ # Only a subset of macro fields are required:
57
57
  # for detection in detections:
58
58
  # new_macros = []
59
59
  # for macro in detection.get("macros",[]):
@@ -62,16 +62,15 @@ class ApiJsonOutput:
62
62
  # new_macro_fields["definition"] = macro.get("definition")
63
63
  # new_macro_fields["description"] = macro.get("description")
64
64
  # if len(macro.get("arguments", [])) > 0:
65
- # new_macro_fields["arguments"] = macro.get("arguments")
65
+ # new_macro_fields["arguments"] = macro.get("arguments")
66
66
  # new_macros.append(new_macro_fields)
67
67
  # detection["macros"] = new_macros
68
68
  # del()
69
-
70
-
69
+
71
70
  JsonWriter.writeJsonObject(
72
71
  os.path.join(self.output_path, "detections.json"), "detections", detections
73
72
  )
74
-
73
+
75
74
  def writeMacros(
76
75
  self,
77
76
  objects: list[Macro],
@@ -81,13 +80,13 @@ class ApiJsonOutput:
81
80
  for macro in objects
82
81
  ]
83
82
  for macro in macros:
84
- for k in ["author", "date","version","id","references"]:
83
+ for k in ["author", "date", "version", "id", "references"]:
85
84
  if k in macro:
86
- del(macro[k])
85
+ del macro[k]
87
86
  JsonWriter.writeJsonObject(
88
87
  os.path.join(self.output_path, "macros.json"), "macros", macros
89
88
  )
90
-
89
+
91
90
  def writeStories(
92
91
  self,
93
92
  objects: list[Story],
@@ -126,8 +125,9 @@ class ApiJsonOutput:
126
125
  }
127
126
  for detection in story["detections"]
128
127
  ]
129
- story["detection_names"] = [f"{self.app_label} - {name} - Rule" for name in story["detection_names"]]
130
-
128
+ story["detection_names"] = [
129
+ f"{self.app_label} - {name} - Rule" for name in story["detection_names"]
130
+ ]
131
131
 
132
132
  JsonWriter.writeJsonObject(
133
133
  os.path.join(self.output_path, "stories.json"), "stories", stories
@@ -159,10 +159,10 @@ class ApiJsonOutput:
159
159
  )
160
160
  for baseline in objects
161
161
  ]
162
-
162
+
163
163
  JsonWriter.writeJsonObject(
164
- os.path.join(self.output_path, "baselines.json"), "baselines", baselines
165
- )
164
+ os.path.join(self.output_path, "baselines.json"), "baselines", baselines
165
+ )
166
166
 
167
167
  def writeInvestigations(
168
168
  self,
@@ -221,9 +221,9 @@ class ApiJsonOutput:
221
221
  for lookup in objects
222
222
  ]
223
223
  for lookup in lookups:
224
- for k in ["author","date","version","id","references"]:
224
+ for k in ["author", "date", "version", "id", "references"]:
225
225
  if k in lookup:
226
- del(lookup[k])
226
+ del lookup[k]
227
227
  JsonWriter.writeJsonObject(
228
228
  os.path.join(self.output_path, "lookups.json"), "lookups", lookups
229
229
  )
@@ -244,16 +244,16 @@ class ApiJsonOutput:
244
244
  "description",
245
245
  "scheduling",
246
246
  "rba",
247
- "tags"
248
- ]
247
+ "tags",
248
+ ]
249
249
  )
250
250
  )
251
251
  for deployment in objects
252
252
  ]
253
- #references are not to be included, but have been deleted in the
254
- #model_serialization logic
253
+ # references are not to be included, but have been deleted in the
254
+ # model_serialization logic
255
255
  JsonWriter.writeJsonObject(
256
256
  os.path.join(self.output_path, "deployments.json"),
257
257
  "deployments",
258
258
  deployments,
259
- )
259
+ )