Skip to content

Commit 998187b

Browse files
authored
Merge pull request #87 from rabobank-cdc/dev-campaigns
Campaigns
2 parents 4584e84 + a716825 commit 998187b

File tree

8 files changed

+358
-96
lines changed

8 files changed

+358
-96
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM python:3.10-slim-bullseye
22

3-
LABEL version="1.7.0"
3+
LABEL version="1.8.0"
44

55
# copy DeTT&CT and install the requirements
66
COPY . /opt/DeTTECT

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<img src="https://github.com/rabobank-cdc/DeTTECT/wiki/images/logo.png#gh-light-mode-only" alt="DeTT&CT" width=30% height=30%>
33

44
#### Detect Tactics, Techniques & Combat Threats
5-
Latest version: [1.7.0](https://github.com/rabobank-cdc/DeTTECT/wiki/Changelog#version-170)
5+
Latest version: [1.8.0](https://github.com/rabobank-cdc/DeTTECT/wiki/Changelog#version-170)
66

77
To get started with DeTT&CT, check out one of these resources:
88
- This [page](https://github.com/rabobank-cdc/DeTTECT/wiki/Getting-started) on the Wiki.

constants.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,22 @@
44

55
APP_NAME = 'DeTT&CT'
66
APP_DESC = 'Detect Tactics, Techniques & Combat Threats'
7-
VERSION = '1.7.0'
7+
VERSION = '1.8.0'
88

9-
EXPIRE_TIME = 60 * 60 * 24
9+
EXPIRE_TIME = 60 * 60 * 24 * 7
1010

1111
# MITRE ATT&CK data types for custom schema and STIX
1212
DATA_TYPE_CUSTOM_TECH_BY_GROUP = 'mitre_techniques_used_by_group'
1313
DATA_TYPE_CUSTOM_TECH_BY_SOFTWARE = 'mitre_techniques_used_by_software'
14+
DATA_TYPE_CUSTOM_TECH_IN_CAMPAIGN = 'mitre_techniques_used_in_campaign'
1415
DATA_TYPE_CUSTOM_SOFTWARE_BY_GROUP = 'mitre_software_used_by_group'
16+
DATA_TYPE_CUSTOM_SOFTWARE_IN_CAMPAIGN = 'mitre_software_used_in_campaign'
1517
DATA_TYPE_STIX_ALL_TECH = 'mitre_all_techniques'
1618
DATA_TYPE_STIX_ALL_TECH_ENTERPRISE = 'mitre_all_techniques_enterprise'
1719
DATA_TYPE_STIX_ALL_TECH_ICS = 'mitre_all_techniques_ics'
1820
DATA_TYPE_STIX_ALL_TECH_MOBILE = 'mitre_all_techniques_mobile'
1921
DATA_TYPE_STIX_ALL_GROUPS = 'mitre_all_groups'
22+
DATA_TYPE_STIX_ALL_CAMPAIGNS = 'mitre_all_campaigns'
2023
DATA_TYPE_STIX_ALL_SOFTWARE = 'mitre_all_software'
2124
DATA_TYPE_STIX_ALL_RELATIONSHIPS = 'mitre_all_relationships'
2225
DATA_TYPE_STIX_ALL_ENTERPRISE_MITIGATIONS = 'mitre_all_mitigations_enterprise'
@@ -83,6 +86,7 @@
8386

8487
# Overlay types as used within the group functionality
8588
OVERLAY_TYPE_GROUP = 'group'
89+
OVERLAY_TYPE_CAMPAIGN = 'campaign'
8690
OVERLAY_TYPE_VISIBILITY = 'visibility'
8791
OVERLAY_TYPE_DETECTION = 'detection'
8892

dettect.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -161,29 +161,43 @@ def _init_menu():
161161
description='Create threat actor group heat maps, compare group(s) and '
162162
'compare group(s) with visibility and detection coverage.',
163163
help='threat actor group mapping')
164-
parser_group.add_argument('-g', '--groups', help='specify the ATT&CK Groups to include. Group can be its ID, '
165-
'name or alias (default is all groups). Multiple Groups can be '
166-
'provided with extra \'-g/--group\' arguments. Another option is '
167-
'to provide a YAML file with a custom group(s)',
164+
parser_group.add_argument('-g', '--groups', help='specify the ATT&CK Groups to include. A group can be its ID, name or alias. '
165+
'If no group is specified, all groups are used (except when a -c/--campaign '
166+
'is specified). The -g/--groups and -c/--campaign options complement each other. '
167+
'Multiple Groups can be provided with extra -g/--group arguments. Another '
168+
'option is to provide a YAML file with a custom group(s)',
169+
default=None, action='append')
170+
parser_group.add_argument('-c', '--campaigns', help='specify the ATT&CK Campaigns to include. A campaign can be its ID or name. '
171+
'If no campaign is specified, all campaigns are used (except when a -g/--group '
172+
'is specified). The -c/--campaign and -g/--groups options complement each other. '
173+
'Multiple Campaigns can be provided with extra -c/--campaign arguments.',
168174
default=None, action='append')
169175
parser_group.add_argument('-d', '--domain', help='specify the ATT&CK domain (default = enterprise). This argument '
170176
'is ignored if a domain is specified in the Group YAML file.',
171177
required=False, choices=['enterprise', 'ics', 'mobile'])
172-
parser_group.add_argument('-o', '--overlay', help='specify what to overlay on the group(s) (provided using the '
173-
'arguments \-g/--groups\): group(s), visibility or detection. '
174-
'When overlaying a GROUP: the group can be its ATT&CK ID, '
175-
'name or alias. Multiple Groups can be provided with extra '
176-
'\'-o/--overlay\' arguments. Another option is to provide a '
178+
parser_group.add_argument('-o', '--overlay', help='specify what to overlay: group(s), campaign(s), visibility or detection. '
179+
'Default overlay type is Groups, to change it use -t/--overlay-type. '
180+
'When overlaying a Group: it can be its ATT&CK ID, name or alias. '
181+
'When overlaying a Campaign: it can be its ID or name. '
182+
'Multiple Groups or Campaigns can be provided with extra '
183+
'-o/--overlay arguments. Another option is to provide a '
177184
'YAML file with a custom group(s). When overlaying VISIBILITY '
178-
'or DETECTION provide a YAML with the technique administration.)',
185+
'or DETECTION provide a YAML with the technique administration. ',
179186
action='append')
180187
parser_group.add_argument('-t', '--overlay-type', help='specify the type of overlay (default = group)',
181-
choices=['group', 'visibility', 'detection'], default='group')
182-
parser_group.add_argument('--software-group', help='add techniques to the heat map by checking which software is '
183-
'used by group(s), and hence which techniques the software '
184-
'supports (does not influence the scores). If overlay group(s) '
185-
'are provided, only software related to those group(s) are '
186-
'included', action='store_true', default=False)
188+
choices=['group', 'campaign', 'visibility', 'detection'], default='group')
189+
190+
software_parse_group = parser_group.add_mutually_exclusive_group()
191+
software_parse_group.add_argument('--software', help='add techniques to the heat map by checking which software is used by '
192+
'groups/campaigns, and hence which techniques the software '
193+
'supports (does not influence the scores). If overlay groups/campaigns '
194+
'are provided, only software related to those groups/campaigns are '
195+
'included. Cannot be used together with --include-software',
196+
action='store_true', default=False)
197+
software_parse_group.add_argument('--include-software', help='include techniques that software supports in the scores for '
198+
'groups/campaigns in scope. Cannot be used together with --software',
199+
action='store_true', default=False)
200+
187201
parser_group.add_argument('-p', '--platform', action='append', help='specify the platform (default = all). Multiple platforms '
188202
'can be provided with extra \'-p/--platform\' arguments. The available platforms '
189203
' can be listed from the generic mode: \'ge --list-platforms\'')
@@ -300,8 +314,8 @@ def _menu(menu_parser):
300314
# TODO add Group EQL search capabilities
301315
elif args.subparser in ['group', 'g']:
302316
layer_settings = _parse_layer_settings(args.layer_settings)
303-
generate_group_heat_map(args.groups, args.overlay, args.overlay_type, args.platform,
304-
args.software_group, args.search_visibility, args.search_detection, args.health,
317+
generate_group_heat_map(args.groups, args.campaigns, args.overlay, args.overlay_type, args.platform,
318+
args.software, args.include_software, args.search_visibility, args.search_detection, args.health,
305319
args.output_filename, args.layer_name, args.domain, layer_settings,
306320
include_all_score_objs=args.all_scores)
307321

generic.py

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,30 @@ def _convert_stix_groups_to_dict(stix_attack_data):
9494
:return: list with dictionaries containing all groups from the input stix_attack_data
9595
"""
9696
attack_data = []
97-
for stix_tech in stix_attack_data:
98-
tech = json.loads(stix_tech.serialize(), object_hook=_date_hook)
97+
for stix_group in stix_attack_data:
98+
group = json.loads(stix_group.serialize(), object_hook=_date_hook)
9999

100100
# Add group_id as key, because it's hard to get from STIX:
101-
tech['group_id'] = get_attack_id(stix_tech)
101+
group['group_id'] = get_attack_id(stix_group)
102102

103-
attack_data.append(tech)
103+
attack_data.append(group)
104+
105+
return attack_data
106+
107+
def _convert_stix_campaigns_to_dict(stix_attack_data):
108+
"""
109+
Convert the STIX list with Campaign to a dictionary for easier use in python and also include the campaign_id.
110+
:param stix_attack_data: the MITRE ATT&CK STIX dataset with campaigns
111+
:return: list with dictionaries containing all campaigns from the input stix_attack_data
112+
"""
113+
attack_data = []
114+
for stix_campaign in stix_attack_data:
115+
campaign = json.loads(stix_campaign.serialize(), object_hook=_date_hook)
116+
117+
# Add campaign_id as key, because it's hard to get from STIX:
118+
campaign['campaign_id'] = get_attack_id(stix_campaign)
119+
120+
attack_data.append(campaign)
104121

105122
return attack_data
106123

@@ -191,6 +208,43 @@ def load_attack_data(data_type):
191208
})
192209

193210
attack_data = all_group_use
211+
elif data_type == DATA_TYPE_CUSTOM_TECH_IN_CAMPAIGN:
212+
# First we need to know which technique references (STIX Object type 'attack-pattern') we have for all
213+
# campaigns. This results in a dict: {campaign_id: Cxxxx, technique_ref/attack-pattern_ref: ...}
214+
campaigns = load_attack_data(DATA_TYPE_STIX_ALL_CAMPAIGNS)
215+
relationships = load_attack_data(DATA_TYPE_STIX_ALL_RELATIONSHIPS)
216+
all_campaigns_relationships = []
217+
for c in campaigns:
218+
for r in relationships:
219+
if c['id'] == r['source_ref'] and r['relationship_type'] == 'uses' and \
220+
r['target_ref'].startswith('attack-pattern--'):
221+
# more information on the campaign can be added. Only the minimal required data is added.
222+
all_campaigns_relationships.append(
223+
{
224+
'campaign_id': get_attack_id(c),
225+
'name': c['name'],
226+
'technique_ref': r['target_ref'],
227+
'x_mitre_domains': c['x_mitre_domains'] if 'x_mitre_domains' in c.keys() else ['enterprise-attack']
228+
})
229+
230+
# Now we start resolving this part of the dict created above: 'technique_ref/attack-pattern_ref'.
231+
# and we add some more data to the final result.
232+
all_campaigns_use = []
233+
techniques = load_attack_data(DATA_TYPE_STIX_ALL_TECH)
234+
for cr in all_campaigns_relationships:
235+
for t in techniques:
236+
if t['id'] == cr['technique_ref']:
237+
all_campaigns_use.append(
238+
{
239+
'campaign_id': cr['campaign_id'],
240+
'name': cr['name'],
241+
'technique_id': get_attack_id(t),
242+
'x_mitre_platforms': t.get('x_mitre_platforms', None),
243+
'x_mitre_domains': cr['x_mitre_domains'],
244+
'matrix': t['external_references'][0]['source_name']
245+
})
246+
247+
attack_data = all_campaigns_use
194248

195249
elif data_type == DATA_TYPE_STIX_ALL_TECH:
196250
stix_attack_data = mitre.get_techniques()
@@ -216,6 +270,10 @@ def load_attack_data(data_type):
216270
# Combine groups from all matrices together:
217271
attack_data = _convert_stix_groups_to_dict(groups_enterprise + groups_ics + groups_mobile)
218272

273+
elif data_type == DATA_TYPE_STIX_ALL_CAMPAIGNS:
274+
campaigns = mitre.get_campaigns()
275+
attack_data = _convert_stix_campaigns_to_dict(campaigns)
276+
219277
elif data_type == DATA_TYPE_STIX_ALL_SOFTWARE:
220278
attack_data = mitre.get_software()
221279
elif data_type == DATA_TYPE_CUSTOM_TECH_BY_SOFTWARE:
@@ -283,6 +341,42 @@ def load_attack_data(data_type):
283341
})
284342
attack_data = all_group_use
285343

344+
elif data_type == DATA_TYPE_CUSTOM_SOFTWARE_IN_CAMPAIGN:
345+
# First we need to know which software references (STIX Object type 'malware' or 'tool') we have for all
346+
# campaigns. This results in a dict: {campaign_id: Cxxxx, software_ref/malware-tool_ref: ...}
347+
campaigns = load_attack_data(DATA_TYPE_STIX_ALL_CAMPAIGNS)
348+
relationships = load_attack_data(DATA_TYPE_STIX_ALL_RELATIONSHIPS)
349+
all_campaigns_relationships = []
350+
for campaign in campaigns:
351+
for r in relationships:
352+
if campaign['id'] == r['source_ref'] and r['relationship_type'] == 'uses' and \
353+
(r['target_ref'].startswith('tool--') or r['target_ref'].startswith('malware--')):
354+
all_campaigns_relationships.append(
355+
{
356+
'campaign_id': get_attack_id(campaign),
357+
'name': campaign['name'],
358+
'software_ref': r['target_ref'],
359+
'x_mitre_domains': campaign['x_mitre_domains']
360+
})
361+
362+
# Now we start resolving this part of the dict created above: 'software_ref/malware-tool_ref'.
363+
# and we add some more data to the final result.
364+
all_campaign_use = []
365+
software = load_attack_data(DATA_TYPE_STIX_ALL_SOFTWARE)
366+
for campaign in all_campaigns_relationships:
367+
for s in software:
368+
if s['id'] == campaign['software_ref']:
369+
all_campaign_use.append(
370+
{
371+
'campaign_id': campaign['campaign_id'],
372+
'name': campaign['name'],
373+
'software_id': get_attack_id(s),
374+
'x_mitre_platforms': s.get('x_mitre_platforms', None),
375+
'x_mitre_domains': campaign['x_mitre_domains'],
376+
'matrix': s['external_references'][0]['source_name']
377+
})
378+
attack_data = all_campaign_use
379+
286380
elif data_type == DATA_TYPE_STIX_ALL_ENTERPRISE_MITIGATIONS:
287381
attack_data = mitre.get_enterprise_mitigations()
288382
attack_data = mitre.remove_revoked_deprecated(attack_data)
@@ -983,3 +1077,19 @@ def check_platform(arg_platforms, filename=None, domain=None):
9831077

9841078
return False
9851079
return True
1080+
1081+
1082+
def merge_group_dict(dict1, dict2):
1083+
"""
1084+
Merge the techniques from dict2 with the techniques in dict1.
1085+
:param dict1 The first dictionary
1086+
:param dict2 The other dictionary
1087+
"""
1088+
for group_name, values in dict2.items():
1089+
if group_name not in dict1.keys():
1090+
dict1[group_name] = values
1091+
else:
1092+
for technique in values['techniques']:
1093+
if technique not in dict1[group_name]['techniques']:
1094+
dict1[group_name]['techniques'].add(technique)
1095+
dict1[group_name]['weight'][technique] = 1

0 commit comments

Comments
 (0)