Skip to content

Commit 0ef9754

Browse files
authored
Merge pull request #274 from splunk/improve_lookup_regex
Improve lookup regex - Step 1 - ESCU 5.0
2 parents 285acf1 + 78aa05e commit 0ef9754

File tree

15 files changed

+546
-421
lines changed

15 files changed

+546
-421
lines changed

contentctl/actions/build.py

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
from contentctl.output.conf_writer import ConfWriter
1111
from contentctl.output.api_json_output import ApiJsonOutput
1212
from contentctl.output.data_source_writer import DataSourceWriter
13-
from contentctl.objects.lookup import Lookup
13+
from contentctl.objects.lookup import CSVLookup, Lookup_Type
1414
import pathlib
1515
import json
1616
import datetime
17-
from typing import Union
17+
import uuid
1818

1919
from contentctl.objects.config import build
2020

@@ -34,27 +34,41 @@ def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
3434
updated_conf_files:set[pathlib.Path] = set()
3535
conf_output = ConfOutput(input_dto.config)
3636

37+
38+
# Construct a path to a YML that does not actually exist.
39+
# We mock this "fake" path since the YML does not exist.
40+
# This ensures the checking for the existence of the CSV is correct
41+
data_sources_fake_yml_path = input_dto.config.getPackageDirectoryPath() / "lookups" / "data_sources.yml"
42+
3743
# Construct a special lookup whose CSV is created at runtime and
38-
# written directly into the output folder. It is created with model_construct,
39-
# not model_validate, because the CSV does not exist yet.
44+
# written directly into the lookups folder. We will delete this after a build,
45+
# assuming that it is successful.
4046
data_sources_lookup_csv_path = input_dto.config.getPackageDirectoryPath() / "lookups" / "data_sources.csv"
41-
DataSourceWriter.writeDataSourceCsv(input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path)
42-
input_dto.director_output_dto.addContentToDictMappings(Lookup.model_construct(description= "A lookup file that will contain the data source objects for detections.",
43-
filename=data_sources_lookup_csv_path,
44-
name="data_sources"))
4547

48+
49+
50+
DataSourceWriter.writeDataSourceCsv(input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path)
51+
input_dto.director_output_dto.addContentToDictMappings(CSVLookup.model_construct(name="data_sources",
52+
id=uuid.UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
53+
version=1,
54+
author=input_dto.config.app.author_name,
55+
date = datetime.date.today(),
56+
description= "A lookup file that will contain the data source objects for detections.",
57+
lookup_type=Lookup_Type.csv,
58+
file_path=data_sources_fake_yml_path))
4659
updated_conf_files.update(conf_output.writeHeaders())
47-
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.detections, SecurityContentType.detections))
48-
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.stories, SecurityContentType.stories))
49-
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.baselines, SecurityContentType.baselines))
50-
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.investigations, SecurityContentType.investigations))
51-
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.lookups, SecurityContentType.lookups))
52-
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.macros, SecurityContentType.macros))
53-
updated_conf_files.update(conf_output.writeObjects(input_dto.director_output_dto.dashboards, SecurityContentType.dashboards))
60+
updated_conf_files.update(conf_output.writeLookups(input_dto.director_output_dto.lookups))
61+
updated_conf_files.update(conf_output.writeDetections(input_dto.director_output_dto.detections))
62+
updated_conf_files.update(conf_output.writeStories(input_dto.director_output_dto.stories))
63+
updated_conf_files.update(conf_output.writeBaselines(input_dto.director_output_dto.baselines))
64+
updated_conf_files.update(conf_output.writeInvestigations(input_dto.director_output_dto.investigations))
65+
updated_conf_files.update(conf_output.writeMacros(input_dto.director_output_dto.macros))
66+
updated_conf_files.update(conf_output.writeDashboards(input_dto.director_output_dto.dashboards))
5467
updated_conf_files.update(conf_output.writeMiscellaneousAppFiles())
5568

5669

5770

71+
5872
#Ensure that the conf file we just generated/update is syntactically valid
5973
for conf_file in updated_conf_files:
6074
ConfWriter.validateConfFile(conf_file)
@@ -67,17 +81,15 @@ def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
6781
if input_dto.config.build_api:
6882
shutil.rmtree(input_dto.config.getAPIPath(), ignore_errors=True)
6983
input_dto.config.getAPIPath().mkdir(parents=True)
70-
api_json_output = ApiJsonOutput()
71-
for output_objects, output_type in [(input_dto.director_output_dto.detections, SecurityContentType.detections),
72-
(input_dto.director_output_dto.stories, SecurityContentType.stories),
73-
(input_dto.director_output_dto.baselines, SecurityContentType.baselines),
74-
(input_dto.director_output_dto.investigations, SecurityContentType.investigations),
75-
(input_dto.director_output_dto.lookups, SecurityContentType.lookups),
76-
(input_dto.director_output_dto.macros, SecurityContentType.macros),
77-
(input_dto.director_output_dto.deployments, SecurityContentType.deployments)]:
78-
api_json_output.writeObjects(output_objects, input_dto.config.getAPIPath(), input_dto.config.app.label, output_type )
79-
80-
84+
api_json_output = ApiJsonOutput(input_dto.config.getAPIPath(), input_dto.config.app.label)
85+
api_json_output.writeDetections(input_dto.director_output_dto.detections)
86+
api_json_output.writeStories(input_dto.director_output_dto.stories)
87+
api_json_output.writeBaselines(input_dto.director_output_dto.baselines)
88+
api_json_output.writeInvestigations(input_dto.director_output_dto.investigations)
89+
api_json_output.writeLookups(input_dto.director_output_dto.lookups)
90+
api_json_output.writeMacros(input_dto.director_output_dto.macros)
91+
api_json_output.writeDeployments(input_dto.director_output_dto.deployments)
92+
8193

8294
#create version file for sse api
8395
version_file = input_dto.config.getAPIPath()/"version.json"

contentctl/actions/validate.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from contentctl.enrichments.attack_enrichment import AttackEnrichment
77
from contentctl.enrichments.cve_enrichment import CveEnrichment
88
from contentctl.objects.atomic import AtomicEnrichment
9+
from contentctl.objects.lookup import FileBackedLookup
910
from contentctl.helper.utils import Utils
1011
from contentctl.objects.data_source import DataSource
1112
from contentctl.helper.splunk_app import SplunkApp
@@ -64,7 +65,7 @@ def ensure_no_orphaned_files_in_lookups(self, repo_path:pathlib.Path, director_o
6465
lookupsDirectory = repo_path/"lookups"
6566

6667
# Get all of the files referneced by Lookups
67-
usedLookupFiles:list[pathlib.Path] = [lookup.filename for lookup in director_output_dto.lookups if lookup.filename is not None] + [lookup.file_path for lookup in director_output_dto.lookups if lookup.file_path is not None]
68+
usedLookupFiles:list[pathlib.Path] = [lookup.filename for lookup in director_output_dto.lookups if isinstance(lookup, FileBackedLookup)] + [lookup.file_path for lookup in director_output_dto.lookups if lookup.file_path is not None]
6869

6970
# Get all of the mlmodel and csv files in the lookups directory
7071
csvAndMlmodelFiles = Utils.get_security_content_files_from_directory(lookupsDirectory, allowedFileExtensions=[".yml",".csv",".mlmodel"], fileExtensionsToReturn=[".csv",".mlmodel"])

contentctl/input/director.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from contentctl.objects.playbook import Playbook
1515
from contentctl.objects.deployment import Deployment
1616
from contentctl.objects.macro import Macro
17-
from contentctl.objects.lookup import Lookup
17+
from contentctl.objects.lookup import LookupAdapter, Lookup
1818
from contentctl.objects.atomic import AtomicEnrichment
1919
from contentctl.objects.security_content_object import SecurityContentObject
2020
from contentctl.objects.data_source import DataSource
@@ -58,13 +58,12 @@ def addContentToDictMappings(self, content: SecurityContentObject):
5858
f" - {content.file_path}\n"
5959
f" - {self.name_to_content_map[content_name].file_path}"
6060
)
61-
61+
6262
if content.id in self.uuid_to_content_map:
6363
raise ValueError(
6464
f"Duplicate id '{content.id}' with paths:\n"
6565
f" - {content.file_path}\n"
66-
f" - {self.uuid_to_content_map[content.id].file_path}"
67-
)
66+
f" - {self.uuid_to_content_map[content.id].file_path}")
6867

6968
if isinstance(content, Lookup):
7069
self.lookups.append(content)
@@ -157,7 +156,8 @@ def createSecurityContent(self, contentType: SecurityContentType) -> None:
157156
modelDict = YmlReader.load_file(file)
158157

159158
if contentType == SecurityContentType.lookups:
160-
lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
159+
lookup = LookupAdapter.validate_python(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
160+
#lookup = Lookup.model_validate(modelDict, context={"output_dto":self.output_dto, "config":self.input_dto})
161161
self.output_dto.addContentToDictMappings(lookup)
162162

163163
elif contentType == SecurityContentType.macros:

contentctl/objects/abstract_security_content_objects/detection_abstract.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717

1818
from contentctl.objects.macro import Macro
19-
from contentctl.objects.lookup import Lookup
19+
from contentctl.objects.lookup import Lookup, FileBackedLookup, KVStoreLookup
2020
if TYPE_CHECKING:
2121
from contentctl.input.director import DirectorOutputDto
2222
from contentctl.objects.baseline import Baseline
@@ -285,10 +285,8 @@ def annotations(self) -> dict[str, Union[List[str], int, str]]:
285285

286286
annotations_dict: dict[str, str | list[str] | int] = {}
287287
annotations_dict["analytic_story"] = [story.name for story in self.tags.analytic_story]
288-
annotations_dict["confidence"] = self.tags.confidence
289288
if len(self.tags.cve or []) > 0:
290289
annotations_dict["cve"] = self.tags.cve
291-
annotations_dict["impact"] = self.tags.impact
292290
annotations_dict["type"] = self.type
293291
annotations_dict["type_list"] = [self.type]
294292
# annotations_dict["version"] = self.version
@@ -480,6 +478,11 @@ def serialize_model(self):
480478
"source": self.source,
481479
"nes_fields": self.nes_fields,
482480
}
481+
if self.rba is not None:
482+
model["risk_severity"] = self.rba.severity
483+
model['tags']['risk_score'] = self.rba.risk_score
484+
else:
485+
model['tags']['risk_score'] = 0
483486

484487
# Only a subset of macro fields are required:
485488
all_macros: list[dict[str, str | list[str]]] = []
@@ -497,27 +500,26 @@ def serialize_model(self):
497500

498501
all_lookups: list[dict[str, str | int | None]] = []
499502
for lookup in self.lookups:
500-
if lookup.collection is not None:
503+
if isinstance(lookup, KVStoreLookup):
501504
all_lookups.append(
502505
{
503506
"name": lookup.name,
504507
"description": lookup.description,
505508
"collection": lookup.collection,
506509
"case_sensitive_match": None,
507-
"fields_list": lookup.fields_list
510+
"fields_list": lookup.fields_to_fields_list_conf_format
508511
}
509512
)
510-
elif lookup.filename is not None:
513+
elif isinstance(lookup, FileBackedLookup):
511514
all_lookups.append(
512515
{
513516
"name": lookup.name,
514517
"description": lookup.description,
515518
"filename": lookup.filename.name,
516519
"default_match": "true" if lookup.default_match else "false",
517520
"case_sensitive_match": "true" if lookup.case_sensitive_match else "false",
518-
"match_type": lookup.match_type,
519-
"min_matches": lookup.min_matches,
520-
"fields_list": lookup.fields_list
521+
"match_type": lookup.match_type_to_conf_format,
522+
"min_matches": lookup.min_matches
521523
}
522524
)
523525
model['lookups'] = all_lookups # type: ignore
@@ -790,7 +792,7 @@ def ensureProperRBAConfig(self):
790792
"""
791793

792794

793-
if self.deployment.alert_action.rba.enabled is False or self.deployment.alert_action.rba is None:
795+
if self.deployment.alert_action.rba is None or self.deployment.alert_action.rba.enabled is False:
794796
# confirm we don't have an RBA config
795797
if self.rba is None:
796798
return self

contentctl/objects/detection_tags.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
Cis18Value,
2828
AssetType,
2929
SecurityDomain,
30-
RiskSeverity,
3130
KillChainPhase,
3231
NistCategory,
3332
SecurityContentProductName
@@ -42,30 +41,7 @@ class DetectionTags(BaseModel):
4241
analytic_story: list[Story] = Field(...)
4342
asset_type: AssetType = Field(...)
4443
group: list[str] = []
45-
confidence: NonNegativeInt = Field(...,le=100)
46-
impact: NonNegativeInt = Field(...,le=100)
47-
@computed_field
48-
@property
49-
def risk_score(self) -> int:
50-
return round((self.confidence * self.impact)/100)
5144

52-
@computed_field
53-
@property
54-
def severity(self)->RiskSeverity:
55-
if 0 <= self.risk_score <= 20:
56-
return RiskSeverity.INFORMATIONAL
57-
elif 20 < self.risk_score <= 40:
58-
return RiskSeverity.LOW
59-
elif 40 < self.risk_score <= 60:
60-
return RiskSeverity.MEDIUM
61-
elif 60 < self.risk_score <= 80:
62-
return RiskSeverity.HIGH
63-
elif 80 < self.risk_score <= 100:
64-
return RiskSeverity.CRITICAL
65-
else:
66-
raise Exception(f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}")
67-
68-
6945
mitre_attack_id: List[MITRE_ATTACK_ID_TYPE] = []
7046
nist: list[NistCategory] = []
7147

@@ -80,9 +56,6 @@ def severity(self)->RiskSeverity:
8056

8157
# enrichment
8258
mitre_attack_enrichments: List[MitreAttackEnrichment] = Field([], validate_default=True)
83-
confidence_id: Optional[PositiveInt] = Field(None, ge=1, le=3)
84-
impact_id: Optional[PositiveInt] = Field(None, ge=1, le=5)
85-
evidence_str: Optional[str] = None
8659

8760
@computed_field
8861
@property
@@ -153,9 +126,7 @@ def serialize_model(self):
153126
"cis20": self.cis20,
154127
"kill_chain_phases": self.kill_chain_phases,
155128
"nist": self.nist,
156-
"risk_score": self.risk_score,
157129
"security_domain": self.security_domain,
158-
"risk_severity": self.severity,
159130
"mitre_attack_id": self.mitre_attack_id,
160131
"mitre_attack_enrichments": self.mitre_attack_enrichments
161132
}

0 commit comments

Comments
 (0)