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.
- contentctl/__init__.py +1 -1
- contentctl/actions/build.py +102 -57
- contentctl/actions/deploy_acs.py +29 -24
- contentctl/actions/detection_testing/DetectionTestingManager.py +66 -42
- contentctl/actions/detection_testing/GitService.py +134 -76
- contentctl/actions/detection_testing/generate_detection_coverage_badge.py +48 -30
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +192 -147
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +45 -32
- contentctl/actions/detection_testing/progress_bar.py +9 -6
- contentctl/actions/detection_testing/views/DetectionTestingView.py +16 -19
- contentctl/actions/detection_testing/views/DetectionTestingViewCLI.py +1 -5
- contentctl/actions/detection_testing/views/DetectionTestingViewFile.py +2 -2
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +1 -4
- contentctl/actions/doc_gen.py +9 -5
- contentctl/actions/initialize.py +45 -33
- contentctl/actions/inspect.py +118 -61
- contentctl/actions/new_content.py +155 -108
- contentctl/actions/release_notes.py +276 -146
- contentctl/actions/reporting.py +23 -19
- contentctl/actions/test.py +33 -28
- contentctl/actions/validate.py +55 -34
- contentctl/api.py +54 -45
- contentctl/contentctl.py +124 -90
- contentctl/enrichments/attack_enrichment.py +112 -72
- contentctl/enrichments/cve_enrichment.py +34 -28
- contentctl/enrichments/splunk_app_enrichment.py +38 -36
- contentctl/helper/link_validator.py +101 -78
- contentctl/helper/splunk_app.py +69 -41
- contentctl/helper/utils.py +58 -53
- contentctl/input/director.py +68 -36
- contentctl/input/new_content_questions.py +27 -35
- contentctl/input/yml_reader.py +28 -18
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +303 -259
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +115 -52
- contentctl/objects/alert_action.py +10 -9
- contentctl/objects/annotated_types.py +1 -1
- contentctl/objects/atomic.py +65 -54
- contentctl/objects/base_test.py +5 -3
- contentctl/objects/base_test_result.py +19 -11
- contentctl/objects/baseline.py +62 -30
- contentctl/objects/baseline_tags.py +30 -24
- contentctl/objects/config.py +790 -597
- contentctl/objects/constants.py +33 -56
- contentctl/objects/correlation_search.py +150 -136
- contentctl/objects/dashboard.py +55 -41
- contentctl/objects/data_source.py +16 -17
- contentctl/objects/deployment.py +43 -44
- contentctl/objects/deployment_email.py +3 -2
- contentctl/objects/deployment_notable.py +4 -2
- contentctl/objects/deployment_phantom.py +7 -6
- contentctl/objects/deployment_rba.py +3 -2
- contentctl/objects/deployment_scheduling.py +3 -2
- contentctl/objects/deployment_slack.py +3 -2
- contentctl/objects/detection.py +5 -2
- contentctl/objects/detection_metadata.py +1 -0
- contentctl/objects/detection_stanza.py +7 -2
- contentctl/objects/detection_tags.py +58 -103
- contentctl/objects/drilldown.py +66 -34
- contentctl/objects/enums.py +81 -100
- contentctl/objects/errors.py +16 -24
- contentctl/objects/integration_test.py +3 -3
- contentctl/objects/integration_test_result.py +1 -0
- contentctl/objects/investigation.py +59 -36
- contentctl/objects/investigation_tags.py +30 -19
- contentctl/objects/lookup.py +304 -101
- contentctl/objects/macro.py +55 -39
- contentctl/objects/manual_test.py +3 -3
- contentctl/objects/manual_test_result.py +1 -0
- contentctl/objects/mitre_attack_enrichment.py +17 -16
- contentctl/objects/notable_action.py +2 -1
- contentctl/objects/notable_event.py +1 -3
- contentctl/objects/playbook.py +37 -35
- contentctl/objects/playbook_tags.py +23 -13
- contentctl/objects/rba.py +96 -0
- contentctl/objects/risk_analysis_action.py +15 -11
- contentctl/objects/risk_event.py +110 -160
- contentctl/objects/risk_object.py +1 -0
- contentctl/objects/savedsearches_conf.py +9 -7
- contentctl/objects/security_content_object.py +5 -2
- contentctl/objects/story.py +54 -49
- contentctl/objects/story_tags.py +56 -45
- contentctl/objects/test_attack_data.py +2 -1
- contentctl/objects/test_group.py +5 -2
- contentctl/objects/threat_object.py +1 -0
- contentctl/objects/throttling.py +27 -18
- contentctl/objects/unit_test.py +3 -4
- contentctl/objects/unit_test_baseline.py +5 -5
- contentctl/objects/unit_test_result.py +6 -6
- contentctl/output/api_json_output.py +233 -220
- contentctl/output/attack_nav_output.py +21 -21
- contentctl/output/attack_nav_writer.py +29 -37
- contentctl/output/conf_output.py +235 -172
- contentctl/output/conf_writer.py +201 -125
- contentctl/output/data_source_writer.py +38 -26
- contentctl/output/doc_md_output.py +53 -27
- contentctl/output/jinja_writer.py +19 -15
- contentctl/output/json_writer.py +21 -11
- contentctl/output/svg_output.py +56 -38
- contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/collections.j2 +1 -1
- contentctl/output/templates/doc_detections.j2 +0 -5
- contentctl/output/templates/es_investigations_investigations.j2 +1 -1
- contentctl/output/templates/es_investigations_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -2
- contentctl/output/templates/savedsearches_detections.j2 +10 -11
- contentctl/output/templates/savedsearches_investigations.j2 +2 -2
- contentctl/output/templates/transforms.j2 +6 -8
- contentctl/output/yml_writer.py +29 -20
- contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +16 -34
- contentctl/templates/stories/cobalt_strike.yml +1 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/METADATA +5 -4
- contentctl-5.0.0.dist-info/RECORD +168 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/WHEEL +1 -1
- contentctl/actions/initialize_old.py +0 -245
- contentctl/objects/event_source.py +0 -11
- contentctl/objects/observable.py +0 -37
- contentctl/output/detection_writer.py +0 -28
- contentctl/output/new_content_yml_output.py +0 -56
- contentctl/output/yml_output.py +0 -66
- contentctl-4.4.7.dist-info/RECORD +0 -173
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/LICENSE.md +0 -0
- {contentctl-4.4.7.dist-info → contentctl-5.0.0.dist-info}/entry_points.txt +0 -0
contentctl/objects/story_tags.py
CHANGED
|
@@ -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
|
|
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
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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?
|
contentctl/objects/test_group.py
CHANGED
|
@@ -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(
|
|
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:
|
contentctl/objects/throttling.py
CHANGED
|
@@ -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(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
18
|
-
raise ValueError(
|
|
19
|
-
|
|
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])
|
contentctl/objects/unit_test.py
CHANGED
|
@@ -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(
|
|
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 =
|
|
75
|
+
self.message = "ERROR with no more specific message available."
|
|
76
76
|
self.sid_link = NO_SID
|
|
77
77
|
|
|
78
78
|
return self.success
|