Skip to content

Commit 7f30cf9

Browse files
Merge branch 'main' into feature/risk-observable-matching
2 parents 4f192d4 + a460016 commit 7f30cf9

File tree

19 files changed

+245
-1213
lines changed

19 files changed

+245
-1213
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ If you are already familiar with contentctl, the following common commands may b
1010
|-----------|---------|
1111
| Create a repository | `contentctl init` |
1212
| Validate Your Content | `contentctl validate` |
13-
| Validate Your Content, performing MITRE Enrichments | `contentctl validate -enrichments`|
13+
| Validate Your Content, performing MITRE Enrichments | `contentctl validate --enrichments`|
1414
| Build Your App | `contentctl build` |
15-
| Test All the content in your app, pausing so that you can debug a search if it fails | `contentctl test -post-test-behavior pause_on_failure mode:all` |
16-
| Test All the content in your app, pausing after every detection to allow debugging | `contentctl test -post-test-behavior always_pause mode:all` |
17-
| Test 1 or more specified detections. If you are testing more than one detection, the paths are space-separated. You may also use shell-expanded regexes | `contentctl test -post-test-behavior always_pause mode:selected --mode.files detections/endpoint/7zip_commandline_to_smb_share_path.yml detections/cloud/aws_multi_factor_authentication_disabled.yml detections/application/okta*` |
18-
| Diff your current branch with a target_branch and test detections that have been updated. Your current branch **must be DIFFERENT** than the target_branch | `contentctl test –-post-test-behavior always_pause mode:changes -mode.target_branch develop` |
19-
| Perform Integration Testing of all content. Note that Enterprise Security MUST be listed as an app in your contentctl.yml folder, otherwise all tests will subsequently fail | `contentctl test -enable-integration-testing --post-test-behavior never_pause mode:all` |
15+
| Test All the content in your app, pausing so that you can debug a search if it fails | `contentctl test --post-test-behavior pause_on_failure mode:all` |
16+
| Test All the content in your app, pausing after every detection to allow debugging | `contentctl test --post-test-behavior always_pause mode:all` |
17+
| Test 1 or more specified detections. If you are testing more than one detection, the paths are space-separated. You may also use shell-expanded regexes | `contentctl test --post-test-behavior always_pause mode:selected --mode.files detections/endpoint/7zip_commandline_to_smb_share_path.yml detections/cloud/aws_multi_factor_authentication_disabled.yml detections/application/okta*` |
18+
| Diff your current branch with a target_branch and test detections that have been updated. Your current branch **must be DIFFERENT** than the target_branch | `contentctl test --post-test-behavior always_pause mode:changes --mode.target_branch develop` |
19+
| Perform Integration Testing of all content. Note that Enterprise Security MUST be listed as an app in your contentctl.yml folder, otherwise all tests will subsequently fail | `contentctl test --enable-integration-testing --post-test-behavior never_pause mode:all` |
2020

2121
# Introduction
2222
#### Security Is Hard

contentctl/actions/build.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from contentctl.input.director import Director, DirectorOutputDto
99
from contentctl.output.conf_output import ConfOutput
1010
from contentctl.output.conf_writer import ConfWriter
11-
from contentctl.output.ba_yml_output import BAYmlOutput
1211
from contentctl.output.api_json_output import ApiJsonOutput
1312
from contentctl.output.data_source_writer import DataSourceWriter
1413
from contentctl.objects.lookup import Lookup
@@ -86,17 +85,4 @@ def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
8685

8786
print(f"Build of '{input_dto.config.app.title}' API successful to {input_dto.config.getAPIPath()}")
8887

89-
if input_dto.config.build_ssa:
90-
91-
srs_path = input_dto.config.getSSAPath() / 'srs'
92-
complex_path = input_dto.config.getSSAPath() / 'complex'
93-
shutil.rmtree(srs_path, ignore_errors=True)
94-
shutil.rmtree(complex_path, ignore_errors=True)
95-
srs_path.mkdir(parents=True)
96-
complex_path.mkdir(parents=True)
97-
ba_yml_output = BAYmlOutput()
98-
ba_yml_output.writeObjects(input_dto.director_output_dto.ssa_detections, str(input_dto.config.getSSAPath()))
99-
100-
print(f"Build of 'SSA' successful to {input_dto.config.getSSAPath()}")
101-
10288
return input_dto.director_output_dto

contentctl/actions/convert.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1054,15 +1054,20 @@ def retry_search_until_timeout(
10541054
results = JSONResultsReader(job.results(output_mode="json"))
10551055

10561056
# Consolidate a set of the distinct observable field names
1057-
observable_fields_set = set([o.name for o in detection.tags.observable])
1057+
observable_fields_set = set([o.name for o in detection.tags.observable]) # keeping this around for later
1058+
risk_object_fields_set = set([o.name for o in detection.tags.observable if "Victim" in o.role ]) # just the "Risk Objects"
1059+
threat_object_fields_set = set([o.name for o in detection.tags.observable if "Attacker" in o.role]) # just the "threat objects"
10581060

10591061
# Ensure the search had at least one result
10601062
if int(job.content.get("resultCount", "0")) > 0:
10611063
# Initialize the test result
10621064
test.result = UnitTestResult()
10631065

10641066
# Initialize the collection of fields that are empty that shouldn't be
1067+
present_threat_objects: set[str] = set()
10651068
empty_fields: set[str] = set()
1069+
1070+
10661071

10671072
# Filter out any messages in the results
10681073
for result in results:
@@ -1071,30 +1076,50 @@ def retry_search_until_timeout(
10711076

10721077
# If not a message, it is a dict and we will process it
10731078
results_fields_set = set(result.keys())
1079+
# Guard against first events (relevant later)
10741080

1075-
# Identify any observable fields that are not available in the results
1076-
missing_fields = observable_fields_set - results_fields_set
1077-
if len(missing_fields) > 0:
1081+
# Identify any risk object fields that are not available in the results
1082+
missing_risk_objects = risk_object_fields_set - results_fields_set
1083+
if len(missing_risk_objects) > 0:
10781084
# Report a failure in such cases
1079-
e = Exception(f"The observable field(s) {missing_fields} are missing in the detection results")
1085+
e = Exception(f"The observable field(s) {missing_risk_objects} are missing in the detection results")
10801086
test.result.set_job_content(
10811087
job.content,
10821088
self.infrastructure,
1083-
TestResultStatus.ERROR,
1089+
TestResultStatus.FAIL,
10841090
exception=e,
10851091
duration=time.time() - search_start_time,
10861092
)
10871093

1088-
return
1094+
return
10891095

1090-
# If we find one or more fields that contain the string "null" then they were
1096+
# If we find one or more risk object fields that contain the string "null" then they were
10911097
# not populated and we should throw an error. This can happen if there is a typo
10921098
# on a field. In this case, the field will appear but will not contain any values
1093-
current_empty_fields = set()
1099+
current_empty_fields: set[str] = set()
1100+
10941101
for field in observable_fields_set:
10951102
if result.get(field, 'null') == 'null':
1096-
current_empty_fields.add(field)
1097-
1103+
if field in risk_object_fields_set:
1104+
e = Exception(f"The risk object field {field} is missing in at least one result.")
1105+
test.result.set_job_content(
1106+
job.content,
1107+
self.infrastructure,
1108+
TestResultStatus.FAIL,
1109+
exception=e,
1110+
duration=time.time() - search_start_time,
1111+
)
1112+
return
1113+
else:
1114+
if field in threat_object_fields_set:
1115+
current_empty_fields.add(field)
1116+
else:
1117+
if field in threat_object_fields_set:
1118+
present_threat_objects.add(field)
1119+
continue
1120+
1121+
1122+
10981123
# If everything succeeded up until now, and no empty fields are found in the
10991124
# current result, then the search was a success
11001125
if len(current_empty_fields) == 0:
@@ -1108,21 +1133,32 @@ def retry_search_until_timeout(
11081133

11091134
else:
11101135
empty_fields = empty_fields.union(current_empty_fields)
1111-
1112-
# Report a failure if there were empty fields in all results
1113-
e = Exception(
1114-
f"One or more required observable fields {empty_fields} contained 'null' values. Is the "
1115-
"data being parsed correctly or is there an error in the naming of a field?"
1136+
1137+
1138+
missing_threat_objects = threat_object_fields_set - present_threat_objects
1139+
# Report a failure if there were empty fields in a threat object in all results
1140+
if len(missing_threat_objects) > 0:
1141+
e = Exception(
1142+
f"One or more required threat object fields {missing_threat_objects} contained 'null' values in all events. "
1143+
"Is the data being parsed correctly or is there an error in the naming of a field?"
11161144
)
1117-
test.result.set_job_content(
1118-
job.content,
1119-
self.infrastructure,
1120-
TestResultStatus.ERROR,
1121-
exception=e,
1122-
duration=time.time() - search_start_time,
1123-
)
1145+
test.result.set_job_content(
1146+
job.content,
1147+
self.infrastructure,
1148+
TestResultStatus.FAIL,
1149+
exception=e,
1150+
duration=time.time() - search_start_time,
1151+
)
1152+
return
1153+
11241154

1125-
return
1155+
test.result.set_job_content(
1156+
job.content,
1157+
self.infrastructure,
1158+
TestResultStatus.PASS,
1159+
duration=time.time() - search_start_time,
1160+
)
1161+
return
11261162

11271163
else:
11281164
# Report a failure if there were no results at all

contentctl/actions/validate.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ def execute(self, input_dto: validate) -> DirectorOutputDto:
3030
[],
3131
[],
3232
[],
33-
[],
3433
)
3534

3635
director = Director(director_output_dto)

contentctl/enrichments/attack_enrichment.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
from pydantic import BaseModel, Field
99
from dataclasses import field
10-
from typing import Annotated
10+
from typing import Annotated,Any
1111
from contentctl.objects.mitre_attack_enrichment import MitreAttackEnrichment
1212
from contentctl.objects.config import validate
1313
logging.getLogger('taxii2client').setLevel(logging.CRITICAL)
@@ -33,21 +33,33 @@ def getEnrichmentByMitreID(self, mitre_id:Annotated[str, Field(pattern=r"^T\d{4}
3333
else:
3434
raise Exception(f"Error, Unable to find Mitre Enrichment for MitreID {mitre_id}")
3535

36-
37-
def addMitreID(self, technique:dict, tactics:list[str], groups:list[str])->None:
38-
36+
def addMitreIDViaGroupNames(self, technique:dict, tactics:list[str], groupNames:list[str])->None:
3937
technique_id = technique['technique_id']
4038
technique_obj = technique['technique']
4139
tactics.sort()
42-
groups.sort()
43-
40+
4441
if technique_id in self.data:
4542
raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
43+
self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
44+
mitre_attack_technique=technique_obj,
45+
mitre_attack_tactics=tactics,
46+
mitre_attack_groups=groupNames,
47+
mitre_attack_group_objects=[])
48+
49+
def addMitreIDViaGroupObjects(self, technique:dict, tactics:list[str], groupObjects:list[dict[str,Any]])->None:
50+
technique_id = technique['technique_id']
51+
technique_obj = technique['technique']
52+
tactics.sort()
4653

54+
groupNames:list[str] = sorted([group['group'] for group in groupObjects])
55+
56+
if technique_id in self.data:
57+
raise Exception(f"Error, trying to redefine MITRE ID '{technique_id}'")
4758
self.data[technique_id] = MitreAttackEnrichment(mitre_attack_id=technique_id,
4859
mitre_attack_technique=technique_obj,
4960
mitre_attack_tactics=tactics,
50-
mitre_attack_groups=groups)
61+
mitre_attack_groups=groupNames,
62+
mitre_attack_group_objects=groupObjects)
5163

5264

5365
def get_attack_lookup(self, input_path: str, store_csv: bool = False, force_cached_or_offline: bool = False, skip_enrichment:bool = False) -> dict:
@@ -86,19 +98,20 @@ def get_attack_lookup(self, input_path: str, store_csv: bool = False, force_cach
8698
progress_percent = ((index+1)/len(all_enterprise_techniques)) * 100
8799
if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()):
88100
print(f"\r\t{'MITRE Technique Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
89-
apt_groups = []
101+
apt_groups:list[dict[str,Any]] = []
90102
for relationship in enterprise_relationships:
91103
if (relationship['target_object'] == technique['id']) and relationship['source_object'].startswith('intrusion-set'):
92104
for group in enterprise_groups:
93105
if relationship['source_object'] == group['id']:
94-
apt_groups.append(group['group'])
106+
apt_groups.append(group)
107+
#apt_groups.append(group['group'])
95108

96109
tactics = []
97110
if ('tactic' in technique):
98111
for tactic in technique['tactic']:
99112
tactics.append(tactic.replace('-',' ').title())
100113

101-
self.addMitreID(technique, tactics, apt_groups)
114+
self.addMitreIDViaGroupObjects(technique, tactics, apt_groups)
102115
attack_lookup[technique['technique_id']] = {'technique': technique['technique'], 'tactics': tactics, 'groups': apt_groups}
103116

104117
if store_csv:
@@ -131,7 +144,7 @@ def get_attack_lookup(self, input_path: str, store_csv: bool = False, force_cach
131144
technique_input = {'technique_id': key , 'technique': attack_lookup[key]['technique'] }
132145
tactics_input = attack_lookup[key]['tactics']
133146
groups_input = attack_lookup[key]['groups']
134-
self.addMitreID(technique=technique_input, tactics=tactics_input, groups=groups_input)
147+
self.addMitreIDViaGroupNames(technique=technique_input, tactics=tactics_input, groups=groups_input)
135148

136149

137150

0 commit comments

Comments
 (0)