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
@@ -8,7 +8,7 @@ from contentctl.objects.security_content_object import SecurityContentObject
8
8
  from contentctl.objects.config import build
9
9
  from enum import StrEnum
10
10
 
11
- DEFAULT_DASHBAORD_JINJA2_TEMPLATE = '''<dashboard version="2" theme="{{ dashboard.theme }}">
11
+ DEFAULT_DASHBAORD_JINJA2_TEMPLATE = """<dashboard version="2" theme="{{ dashboard.theme }}">
12
12
  <label>{{ dashboard.label(config) }}</label>
13
13
  <description></description>
14
14
  <definition><![CDATA[
@@ -21,28 +21,40 @@ DEFAULT_DASHBAORD_JINJA2_TEMPLATE = '''<dashboard version="2" theme="{{ dashboar
21
21
  "hideExport": false
22
22
  }
23
23
  ]]></meta>
24
- </dashboard>'''
24
+ </dashboard>"""
25
+
25
26
 
26
27
  class DashboardTheme(StrEnum):
27
28
  light = "light"
28
29
  dark = "dark"
29
30
 
31
+
30
32
  class Dashboard(SecurityContentObject):
31
- j2_template: str = Field(default=DEFAULT_DASHBAORD_JINJA2_TEMPLATE, description="Jinja2 Template used to construct the dashboard")
32
- description: str = Field(...,description="A description of the dashboard. This does not have to match "
33
- "the description of the dashboard in the JSON file.", max_length=10000)
34
- theme: DashboardTheme = Field(default=DashboardTheme.light, description="The theme of the dashboard. Choose between 'light' and 'dark'.")
35
- json_obj: Json[dict[str,Any]] = Field(..., description="Valid JSON object that describes the dashboard")
36
-
37
-
38
-
39
- def label(self, config:build)->str:
33
+ j2_template: str = Field(
34
+ default=DEFAULT_DASHBAORD_JINJA2_TEMPLATE,
35
+ description="Jinja2 Template used to construct the dashboard",
36
+ )
37
+ description: str = Field(
38
+ ...,
39
+ description="A description of the dashboard. This does not have to match "
40
+ "the description of the dashboard in the JSON file.",
41
+ max_length=10000,
42
+ )
43
+ theme: DashboardTheme = Field(
44
+ default=DashboardTheme.light,
45
+ description="The theme of the dashboard. Choose between 'light' and 'dark'.",
46
+ )
47
+ json_obj: Json[dict[str, Any]] = Field(
48
+ ..., description="Valid JSON object that describes the dashboard"
49
+ )
50
+
51
+ def label(self, config: build) -> str:
40
52
  return f"{config.app.label} - {self.name}"
41
-
53
+
42
54
  @model_validator(mode="before")
43
55
  @classmethod
44
- def validate_fields_from_json(cls, data:Any)->Any:
45
- yml_file_name:str|None = data.get("file_path", None)
56
+ def validate_fields_from_json(cls, data: Any) -> Any:
57
+ yml_file_name: str | None = data.get("file_path", None)
46
58
  if yml_file_name is None:
47
59
  raise ValueError("File name not passed to dashboard constructor")
48
60
  yml_file_path = pathlib.Path(yml_file_name)
@@ -50,51 +62,53 @@ class Dashboard(SecurityContentObject):
50
62
 
51
63
  if not json_file_path.is_file():
52
64
  raise ValueError(f"Required file {json_file_path} does not exist.")
53
-
54
- with open(json_file_path,'r') as jsonFilePointer:
65
+
66
+ with open(json_file_path, "r") as jsonFilePointer:
55
67
  try:
56
- json_obj:dict[str,Any] = json.load(jsonFilePointer)
68
+ json_obj: dict[str, Any] = json.load(jsonFilePointer)
57
69
  except Exception as e:
58
70
  raise ValueError(f"Unable to load data from {json_file_path}: {str(e)}")
59
71
 
60
- name_from_file = data.get("name",None)
61
- name_from_json = json_obj.get("title",None)
72
+ name_from_file = data.get("name", None)
73
+ name_from_json = json_obj.get("title", None)
62
74
 
63
- errors:list[str] = []
75
+ errors: list[str] = []
64
76
  if name_from_json is None:
65
77
  errors.append(f"'title' field is missing from {json_file_path}")
66
78
  elif name_from_json != name_from_file:
67
- errors.append(f"The 'title' field in the JSON file [{json_file_path}] does not match the 'name' field in the YML object [{yml_file_path}]. These two MUST match:\n "
68
- f"title in JSON : {name_from_json}\n "
69
- f"title in YML : {name_from_file}\n ")
70
-
71
- description_from_json = json_obj.get("description",None)
79
+ errors.append(
80
+ f"The 'title' field in the JSON file [{json_file_path}] does not match the 'name' field in the YML object [{yml_file_path}]. These two MUST match:\n "
81
+ f"title in JSON : {name_from_json}\n "
82
+ f"title in YML : {name_from_file}\n "
83
+ )
84
+
85
+ description_from_json = json_obj.get("description", None)
72
86
  if description_from_json is None:
73
87
  errors.append("'description' field is missing from field 'json_object'")
74
-
75
- if len(errors) > 0 :
88
+
89
+ if len(errors) > 0:
76
90
  err_string = "\n - ".join(errors)
77
91
  raise ValueError(f"Error(s) validating dashboard:\n - {err_string}")
78
-
79
- data['name'] = name_from_file
80
- data['json_obj'] = json.dumps(json_obj)
92
+
93
+ data["name"] = name_from_file
94
+ data["json_obj"] = json.dumps(json_obj)
81
95
  return data
82
96
 
83
-
84
97
  def pretty_print_json_obj(self):
85
98
  return json.dumps(self.json_obj, indent=4)
86
-
87
- def getOutputFilepathRelativeToAppRoot(self, config:build)->pathlib.Path:
99
+
100
+ def getOutputFilepathRelativeToAppRoot(self, config: build) -> pathlib.Path:
88
101
  filename = f"{self.file_path.stem}.xml".lower()
89
- return pathlib.Path("default/data/ui/views")/filename
90
-
91
-
92
- def writeDashboardFile(self, j2_env:Environment, config:build):
102
+ return pathlib.Path("default/data/ui/views") / filename
103
+
104
+ def writeDashboardFile(self, j2_env: Environment, config: build):
93
105
  template = j2_env.from_string(self.j2_template)
94
106
  dashboard_text = template.render(config=config, dashboard=self)
95
107
 
96
- with open(config.getPackageDirectoryPath()/self.getOutputFilepathRelativeToAppRoot(config), 'a') as f:
97
- output_xml = dashboard_text.encode('utf-8', 'ignore').decode('utf-8')
108
+ with open(
109
+ config.getPackageDirectoryPath()
110
+ / self.getOutputFilepathRelativeToAppRoot(config),
111
+ "a",
112
+ ) as f:
113
+ output_xml = dashboard_text.encode("utf-8", "ignore").decode("utf-8")
98
114
  f.write(output_xml)
99
-
100
-
@@ -2,32 +2,32 @@ from __future__ import annotations
2
2
  from typing import Optional, Any
3
3
  from pydantic import Field, HttpUrl, model_serializer, BaseModel
4
4
  from contentctl.objects.security_content_object import SecurityContentObject
5
- from contentctl.objects.event_source import EventSource
6
5
 
7
6
 
8
7
  class TA(BaseModel):
9
8
  name: str
10
9
  url: HttpUrl | None = None
11
10
  version: str
11
+
12
+
12
13
  class DataSource(SecurityContentObject):
13
14
  source: str = Field(...)
14
15
  sourcetype: str = Field(...)
15
16
  separator: Optional[str] = None
16
17
  configuration: Optional[str] = None
17
18
  supported_TA: list[TA] = []
18
- fields: Optional[list] = None
19
- field_mappings: Optional[list] = None
20
- convert_to_log_source: Optional[list] = None
21
- example_log: Optional[str] = None
22
-
19
+ fields: None | list = None
20
+ field_mappings: None | list = None
21
+ convert_to_log_source: None | list = None
22
+ example_log: None | str = None
23
23
 
24
24
  @model_serializer
25
25
  def serialize_model(self):
26
- #Call serializer for parent
26
+ # Call serializer for parent
27
27
  super_fields = super().serialize_model()
28
-
29
- #All fields custom to this model
30
- model:dict[str,Any] = {
28
+
29
+ # All fields custom to this model
30
+ model: dict[str, Any] = {
31
31
  "source": self.source,
32
32
  "sourcetype": self.sourcetype,
33
33
  "separator": self.separator,
@@ -36,12 +36,11 @@ class DataSource(SecurityContentObject):
36
36
  "fields": self.fields,
37
37
  "field_mappings": self.field_mappings,
38
38
  "convert_to_log_source": self.convert_to_log_source,
39
- "example_log":self.example_log
39
+ "example_log": self.example_log,
40
40
  }
41
-
42
-
43
- #Combine fields from this model with fields from parent
41
+
42
+ # Combine fields from this model with fields from parent
44
43
  super_fields.update(model)
45
-
46
- #return the model
47
- return super_fields
44
+
45
+ # return the model
46
+ return super_fields
@@ -1,5 +1,11 @@
1
1
  from __future__ import annotations
2
- from pydantic import Field, computed_field,ValidationInfo, model_serializer, NonNegativeInt
2
+ from pydantic import (
3
+ Field,
4
+ computed_field,
5
+ ValidationInfo,
6
+ model_serializer,
7
+ NonNegativeInt,
8
+ )
3
9
  from typing import Any
4
10
  import uuid
5
11
  import datetime
@@ -11,75 +17,68 @@ from contentctl.objects.enums import DeploymentType
11
17
 
12
18
 
13
19
  class Deployment(SecurityContentObject):
14
- #id: str = None
15
- #date: str = None
16
- #author: str = None
17
- #description: str = None
18
- #contentType: SecurityContentType = SecurityContentType.deployments
19
-
20
-
21
20
  scheduling: DeploymentScheduling = Field(...)
22
21
  alert_action: AlertAction = AlertAction()
23
22
  type: DeploymentType = Field(...)
24
- author: str = Field(...,max_length=255)
23
+ author: str = Field(..., max_length=255)
25
24
  version: NonNegativeInt = 1
26
25
 
27
- #Type was the only tag exposed and should likely be removed/refactored.
28
- #For transitional reasons, provide this as a computed_field in prep for removal
26
+ # Type was the only tag exposed and should likely be removed/refactored.
27
+ # For transitional reasons, provide this as a computed_field in prep for removal
29
28
  @computed_field
30
29
  @property
31
- def tags(self)->dict[str,DeploymentType]:
30
+ def tags(self) -> dict[str, DeploymentType]:
32
31
  return {"type": self.type}
33
32
 
34
-
35
33
  @staticmethod
36
- def getDeployment(v:dict[str,Any], info:ValidationInfo)->Deployment:
34
+ def getDeployment(v: dict[str, Any], info: ValidationInfo) -> Deployment:
37
35
  if v != {}:
38
36
  # If the user has defined a deployment, then allow it to be validated
39
37
  # and override the default deployment info defined in type:Baseline
40
- v['type'] = DeploymentType.Embedded
41
-
38
+ v["type"] = DeploymentType.Embedded
39
+
42
40
  detection_name = info.data.get("name", None)
43
41
  if detection_name is None:
44
- raise ValueError("Could not create inline deployment - Baseline or Detection lacking 'name' field,")
42
+ raise ValueError(
43
+ "Could not create inline deployment - Baseline or Detection lacking 'name' field,"
44
+ )
45
45
 
46
- # Add a number of static values
47
- v.update({
48
- 'name': f"{detection_name} - Inline Deployment",
49
- 'id':uuid.uuid4(),
50
- 'date': datetime.date.today(),
51
- 'description': "Inline deployment created at runtime.",
52
- 'author': "contentctl tool"
53
- })
46
+ # Add a number of static values
47
+ v.update(
48
+ {
49
+ "name": f"{detection_name} - Inline Deployment",
50
+ "id": uuid.uuid4(),
51
+ "date": datetime.date.today(),
52
+ "description": "Inline deployment created at runtime.",
53
+ "author": "contentctl tool",
54
+ }
55
+ )
54
56
 
55
-
56
57
  # This constructs a temporary in-memory deployment,
57
- # allowing the deployment to be easily defined in the
58
+ # allowing the deployment to be easily defined in the
58
59
  # detection on a per detection basis.
59
60
  return Deployment.model_validate(v)
60
-
61
+
61
62
  else:
62
- return SecurityContentObject.getDeploymentFromType(info.data.get("type",None), info)
63
-
63
+ return SecurityContentObject.getDeploymentFromType(
64
+ info.data.get("type", None), info
65
+ )
66
+
64
67
  @model_serializer
65
68
  def serialize_model(self):
66
- #Call serializer for parent
69
+ # Call serializer for parent
67
70
  super_fields = super().serialize_model()
68
-
69
- #All fields custom to this model
70
- model= {
71
- "scheduling": self.scheduling.model_dump(),
72
- "tags": self.tags
73
- }
74
71
 
75
-
76
- #Combine fields from this model with fields from parent
72
+ # All fields custom to this model
73
+ model = {"scheduling": self.scheduling.model_dump(), "tags": self.tags}
74
+
75
+ # Combine fields from this model with fields from parent
77
76
  model.update(super_fields)
78
-
77
+
79
78
  alert_action_fields = self.alert_action.model_dump()
80
79
  model.update(alert_action_fields)
81
80
 
82
- del(model['references'])
83
-
84
- #return the model
85
- return model
81
+ del model["references"]
82
+
83
+ # return the model
84
+ return model
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel
2
+ from pydantic import BaseModel, ConfigDict
3
3
 
4
4
 
5
5
  class DeploymentEmail(BaseModel):
6
+ model_config = ConfigDict(extra="forbid")
6
7
  message: str
7
8
  subject: str
8
- to: str
9
+ to: str
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel
2
+ from pydantic import BaseModel, ConfigDict
3
3
  from typing import List
4
4
 
5
+
5
6
  class DeploymentNotable(BaseModel):
7
+ model_config = ConfigDict(extra="forbid")
6
8
  rule_description: str
7
9
  rule_title: str
8
- nes_fields: List[str]
10
+ nes_fields: List[str]
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel
2
+ from pydantic import BaseModel, ConfigDict
3
3
 
4
4
 
5
5
  class DeploymentPhantom(BaseModel):
6
- cam_workers : str
7
- label : str
8
- phantom_server : str
9
- sensitivity : str
10
- severity : str
6
+ model_config = ConfigDict(extra="forbid")
7
+ cam_workers: str
8
+ label: str
9
+ phantom_server: str
10
+ sensitivity: str
11
+ severity: str
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel
2
+ from pydantic import BaseModel, ConfigDict
3
3
 
4
4
 
5
5
  class DeploymentRBA(BaseModel):
6
- enabled: bool = False
6
+ model_config = ConfigDict(extra="forbid")
7
+ enabled: bool = False
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel
2
+ from pydantic import BaseModel, ConfigDict
3
3
 
4
4
 
5
5
  class DeploymentScheduling(BaseModel):
6
+ model_config = ConfigDict(extra="forbid")
6
7
  cron_schedule: str
7
8
  earliest_time: str
8
9
  latest_time: str
9
- schedule_window: str
10
+ schedule_window: str
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel
2
+ from pydantic import BaseModel, ConfigDict
3
3
 
4
4
 
5
5
  class DeploymentSlack(BaseModel):
6
+ model_config = ConfigDict(extra="forbid")
6
7
  channel: str
7
- message: str
8
+ message: str
@@ -1,5 +1,8 @@
1
1
  from __future__ import annotations
2
- from contentctl.objects.abstract_security_content_objects.detection_abstract import Detection_Abstract
2
+ from contentctl.objects.abstract_security_content_objects.detection_abstract import (
3
+ Detection_Abstract,
4
+ )
5
+
3
6
 
4
7
  class Detection(Detection_Abstract):
5
8
  # Customization to the Detection Class go here.
@@ -12,4 +15,4 @@ class Detection(Detection_Abstract):
12
15
  # them or modifying their behavior may cause
13
16
  # undefined issues with the contentctl tooling
14
17
  # or output of the tooling.
15
- pass
18
+ pass
@@ -8,6 +8,7 @@ class DetectionMetadata(BaseModel):
8
8
  """
9
9
  A model of the metadata line in a detection stanza in savedsearches.conf
10
10
  """
11
+
11
12
  # A bool indicating whether the detection is deprecated (serialized as an int, 1 or 0)
12
13
  deprecated: bool = Field(...)
13
14
 
@@ -11,6 +11,7 @@ class DetectionStanza(BaseModel):
11
11
  """
12
12
  A model representing a stanza for a detection in savedsearches.conf
13
13
  """
14
+
14
15
  # The lines that comprise this stanza, in the order they appear in the conf
15
16
  lines: list[str] = Field(...)
16
17
 
@@ -47,7 +48,9 @@ class DetectionStanza(BaseModel):
47
48
  raise Exception(f"No metadata for detection '{self.name}' found in stanza.")
48
49
 
49
50
  # Parse the metadata JSON into a model
50
- return DetectionMetadata.model_validate_json(meta_line[len(DetectionStanza.METADATA_LINE_PREFIX):])
51
+ return DetectionMetadata.model_validate_json(
52
+ meta_line[len(DetectionStanza.METADATA_LINE_PREFIX) :]
53
+ )
51
54
 
52
55
  @computed_field
53
56
  @cached_property
@@ -76,4 +79,6 @@ class DetectionStanza(BaseModel):
76
79
  :returns: True if the version still needs to be bumped
77
80
  :rtype: bool
78
81
  """
79
- return (self.hash != previous.hash) and (self.metadata.detection_version <= previous.metadata.detection_version)
82
+ return (self.hash != previous.hash) and (
83
+ self.metadata.detection_version <= previous.metadata.detection_version
84
+ )