Skip to content

Commit 825beaf

Browse files
committed
Able to build without any errors,
but that does not mean it is correct yet. Outputs must be diffed against prior versions to make sure there are no unintended changes.
1 parent f04d92c commit 825beaf

File tree

14 files changed

+111
-73
lines changed

14 files changed

+111
-73
lines changed

contentctl/actions/build.py

Lines changed: 23 additions & 9 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-
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())
60+
updated_conf_files.update(conf_output.writeLookups(input_dto.director_output_dto.lookups))
4761
updated_conf_files.update(conf_output.writeDetections(input_dto.director_output_dto.detections))
4862
updated_conf_files.update(conf_output.writeStories(input_dto.director_output_dto.stories))
4963
updated_conf_files.update(conf_output.writeBaselines(input_dto.director_output_dto.baselines))
5064
updated_conf_files.update(conf_output.writeInvestigations(input_dto.director_output_dto.investigations))
51-
updated_conf_files.update(conf_output.writeLookups(input_dto.director_output_dto.lookups))
5265
updated_conf_files.update(conf_output.writeMacros(input_dto.director_output_dto.macros))
5366
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)

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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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)

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 & 20 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
@@ -43,23 +42,6 @@ class DetectionTags(BaseModel):
4342
asset_type: AssetType = Field(...)
4443
group: list[str] = []
4544

46-
@computed_field
47-
@property
48-
def severity(self)->RiskSeverity:
49-
if 0 <= self.risk_score <= 20:
50-
return RiskSeverity.INFORMATIONAL
51-
elif 20 < self.risk_score <= 40:
52-
return RiskSeverity.LOW
53-
elif 40 < self.risk_score <= 60:
54-
return RiskSeverity.MEDIUM
55-
elif 60 < self.risk_score <= 80:
56-
return RiskSeverity.HIGH
57-
elif 80 < self.risk_score <= 100:
58-
return RiskSeverity.CRITICAL
59-
else:
60-
raise Exception(f"Error getting severity - risk_score must be between 0-100, but was actually {self.risk_score}")
61-
62-
6345
mitre_attack_id: List[MITRE_ATTACK_ID_TYPE] = []
6446
nist: list[NistCategory] = []
6547

@@ -144,9 +126,7 @@ def serialize_model(self):
144126
"cis20": self.cis20,
145127
"kill_chain_phases": self.kill_chain_phases,
146128
"nist": self.nist,
147-
"risk_score": self.risk_score,
148129
"security_domain": self.security_domain,
149-
"risk_severity": self.severity,
150130
"mitre_attack_id": self.mitre_attack_id,
151131
"mitre_attack_enrichments": self.mitre_attack_enrichments
152132
}

contentctl/objects/lookup.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,10 @@ def fix_lookup_path(cls, data:Any, info: ValidationInfo)->Any:
8080
return data
8181

8282

83-
83+
@computed_field
84+
@cached_property
85+
def match_type_to_conf_format(self)->str:
86+
return ', '.join(self.match_type)
8487

8588

8689
@staticmethod
@@ -196,6 +199,16 @@ def ensure_key(cls, values: list[str]):
196199
raise ValueError(f"fields MUST begin with '_key', not '{values[0]}'")
197200
return values
198201

202+
@computed_field
203+
@cached_property
204+
def collection(self)->str:
205+
return self.name
206+
207+
@computed_field
208+
@cached_property
209+
def fields_to_fields_list_conf_format(self)->str:
210+
return ', '.join(self.fields)
211+
199212
@model_serializer
200213
def serialize_model(self):
201214
#Call parent serializer
@@ -204,7 +217,7 @@ def serialize_model(self):
204217
#All fields custom to this model
205218
model= {
206219
"collection": self.collection,
207-
"fields_list": ", ".join(self.fields)
220+
"fields_list": self.fields_to_fields_list_conf_format
208221
}
209222

210223
#return the model

contentctl/objects/macro.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ def serialize_model(self):
4848
return model
4949

5050
@staticmethod
51-
5251
def get_macros(text_field:str, director:DirectorOutputDto , ignore_macros:set[str]=MACROS_TO_IGNORE)->list[Macro]:
5352
#Remove any comments, allowing there to be macros (which have a single backtick) inside those comments
5453
#If a comment ENDS in a macro, for example ```this is a comment with a macro `macro_here````
@@ -59,10 +58,10 @@ def get_macros(text_field:str, director:DirectorOutputDto , ignore_macros:set[st
5958
"This may have occurred when a macro was commented out.\n"
6059
"Please ammend your search to remove the substring '````'")
6160

62-
# replace all the macros with a space
61+
# Replace all the comments with a space. This prevents a comment from looking like a macro to the parser below
6362
text_field = re.sub(r"\`\`\`[\s\S]*?\`\`\`", " ", text_field)
6463

65-
64+
# Find all the macros, which start and end with a '`' character
6665
macros_to_get = re.findall(r'`([^\s]+)`', text_field)
6766
#If macros take arguments, stop at the first argument. We just want the name of the macro
6867
macros_to_get = set([macro[:macro.find('(')] if macro.find('(') != -1 else macro for macro in macros_to_get])

contentctl/objects/rba.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from enum import Enum
2-
from pydantic import BaseModel
2+
from pydantic import BaseModel, computed_field, Field
33
from abc import ABC
4-
from typing import Set
4+
from typing import Set, Annotated
5+
from contentctl.objects.enums import RiskSeverity
56

67

8+
RiskScoreValue_Type = Annotated[int, Field(ge=1, le=100)]
79

810
class RiskObjectType(str, Enum):
911
SYSTEM = "system"
@@ -40,7 +42,7 @@ class ThreatObjectType(str, Enum):
4042
class risk_object(BaseModel):
4143
field: str
4244
type: RiskObjectType
43-
score: int
45+
score: RiskScoreValue_Type
4446

4547
def __hash__(self):
4648
return hash((self.field, self.type, self.score))
@@ -54,5 +56,34 @@ def __hash__(self):
5456

5557
class rba_object(BaseModel, ABC):
5658
message: str
57-
risk_objects: Set[risk_object]
58-
threat_objects: Set[threat_object]
59+
risk_objects: Annotated[Set[risk_object], Field(min_length=1)]
60+
threat_objects: Set[threat_object]
61+
62+
63+
64+
@computed_field
65+
@property
66+
def risk_score(self)->RiskScoreValue_Type:
67+
# First get the maximum score associated with
68+
# a risk object. If there are no objects, then
69+
# we should throw an exception.
70+
if len(self.risk_objects) == 0:
71+
raise Exception("There must be at least one Risk Object present to get Severity.")
72+
return max([risk_object.score for risk_object in self.risk_objects])
73+
74+
@computed_field
75+
@property
76+
def severity(self)->RiskSeverity:
77+
if 0 <= self.risk_score <= 20:
78+
return RiskSeverity.INFORMATIONAL
79+
elif 20 < self.risk_score <= 40:
80+
return RiskSeverity.LOW
81+
elif 40 < self.risk_score <= 60:
82+
return RiskSeverity.MEDIUM
83+
elif 60 < self.risk_score <= 80:
84+
return RiskSeverity.HIGH
85+
elif 80 < self.risk_score <= 100:
86+
return RiskSeverity.CRITICAL
87+
else:
88+
raise Exception(f"Error getting severity - risk_score must be between 0-100, but was actually {max_score}")
89+

contentctl/output/api_json_output.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def writeLookups(
215215
"name",
216216
"description",
217217
"collection",
218-
"fields_list",
218+
"fields_to_fields_list_conf_format",
219219
"filename",
220220
"default_match",
221221
"match_type",

contentctl/output/conf_output.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,22 @@
11
from __future__ import annotations
22
from typing import TYPE_CHECKING, Callable
33
if TYPE_CHECKING:
4-
from contentctl.objects.security_content_object import SecurityContentObject
54
from contentctl.objects.detection import Detection
6-
from contentctl.objects.lookup import Lookup, FileBackedLookup
5+
from contentctl.objects.lookup import Lookup
76
from contentctl.objects.macro import Macro
87
from contentctl.objects.dashboard import Dashboard
98
from contentctl.objects.story import Story
109
from contentctl.objects.baseline import Baseline
1110
from contentctl.objects.investigation import Investigation
1211

13-
from dataclasses import dataclass
14-
import os
15-
import glob
12+
from contentctl.objects.lookup import FileBackedLookup
1613
import shutil
17-
import sys
1814
import tarfile
19-
from typing import Union
20-
from pathlib import Path
2115
import pathlib
22-
import time
2316
import timeit
2417
import datetime
25-
import shutil
26-
import json
2718
from contentctl.output.conf_writer import ConfWriter
28-
from contentctl.objects.enums import SecurityContentType
2919
from contentctl.objects.config import build
30-
from requests import Session, post, get
31-
from requests.auth import HTTPBasicAuth
3220

3321
class ConfOutput:
3422
config: build

0 commit comments

Comments
 (0)