Skip to content

Commit 68e4102

Browse files
authored
Merge pull request #253 from splunk/add_full_mitre_groups
add support for the entire mitre group metadata
2 parents 8a07fcf + 4c2f9ac commit 68e4102

File tree

3 files changed

+97
-21
lines changed

3 files changed

+97
-21
lines changed

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

contentctl/objects/mitre_attack_enrichment.py

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from __future__ import annotations
2-
from pydantic import BaseModel, Field, ConfigDict
2+
from pydantic import BaseModel, Field, ConfigDict, HttpUrl, field_validator
33
from typing import List, Annotated
44
from enum import StrEnum
5-
5+
import datetime
66

77
class MitreTactics(StrEnum):
88
RECONNAISSANCE = "Reconnaissance"
@@ -21,12 +21,75 @@ class MitreTactics(StrEnum):
2121
IMPACT = "Impact"
2222

2323

24+
class AttackGroupMatrix(StrEnum):
25+
enterprise_attack = "enterprise-attack"
26+
ics_attack = "ics-attack"
27+
mobile_attack = "mobile-attack"
28+
29+
30+
class AttackGroupType(StrEnum):
31+
intrusion_set = "intrusion-set"
32+
33+
class MitreExternalReference(BaseModel):
34+
model_config = ConfigDict(extra='forbid')
35+
source_name: str
36+
external_id: None | str = None
37+
url: None | HttpUrl = None
38+
description: None | str = None
39+
40+
41+
class MitreAttackGroup(BaseModel):
42+
model_config = ConfigDict(extra='forbid')
43+
contributors: list[str] = []
44+
created: datetime.datetime
45+
created_by_ref: str
46+
external_references: list[MitreExternalReference]
47+
group: str
48+
group_aliases: list[str]
49+
group_description: str
50+
group_id: str
51+
id: str
52+
matrix: list[AttackGroupMatrix]
53+
mitre_attack_spec_version: None | str
54+
mitre_version: str
55+
#assume that if the deprecated field is not present, then the group is not deprecated
56+
mitre_deprecated: bool
57+
modified: datetime.datetime
58+
modified_by_ref: str
59+
object_marking_refs: list[str]
60+
type: AttackGroupType
61+
url: HttpUrl
62+
63+
64+
@field_validator("mitre_deprecated", mode="before")
65+
def standardize_mitre_deprecated(cls, mitre_deprecated:bool | None) -> bool:
66+
'''
67+
For some reason, the API will return either a bool for mitre_deprecated OR
68+
None. We simplify our typing by converting None to False, and assuming that
69+
if deprecated is None, then the group is not deprecated.
70+
'''
71+
if mitre_deprecated is None:
72+
return False
73+
return mitre_deprecated
74+
75+
@field_validator("contributors", mode="before")
76+
def standardize_contributors(cls, contributors:list[str] | None) -> list[str]:
77+
'''
78+
For some reason, the API will return either a list of strings for contributors OR
79+
None. We simplify our typing by converting None to an empty list.
80+
'''
81+
if contributors is None:
82+
return []
83+
return contributors
84+
2485
class MitreAttackEnrichment(BaseModel):
2586
ConfigDict(use_enum_values=True)
2687
mitre_attack_id: Annotated[str, Field(pattern=r"^T\d{4}(.\d{3})?$")] = Field(...)
2788
mitre_attack_technique: str = Field(...)
2889
mitre_attack_tactics: List[MitreTactics] = Field(...)
2990
mitre_attack_groups: List[str] = Field(...)
30-
91+
#Exclude this field from serialization - it is very large and not useful in JSON objects
92+
mitre_attack_group_objects: list[MitreAttackGroup] = Field(..., exclude=True)
3193
def __hash__(self) -> int:
3294
return id(self)
95+

pyproject.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ contentctl = 'contentctl.contentctl:main'
1111

1212
[tool.poetry.dependencies]
1313
python = "^3.11"
14-
pydantic = "^2.7.1"
15-
PyYAML = "^6.0.1"
16-
requests = "~2.32.2"
14+
pydantic = "^2.8.2"
15+
PyYAML = "^6.0.2"
16+
requests = "~2.32.3"
1717
pycvesearch = "^1.2"
1818
xmltodict = "^0.13.0"
19-
attackcti = ">=0.3.7,<0.5.0"
19+
attackcti = "^0.4.0"
2020
Jinja2 = "^3.1.4"
2121
questionary = "^2.0.1"
2222
docker = "^7.1.0"
23-
splunk-sdk = "^2.0.1"
23+
splunk-sdk = "^2.0.2"
2424
semantic-version = "^2.10.0"
2525
bottle = "^0.12.25"
26-
tqdm = "^4.66.4"
27-
pygit2 = "^1.14.1"
26+
tqdm = "^4.66.5"
27+
pygit2 = "^1.15.1"
2828
tyro = "^0.8.3"
2929
gitpython = "^3.1.43"
3030
setuptools = ">=69.5.1,<74.0.0"

0 commit comments

Comments
 (0)