Skip to content

Commit 32bad0b

Browse files
twistlock: parse compliances (#12772)
* twistlock: parse compliances * twistlock: finetune
1 parent 2e5de02 commit 32bad0b

File tree

5 files changed

+292
-39
lines changed

5 files changed

+292
-39
lines changed

dojo/tools/twistlock/parser.py

Lines changed: 168 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import json
55
import logging
66
import textwrap
7+
from datetime import datetime
78

89
from dojo.models import Finding
910

@@ -24,6 +25,71 @@ def parse_issue(self, row, test):
2425
data_cvss = row.get("CVSS", "")
2526
data_description = row.get("Description", "")
2627

28+
# Parse timestamp information (Item 4)
29+
published_date = row.get("Published", "")
30+
discovered_date = row.get("Discovered", "")
31+
finding_date = None
32+
33+
# Use Published date as primary, fallback to Discovered
34+
date_str = published_date or discovered_date
35+
if date_str:
36+
try:
37+
# Handle format like "2020-09-04 00:15:00.000"
38+
finding_date = datetime.strptime(date_str.split(".")[0], "%Y-%m-%d %H:%M:%S").date()
39+
except ValueError:
40+
try:
41+
# Handle alternative formats
42+
finding_date = datetime.strptime(date_str[:10], "%Y-%m-%d").date()
43+
except ValueError:
44+
logger.warning(f"Could not parse date: {date_str}")
45+
46+
# Build container/image metadata for impact field (Item 3)
47+
impact_parts = []
48+
49+
# Registry and repository information which can change between scans, so we add it to the impact field as the description field is sometimes used for hash code calculation
50+
registry = row.get("Registry", "")
51+
repository = row.get("Repository", "")
52+
tag = row.get("Tag", "")
53+
image_id = row.get("Id", "")
54+
distro = row.get("Distro", "")
55+
56+
if registry:
57+
impact_parts.append(f"Registry: {registry}")
58+
if repository:
59+
impact_parts.append(f"Repository: {repository}")
60+
if tag:
61+
impact_parts.append(f"Tag: {tag}")
62+
if image_id:
63+
impact_parts.append(f"Image ID: {image_id}")
64+
if distro:
65+
impact_parts.append(f"Distribution: {distro}")
66+
67+
# Host and container information
68+
hosts = row.get("Hosts", "")
69+
containers = row.get("Containers", "")
70+
clusters = row.get("Clusters", "")
71+
binaries = row.get("Binaries", "")
72+
custom_labels = row.get("Custom Labels", "")
73+
74+
if hosts:
75+
impact_parts.append(f"Hosts: {hosts}")
76+
if containers:
77+
impact_parts.append(f"Containers: {containers}")
78+
if clusters:
79+
impact_parts.append(f"Clusters: {clusters}")
80+
if binaries:
81+
impact_parts.append(f"Binaries: {binaries}")
82+
if custom_labels:
83+
impact_parts.append(f"Custom Labels: {custom_labels}")
84+
85+
# Add timestamp information to impact
86+
if published_date:
87+
impact_parts.append(f"Published: {published_date}")
88+
if discovered_date:
89+
impact_parts.append(f"Discovered: {discovered_date}")
90+
91+
impact_text = "\n".join(impact_parts) if impact_parts else data_severity
92+
2793
if data_vulnerability_id and data_package_name:
2894
title = (
2995
data_vulnerability_id
@@ -40,6 +106,7 @@ def parse_issue(self, row, test):
40106
finding = Finding(
41107
title=textwrap.shorten(title, width=255, placeholder="..."),
42108
test=test,
109+
date=finding_date,
43110
severity=convert_severity(data_severity),
44111
description=data_description
45112
+ "<p> Vulnerable Package: "
@@ -52,12 +119,8 @@ def parse_issue(self, row, test):
52119
data_package_name, width=200, placeholder="...",
53120
),
54121
component_version=data_package_version,
55-
false_p=False,
56-
duplicate=False,
57-
out_of_scope=False,
58-
mitigated=None,
59122
severity_justification=f"(CVSS v3 base score: {data_cvss})",
60-
impact=data_severity,
123+
impact=impact_text,
61124
)
62125
finding.description = finding.description.strip()
63126
if data_vulnerability_id:
@@ -116,19 +179,53 @@ def parse_json(self, json_output):
116179
def get_items(self, tree, test):
117180
items = {}
118181
if "results" in tree:
119-
vulnerabilityTree = tree["results"][0].get("vulnerabilities", [])
182+
# Extract image metadata for impact field (Item 3)
183+
result = tree["results"][0]
184+
image_metadata = self.build_image_metadata(result)
185+
186+
# Parse vulnerabilities
187+
vulnerabilityTree = result.get("vulnerabilities", [])
120188
for node in vulnerabilityTree:
121-
item = get_item(node, test)
189+
item = get_item(node, test, image_metadata)
122190
unique_key = node["id"] + str(
123191
node["packageName"]
124192
+ str(node["packageVersion"])
125193
+ str(node["severity"]),
126194
)
127195
items[unique_key] = item
196+
197+
# Parse compliance findings
198+
complianceTree = result.get("compliances", [])
199+
for node in complianceTree:
200+
item = get_compliance_item(node, test, image_metadata)
201+
# Create unique key for compliance findings - prefer ID if available
202+
if node.get("id"):
203+
unique_key = f"compliance_{node['id']}"
204+
else:
205+
# Fallback to hash of title + description
206+
unique_key = "compliance_" + hashlib.md5(
207+
(node.get("title", "") + node.get("description", "")).encode("utf-8"),
208+
usedforsecurity=False,
209+
).hexdigest()
210+
items[unique_key] = item
128211
return list(items.values())
129212

213+
def build_image_metadata(self, result):
214+
"""Build image metadata string for impact field"""
215+
metadata_parts = []
216+
217+
image_id = result.get("id", "")
218+
distro = result.get("distro", "")
219+
220+
if image_id:
221+
metadata_parts.append(f"Image ID: {image_id}")
222+
if distro:
223+
metadata_parts.append(f"Distribution: {distro}")
130224

131-
def get_item(vulnerability, test):
225+
return "\n".join(metadata_parts)
226+
227+
228+
def get_item(vulnerability, test, image_metadata=""):
132229
severity = (
133230
convert_severity(vulnerability["severity"])
134231
if "severity" in vulnerability
@@ -147,6 +244,12 @@ def get_item(vulnerability, test):
147244
vulnerability.get("riskFactors", "No risk factors.")
148245
)
149246

247+
# Build impact field combining severity and image metadata which can change between scans, so we add it to the impact field as the description field is sometimes used for hash code calculation
248+
impact_parts = [severity]
249+
if image_metadata:
250+
impact_parts.append(image_metadata)
251+
impact_text = "\n".join(impact_parts)
252+
150253
# create the finding object
151254
finding = Finding(
152255
title=vulnerability.get("id", "Unknown Vulnerability")
@@ -166,19 +269,71 @@ def get_item(vulnerability, test):
166269
references=vulnerability.get("link"),
167270
component_name=vulnerability.get("packageName", ""),
168271
component_version=vulnerability.get("packageVersion", ""),
169-
false_p=False,
170-
duplicate=False,
171-
out_of_scope=False,
172-
mitigated=None,
173272
severity_justification=f"{vector} (CVSS v3 base score: {cvss})\n\n{riskFactors}",
174-
impact=severity,
273+
cvssv3_score=cvss,
274+
impact=impact_text,
175275
)
176276
finding.unsaved_vulnerability_ids = [vulnerability["id"]] if "id" in vulnerability else None
177277
finding.description = finding.description.strip()
178278

179279
return finding
180280

181281

282+
def get_compliance_item(compliance, test, image_metadata=""):
283+
"""Create a Finding object for compliance issues"""
284+
severity = (
285+
convert_severity(compliance["severity"])
286+
if "severity" in compliance
287+
else "Info"
288+
)
289+
290+
title = compliance.get("title", "Unknown Compliance Issue")
291+
description = compliance.get("description", "No description specified")
292+
compliance_id = compliance.get("id", "")
293+
category = compliance.get("category", "")
294+
layer_time = compliance.get("layerTime", "")
295+
296+
# Build comprehensive description
297+
desc_parts = [f"<p><strong>Compliance Issue:</strong> {title}</p>"]
298+
299+
if compliance_id:
300+
desc_parts.append(f"<p><strong>Compliance ID:</strong> {compliance_id}</p>")
301+
302+
if category:
303+
desc_parts.append(f"<p><strong>Category:</strong> {category}</p>")
304+
305+
desc_parts.append(f"<p><strong>Description:</strong> {description}</p>")
306+
307+
# Build impact field combining severity and image metadata
308+
impact_parts = [severity]
309+
if image_metadata:
310+
impact_parts.append(image_metadata)
311+
if layer_time:
312+
desc_parts.append(f"Layer Time: {layer_time}")
313+
impact_text = "\n".join(impact_parts)
314+
315+
# create the finding object for compliance
316+
finding = Finding(
317+
title=f"Compliance: {title}",
318+
test=test,
319+
severity=severity,
320+
description="".join(desc_parts),
321+
mitigation="Review and address the compliance issue as described in the description.",
322+
severity_justification=f"Compliance severity: {severity}",
323+
impact=impact_text,
324+
vuln_id_from_tool=str(compliance_id) if compliance_id else None,
325+
)
326+
finding.description = finding.description.strip()
327+
328+
# Add compliance-specific tags
329+
tags = ["compliance"]
330+
if category:
331+
tags.append(category.lower())
332+
finding.unsaved_tags = tags
333+
334+
return finding
335+
336+
182337
def convert_severity(severity):
183338
if severity.lower() == "important":
184339
return "High"

unittests/scans/twistlock/findings_include_packages.json

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,30 @@
44
"id": "sha256:f4c0503d26c8da0a04e6190c9d4f2a30f38958852b9fb80bcd2b819a7571e7f7",
55
"distro": "Debian GNU/Linux 9 (stretch)",
66
"compliances": [
7-
{
8-
"title": "Sensitive information provided in environment variables",
9-
"severity": "high",
10-
"cause": "The environment variables DD_CELERY_BROKER_PASSWORD,DD_DATABASE_PASSWORD,DD_SECRET_KEY contain sensitive data"
11-
}
7+
{
8+
"id": 912,
9+
"title": "(CIS_Kubernetes_v1.6.0 - 1.1) Ensure API server encryption is enabled",
10+
"severity": "high",
11+
"description": "Encrypting etcd data at rest ensures sensitive information is protected from unauthorized access.",
12+
"layerTime": "2024-07-01T12:00:00Z",
13+
"category": "Kubernetes"
14+
},
15+
{
16+
"id": 914,
17+
"title": "(CIS_Kubernetes_v1.6.0 - 1.3) Ensure audit logging is enabled",
18+
"severity": "medium",
19+
"cause": "Audit logs are not configured on the API server.",
20+
"description": "Enabling audit logs helps monitor access and detect suspicious activity.",
21+
"layerTime": "2024-07-01T12:00:00Z",
22+
"category": "Kubernetes"
23+
}
1224
],
1325
"complianceDistribution": {
1426
"critical": 0,
1527
"high": 1,
16-
"medium": 0,
28+
"medium": 1,
1729
"low": 0,
18-
"total": 1
30+
"total": 2
1931
},
2032
"vulnerabilities": [
2133
{

unittests/scans/twistlock/no_vuln.json

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,13 @@
44
"id": "sha256:184c9823bc3e7795040025a33d174987897783ee9296545abcb1a1937f12d391",
55
"distro": "",
66
"compliances": [
7-
{
8-
"title": "(CIS_Docker_CE_v1.1.0 - 4.6) Add HEALTHCHECK instruction to the container image",
9-
"severity": "medium"
10-
}
117
],
128
"complianceDistribution": {
139
"critical": 0,
1410
"high": 0,
15-
"medium": 1,
11+
"medium": 0,
1612
"low": 0,
17-
"total": 1
13+
"total": 0
1814
},
1915
"vulnerabilityDistribution": {
2016
"critical": 0,

unittests/scans/twistlock/one_vuln.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
"distro": "Debian GNU/Linux 9 (stretch)",
66
"compliances": [
77
{
8-
"title": "Sensitive information provided in environment variables",
8+
"id": 912,
9+
"title": "(CIS_Kubernetes_v1.6.0 - 1.1) Ensure API server encryption is enabled",
910
"severity": "high",
10-
"cause": "The environment variables DD_CELERY_BROKER_PASSWORD,DD_DATABASE_PASSWORD,DD_SECRET_KEY contain sensitive data"
11+
"description": "Encrypting etcd data at rest ensures sensitive information is protected from unauthorized access.",
12+
"layerTime": "2024-07-01T12:00:00Z",
13+
"category": "Kubernetes"
1114
}
1215
],
1316
"complianceDistribution": {
1417
"critical": 0,
1518
"high": 1,
16-
"medium": 0,
19+
"medium": 1,
1720
"low": 0,
18-
"total": 1
21+
"total": 2
1922
},
2023
"vulnerabilities": [
2124
{

0 commit comments

Comments
 (0)