Skip to content
This repository was archived by the owner on Dec 11, 2023. It is now read-only.

Commit 6ddedd3

Browse files
committed
Merge branch 'develop'
2 parents 04dd64e + 0224e42 commit 6ddedd3

File tree

7 files changed

+443
-11
lines changed

7 files changed

+443
-11
lines changed

CHANGELOG.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
1-
# V1.4.1 - 18 May 2020
1+
# v1.5 - 8 July 2020
2+
## New Scripts
3+
Added scripts used to generate the [sample layers in the ATT&CK Navigator repository](https://github.com/mitre-attack/attack-navigator/tree/develop/layers/data/samples). See issue [#21](https://github.com/mitre-attack/attack-scripts/issues/21) and [the sample layer README](scripts/layers/samples/README.md) for more details. The following scripts were added:
4+
- [heatmap.py](scripts/layers/samples/heatmap.py)
5+
- [bear_APT.py](scripts/layers/samples/bear_APT.py)
6+
- [apt3_apt29_software.py](scripts/layers/samples/apt3_apt29_software.py)
7+
- [software_execution.py](scripts/layers/samples/software_execution.py)
8+
## Fixes
9+
- Fixed a bug in diff_stix where sub-techniques had the wrong URL in hyperlinks.
10+
11+
# v1.4.1 - 18 May 2020
212
## New Scripts
313
- New script [technique_mappings_to_csv.py](technique_mappings_to_csv.py) added to support mapping Techniques with Mitigations, Groups or Software. The output is a CSV file. Added in PR [#23](https://github.com/mitre-attack/attack-scripts/pull/23)
414
## Improvements
515
- Updated [diff_stix.py](scripts/diff_stix.py) with sub-techniques support. See issue [#12](https://github.com/mitre-attack/attack-scripts/issues/12).
616
## Fixes
717
- Fixed bug in LayerOps causing issues with cross-tactic techniques, as well as a bug where a score lambda could affect the outcome of other lambdas.
818

9-
# V1.4 - 5 May 2020
19+
# v1.4 - 5 May 2020
1020
## New Scripts
1121
- Added Layers folder with utility scripts for working with [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) Layers. See the Layers [README](layers/README.md) for more details. See issues [#2](https://github.com/mitre-attack/attack-scripts/issues/2) and [#3](https://github.com/mitre-attack/attack-scripts/issues/3).
1222

13-
# V1.3 - 8 January 2019
23+
# v1.3 - 8 January 2019
1424
## New Scripts
1525
- Added [diff_stix.py](scripts/diff_stix.py).
1626

17-
# V1.2 - 24 October 2019
27+
# v1.2 - 24 October 2019
1828
- Added ATT&CKcon 2.0 Detection Training. See [the readme](/trainings/detection-training/README.md) for details.
1929

20-
# V1.1 - 29 March 2019
30+
# v1.1 - 29 March 2019
2131
## New Scripts
2232
- Added [techniques_from_data_source.py](scripts/techniques_from_data_source.py).
2333

24-
# V1.0 - 1 March 2019
34+
# v1.0 - 1 March 2019
2535
## New Scripts
2636
- Added [techniques_data_sources_vis.py](scripts/techniques_data_sources_vis.py).

scripts/diff_stix.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,14 @@ def verboseprint(self, *args, **kwargs):
139139
print(*args, **kwargs)
140140

141141

142-
def getUrlFromStix(self, datum):
142+
def getUrlFromStix(self, datum, is_subtechnique=False):
143143
"""
144144
Parse the website url from a stix object.
145145
"""
146146
url = datum['external_references'][0]['url']
147147
split_url = url.split('/')
148-
link = '/'.join(split_url[-2:])
148+
splitfrom = -3 if is_subtechnique else -2
149+
link = '/'.join(split_url[splitfrom:])
149150
return link
150151

151152

@@ -400,13 +401,14 @@ def placard(item):
400401
# get revoking technique's parent for display
401402
parentID = list(filter(lambda rel: rel["source_ref"] == revoker["id"], subtechnique_of_rels))[0]["target_ref"]
402403
parentName = id_to_technique[parentID]["name"] if parentID in id_to_technique else "ERROR NO PARENT"
403-
return f"{item['name']} (revoked by { parentName}: [{revoker['name']}]({self.site_prefix}/{self.getUrlFromStix(revoker)}))"
404+
return f"{item['name']} (revoked by { parentName}: [{revoker['name']}]({self.site_prefix}/{self.getUrlFromStix(revoker, True)}))"
404405
else:
405406
return f"{item['name']} (revoked by [{revoker['name']}]({self.site_prefix}/{self.getUrlFromStix(revoker)}))"
406407
if section == "deletions":
407408
return f"{item['name']}"
408409
else:
409-
return f"[{item['name']}]({self.site_prefix}/{self.getUrlFromStix(item)})"
410+
is_subtechnique = item["type"] == "attack-pattern" and "x_mitre_is_subtechnique" in item and item["x_mitre_is_subtechnique"]
411+
return f"[{item['name']}]({self.site_prefix}/{self.getUrlFromStix(item, is_subtechnique)})"
410412

411413

412414
# build sectionList string
@@ -688,4 +690,4 @@ def verboseprint(*args, **kwargs):
688690
parser.error('-layers requires exactly three files to be specified or none at all')
689691

690692
layers_dict = diffStix.get_layers_dict()
691-
layers_dict_to_files(args.layers, layers_dict)
693+
layers_dict_to_files(args.layers, layers_dict)

scripts/layers/samples/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Sample Scripts
2+
3+
The scripts in this folder are used to generate the [sample layers in the ATT&CK Navigator repository](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples). Run the scripts with the -h flag for usage instructions.
4+
5+
| script | sample layers | description |
6+
|:-------|:------------|:--------|
7+
| [heatmap.py](heatmap.py) | [heatmap_layer.json](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples/heatmap_layer.json) | Generates a layer wherein all techniques have randomized scores from 1-100. |
8+
| [bear_APT.py](bear_APT.py) | [Bear_APT.json](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples/Bear_APT.json) | Parses STIX data to create a layer showing all techniques used by an APT group with phrase 'bear' in the group aliases. |
9+
| [apt3_apt29_software.py](apt3_apt29_software.py) | [APT3_+_APT29_with_software.json](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples/APT3_+_APT29_with_software.json), [APT3_+_APT29_with_software_and_notional_no_detection.json](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples/APT3_+_APT29_with_software_and_notional_no_detection.json) | Creates a layer file showing techniques used by APT3 and APT29 as well as software used by those groups, and a second layer showing the same but with the added concept of detectability by a notional organization. |
10+
| [software_execution.py](software_execution.py) | [software_execution.json](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples/software_execution.json), [software_malware_execution.json](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples/software_malware_execution.json), [software_tool_execution.json](https://github.com/mitre-attack/attack-navigator/tree/master/layers/data/samples/software_tool_execution.json) | Generates layers showing all techniques that can be executed by software. |
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import argparse
2+
import requests
3+
import json
4+
import stix2
5+
6+
def generate(show_nodetect=False):
7+
"""
8+
generate and return a layer dict showing techniques used by APT3 and APT29 as well as software used by those groups
9+
param show_nodetect, if true, causes techniques that have no data-sources to be highlighted as well
10+
"""
11+
stix = requests.get("https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json").json()
12+
ms = stix2.MemoryStore(stix_data=stix["objects"])
13+
apt3 = ms.get("intrusion-set--0bbdf25b-30ff-4894-a1cd-49260d0dd2d9")
14+
apt29 = ms.get("intrusion-set--899ce53f-13a0-479b-a0e4-67d46e241542")
15+
16+
techniques_used = {} # attackID => {apt3: boolean, apt29: boolean, software: Set, detection: boolean}
17+
18+
for apt in [apt3, apt29]:
19+
20+
def use_technique(technique, software=None):
21+
"""helper function to record a technique as used"""
22+
techniqueID = technique["external_references"][0]["external_id"]
23+
# init struct if the technique has not been seen before
24+
if not techniqueID in techniques_used:
25+
techniques_used[techniqueID] = {
26+
"APT3": False,
27+
"APT29": False,
28+
"software": set(),
29+
"datasources": []
30+
}
31+
# record new data
32+
techniques_used[techniqueID][apt["name"]] = True
33+
if "x_mitre_data_sources" in technique and len(technique["x_mitre_data_sources"]) > 0:
34+
techniques_used[techniqueID]["datasources"] = technique["x_mitre_data_sources"]
35+
if software:
36+
techniques_used[techniqueID]["software"].add(software["name"])
37+
38+
# traverse relationships
39+
for relationship in ms.relationships(apt["id"]):
40+
target_obj = ms.get(relationship["target_ref"])
41+
# skip relationships with deprecated objects
42+
if ("x_mitre_deprecated" in target_obj and target_obj["x_mitre_deprecated"]) or ("revoked" in target_obj and target_obj["revoked"]): continue
43+
# technique type relationship
44+
if target_obj["type"] == "attack-pattern":
45+
# record technique usage
46+
use_technique(target_obj)
47+
# software type relationship, traverse to find software-used techniques
48+
if target_obj["type"] == "malware" or target_obj["type"] == "tool":
49+
software = target_obj
50+
for software_relationship in ms.relationships(software["id"]):
51+
software_target_obj = ms.get(software_relationship["target_ref"])
52+
# skip relationships with deprecated objects
53+
if ("x_mitre_deprecated" in software_target_obj and software_target_obj["x_mitre_deprecated"]) or ("revoked" in software_target_obj and software_target_obj["revoked"]): continue
54+
if software_target_obj["type"] == "attack-pattern":
55+
# record technique usage
56+
use_technique(software_target_obj, software)
57+
58+
# format the techniques for the output layer
59+
techniques_list = []
60+
61+
def color_lookup(usage):
62+
if show_nodetect and not len(usage["datasources"]) > 0:
63+
return "#fc3b3b"
64+
if usage["APT3"] and usage["APT29"]:
65+
return "#74c476"
66+
if usage["APT3"]: return "#6baed6"
67+
if usage["APT29"]: return "#fce93b"
68+
69+
for techniqueID in techniques_used:
70+
# determine the number of used techniques for the score
71+
comment = ""
72+
if show_nodetect:
73+
if len(techniques_used[techniqueID]["datasources"]) > 0:
74+
comment = f"considered detectable by a notional organization because it has data-sources {', '.join(techniques_used[techniqueID]['datasources'])}"
75+
else:
76+
comment = "considered undetectable by a notional organization because it has no data-sources"
77+
else:
78+
used = []
79+
if techniques_used[techniqueID]["APT3"]: used.append("APT3")
80+
if techniques_used[techniqueID]["APT29"]: used.append("APT29")
81+
used += list(techniques_used[techniqueID]["software"])
82+
comment = f"used by {', '.join(used)}"
83+
# append technique struct to list of layer-formatted techniques
84+
techniques_list.append({
85+
"techniqueID": techniqueID,
86+
"color": color_lookup(techniques_used[techniqueID]),
87+
"comment": comment,
88+
})
89+
90+
# construct and return the layer as a dict
91+
# set up layer information according to show_nodetect
92+
name = "APT3 + APT29 with software"
93+
description = "This layer shows techniques (including techniques from software used by the groups) used by APT3 only in blue, APT29 only in yellow, and both APT3 and APT29 in green."
94+
legend = [
95+
{
96+
"label": "Used by APT3 or a software APT3 uses",
97+
"color": color_lookup({"APT3": True, "APT29": False, "datasources": ["placeholder"]})
98+
},
99+
{
100+
"label": "Used by APT29 or a software APT29 uses",
101+
"color": color_lookup({"APT3": False, "APT29": True, "datasources": ["placeholder"]})
102+
},
103+
{
104+
"label": "Used by both APT3 or a softare APT3 uses and APT29 or a software APT29 uses",
105+
"color": color_lookup({"APT3": True, "APT29": True, "datasources": ["placeholder"]})
106+
}
107+
]
108+
# additional formatting when displaying notional detectability
109+
if show_nodetect:
110+
name += " and notional no detection"
111+
description += " The techniques in red denote techniques considered undetectable by a notional organization because they have no data-sources. Disclaimer: Data-sources in ATT&CK are sources of information that COULD be used to identify adversary actions, however the exactness of that evidence varies greatly. Therefore the presence of a data source for technique should only be considered a potential metric for detectability."
112+
legend.append({
113+
"label": "Used by either APT3 or APT29 but considered undetectable by a notional organization because it has no data-sources",
114+
"color": color_lookup({"APT3": True, "APT29": True, "datasources": []})
115+
})
116+
117+
# layer struct
118+
return {
119+
"name": name,
120+
"version": "3.0",
121+
"description": description,
122+
"domain": "mitre-enterprise",
123+
"techniques": techniques_list,
124+
"legendItems": legend
125+
}
126+
127+
128+
if __name__ == '__main__':
129+
# download data depending on domain
130+
parser = argparse.ArgumentParser(
131+
description="Creates a layer file showing techniques used by APT3 and APT29 as well as software used by those groups, and a second layer showing the same but with the added concept of detectability by a notional organization."
132+
)
133+
parser.add_argument("--output",
134+
type=str,
135+
default="APT3_+_APT29_with_software.json",
136+
help="output filepath for layer showing APT3 and APT29"
137+
)
138+
parser.add_argument("--output-notional",
139+
type=str,
140+
dest="notional",
141+
default="APT3_+_APT29_with_software_and_notional_no_detection.json",
142+
help="output filepath for layer showing APT3 and APT29 with detectability by a notional organization"
143+
)
144+
args = parser.parse_args()
145+
for arg in ["output", "notional"]:
146+
# get the layer
147+
layer = generate(arg == "notional")
148+
# write the layerfile
149+
with open(vars(args)[arg], "w") as f:
150+
print("writing", vars(args)[arg])
151+
f.write(json.dumps(layer, indent=4))

scripts/layers/samples/bear_APT.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import argparse
2+
import requests
3+
import json
4+
import stix2
5+
import re
6+
7+
def generate():
8+
"""parse the STIX on MITRE/CTI and return a layer dict showing all techniques used by an APT group with phrase 'bear' in the group aliases."""
9+
# import the STIX data from MITRE/CTI
10+
stix = requests.get("https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json").json()
11+
ms = stix2.MemoryStore(stix_data=stix["objects"])
12+
13+
groups = ms.query([ stix2.Filter("type", "=", "intrusion-set") ])
14+
15+
# find bear groups
16+
bear_groups = [] #list of groups with bear in name
17+
for group in groups:
18+
# filter out deprecated and revoked groups
19+
if ("x_mitre_deprecated" in group and group["x_mitre_deprecated"]) or ("revoked" in group and group["revoked"]): continue
20+
# check all aliases for bear
21+
for alias in group["aliases"]:
22+
if re.match(".*bear.*", alias, re.IGNORECASE) is not None:
23+
bear_groups.append(group)
24+
break # don't match the same group multiple times
25+
26+
# find techniques used by bear groups
27+
techniques_used = {} #attackID => using bear groups
28+
for bear in bear_groups:
29+
# construct the "bear" name for the comment
30+
# if bear occurs in multiple aliases, list them all
31+
bearnames = []
32+
for alias in bear["aliases"]:
33+
if re.match(".*bear.*", alias, re.IGNORECASE) is not None:
34+
bearnames.append(alias)
35+
bearname = bearnames[0]
36+
if len(bearnames) > 1:
37+
bearname += " (AKA " + ",".join(bearnames[1:]) + ")"
38+
39+
# get techniques used by this group
40+
relationships = ms.relationships(bear["id"])
41+
for relationship in relationships:
42+
# skip all non-technique relationships
43+
if "attack-pattern" not in relationship["target_ref"]: continue
44+
technique = ms.get(relationship["target_ref"])
45+
# filter out deprecated and revoked techniques
46+
if ("x_mitre_deprecated" in technique and technique["x_mitre_deprecated"]) or ("revoked" in technique and technique["revoked"]): continue
47+
techniqueID = technique["external_references"][0]["external_id"]
48+
# store usage in techniques_used struct
49+
if techniqueID in techniques_used:
50+
techniques_used[techniqueID].append(bearname)
51+
else:
52+
techniques_used[techniqueID] = [bearname]
53+
54+
# format the techniques for the output layer
55+
techniques_list = []
56+
for techniqueID in techniques_used:
57+
techniques_list.append({
58+
"techniqueID": techniqueID,
59+
"comment": "used by " + ", ".join(techniques_used[techniqueID]),
60+
"color": "#ff6666"
61+
})
62+
# construct and return the layer as a dict
63+
return {
64+
"name": "*Bear APTs",
65+
"version": "3.0",
66+
"description": "All techniques used by an APT group with phrase 'bear' in the group aliases",
67+
"domain": "mitre-enterprise",
68+
"techniques": techniques_list,
69+
"legendItems": [{
70+
"label": "Used by a group the phrase 'bear' in the group aliases",
71+
"color": "#ff6666"
72+
}]
73+
}
74+
75+
76+
if __name__ == '__main__':
77+
parser = argparse.ArgumentParser(
78+
description="Parses STIX data to create a layer showing all techniques used by an APT group with phrase 'bear' in the group aliases."
79+
)
80+
parser.add_argument("--output",
81+
type=str,
82+
default="Bear_APT.json",
83+
help="output filepath"
84+
)
85+
args = parser.parse_args()
86+
# get the layer
87+
layer = generate()
88+
# write the layerfile
89+
with open(args.output, "w") as f:
90+
print("writing", args.output)
91+
f.write(json.dumps(layer, indent=4))

scripts/layers/samples/heatmap.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import argparse
2+
import requests
3+
import json
4+
import stix2
5+
import random
6+
7+
def generate():
8+
"""parse the STIX on MITRE/CTI and return a layer dict with techniques with randomized scores"""
9+
# import the STIX data from MITRE/CTI
10+
stix = requests.get("https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json").json()
11+
ms = stix2.MemoryStore(stix_data=stix["objects"])
12+
# get all techniques in STIX
13+
techniques = ms.query([
14+
stix2.Filter("type", "=", "attack-pattern")
15+
])
16+
# parse techniques into layer format
17+
techniques_list = []
18+
for technique in techniques:
19+
# skip deprecated and revoked
20+
if ("x_mitre_deprecated" in technique and technique["x_mitre_deprecated"]) or ("revoked" in technique and technique["revoked"]): continue
21+
techniqueID = technique["external_references"][0]["external_id"] # get the attackID
22+
techniques_list.append({
23+
"techniqueID": techniqueID,
24+
"score": random.randint(1,100) # random score
25+
})
26+
# return the techniques in a layer dict
27+
return {
28+
"name": "heatmap example",
29+
"version": "3.0",
30+
"sorting": 3, # descending order of score
31+
"description": "An example layer where all techniques have a randomized score",
32+
"domain": "mitre-enterprise",
33+
"techniques": techniques_list,
34+
}
35+
36+
37+
if __name__ == '__main__':
38+
# download data depending on domain
39+
parser = argparse.ArgumentParser(
40+
description="Generates a layer wherein all techniques have randomized scores from 1-100."
41+
)
42+
parser.add_argument("--output",
43+
type=str,
44+
default="heatmap_layer.json",
45+
help="output filepath"
46+
)
47+
args = parser.parse_args()
48+
# get the layer
49+
layer = generate()
50+
# write the layerfile
51+
with open(args.output, "w") as f:
52+
print("writing", args.output)
53+
f.write(json.dumps(layer, indent=4))

0 commit comments

Comments
 (0)