Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,7 @@ def saml2_attrib_map_format(dict):
'Cloudsploit Scan': ['title', 'description'],
'SonarQube Scan': ['cwe', 'severity', 'file_path'],
'SonarQube API Import': ['title', 'file_path', 'line'],
'Sonatype Application Scan': ['title', 'cwe', 'file_path', 'component_name', 'component_version', 'vulnerability_ids'],
'Dependency Check Scan': ['title', 'cwe', 'file_path'],
'Dockle Scan': ['title', 'description', 'vuln_id_from_tool'],
'Dependency Track Finding Packaging Format (FPF) Export': ['component_name', 'component_version', 'vulnerability_ids'],
Expand Down Expand Up @@ -1400,6 +1401,7 @@ def saml2_attrib_map_format(dict):
'SonarQube Scan detailed': DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL,
'SonarQube Scan': DEDUPE_ALGO_HASH_CODE,
'SonarQube API Import': DEDUPE_ALGO_HASH_CODE,
'Sonatype Application Scan': DEDUPE_ALGO_HASH_CODE,
'Dependency Check Scan': DEDUPE_ALGO_HASH_CODE,
'Dockle Scan': DEDUPE_ALGO_HASH_CODE,
'Tenable Scan': DEDUPE_ALGO_HASH_CODE,
Expand Down
53 changes: 53 additions & 0 deletions dojo/tools/sonatype/identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Implemented according to Sonatype Component Identifiers
# https://help.sonatype.com/en/referencing-package-url--purl--and-component-identifiers.html
class ComponentIdentifier:

def __init__(self, component):
self._component_id = ""
self._component_name = ""
self._component_version = ""

if "componentIdentifier" in component:
component_coordinates = component["componentIdentifier"]["coordinates"]
componant_format = component["componentIdentifier"]["format"]

if componant_format in ["a-name", "pypi", "rpm", "gem", "golang", "conan", "conda", "bower", "composer",
"cran", "cargo", "cocoapods", "drupal", "pecoff", "swift", "generic",
"operating-system"]:
self.set_name_version_component(component_coordinates)
elif componant_format == "maven":
self.set_maven_component(component_coordinates)
elif componant_format in ["npm", "nuget"]:
self.set_package_id_version_component(component_coordinates)
elif "displayName" in component:
self._component_id = component["displayName"]
self._component_name = component["displayName"]

@property
def component_id(self):
return self._component_id

@property
def component_name(self):
return self._component_name

@property
def component_version(self):
return self._component_version

def set_name_version_component(self, component_coordinates):
self._component_id = f"{component_coordinates['name']} {component_coordinates['version']}"
self._component_name = component_coordinates["name"]
self._component_version = component_coordinates["version"]

def set_maven_component(self, component_coordinates):
self._component_id = (f"{component_coordinates['artifactId']} "
f"{component_coordinates['groupId']} "
f"{component_coordinates['version']}")
self._component_name = component_coordinates["artifactId"]
self._component_version = component_coordinates["version"]

def set_package_id_version_component(self, component_coordinates):
self._component_id = f"{component_coordinates['packageId']} {component_coordinates['version']}"
self._component_name = component_coordinates["packageId"]
self._component_version = component_coordinates["version"]
203 changes: 67 additions & 136 deletions dojo/tools/sonatype/parser.py
Original file line number Diff line number Diff line change
@@ -1,154 +1,85 @@
""" Surely a fragile parser, but gets things started and will evolve over time I guess.
It seems that a lot of the json data does not have any securityData data
So these are just skipped, since there is nothing much to do with them here
"""
import json

from dojo.models import Finding
from dojo.tools.sonatype.identifier import ComponentIdentifier


class SonatypeParser(object):
class SonatypeParser:
# This parser does not deal with licenses information.

def get_scan_types(self):
return ["Sonatype Application Scan"]

def get_label_for_scan_types(self, scan_type):
return scan_type # no custom label for now
return "Sonatype Application Scan"

def get_description_for_scan_types(self, scan_type):
return "Can be imported in JSON format"

def get_findings(self, json_output, test):
tree = json.load(json_output)
return self.get_items(tree, test)

def get_items(self, tree, test):
items = {}
if "components" in tree:
vulnerability_tree = tree["components"]
sonatype_report = json.load(json_output)
findings = []
if "components" in sonatype_report:
components = sonatype_report["components"]

for node in vulnerability_tree:
item = get_item(node, test)
if item is None:
for component in components:
if component["securityData"] is None or len(component["securityData"]["securityIssues"]) < 1:
continue
# TODO
unique_key = node["hash"]
items[unique_key] = item

return list(items.values())


def get_item(vulnerability, test):
# Following the CVSS Scoring per https://nvd.nist.gov/vuln-metrics/cvss
if (
vulnerability["securityData"] is not None
and len(vulnerability["securityData"]["securityIssues"]) >= 1
):
# there can be nothing in the array, or securityData can be null altogether. If the latter, well, nothing much to do?
# issues is an array, and there can be 2+ of them, e.g. a cve and a sonatype entry or two cves
# Given the current Finding class, if a cve, will be the main. If not a cve, then CVE ref will remain null due to regex.
# Others go to references.
main_finding = vulnerability["securityData"]["securityIssues"][0]

if main_finding.get("source") == "cve":
vulnerability_id = main_finding.get("reference")
else:
# if sonatype of else, will not match Finding model today
vulnerability_id = None

if main_finding["severity"] <= 3.9:
severity = "Low"
elif (
main_finding["severity"] > 4.0 and main_finding["severity"] <= 6.9
):
severity = "Medium"
elif main_finding["severity"] and main_finding["severity"] <= 8.9:
severity = "High"
else:
severity = "Critical"

references = []
if len(vulnerability["securityData"]["securityIssues"]) > 1:
for additional_issue in vulnerability["securityData"][
"securityIssues"
]:
references.append(
"{}, {}, {}, {}, {} ".format(
additional_issue.get("reference"),
additional_issue.get("status"),
additional_issue.get("severity"),
additional_issue.get("threatCategory"),
additional_issue.get("url"),
)
)

component_id = ""
if "componentIdentifier" in vulnerability:
if vulnerability["componentIdentifier"]["format"] == "maven":
component_id = "{} {} {}".format(
vulnerability["componentIdentifier"]["coordinates"][
"artifactId"
],
vulnerability["componentIdentifier"]["coordinates"][
"groupId"
],
vulnerability["componentIdentifier"]["coordinates"][
"version"
],
)
elif vulnerability["componentIdentifier"]["format"] == "a-name":
component_id = "{} {} {}".format(
vulnerability["componentIdentifier"]["coordinates"][
"name"
],
vulnerability["componentIdentifier"]["coordinates"][
"qualifier"
],
vulnerability["componentIdentifier"]["coordinates"][
"version"
],
)

finding_title = "{} - {}".format(
main_finding["reference"], component_id
)

finding_description = "Hash {}\n\n".format(vulnerability["hash"])
finding_description += component_id
finding_description += '\n\nPlease check the CVE details of this finding for a detailed description. The details of issues beginning with "SONATYPE-" can be found by contacting Sonatype, Inc. or through mechanisms they have provided in their product.'
threat_category = main_finding.get(
"threatCategory", "CVSS vector not provided. "
).title()
status = main_finding["status"]
main_finding.get("severity", "No CVSS score yet.")
if "pathnames" in vulnerability:
file_path = " ".join(vulnerability["pathnames"])[:1000]
else:
file_path = ""

# create the finding object
finding = Finding(
title=finding_title,
test=test,
severity=severity,
description=finding_description,
mitigation=status,
references="{}\n{}\n".format(
main_finding["url"], "\n".join(references)
),
false_p=False,
duplicate=False,
out_of_scope=False,
mitigated=None,
file_path=file_path,
impact=threat_category,
static_finding=True
)
if vulnerability_id:
finding.unsaved_vulnerability_ids = [vulnerability_id]
finding.description = finding.description.strip()

return finding

for security_issue in component["securityData"]["securityIssues"]:
finding = get_finding(security_issue, component, test)
findings.append(finding)

return findings


def get_finding(security_issue, component, test):

severity = get_severity(security_issue)
threat_category = security_issue.get("threatCategory", "CVSS vector not provided. ").title()
status = security_issue["status"]
reference = security_issue["url"]

identifier = ComponentIdentifier(component)
title = f"{security_issue['reference']} - {identifier.component_id}"

finding_description = f"Hash {component['hash']}\n\n"
finding_description += identifier.component_id
finding_description = finding_description.strip()

finding = Finding(
test=test,
title=title,
description=finding_description,
component_name=identifier.component_name,
component_version=identifier.component_version,
severity=severity,
mitigation=status,
references=reference,
impact=threat_category,
static_finding=True
)
if "cwe" in security_issue:
finding.cwe = security_issue["cwe"]

if "cvssVector" in security_issue:
finding.cvssv3 = security_issue["cvssVector"]

if "pathnames" in component:
finding.file_path = " ".join(component["pathnames"])[:1000]

if security_issue.get("source") == "cve":
vulnerability_id = security_issue.get("reference")
finding.unsaved_vulnerability_ids = [vulnerability_id]

return finding


def get_severity(vulnerability):
if vulnerability["severity"] <= 3.9:
return "Low"
elif vulnerability["severity"] <= 6.9:
return "Medium"
elif vulnerability["severity"] <= 8.9:
return "High"
else:
return None
return "Critical"
11 changes: 11 additions & 0 deletions unittests/scans/sonatype/many_vulns.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,17 @@
"status": "Open",
"url": null,
"threatCategory": "severe"
},
{
"source": "sonatype",
"reference": "sonatype-2023-4856",
"severity": 5.3,
"status": "Not Applicable",
"url": null,
"threatCategory": "severe",
"cwe": "693",
"cvssVector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N",
"cvssVectorSource": "sonatype_cvss_3"
}
]
}
Expand Down
52 changes: 52 additions & 0 deletions unittests/scans/sonatype/no_vuln.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"components": [
{
"hash": "212387a39088ecd3daff",
"componentIdentifier": {
"format": "maven",
"coordinates": {
"artifactId": "okhttp",
"classifier": "",
"extension": "jar",
"groupId": "com.squareup.okhttp",
"version": "2.6.0"
}
},
"proprietary": false,
"matchState": "exact",
"pathnames": [
"WEB-INF/lib/okhttp-2.6.0.jar"
],
"licenseData": {
"declaredLicenses": [
{
"licenseId": "Apache-2.0",
"licenseName": "Apache-2.0"
}
],
"observedLicenses": [
{
"licenseId": "Apache-2.0",
"licenseName": "Apache-2.0"
}
],
"overriddenLicenses": [],
"status": "Open",
"effectiveLicenseThreats": [
{
"licenseThreatGroupName": "Liberal",
"licenseThreatGroupLevel": 0,
"licenseThreatGroupCategory": "no-threat"
}
]
},
"securityData": {
"securityIssues": []
}
}
],
"matchSummary": {
"totalComponentCount": 1011,
"knownComponentCount": 826
}
}
Loading