Skip to content

Commit d66a9f6

Browse files
committed
Updates in response to PR review
with colleague.
1 parent 3605586 commit d66a9f6

File tree

7 files changed

+72
-52
lines changed

7 files changed

+72
-52
lines changed

contentctl/objects/abstract_security_content_objects/detection_abstract.py

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
from contentctl.objects.enums import ProvidingTechnology
4141
from contentctl.enrichments.cve_enrichment import CveEnrichmentObj
4242
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+
)
48+
4349
MISSING_SOURCES: set[str] = set()
4450

4551
# Those AnalyticsTypes that we do not test via contentctl
@@ -51,7 +57,7 @@
5157
# TODO (#266): disable the use_enum_values configuration
5258
class Detection_Abstract(SecurityContentObject):
5359
model_config = ConfigDict(use_enum_values=True)
54-
name:str = Field(...,max_length=67)
60+
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
5561
#contentType: SecurityContentType = SecurityContentType.detections
5662
type: AnalyticsType = Field(...)
5763
status: DetectionStatus = Field(...)
@@ -74,26 +80,19 @@ class Detection_Abstract(SecurityContentObject):
7480

7581
data_source_objects: list[DataSource] = []
7682

77-
def get_action_dot_correlationsearch_dot_label(self, app:CustomApp, max_stanza_length:int=99)->str:
78-
label = self.get_conf_stanza_name(app)
79-
label_after_saving_in_product = f"{self.tags.security_domain.value} - {label} - Rule"
80-
81-
if len(label_after_saving_in_product) > max_stanza_length:
82-
raise ValueError(f"label may only be {max_stanza_length} characters to allow updating in-product, "
83-
f"but stanza was actually {len(label_after_saving_in_product)} characters: '{label_after_saving_in_product}' ")
83+
def get_action_dot_correlationsearch_dot_label(self, app:CustomApp, max_stanza_length:int=ES_MAX_STANZA_LENGTH)->str:
84+
stanza_name = self.get_conf_stanza_name(app)
85+
stanza_name_after_saving_in_es = ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE.format(
86+
security_domain_value = self.tags.security_domain.value,
87+
search_name = stanza_name
88+
)
8489

85-
return label
86-
87-
def get_conf_stanza_name(self, app:CustomApp, max_stanza_length:int=81)->str:
88-
stanza_name = f"{app.label} - {self.name} - Rule"
89-
if len(stanza_name) > max_stanza_length:
90-
raise ValueError(f"conf stanza may only be {max_stanza_length} characters, "
91-
f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' ")
92-
#print(f"Stanza Length[{len(stanza_name)}]")
93-
return stanza_name
9490

91+
if len(stanza_name_after_saving_in_es) > max_stanza_length:
92+
raise ValueError(f"label may only be {max_stanza_length} characters to allow updating in-product, "
93+
f"but stanza was actually {len(stanza_name_after_saving_in_es)} characters: '{stanza_name_after_saving_in_es}' ")
9594

96-
95+
return stanza_name
9796

9897
@field_validator("search", mode="before")
9998
@classmethod
@@ -674,7 +673,7 @@ def addTags_nist(self):
674673
else:
675674
self.tags.nist = [NistCategory.DE_AE]
676675
return self
677-
676+
678677

679678
@model_validator(mode="after")
680679
def ensureThrottlingFieldsExist(self):
@@ -685,10 +684,6 @@ def ensureThrottlingFieldsExist(self):
685684
if self.tags.throttling is None:
686685
# No throttling configured for this detection
687686
return self
688-
689-
if not isinstance(self.search, str):
690-
# Search is sigma-formatted, so we cannot perform this validation.
691-
return self
692687

693688
missing_fields:list[str] = [field for field in self.tags.throttling.fields if field not in self.search]
694689
if len(missing_fields) > 0:

contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
from contentctl.objects.deployment import Deployment
66
from contentctl.objects.security_content_object import SecurityContentObject
77
from contentctl.input.director import DirectorOutputDto
8+
from contentctl.objects.config import CustomApp
89

910
from contentctl.objects.enums import AnalyticsType
11+
from contentctl.objects.constants import CONTENTCTL_MAX_STANZA_LENGTH, CONTENTCTL_STANZA_NAME_FORMAT_TEMPLATE
1012
import abc
1113
import uuid
1214
import datetime
@@ -56,7 +58,14 @@ def serialize_model(self):
5658
"description": self.description,
5759
"references": [str(url) for url in self.references or []]
5860
}
59-
61+
62+
def get_conf_stanza_name(self, app:CustomApp, max_stanza_length:int=CONTENTCTL_MAX_STANZA_LENGTH)->str:
63+
stanza_name = CONTENTCTL_STANZA_NAME_FORMAT_TEMPLATE.format(app_label=app.label, detection_name=self.name)
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+
return stanza_name
68+
6069
@staticmethod
6170
def objectListToNameList(objects: list[SecurityContentObject]) -> list[str]:
6271
return [object.getName() for object in objects]

contentctl/objects/baseline.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,11 @@
66
from contentctl.objects.security_content_object import SecurityContentObject
77
from contentctl.objects.enums import DataModel
88
from contentctl.objects.baseline_tags import BaselineTags
9-
from contentctl.objects.config import CustomApp
10-
#from contentctl.objects.deployment import Deployment
11-
12-
# from typing import TYPE_CHECKING
13-
# if TYPE_CHECKING:
14-
# from contentctl.input.director import DirectorOutputDto
159

10+
from contentctl.objects.constants import CONTENTCTL_MAX_SEARCH_NAME_LENGTH
1611

1712
class Baseline(SecurityContentObject):
18-
name:str = Field(...,max_length=67)
13+
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
1914
type: Annotated[str,Field(pattern="^Baseline$")] = Field(...)
2015
datamodel: Optional[List[DataModel]] = None
2116
search: str = Field(..., min_length=4)
@@ -26,14 +21,6 @@ class Baseline(SecurityContentObject):
2621
# enrichment
2722
deployment: Deployment = Field({})
2823

29-
def get_conf_stanza_name(self, app:CustomApp, max_stanza_length:int=81)->str:
30-
stanza_name = f"{app.label} - {self.name}"
31-
if len(stanza_name) > max_stanza_length:
32-
raise ValueError(f"conf stanza may only be {max_stanza_length} characters, "
33-
f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' ")
34-
#print(f"Stanza Length[{len(stanza_name)}]")
35-
return stanza_name
36-
3724

3825
@field_validator("deployment", mode="before")
3926
def getDeployment(cls, v:Any, info:ValidationInfo)->Deployment:

contentctl/objects/constants.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# Use for calculation of maximum length of name field
2+
from contentctl.objects.enums import SecurityDomain
13

24
ATTACK_TACTICS_KILLCHAIN_MAPPING = {
35
"Reconnaissance": "Reconnaissance",
@@ -140,3 +142,28 @@
140142

141143
# The relative path to the directory where any apps/packages will be downloaded
142144
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_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name} - Rule"
166+
ES_SEARCH_STANZA_NAME_FORMAT_AFTER_CLONING_IN_PRODUCT_TEMPLATE = "{security_domain_value} - {search_name} - Rule"
167+
SECURITY_DOMAIN_MAX_LENGTH = max([len(SecurityDomain[value]) for value in SecurityDomain._member_map_])
168+
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=""))
169+
CONTENTCTL_MAX_SEARCH_NAME_LENGTH = CONTENTCTL_MAX_STANZA_LENGTH - len(CONTENTCTL_STANZA_NAME_FORMAT_TEMPLATE.format(app_label="ESCU", detection_name=""))

contentctl/objects/dashboard.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
from pydantic import Field, Json, model_validator
33

44
import pathlib
5-
import copy
65
from jinja2 import Environment
76
import json
87
from contentctl.objects.security_content_object import SecurityContentObject
98
from contentctl.objects.config import build
9+
from enum import StrEnum
1010

11-
DEFAULT_DASHBAORD_JINJA2_TEMPLATE = '''<dashboard version="2" theme="light">
11+
DEFAULT_DASHBAORD_JINJA2_TEMPLATE = '''<dashboard version="2" theme="{{ dashboard.theme }}">
1212
<label>{{ dashboard.label(config) }}</label>
1313
<description></description>
1414
<definition><![CDATA[
@@ -23,11 +23,15 @@
2323
]]></meta>
2424
</dashboard>'''
2525

26+
class DashboardTheme(StrEnum):
27+
light = "light"
28+
dark = "dark"
29+
2630
class Dashboard(SecurityContentObject):
2731
j2_template: str = Field(default=DEFAULT_DASHBAORD_JINJA2_TEMPLATE, description="Jinja2 Template used to construct the dashboard")
2832
description: str = Field(...,description="A description of the dashboard. This does not have to match "
2933
"the description of the dashboard in the JSON file.", max_length=10000)
30-
34+
theme: DashboardTheme = Field(default=DashboardTheme.dark, description="The theme of the dashboard. Choose between 'light' and 'dark'.")
3135
json_obj: Json[dict[str,Any]] = Field(..., description="Valid JSON object that describes the dashboard")
3236

3337

contentctl/objects/investigation.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
from contentctl.objects.security_content_object import SecurityContentObject
66
from contentctl.objects.enums import DataModel
77
from contentctl.objects.investigation_tags import InvestigationTags
8-
from contentctl.objects.config import CustomApp
8+
9+
from contentctl.objects.constants import CONTENTCTL_MAX_SEARCH_NAME_LENGTH
910

1011
# TODO (#266): disable the use_enum_values configuration
1112
class Investigation(SecurityContentObject):
1213
model_config = ConfigDict(use_enum_values=True,validate_default=False)
1314
type: str = Field(...,pattern="^Investigation$")
1415
datamodel: list[DataModel] = Field(...)
15-
name:str = Field(...,max_length=67)
16+
name:str = Field(...,max_length=CONTENTCTL_MAX_SEARCH_NAME_LENGTH)
1617
search: str = Field(...)
1718
how_to_implement: str = Field(...)
1819
known_false_positives: str = Field(...)
@@ -69,13 +70,5 @@ def model_post_init(self, ctx:dict[str,Any]):
6970
for story in self.tags.analytic_story:
7071
story.investigations.append(self)
7172

72-
def get_conf_stanza_name(self, app:CustomApp, max_stanza_length:int=81)->str:
73-
stanza_name = f"{app.label} - {self.name} - Response Task"
74-
if len(stanza_name) > max_stanza_length:
75-
raise ValueError(f"conf stanza may only be {max_stanza_length} characters, "
76-
f"but stanza was actually {len(stanza_name)} characters: '{stanza_name}' ")
77-
#print(f"Stanza Length[{len(stanza_name)}]")
78-
return stanza_name
79-
8073

8174

contentctl/output/conf_writer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ def writeDashboardFiles(config:build, dashboards:list[Dashboard])->set[pathlib.P
113113
written_files:set[pathlib.Path] = set()
114114
for dashboard in dashboards:
115115
output_file_path = dashboard.getOutputFilepathRelativeToAppRoot(config)
116+
# Check that the full output path does not exist so that we are not having an
117+
# name collision with a file in app_template
118+
if (config.getPackageDirectoryPath()/output_file_path).exists():
119+
raise FileExistsError(f"ERROR: Overwriting Dashboard File {output_file_path}. Does this file exist in {config.getAppTemplatePath()} AND {config.path/'dashboards'}?")
120+
116121
ConfWriter.writeXmlFileHeader(output_file_path, config)
117122
dashboard.writeDashboardFile(ConfWriter.getJ2Environment(), config)
118123
ConfWriter.validateXmlFile(config.getPackageDirectoryPath()/output_file_path)

0 commit comments

Comments
 (0)