Skip to content

Commit 36652e8

Browse files
authored
Add new parser - Threat Composer (#10795)
* Add parser, datasets, unittests, doc and dedup alg * quality changes * ruff linting * Add new checksum * status in parser * settings hash updated
1 parent acec6af commit 36652e8

12 files changed

Lines changed: 2116 additions & 1 deletion
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: "Threat Composer"
3+
toc_hide: true
4+
---
5+
### File Types
6+
This DefectDojo parser accepts JSON files from Threat Composer. The tool supports the [export](https://github.com/awslabs/threat-composer/tree/main?#features) of JSON report out of the browser local storage to a local file.
7+
8+
### Sample Scan Data
9+
Sample scan data for testing purposes can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/threat_composer).
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
71285f56a01869df55a802d79343f43c2e6a42ed52c4bb3591202e62b8569c64
1+
5b5b80d9559990d731f28be5d02e2cdeafe00070c83174008cceaeec74fe1813

dojo/settings/settings.dist.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,7 @@ def saml2_attrib_map_format(dict):
12781278
"Rapplex Scan": ["title", "endpoints", "severity"],
12791279
"AppCheck Web Application Scanner": ["title", "severity"],
12801280
"Legitify Scan": ["title", "endpoints", "severity"],
1281+
"ThreatComposer Scan": ["title", "description"],
12811282
}
12821283

12831284
# Override the hardcoded settings here via the env var
@@ -1501,6 +1502,7 @@ def saml2_attrib_map_format(dict):
15011502
"Rapplex Scan": DEDUPE_ALGO_HASH_CODE,
15021503
"AppCheck Web Application Scanner": DEDUPE_ALGO_HASH_CODE,
15031504
"Legitify Scan": DEDUPE_ALGO_HASH_CODE,
1505+
"ThreatComposer Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE,
15041506
}
15051507

15061508
# Override the hardcoded settings here via the env var

dojo/tools/threat_composer/__init__.py

Whitespace-only changes.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import json
2+
from collections import defaultdict
3+
from os import linesep
4+
5+
from dojo.models import Finding
6+
7+
8+
class ThreatComposerParser:
9+
"""
10+
Threat Composer JSON can be imported. See here for more info on this JSON format.
11+
"""
12+
13+
PRIORITY_VALUES = ["Low", "Medium", "High"]
14+
STRIDE_VALUES = {
15+
"S": "Spoofing",
16+
"T": "Tampering",
17+
"R": "Repudiation",
18+
"I": "Information Disclosure",
19+
"D": "Denial of Service",
20+
"E": "Elevation of Privilege",
21+
}
22+
23+
def get_scan_types(self):
24+
return ["ThreatComposer Scan"]
25+
26+
def get_label_for_scan_types(self, scan_type):
27+
return "ThreatComposer Scan"
28+
29+
def get_description_for_scan_types(self, scan_type):
30+
return "ThreatComposer report file can be imported in JSON format."
31+
32+
def get_findings(self, file, test):
33+
data = json.load(file)
34+
findings = []
35+
36+
if "threats" not in data:
37+
msg = "Invalid ThreatComposer data"
38+
raise ValueError(msg)
39+
40+
if "assumptionLinks" in data:
41+
assumptions = {assumption["id"]: assumption for assumption in data["assumptions"]}
42+
assumption_mitigation_links = defaultdict(list)
43+
assumption_threat_links = defaultdict(list)
44+
for link in data["assumptionLinks"]:
45+
linked_id = link["linkedId"]
46+
assumption_id = link["assumptionId"]
47+
assumption_type = link["type"]
48+
if assumption_id in assumptions:
49+
if assumption_type == "Threat":
50+
assumption_threat_links[linked_id].append(assumptions[assumption_id])
51+
elif assumption_type == "Mitigation":
52+
assumption_mitigation_links[linked_id].append(assumptions[assumption_id])
53+
54+
if "mitigationLinks" in data:
55+
mitigations = {
56+
mitigation["id"]: {
57+
"mitigation": mitigation,
58+
"assumptions": assumption_mitigation_links[mitigation["id"]],
59+
}
60+
for mitigation in data["mitigations"]
61+
}
62+
mitigation_links = defaultdict(list)
63+
for link in data["mitigationLinks"]:
64+
linked_id = link["linkedId"]
65+
mitigation_id = link["mitigationId"]
66+
if mitigation_id in mitigations:
67+
mitigation_links[linked_id].append(mitigations[mitigation_id])
68+
69+
for threat in data["threats"]:
70+
71+
if "threatAction" in threat:
72+
title = threat["threatAction"]
73+
severity, impact, comments = self.parse_threat_metadata(threat["metadata"])
74+
description = self.to_description_text(threat, comments, assumption_threat_links[threat["id"]])
75+
mitigation = self.to_mitigation_text(mitigation_links[threat["id"]])
76+
unique_id_from_tool = threat["id"]
77+
vuln_id_from_tool = threat["numericId"]
78+
tags = threat["tags"] if "tags" in threat else []
79+
80+
finding = Finding(
81+
title=title,
82+
description=description,
83+
severity=severity,
84+
vuln_id_from_tool=vuln_id_from_tool,
85+
unique_id_from_tool=unique_id_from_tool,
86+
mitigation=mitigation,
87+
impact=impact,
88+
tags=tags,
89+
static_finding=True,
90+
dynamic_finding=False,
91+
)
92+
93+
match threat.get("status", "threatIdentified"):
94+
case "threatResolved":
95+
finding.active = False
96+
finding.is_mitigated = True
97+
finding.false_p = False
98+
case "threatResolvedNotUseful":
99+
finding.active = False
100+
finding.is_mitigated = True
101+
finding.false_p = True
102+
103+
findings.append(finding)
104+
105+
return findings
106+
107+
def to_mitigation_text(self, mitigations):
108+
text = ""
109+
for i, current in enumerate(mitigations):
110+
mitigation = current["mitigation"]
111+
assumption_links = current["assumptions"]
112+
counti = i + 1
113+
text += f"**Mitigation {counti} (ID: {mitigation['numericId']}, Status: {mitigation.get('status', 'Not defined')})**: {mitigation['content']}"
114+
115+
for item in mitigation["metadata"]:
116+
if item["key"] == "Comments":
117+
text += f"\n*Comments*: {item['value'].replace(linesep, ' ')} "
118+
break
119+
120+
for j, assumption in enumerate(assumption_links):
121+
countj = j + 1
122+
text += f"\n- *Assumption {countj} (ID: {assumption['numericId']})*: {assumption['content'].replace(linesep, ' ')}"
123+
124+
text += "\n"
125+
126+
return text
127+
128+
def parse_threat_metadata(self, metadata):
129+
severity = "Info"
130+
impact = None
131+
comments = None
132+
133+
for item in metadata:
134+
if item["key"] == "Priority" and item["value"] in self.PRIORITY_VALUES:
135+
severity = item["value"]
136+
elif item["key"] == "STRIDE" and all(element in self.STRIDE_VALUES for element in item["value"]):
137+
impact = ", ".join([self.STRIDE_VALUES[element] for element in item["value"]])
138+
elif item["key"] == "Comments":
139+
comments = item["value"]
140+
141+
return severity, impact, comments
142+
143+
def to_description_text(self, threat, comments, assumption_links):
144+
text = f"**Threat**: {threat['statement']}"
145+
if comments:
146+
text += f"\n*Comments*: {comments}"
147+
148+
for i, assumption in enumerate(assumption_links):
149+
counti = i + 1
150+
text += f"\n- *Assumption {counti} (ID: {assumption['numericId']})*: {assumption['content'].replace(linesep, ' ')}"
151+
152+
return text
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
{
2+
"schema": 1,
3+
"applicationInfo": {
4+
"name": "Threat composer",
5+
"description": ""
6+
},
7+
"architecture": {
8+
"image": "",
9+
"description": ""
10+
},
11+
"dataflow": {
12+
"image": "",
13+
"description": ""
14+
},
15+
"assumptions": [
16+
{
17+
"id": "2d2a1ddf-5bb8-4a55-8f60-e195bc0b4b90",
18+
"numericId": 7,
19+
"content": "lorem ipsum",
20+
"tags": [
21+
"lorem ipsum"
22+
],
23+
"metadata": [
24+
{
25+
"key": "Comments",
26+
"value": "lorem ipsum"
27+
}
28+
],
29+
"displayOrder": 7
30+
}
31+
],
32+
"mitigations": [
33+
{
34+
"id": "bdef5b69-e690-4c9c-bfc1-960390779d3b",
35+
"numericId": 21,
36+
"content": "lorem ipsum",
37+
"tags": [
38+
"lorem ipsum"
39+
],
40+
"metadata": [
41+
{
42+
"key": "Comments",
43+
"value": "lorem ipsum"
44+
}
45+
],
46+
"displayOrder": 21
47+
},
48+
{
49+
"id": "11fb1c71-42f0-4004-89a7-09d8bf6f8b11",
50+
"numericId": 20,
51+
"content": "lorem ipsum",
52+
"metadata": [
53+
{
54+
"key": "Comments",
55+
"value": "lorem ipsum"
56+
}
57+
],
58+
"displayOrder": 20
59+
}
60+
],
61+
"assumptionLinks": [
62+
{
63+
"linkedId": "46db1eb4-a451-4d05-afe1-c695491e2387",
64+
"assumptionId": "d8edcf30-5c76-49f7-a408-20e071bbea1c",
65+
"type": "Threat"
66+
}
67+
],
68+
"mitigationLinks": [
69+
{
70+
"linkedId": "46db1eb4-a451-4d05-afe1-c695491e2387",
71+
"mitigationId": "11fb1c71-42f0-4004-89a7-09d8bf6f8b11"
72+
},
73+
{
74+
"linkedId": "46db1eb4-a451-4d05-afe1-c695491e2387",
75+
"mitigationId": "bdef5b69-e690-4c9c-bfc1-960390779d3b"
76+
}
77+
],
78+
"threats": [
79+
{
80+
"id": "46db1eb4-a451-4d05-afe1-c695491e2387",
81+
"numericId": 23,
82+
"statement": "A lorem ipsum lorem ipsum can lorem ipsum, which leads to lorem ipsum, negatively impacting lorem ipsum",
83+
"threatSource": "lorem ipsum",
84+
"prerequisites": "lorem ipsum",
85+
"threatAction": "lorem ipsum",
86+
"threatImpact": "lorem ipsum",
87+
"impactedAssets": [
88+
"lorem ipsum"
89+
],
90+
"displayOrder": 23,
91+
"metadata": [
92+
{
93+
"key": "Priority",
94+
"value": "High"
95+
},
96+
{
97+
"key": "STRIDE",
98+
"value": [
99+
"S",
100+
"T",
101+
"R",
102+
"I",
103+
"D",
104+
"E"
105+
]
106+
},
107+
{
108+
"key": "Comments",
109+
"value": "lorem ipsum. lorem ipsum lorem ipsum"
110+
}
111+
],
112+
"tags": [
113+
"CWE-156",
114+
"CVE-45",
115+
"lorem ipsum"
116+
]
117+
}
118+
]
119+
}

0 commit comments

Comments
 (0)