Skip to content

Commit 160b73a

Browse files
authored
Merge branch 'main' into snapattack_datasource_enrichments
2 parents b5fbe24 + fcf60ca commit 160b73a

File tree

9 files changed

+105
-28
lines changed

9 files changed

+105
-28
lines changed
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
from pydantic import Field
21
from typing import Annotated
32

3+
from pydantic import Field
4+
45
CVE_TYPE = Annotated[str, Field(pattern=r"^CVE-[1|2]\d{3}-\d+$")]
5-
MITRE_ATTACK_ID_TYPE = Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")]
6+
MITRE_ATTACK_ID_TYPE_PARENT = Annotated[str, Field(pattern=r"^T\d{4}$")]
7+
MITRE_ATTACK_ID_TYPE_SUBTYPE = Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})$")]
8+
MITRE_ATTACK_ID_TYPE = MITRE_ATTACK_ID_TYPE_PARENT | MITRE_ATTACK_ID_TYPE_SUBTYPE
69
APPID_TYPE = Annotated[str, Field(pattern="^[a-zA-Z0-9_-]+$")]

contentctl/objects/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@
123123
CONTENTCTL_DETECTION_STANZA_NAME_FORMAT_TEMPLATE = (
124124
"{app_label} - {detection_name} - Rule"
125125
)
126+
127+
CONTENTCTL_DASHBOARD_LABEL_TEMPLATE = "{app_label} - {dashboard_name}"
126128
CONTENTCTL_BASELINE_STANZA_NAME_FORMAT_TEMPLATE = "{app_label} - {detection_name}"
127129
CONTENTCTL_RESPONSE_TASK_NAME_FORMAT_TEMPLATE = (
128130
"{app_label} - {detection_name} - Response Task"

contentctl/objects/dashboard.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
import json
2+
import pathlib
3+
from enum import StrEnum
14
from typing import Any
2-
from pydantic import Field, Json, model_validator
35

4-
import pathlib
56
from jinja2 import Environment
6-
import json
7-
from contentctl.objects.security_content_object import SecurityContentObject
7+
from pydantic import Field, Json, model_validator
8+
89
from contentctl.objects.config import build
9-
from enum import StrEnum
10+
from contentctl.objects.constants import CONTENTCTL_DASHBOARD_LABEL_TEMPLATE
11+
from contentctl.objects.security_content_object import SecurityContentObject
1012

11-
DEFAULT_DASHBAORD_JINJA2_TEMPLATE = """<dashboard version="2" theme="{{ dashboard.theme }}">
13+
DEFAULT_DASHBOARD_JINJA2_TEMPLATE = """<dashboard version="2" theme="{{ dashboard.theme }}">
1214
<label>{{ dashboard.label(config) }}</label>
1315
<description></description>
1416
<definition><![CDATA[
@@ -31,7 +33,7 @@ class DashboardTheme(StrEnum):
3133

3234
class Dashboard(SecurityContentObject):
3335
j2_template: str = Field(
34-
default=DEFAULT_DASHBAORD_JINJA2_TEMPLATE,
36+
default=DEFAULT_DASHBOARD_JINJA2_TEMPLATE,
3537
description="Jinja2 Template used to construct the dashboard",
3638
)
3739
description: str = Field(
@@ -49,7 +51,9 @@ class Dashboard(SecurityContentObject):
4951
)
5052

5153
def label(self, config: build) -> str:
52-
return f"{config.app.label} - {self.name}"
54+
return CONTENTCTL_DASHBOARD_LABEL_TEMPLATE.format(
55+
app_label=config.app.label, dashboard_name=self.name
56+
)
5357

5458
@model_validator(mode="before")
5559
@classmethod
@@ -98,7 +102,9 @@ def pretty_print_json_obj(self):
98102
return json.dumps(self.json_obj, indent=4)
99103

100104
def getOutputFilepathRelativeToAppRoot(self, config: build) -> pathlib.Path:
101-
filename = f"{self.file_path.stem}.xml".lower()
105+
# for clarity, the name of the dashboard file will follow the same convention
106+
# as we use for detections, prefixing it with app_name -
107+
filename = f"{self.label(config)}.xml"
102108
return pathlib.Path("default/data/ui/views") / filename
103109

104110
def writeDashboardFile(self, j2_env: Environment, config: build):

contentctl/objects/detection_tags.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@
3333
SecurityContentProductName,
3434
SecurityDomain,
3535
)
36-
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
36+
from contentctl.objects.mitre_attack_enrichment import (
37+
MitreAttackEnrichment,
38+
MitreAttackGroup,
39+
)
3740

3841

3942
class DetectionTags(BaseModel):
@@ -44,7 +47,7 @@ class DetectionTags(BaseModel):
4447
asset_type: AssetType = Field(...)
4548
group: list[str] = []
4649

47-
mitre_attack_id: List[MITRE_ATTACK_ID_TYPE] = []
50+
mitre_attack_id: list[MITRE_ATTACK_ID_TYPE] = []
4851
nist: list[NistCategory] = []
4952

5053
product: list[SecurityContentProductName] = Field(..., min_length=1)
@@ -68,6 +71,15 @@ def kill_chain_phases(self) -> list[KillChainPhase]:
6871
phases.add(phase)
6972
return sorted(list(phases))
7073

74+
# We do not want this to be included in serialization. By default, @property
75+
# objects are not included in dumps
76+
@property
77+
def unique_mitre_attack_groups(self) -> list[MitreAttackGroup]:
78+
group_set: set[MitreAttackGroup] = set()
79+
for enrichment in self.mitre_attack_enrichments:
80+
group_set.update(set(enrichment.mitre_attack_group_objects))
81+
return sorted(group_set, key=lambda k: k.group)
82+
7183
# enum is intentionally Cis18 even though field is named cis20 for legacy reasons
7284
@computed_field
7385
@property
@@ -134,8 +146,8 @@ def addAttackEnrichment(self, info: ValidationInfo):
134146

135147
if len(missing_tactics) > 0:
136148
raise ValueError(f"Missing Mitre Attack IDs. {missing_tactics} not found.")
137-
else:
138-
self.mitre_attack_enrichments = mitre_enrichments
149+
150+
self.mitre_attack_enrichments = mitre_enrichments
139151

140152
return self
141153

@@ -159,6 +171,44 @@ def addAttackEnrichments(cls, v:list[MitreAttackEnrichment], info:ValidationInfo
159171
return enrichments
160172
"""
161173

174+
@field_validator("mitre_attack_id", mode="after")
175+
@classmethod
176+
def sameTypeAndSubtypeNotPresent(
177+
cls, techniques_and_subtechniques: list[MITRE_ATTACK_ID_TYPE]
178+
) -> list[MITRE_ATTACK_ID_TYPE]:
179+
techniques: list[str] = [
180+
f"{unknown_technique}."
181+
for unknown_technique in techniques_and_subtechniques
182+
if "." not in unknown_technique
183+
]
184+
subtechniques: list[MITRE_ATTACK_ID_TYPE] = [
185+
unknown_technique
186+
for unknown_technique in techniques_and_subtechniques
187+
if "." in unknown_technique
188+
]
189+
subtype_and_parent_exist_exceptions: list[ValueError] = []
190+
191+
for subtechnique in subtechniques:
192+
for technique in techniques:
193+
if subtechnique.startswith(technique):
194+
subtype_and_parent_exist_exceptions.append(
195+
ValueError(
196+
f" Technique : {technique.split('.')[0]}\n"
197+
f" SubTechnique: {subtechnique}\n"
198+
)
199+
)
200+
201+
if len(subtype_and_parent_exist_exceptions):
202+
error_string = "\n".join(
203+
str(e) for e in subtype_and_parent_exist_exceptions
204+
)
205+
raise ValueError(
206+
"Overlapping MITRE Attack ID Techniques and Subtechniques may not be defined. "
207+
f"Remove the Technique and keep the Subtechnique:\n{error_string}"
208+
)
209+
210+
return techniques_and_subtechniques
211+
162212
@field_validator("analytic_story", mode="before")
163213
@classmethod
164214
def mapStoryNamesToStoryObjects(
@@ -238,3 +288,6 @@ def mapAtomicGuidsToAtomicTests(
238288
return matched_tests + [
239289
AtomicTest.AtomicTestWhenTestIsMissing(test) for test in missing_tests
240290
]
291+
return matched_tests + [
292+
AtomicTest.AtomicTestWhenTestIsMissing(test) for test in missing_tests
293+
]

contentctl/objects/mitre_attack_enrichment.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
2-
from pydantic import BaseModel, Field, ConfigDict, HttpUrl, field_validator
3-
from typing import List
4-
from enum import StrEnum
2+
53
import datetime
4+
from enum import StrEnum
5+
from typing import List
6+
7+
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
8+
69
from contentctl.objects.annotated_types import MITRE_ATTACK_ID_TYPE
710

811

@@ -84,6 +87,16 @@ def standardize_contributors(cls, contributors: list[str] | None) -> list[str]:
8487
return []
8588
return contributors
8689

90+
def __lt__(self, other: MitreAttackGroup) -> bool:
91+
if not isinstance(object, MitreAttackGroup):
92+
raise Exception(
93+
f"Cannot compare object of type MitreAttackGroup to object of type [{type(object).__name__}]"
94+
)
95+
return self.group < other.group
96+
97+
def __hash__(self) -> int:
98+
return hash(self.group)
99+
87100

88101
class MitreAttackEnrichment(BaseModel):
89102
ConfigDict(extra="forbid")

contentctl/objects/story_tags.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
from __future__ import annotations
2-
from pydantic import BaseModel, Field, model_serializer, ConfigDict
3-
from typing import List, Set, Optional
42

53
from enum import Enum
4+
from typing import List, Optional, Set
65

7-
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
6+
from pydantic import BaseModel, ConfigDict, Field, model_serializer
7+
8+
from contentctl.objects.annotated_types import CVE_TYPE, MITRE_ATTACK_ID_TYPE
89
from contentctl.objects.enums import (
9-
StoryCategory,
1010
DataModel,
1111
KillChainPhase,
1212
SecurityContentProductName,
13+
StoryCategory,
1314
)
14-
from contentctl.objects.annotated_types import CVE_TYPE, MITRE_ATTACK_ID_TYPE
15+
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
1516

1617

1718
class StoryUseCase(str, Enum):
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<nav search_view="search" color="#65A637">
22
<view name="escu_summary" default="true"/>
3-
<view name="feedback"/>
43
<view name="search"/>
5-
<view name="dashboards"/>
6-
<a href="http://docs.splunk.com/Documentation/ESSOC">Docs</a>
4+
<collection label="Dashboards">
5+
<view source="unclassified" match=" - "/>
6+
</collection>
77
</nav>

contentctl/templates/detections/endpoint/anomalous_usage_of_7zip.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ tags:
6060
asset_type: Endpoint
6161
mitre_attack_id:
6262
- T1560.001
63-
- T1560
6463
product:
6564
- Splunk Enterprise
6665
- Splunk Enterprise Security

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "contentctl"
3-
version = "5.0.2"
3+
version = "5.0.4"
44

55
description = "Splunk Content Control Tool"
66
authors = ["STRT <[email protected]>"]

0 commit comments

Comments
 (0)