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,55 +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
- # TODO (#266): disable the use_enum_values configuration
22
27
  class StoryTags(BaseModel):
23
- model_config = ConfigDict(extra='forbid', use_enum_values=True)
24
- category: List[StoryCategory] = Field(...,min_length=1)
25
- product: List[SecurityContentProductName] = Field(...,min_length=1)
26
- usecase: StoryUseCase = Field(...)
27
-
28
- # enrichment
29
- mitre_attack_enrichments: Optional[List[MitreAttackEnrichment]] = None
30
- mitre_attack_tactics: Optional[Set[MITRE_ATTACK_ID_TYPE]] = None
31
- datamodels: Optional[Set[DataModel]] = None
32
- kill_chain_phases: Optional[Set[KillChainPhase]] = None
33
- cve: List[CVE_TYPE] = []
34
- group: List[str] = Field([], description="A list of groups who leverage the techniques list in this Analytic Story.")
35
-
36
- def getCategory_conf(self) -> str:
37
- #if len(self.category) > 1:
38
- # print("Story with more than 1 category. We can only have 1 category, fix it!")
39
- return list(self.category)[0]
40
-
41
- @model_serializer
42
- def serialize_model(self):
43
- #no super to call
44
- return {
45
- "category": list(self.category),
46
- "product": list(self.product),
47
- "usecase": self.usecase,
48
- "mitre_attack_enrichments": self.mitre_attack_enrichments,
49
- "mitre_attack_tactics": list(self.mitre_attack_tactics) if self.mitre_attack_tactics is not None else None,
50
- "datamodels": list(self.datamodels) if self.datamodels is not None else None,
51
- "kill_chain_phases": list(self.kill_chain_phases) if self.kill_chain_phases is not None else None
52
- }
53
-
54
-
55
-
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
+ }
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
- from pydantic import BaseModel, HttpUrl, FilePath, Field
2
+ from pydantic import BaseModel, HttpUrl, FilePath, Field, ConfigDict
3
3
 
4
4
 
5
5
  class TestAttackData(BaseModel):
6
+ model_config = ConfigDict(extra="forbid")
6
7
  data: HttpUrl | FilePath = Field(...)
7
8
  # TODO - should source and sourcetype should be mapped to a list
8
9
  # of supported source and sourcetypes in a given environment?
@@ -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,11 +1,11 @@
1
-
2
-
3
- from pydantic import BaseModel
1
+ from pydantic import BaseModel, ConfigDict
4
2
  from typing import Union
5
3
 
4
+
6
5
  class UnitTestBaseline(BaseModel):
6
+ model_config = ConfigDict(extra="forbid")
7
7
  name: str
8
8
  file: str
9
9
  pass_condition: str
10
- earliest_time: Union[str,None] = None
11
- 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