contentctl 4.3.5__py3-none-any.whl → 4.4.1__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/actions/build.py +1 -0
- contentctl/actions/detection_testing/GitService.py +10 -10
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +68 -38
- contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructureContainer.py +5 -1
- contentctl/actions/detection_testing/views/DetectionTestingViewWeb.py +10 -8
- contentctl/actions/inspect.py +6 -4
- contentctl/actions/new_content.py +10 -2
- contentctl/actions/release_notes.py +5 -3
- contentctl/actions/validate.py +2 -1
- contentctl/enrichments/cve_enrichment.py +6 -7
- contentctl/input/director.py +14 -12
- contentctl/input/new_content_questions.py +9 -42
- contentctl/objects/abstract_security_content_objects/detection_abstract.py +147 -7
- contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py +17 -9
- contentctl/objects/base_test_result.py +7 -7
- contentctl/objects/baseline.py +12 -18
- contentctl/objects/baseline_tags.py +2 -5
- contentctl/objects/config.py +15 -9
- contentctl/objects/constants.py +30 -0
- contentctl/objects/correlation_search.py +79 -114
- contentctl/objects/dashboard.py +100 -0
- contentctl/objects/deployment.py +20 -5
- contentctl/objects/detection_tags.py +22 -20
- contentctl/objects/drilldown.py +70 -0
- contentctl/objects/enums.py +26 -22
- contentctl/objects/investigation.py +23 -15
- contentctl/objects/investigation_tags.py +4 -3
- contentctl/objects/lookup.py +8 -1
- contentctl/objects/macro.py +16 -7
- contentctl/objects/notable_event.py +6 -5
- contentctl/objects/risk_analysis_action.py +4 -4
- contentctl/objects/risk_event.py +8 -7
- contentctl/objects/story.py +4 -16
- contentctl/objects/throttling.py +46 -0
- contentctl/output/conf_output.py +4 -0
- contentctl/output/conf_writer.py +20 -3
- contentctl/output/templates/analyticstories_detections.j2 +2 -2
- contentctl/output/templates/analyticstories_investigations.j2 +5 -5
- contentctl/output/templates/analyticstories_stories.j2 +1 -1
- contentctl/output/templates/savedsearches_baselines.j2 +2 -3
- contentctl/output/templates/savedsearches_detections.j2 +12 -7
- contentctl/output/templates/savedsearches_investigations.j2 +3 -4
- contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml +10 -1
- {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/METADATA +3 -2
- {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/RECORD +48 -46
- {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/WHEEL +1 -1
- contentctl/output/templates/finding_report.j2 +0 -30
- {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/LICENSE.md +0 -0
- {contentctl-4.3.5.dist-info → contentctl-4.4.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from contentctl.objects.enums import DataSource
|
|
3
|
+
|
|
4
|
+
|
|
1
5
|
class NewContentQuestions:
|
|
2
6
|
|
|
3
7
|
@classmethod
|
|
4
|
-
def get_questions_detection(
|
|
8
|
+
def get_questions_detection(cls) -> list[dict[str,Any]]:
|
|
5
9
|
questions = [
|
|
6
10
|
{
|
|
7
11
|
"type": "text",
|
|
@@ -45,46 +49,9 @@ class NewContentQuestions:
|
|
|
45
49
|
'type': 'checkbox',
|
|
46
50
|
'message': 'Your data source',
|
|
47
51
|
'name': 'data_source',
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"Sysmon Event ID 1",
|
|
52
|
-
"Sysmon Event ID 3",
|
|
53
|
-
"Sysmon Event ID 5",
|
|
54
|
-
"Sysmon Event ID 6",
|
|
55
|
-
"Sysmon Event ID 7",
|
|
56
|
-
"Sysmon Event ID 8",
|
|
57
|
-
"Sysmon Event ID 9",
|
|
58
|
-
"Sysmon Event ID 10",
|
|
59
|
-
"Sysmon Event ID 11",
|
|
60
|
-
"Sysmon Event ID 13",
|
|
61
|
-
"Sysmon Event ID 15",
|
|
62
|
-
"Sysmon Event ID 20",
|
|
63
|
-
"Sysmon Event ID 21",
|
|
64
|
-
"Sysmon Event ID 22",
|
|
65
|
-
"Sysmon Event ID 23",
|
|
66
|
-
"Windows Security 4624",
|
|
67
|
-
"Windows Security 4625",
|
|
68
|
-
"Windows Security 4648",
|
|
69
|
-
"Windows Security 4663",
|
|
70
|
-
"Windows Security 4688",
|
|
71
|
-
"Windows Security 4698",
|
|
72
|
-
"Windows Security 4703",
|
|
73
|
-
"Windows Security 4720",
|
|
74
|
-
"Windows Security 4732",
|
|
75
|
-
"Windows Security 4738",
|
|
76
|
-
"Windows Security 4741",
|
|
77
|
-
"Windows Security 4742",
|
|
78
|
-
"Windows Security 4768",
|
|
79
|
-
"Windows Security 4769",
|
|
80
|
-
"Windows Security 4771",
|
|
81
|
-
"Windows Security 4776",
|
|
82
|
-
"Windows Security 4781",
|
|
83
|
-
"Windows Security 4798",
|
|
84
|
-
"Windows Security 5136",
|
|
85
|
-
"Windows Security 5145",
|
|
86
|
-
"Windows System 7045"
|
|
87
|
-
]
|
|
52
|
+
#In the future, we should dynamically populate this from the DataSource Objects we have parsed from the data_sources directory
|
|
53
|
+
'choices': sorted(DataSource._value2member_map_ )
|
|
54
|
+
|
|
88
55
|
},
|
|
89
56
|
{
|
|
90
57
|
"type": "text",
|
|
@@ -116,7 +83,7 @@ class NewContentQuestions:
|
|
|
116
83
|
return questions
|
|
117
84
|
|
|
118
85
|
@classmethod
|
|
119
|
-
def get_questions_story(
|
|
86
|
+
def get_questions_story(cls)-> list[dict[str,Any]]:
|
|
120
87
|
questions = [
|
|
121
88
|
{
|
|
122
89
|
"type": "text",
|
|
@@ -20,7 +20,8 @@ from contentctl.objects.lookup import Lookup
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
21
|
from contentctl.input.director import DirectorOutputDto
|
|
22
22
|
from contentctl.objects.baseline import Baseline
|
|
23
|
-
|
|
23
|
+
from contentctl.objects.config import CustomApp
|
|
24
|
+
|
|
24
25
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
25
26
|
from contentctl.objects.enums import AnalyticsType
|
|
26
27
|
from contentctl.objects.enums import DataModel
|
|
@@ -35,11 +36,17 @@ from contentctl.objects.test_group import TestGroup
|
|
|
35
36
|
from contentctl.objects.integration_test import IntegrationTest
|
|
36
37
|
from contentctl.objects.data_source import DataSource
|
|
37
38
|
from contentctl.objects.base_test_result import TestResultStatus
|
|
38
|
-
|
|
39
|
-
# from contentctl.objects.playbook import Playbook
|
|
39
|
+
from contentctl.objects.drilldown import Drilldown, DRILLDOWN_SEARCH_PLACEHOLDER
|
|
40
40
|
from contentctl.objects.enums import ProvidingTechnology
|
|
41
41
|
from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
|
|
42
42
|
import datetime
|
|
43
|
+
from contentctl.objects.constants import (
|
|
44
|
+
ES_MAX_STANZA_LENGTH,
|
|
45
|
+
ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE,
|
|
46
|
+
CONTENTCTL_MAX_SEARCH_NAME_LENGTH,
|
|
47
|
+
CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE
|
|
48
|
+
)
|
|
49
|
+
|
|
43
50
|
MISSING_SOURCES: set[str] = set()
|
|
44
51
|
|
|
45
52
|
# Those AnalyticsTypes that we do not test via contentctl
|
|
@@ -51,8 +58,8 @@ SKIPPED_ANALYTICS_TYPES: set[str] = {
|
|
|
51
58
|
# TODO (#266): disable the use_enum_values configuration
|
|
52
59
|
class Detection_Abstract(SecurityContentObject):
|
|
53
60
|
model_config = ConfigDict(use_enum_values=True)
|
|
54
|
-
|
|
55
|
-
#
|
|
61
|
+
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
|
|
62
|
+
#contentType: SecurityContentType = SecurityContentType.detections
|
|
56
63
|
type: AnalyticsType = Field(...)
|
|
57
64
|
status: DetectionStatus = Field(...)
|
|
58
65
|
data_source: list[str] = []
|
|
@@ -60,6 +67,16 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
60
67
|
search: str = Field(...)
|
|
61
68
|
how_to_implement: str = Field(..., min_length=4)
|
|
62
69
|
known_false_positives: str = Field(..., min_length=4)
|
|
70
|
+
explanation: None | str = Field(
|
|
71
|
+
default=None,
|
|
72
|
+
exclude=True, #Don't serialize this value when dumping the object
|
|
73
|
+
description="Provide an explanation to be included "
|
|
74
|
+
"in the 'Explanation' field of the Detection in "
|
|
75
|
+
"the Use Case Library. If this field is not "
|
|
76
|
+
"defined in the YML, it will default to the "
|
|
77
|
+
"value of the 'description' field when "
|
|
78
|
+
"serialized in analyticstories_detections.j2",
|
|
79
|
+
)
|
|
63
80
|
|
|
64
81
|
enabled_by_default: bool = False
|
|
65
82
|
file_path: FilePath = Field(...)
|
|
@@ -70,9 +87,30 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
70
87
|
# https://github.com/pydantic/pydantic/issues/9101#issuecomment-2019032541
|
|
71
88
|
tests: List[Annotated[Union[UnitTest, IntegrationTest, ManualTest], Field(union_mode='left_to_right')]] = []
|
|
72
89
|
# A list of groups of tests, relying on the same data
|
|
73
|
-
test_groups:
|
|
90
|
+
test_groups: list[TestGroup] = []
|
|
74
91
|
|
|
75
92
|
data_source_objects: list[DataSource] = []
|
|
93
|
+
drilldown_searches: list[Drilldown] = Field(default=[], description="A list of Drilldowns that should be included with this search")
|
|
94
|
+
|
|
95
|
+
def get_conf_stanza_name(self, app:CustomApp)->str:
|
|
96
|
+
stanza_name = CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
|
|
97
|
+
self.check_conf_stanza_max_length(stanza_name)
|
|
98
|
+
return stanza_name
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_action_dot_correlationsearch_dot_label(self, app:CustomApp, max_stanza_length:int=ES_MAX_STANZA_LENGTH)->str:
|
|
102
|
+
stanza_name = self.get_conf_stanza_name(app)
|
|
103
|
+
stanza_name_after_saving_in_es = ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(
|
|
104
|
+
security_domain_value = self.tags.security_domain.value,
|
|
105
|
+
search_name = stanza_name
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if len(stanza_name_after_saving_in_es) > max_stanza_length:
|
|
110
|
+
raise ValueError(f"label may only be {max_stanza_length} characters to allow updating in-product, "
|
|
111
|
+
f"but stanza was actually {len(stanza_name_after_saving_in_es)} characters: '{stanza_name_after_saving_in_es}' ")
|
|
112
|
+
|
|
113
|
+
return stanza_name
|
|
76
114
|
|
|
77
115
|
@field_validator("search", mode="before")
|
|
78
116
|
@classmethod
|
|
@@ -130,6 +168,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
130
168
|
the model from the list of unit tests. Also, preemptively skips all manual tests, as well as
|
|
131
169
|
tests for experimental/deprecated detections and Correlation type detections.
|
|
132
170
|
"""
|
|
171
|
+
|
|
133
172
|
# Since ManualTest and UnitTest are not differentiable without looking at the manual_test
|
|
134
173
|
# tag, Pydantic builds all tests as UnitTest objects. If we see the manual_test flag, we
|
|
135
174
|
# convert these to ManualTest
|
|
@@ -248,6 +287,7 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
248
287
|
annotations_dict["cve"] = self.tags.cve
|
|
249
288
|
annotations_dict["impact"] = self.tags.impact
|
|
250
289
|
annotations_dict["type"] = self.type
|
|
290
|
+
annotations_dict["type_list"] = [self.type]
|
|
251
291
|
# annotations_dict["version"] = self.version
|
|
252
292
|
|
|
253
293
|
annotations_dict["data_source"] = self.data_source
|
|
@@ -518,13 +558,53 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
518
558
|
self.data_source_objects = matched_data_sources
|
|
519
559
|
|
|
520
560
|
for story in self.tags.analytic_story:
|
|
521
|
-
story.detections.append(self)
|
|
561
|
+
story.detections.append(self)
|
|
522
562
|
|
|
523
563
|
self.cve_enrichment_func(__context)
|
|
524
564
|
|
|
525
565
|
# Derive TestGroups and IntegrationTests, adjust for ManualTests, skip as needed
|
|
526
566
|
self.adjust_tests_and_groups()
|
|
527
567
|
|
|
568
|
+
# Ensure that if there is at least 1 drilldown, at least
|
|
569
|
+
# 1 of the drilldowns contains the string Drilldown.SEARCH_PLACEHOLDER.
|
|
570
|
+
# This is presently a requirement when 1 or more drilldowns are added to a detection.
|
|
571
|
+
# Note that this is only required for production searches that are not hunting
|
|
572
|
+
|
|
573
|
+
if self.type == AnalyticsType.Hunting.value or self.status != DetectionStatus.production.value:
|
|
574
|
+
#No additional check need to happen on the potential drilldowns.
|
|
575
|
+
pass
|
|
576
|
+
else:
|
|
577
|
+
found_placeholder = False
|
|
578
|
+
if len(self.drilldown_searches) < 2:
|
|
579
|
+
raise ValueError(f"This detection is required to have 2 drilldown_searches, but only has [{len(self.drilldown_searches)}]")
|
|
580
|
+
for drilldown in self.drilldown_searches:
|
|
581
|
+
if DRILLDOWN_SEARCH_PLACEHOLDER in drilldown.search:
|
|
582
|
+
found_placeholder = True
|
|
583
|
+
if not found_placeholder:
|
|
584
|
+
raise ValueError("Detection has one or more drilldown_searches, but none of them "
|
|
585
|
+
f"contained '{DRILLDOWN_SEARCH_PLACEHOLDER}. This is a requirement "
|
|
586
|
+
"if drilldown_searches are defined.'")
|
|
587
|
+
|
|
588
|
+
# Update the search fields with the original search, if required
|
|
589
|
+
for drilldown in self.drilldown_searches:
|
|
590
|
+
drilldown.perform_search_substitutions(self)
|
|
591
|
+
|
|
592
|
+
#For experimental purposes, add the default drilldowns
|
|
593
|
+
#self.drilldown_searches.extend(Drilldown.constructDrilldownsFromDetection(self))
|
|
594
|
+
|
|
595
|
+
@property
|
|
596
|
+
def drilldowns_in_JSON(self) -> list[dict[str,str]]:
|
|
597
|
+
"""This function is required for proper JSON
|
|
598
|
+
serializiation of drilldowns to occur in savedsearches.conf.
|
|
599
|
+
It returns the list[Drilldown] as a list[dict].
|
|
600
|
+
Without this function, the jinja template is unable
|
|
601
|
+
to convert list[Drilldown] to JSON
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
list[dict[str,str]]: List of Drilldowns dumped to dict format
|
|
605
|
+
"""
|
|
606
|
+
return [drilldown.model_dump() for drilldown in self.drilldown_searches]
|
|
607
|
+
|
|
528
608
|
@field_validator('lookups', mode="before")
|
|
529
609
|
@classmethod
|
|
530
610
|
def getDetectionLookups(cls, v:list[str], info:ValidationInfo) -> list[Lookup]:
|
|
@@ -653,6 +733,27 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
653
733
|
else:
|
|
654
734
|
self.tags.nist = [NistCategory.DE_AE]
|
|
655
735
|
return self
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
@model_validator(mode="after")
|
|
739
|
+
def ensureThrottlingFieldsExist(self):
|
|
740
|
+
'''
|
|
741
|
+
For throttling to work properly, the fields to throttle on MUST
|
|
742
|
+
exist in the search itself. If not, then we cannot apply the throttling
|
|
743
|
+
'''
|
|
744
|
+
if self.tags.throttling is None:
|
|
745
|
+
# No throttling configured for this detection
|
|
746
|
+
return self
|
|
747
|
+
|
|
748
|
+
missing_fields:list[str] = [field for field in self.tags.throttling.fields if field not in self.search]
|
|
749
|
+
if len(missing_fields) > 0:
|
|
750
|
+
raise ValueError(f"The following throttle fields were missing from the search: {missing_fields}")
|
|
751
|
+
|
|
752
|
+
else:
|
|
753
|
+
# All throttling fields present in search
|
|
754
|
+
return self
|
|
755
|
+
|
|
756
|
+
|
|
656
757
|
|
|
657
758
|
@model_validator(mode="after")
|
|
658
759
|
def ensureProperObservablesExist(self):
|
|
@@ -730,6 +831,45 @@ class Detection_Abstract(SecurityContentObject):
|
|
|
730
831
|
# Found everything
|
|
731
832
|
return self
|
|
732
833
|
|
|
834
|
+
@field_validator("tests", mode="before")
|
|
835
|
+
def ensure_yml_test_is_unittest(cls, v:list[dict]):
|
|
836
|
+
"""The typing for the tests field allows it to be one of
|
|
837
|
+
a number of different types of tests. However, ONLY
|
|
838
|
+
UnitTest should be allowed to be defined in the YML
|
|
839
|
+
file. If part of the UnitTest defined in the YML
|
|
840
|
+
is incorrect, such as the attack_data file, then
|
|
841
|
+
it will FAIL to be instantiated as a UnitTest and
|
|
842
|
+
may instead be instantiated as a different type of
|
|
843
|
+
test, such as IntegrationTest (since that requires
|
|
844
|
+
less fields) which is incorrect. Ensure that any
|
|
845
|
+
raw data read from the YML can actually construct
|
|
846
|
+
a valid UnitTest and, if not, return errors right
|
|
847
|
+
away instead of letting Pydantic try to construct
|
|
848
|
+
it into a different type of test
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
v (list[dict]): list of dicts read from the yml.
|
|
852
|
+
Each one SHOULD be a valid UnitTest. If we cannot
|
|
853
|
+
construct a valid unitTest from it, a ValueError should be raised
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
_type_: The input of the function, assuming no
|
|
857
|
+
ValueError is raised.
|
|
858
|
+
"""
|
|
859
|
+
valueErrors:list[ValueError] = []
|
|
860
|
+
for unitTest in v:
|
|
861
|
+
#This raises a ValueError on a failed UnitTest.
|
|
862
|
+
try:
|
|
863
|
+
UnitTest.model_validate(unitTest)
|
|
864
|
+
except ValueError as e:
|
|
865
|
+
valueErrors.append(e)
|
|
866
|
+
if len(valueErrors):
|
|
867
|
+
raise ValueError(valueErrors)
|
|
868
|
+
# All of these can be constructred as UnitTests with no
|
|
869
|
+
# Exceptions, so let the normal flow continue
|
|
870
|
+
return v
|
|
871
|
+
|
|
872
|
+
|
|
733
873
|
@field_validator("tests")
|
|
734
874
|
def tests_validate(
|
|
735
875
|
cls,
|
|
@@ -5,8 +5,10 @@ if TYPE_CHECKING:
|
|
|
5
5
|
from contentctl.objects.deployment import Deployment
|
|
6
6
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
7
7
|
from contentctl.input.director import DirectorOutputDto
|
|
8
|
+
from contentctl.objects.config import CustomApp
|
|
8
9
|
|
|
9
10
|
from contentctl.objects.enums import AnalyticsType
|
|
11
|
+
from contentctl.objects.constants import CONTENTCTL_MAX_STANZA_LENGTH
|
|
10
12
|
import abc
|
|
11
13
|
import uuid
|
|
12
14
|
import datetime
|
|
@@ -31,14 +33,14 @@ NO_FILE_NAME = "NO_FILE_NAME"
|
|
|
31
33
|
|
|
32
34
|
# TODO (#266): disable the use_enum_values configuration
|
|
33
35
|
class SecurityContentObject_Abstract(BaseModel, abc.ABC):
|
|
34
|
-
model_config = ConfigDict(use_enum_values=True,
|
|
35
|
-
|
|
36
|
-
name: str = Field(
|
|
37
|
-
author: str = Field(
|
|
38
|
-
date: datetime.date = Field(
|
|
39
|
-
version: NonNegativeInt =
|
|
40
|
-
id: uuid.UUID = Field(
|
|
41
|
-
description: str = Field(
|
|
36
|
+
model_config = ConfigDict(use_enum_values=True,validate_default=True)
|
|
37
|
+
|
|
38
|
+
name: str = Field(...,max_length=99)
|
|
39
|
+
author: str = Field(...,max_length=255)
|
|
40
|
+
date: datetime.date = Field(...)
|
|
41
|
+
version: NonNegativeInt = Field(...)
|
|
42
|
+
id: uuid.UUID = Field(...) #we set a default here until all content has a uuid
|
|
43
|
+
description: str = Field(...,max_length=10000)
|
|
42
44
|
file_path: Optional[FilePath] = None
|
|
43
45
|
references: Optional[List[HttpUrl]] = None
|
|
44
46
|
|
|
@@ -56,7 +58,13 @@ class SecurityContentObject_Abstract(BaseModel, abc.ABC):
|
|
|
56
58
|
"description": self.description,
|
|
57
59
|
"references": [str(url) for url in self.references or []]
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def check_conf_stanza_max_length(self, stanza_name:str, max_stanza_length:int=CONTENTCTL_MAX_STANZA_LENGTH) -> None:
|
|
64
|
+
if len(stanza_name) > max_stanza_length:
|
|
65
|
+
raise ValueError(f"conf stanza may only be {max_stanza_length} characters, "
|
|
66
|
+
f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' ")
|
|
67
|
+
|
|
60
68
|
@staticmethod
|
|
61
69
|
def objectListToNameList(objects: list[SecurityContentObject]) -> list[str]:
|
|
62
70
|
return [object.getName() for object in objects]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from typing import Union, Any
|
|
2
2
|
from enum import Enum
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel
|
|
5
|
-
from splunklib.data import Record
|
|
4
|
+
from pydantic import ConfigDict, BaseModel
|
|
5
|
+
from splunklib.data import Record # type: ignore
|
|
6
6
|
|
|
7
7
|
from contentctl.helper.utils import Utils
|
|
8
8
|
|
|
@@ -53,11 +53,11 @@ class BaseTestResult(BaseModel):
|
|
|
53
53
|
# The Splunk endpoint URL
|
|
54
54
|
sid_link: Union[None, str] = None
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
# Needed to allow for embedding of Exceptions in the model
|
|
57
|
+
model_config = ConfigDict(
|
|
58
|
+
validate_assignment=True,
|
|
59
|
+
arbitrary_types_allowed=True
|
|
60
|
+
)
|
|
61
61
|
|
|
62
62
|
@property
|
|
63
63
|
def passed(self) -> bool:
|
contentctl/objects/baseline.py
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Annotated, Optional, List,Any
|
|
4
4
|
from pydantic import field_validator, ValidationInfo, Field, model_serializer
|
|
5
|
-
if TYPE_CHECKING:
|
|
6
|
-
from contentctl.input.director import DirectorOutputDto
|
|
7
|
-
|
|
8
5
|
from contentctl.objects.deployment import Deployment
|
|
9
6
|
from contentctl.objects.security_content_object import SecurityContentObject
|
|
10
|
-
from contentctl.objects.enums import DataModel
|
|
7
|
+
from contentctl.objects.enums import DataModel
|
|
11
8
|
from contentctl.objects.baseline_tags import BaselineTags
|
|
12
|
-
from contentctl.objects.enums import DeploymentType
|
|
13
|
-
#from contentctl.objects.deployment import Deployment
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# from contentctl.input.director import DirectorOutputDto
|
|
10
|
+
from contentctl.objects.config import CustomApp
|
|
11
|
+
|
|
18
12
|
|
|
13
|
+
from contentctl.objects.constants import CONTENTCTL_MAX_SEARCH_NAME_LENGTH,CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE
|
|
19
14
|
|
|
20
15
|
class Baseline(SecurityContentObject):
|
|
21
|
-
|
|
22
|
-
#name: str
|
|
23
|
-
#id: str
|
|
24
|
-
#version: int
|
|
25
|
-
#date: str
|
|
26
|
-
#author: str
|
|
27
|
-
#contentType: SecurityContentType = SecurityContentType.baselines
|
|
16
|
+
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
|
|
28
17
|
type: Annotated[str,Field(pattern="^Baseline$")] = Field(...)
|
|
29
18
|
datamodel: Optional[List[DataModel]] = None
|
|
30
|
-
#description: str
|
|
31
19
|
search: str = Field(..., min_length=4)
|
|
32
20
|
how_to_implement: str = Field(..., min_length=4)
|
|
33
21
|
known_false_positives: str = Field(..., min_length=4)
|
|
@@ -35,6 +23,12 @@ class Baseline(SecurityContentObject):
|
|
|
35
23
|
|
|
36
24
|
# enrichment
|
|
37
25
|
deployment: Deployment = Field({})
|
|
26
|
+
|
|
27
|
+
|
|
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)
|
|
30
|
+
self.check_conf_stanza_max_length(stanza_name)
|
|
31
|
+
return stanza_name
|
|
38
32
|
|
|
39
33
|
@field_validator("deployment", mode="before")
|
|
40
34
|
def getDeployment(cls, v:Any, info:ValidationInfo)->Deployment:
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
3
2
|
from pydantic import BaseModel, Field, field_validator, ValidationInfo, model_serializer
|
|
4
3
|
from typing import List, Any, Union
|
|
5
4
|
|
|
6
5
|
from contentctl.objects.story import Story
|
|
7
|
-
from contentctl.objects.deployment import Deployment
|
|
8
6
|
from contentctl.objects.detection import Detection
|
|
9
7
|
from contentctl.objects.enums import SecurityContentProductName
|
|
10
8
|
from contentctl.objects.enums import SecurityDomain
|
|
11
|
-
|
|
12
|
-
from contentctl.input.director import DirectorOutputDto
|
|
9
|
+
|
|
13
10
|
|
|
14
11
|
|
|
15
12
|
|
|
@@ -19,7 +16,7 @@ class BaselineTags(BaseModel):
|
|
|
19
16
|
#deployment: Deployment = Field('SET_IN_GET_DEPLOYMENT_FUNCTION')
|
|
20
17
|
# TODO (#223): can we remove str from the possible types here?
|
|
21
18
|
detections: List[Union[Detection,str]] = Field(...)
|
|
22
|
-
product:
|
|
19
|
+
product: List[SecurityContentProductName] = Field(...,min_length=1)
|
|
23
20
|
required_fields: List[str] = Field(...,min_length=1)
|
|
24
21
|
security_domain: SecurityDomain = Field(...)
|
|
25
22
|
|
contentctl/objects/config.py
CHANGED
|
@@ -158,8 +158,7 @@ class CustomApp(App_Base):
|
|
|
158
158
|
str(destination),
|
|
159
159
|
verbose_print=True)
|
|
160
160
|
return str(destination)
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
|
|
163
162
|
# TODO (#266): disable the use_enum_values configuration
|
|
164
163
|
class Config_Base(BaseModel):
|
|
165
164
|
model_config = ConfigDict(use_enum_values=True,validate_default=True, arbitrary_types_allowed=True)
|
|
@@ -287,7 +286,6 @@ class build(validate):
|
|
|
287
286
|
|
|
288
287
|
def getAppTemplatePath(self)->pathlib.Path:
|
|
289
288
|
return self.path/"app_template"
|
|
290
|
-
|
|
291
289
|
|
|
292
290
|
|
|
293
291
|
class StackType(StrEnum):
|
|
@@ -310,6 +308,16 @@ class inspect(build):
|
|
|
310
308
|
"should be enabled."
|
|
311
309
|
)
|
|
312
310
|
)
|
|
311
|
+
suppress_missing_content_exceptions: bool = Field(
|
|
312
|
+
default=False,
|
|
313
|
+
description=(
|
|
314
|
+
"Suppress exceptions during metadata validation if a detection that existed in "
|
|
315
|
+
"the previous build does not exist in this build. This is to ensure that content "
|
|
316
|
+
"is not accidentally removed. In order to support testing both public and private "
|
|
317
|
+
"content, this warning can be suppressed. If it is suppressed, it will still be "
|
|
318
|
+
"printed out as a warning."
|
|
319
|
+
)
|
|
320
|
+
)
|
|
313
321
|
enrichments: bool = Field(
|
|
314
322
|
default=True,
|
|
315
323
|
description=(
|
|
@@ -951,15 +959,15 @@ class test_servers(test_common):
|
|
|
951
959
|
index+=1
|
|
952
960
|
|
|
953
961
|
|
|
954
|
-
|
|
955
962
|
class release_notes(Config_Base):
|
|
956
963
|
old_tag:Optional[str] = Field(None, description="Name of the tag to diff against to find new content. "
|
|
957
964
|
"If it is not supplied, then it will be inferred as the "
|
|
958
965
|
"second newest tag at runtime.")
|
|
959
966
|
new_tag:Optional[str] = Field(None, description="Name of the tag containing new content. If it is not supplied,"
|
|
960
967
|
" then it will be inferred as the newest tag at runtime.")
|
|
961
|
-
latest_branch:Optional[str] = Field(None, description="Branch for which we are generating release notes")
|
|
962
|
-
|
|
968
|
+
latest_branch:Optional[str] = Field(None, description="Branch name for which we are generating release notes for")
|
|
969
|
+
compare_against:Optional[str] = Field(default="develop", description="Branch name for which we are comparing the files changes against")
|
|
970
|
+
|
|
963
971
|
def releaseNotesFilename(self, filename:str)->pathlib.Path:
|
|
964
972
|
#Assume that notes are written to dist/. This does not respect build_dir since that is
|
|
965
973
|
#only a member of build
|
|
@@ -1033,6 +1041,4 @@ class release_notes(Config_Base):
|
|
|
1033
1041
|
# raise ValueError("The latest_branch '{self.latest_branch}' was not found in the repository")
|
|
1034
1042
|
|
|
1035
1043
|
|
|
1036
|
-
# return self
|
|
1037
|
-
|
|
1038
|
-
|
|
1044
|
+
# return self
|
contentctl/objects/constants.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# Use for calculation of maximum length of name field
|
|
2
|
+
from contentctl.objects.enums import SecurityDomain
|
|
1
3
|
|
|
2
4
|
ATTACK_TACTICS_KILLCHAIN_MAPPING = {
|
|
3
5
|
"Reconnaissance": "Reconnaissance",
|
|
@@ -140,3 +142,31 @@ RBA_OBSERVABLE_ROLE_MAPPING = {
|
|
|
140
142
|
|
|
141
143
|
# The relative path to the directory where any apps/packages will be downloaded
|
|
142
144
|
DOWNLOADS_DIRECTORY = "downloads"
|
|
145
|
+
|
|
146
|
+
# Maximum length of the name field for a search.
|
|
147
|
+
# This number is derived from a limitation that exists in
|
|
148
|
+
# ESCU where a search cannot be edited, due to validation
|
|
149
|
+
# errors, if its name is longer than 99 characters.
|
|
150
|
+
# When an saved search is cloned in Enterprise Security User Interface,
|
|
151
|
+
# it is wrapped in the following:
|
|
152
|
+
# {Detection.tags.security_domain.value} - {SEARCH_STANZA_NAME} - Rule
|
|
153
|
+
# Similarly, when we generate the search stanza name in contentctl, it
|
|
154
|
+
# is app.label - detection.name - Rule
|
|
155
|
+
# However, in product the search name is:
|
|
156
|
+
# {CustomApp.label} - {detection.name} - Rule,
|
|
157
|
+
# or in ESCU:
|
|
158
|
+
# ESCU - {detection.name} - Rule,
|
|
159
|
+
# this gives us a maximum length below.
|
|
160
|
+
# When an ESCU search is cloned, it will
|
|
161
|
+
# have a full name like (the following is NOT a typo):
|
|
162
|
+
# Endpoint - ESCU - Name of Search From YML File - Rule - Rule
|
|
163
|
+
# The math below accounts for all these caveats
|
|
164
|
+
ES_MAX_STANZA_LENGTH = 99
|
|
165
|
+
CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name} - Rule"
|
|
166
|
+
CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name}"
|
|
167
|
+
CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name} - Response Task"
|
|
168
|
+
|
|
169
|
+
ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE = "{security_domain_value} - {search_name} - Rule"
|
|
170
|
+
SECURITY_DOMAIN_MAX_LENGTH = max([len(SecurityDomain[value]) for value in SecurityDomain._member_map_])
|
|
171
|
+
CONTENTCTL_MAX_STANZA_LENGTH = ES_MAX_STANZA_LENGTH - len(ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(security_domain_value="X"*SECURITY_DOMAIN_MAX_LENGTH,search_name=""))
|
|
172
|
+
CONTENTCTL_MAX_SEARCH_NAME_LENGTH = CONTENTCTL_MAX_STANZA_LENGTH - len(CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE.format(app_label="ESCU", detection_name=""))
|