Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #931 : Support for Google OSV #1703

Merged
merged 28 commits into from
Jul 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bcbddff
draft for using google OSV
sahibamittal Jun 10, 2022
ee624bc
ConfigProperties test fix
sahibamittal Jun 10, 2022
594af37
Vulnerability mapping done
sahibamittal Jun 14, 2022
7b6ea43
unit test for osv task
sahibamittal Jun 15, 2022
a9a0783
osv enabled default to true
sahibamittal Jun 16, 2022
a8adf36
fixes and tests
sahibamittal Jun 17, 2022
eeb1372
fix http client
sahibamittal Jun 17, 2022
dba49b5
update source of vulnerability
sahibamittal Jun 23, 2022
0fb6ad1
map credits
sahibamittal Jun 23, 2022
6f725af
minor changes
sahibamittal Jun 24, 2022
b987030
close reader
sahibamittal Jun 24, 2022
cca6c9f
update severity calculation and prioritize
sahibamittal Jun 28, 2022
cdf1e90
handle vulnerability mapping to avoid whole task
sahibamittal Jun 28, 2022
7fe4c5d
fix out of bound exception
sahibamittal Jun 28, 2022
2edf945
changes to avoid clashing with github or nvd
sahibamittal Jun 29, 2022
7fb3b42
fix for commit hash ranges and small changes requested
sahibamittal Jun 30, 2022
d8f836a
handle purl parsing
sahibamittal Jul 1, 2022
985a58f
handle version range types, disable default osv
sahibamittal Jul 1, 2022
94072d5
fix de duplication of vulnerable softwares
sahibamittal Jul 1, 2022
7b0afee
small test fix
sahibamittal Jul 1, 2022
d66e211
Merge branch 'master' into google-osv-support
sahibamittal Jul 1, 2022
0477ecd
Perform `null` check before parsing PURLs
nscuro Jul 2, 2022
a8aba45
Adjust class names to rest of the code base
nscuro Jul 2, 2022
da1d059
Remove redundant QueryManager method; Test more mapped vulnerability …
nscuro Jul 2, 2022
bb57600
Refactor OSV range parsing to avoid infinite loops
nscuro Jul 2, 2022
c6c687e
Fetch `Vulnerability#vulnerableSoftware` lazily
nscuro Jul 3, 2022
d855040
change OSV label from Google
sahibamittal Jul 21, 2022
0c23fac
Merge remote-tracking branch 'upstream/master' into google-osv-support
sahibamittal Jul 21, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.dependencytrack.tasks.EpssMirrorTask;
import org.dependencytrack.tasks.FortifySscUploadTask;
import org.dependencytrack.tasks.GitHubAdvisoryMirrorTask;
import org.dependencytrack.tasks.OsvDownloadTask;
import org.dependencytrack.tasks.IndexTask;
import org.dependencytrack.tasks.InternalComponentIdentificationTask;
import org.dependencytrack.tasks.KennaSecurityUploadTask;
Expand Down Expand Up @@ -80,6 +81,7 @@ public void contextInitialized(final ServletContextEvent event) {
EVENT_SERVICE.subscribe(InternalAnalysisEvent.class, InternalAnalysisTask.class);
EVENT_SERVICE.subscribe(OssIndexAnalysisEvent.class, OssIndexAnalysisTask.class);
EVENT_SERVICE.subscribe(GitHubAdvisoryMirrorEvent.class, GitHubAdvisoryMirrorTask.class);
EVENT_SERVICE.subscribe(OsvMirrorEvent.class, OsvDownloadTask.class);
EVENT_SERVICE.subscribe(VulnDbSyncEvent.class, VulnDbSyncTask.class);
EVENT_SERVICE.subscribe(VulnDbAnalysisEvent.class, VulnDbAnalysisTask.class);
EVENT_SERVICE.subscribe(VulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class);
Expand Down Expand Up @@ -114,6 +116,7 @@ public void contextDestroyed(final ServletContextEvent event) {
EVENT_SERVICE.unsubscribe(InternalAnalysisTask.class);
EVENT_SERVICE.unsubscribe(OssIndexAnalysisTask.class);
EVENT_SERVICE.unsubscribe(GitHubAdvisoryMirrorTask.class);
EVENT_SERVICE.unsubscribe(OsvDownloadTask.class);
EVENT_SERVICE.unsubscribe(VulnDbSyncTask.class);
EVENT_SERVICE.unsubscribe(VulnDbAnalysisTask.class);
EVENT_SERVICE.unsubscribe(VulnerabilityAnalysisTask.class);
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/org/dependencytrack/event/OsvMirrorEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.dependencytrack.event;

import alpine.event.framework.Event;

/**
* Defines an event used to start a mirror of Google OSV.
*/
public class OsvMirrorEvent implements Event {

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public enum ConfigPropertyConstants {
VULNERABILITY_SOURCE_NVD_FEEDS_URL("vuln-source", "nvd.feeds.url", "https://nvd.nist.gov/feeds", PropertyType.URL, "A base URL pointing to the hostname and path of the NVD feeds"),
VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED("vuln-source", "github.advisories.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitHub Advisories"),
VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ACCESS_TOKEN("vuln-source", "github.advisories.access.token", null, PropertyType.STRING, "The access token used for GitHub API authentication"),
VULNERABILITY_SOURCE_GOOGLE_OSV_ENABLED("vuln-source", "google.osv.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable Google OSV"),
VULNERABILITY_SOURCE_EPSS_ENABLED("vuln-source", "epss.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable Exploit Prediction Scoring System"),
VULNERABILITY_SOURCE_EPSS_FEEDS_URL("vuln-source", "epss.feeds.url", "https://epss.cyentia.com", PropertyType.URL, "A base URL pointing to the hostname and path of the EPSS feeds"),
ACCEPT_ARTIFACT_CYCLONEDX("artifact", "cyclonedx.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable the systems ability to accept CycloneDX uploads"),
Expand Down
28 changes: 22 additions & 6 deletions src/main/java/org/dependencytrack/model/Severity.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,33 @@
*/
package org.dependencytrack.model;

import java.util.Arrays;

/**
* Defines internal severity labels.
*
* @author Steve Springett
* @since 3.0.0
*/
public enum Severity {
CRITICAL,
HIGH,
MEDIUM,
LOW,
INFO,
UNASSIGNED
CRITICAL (5),
HIGH (4),
MEDIUM (3),
LOW (2),
INFO (1),
UNASSIGNED (0);

private final int level;

Severity(final int level) {
this.level = level;
}

public int getLevel() {
return level;
}

public static Severity getSeverityByLevel(final int level){
return Arrays.stream(values()).filter(value -> value.level == level).findFirst().orElse(UNASSIGNED);
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/dependencytrack/model/Vulnerability.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public enum Source {
VULNDB, // VulnDB from Risk Based Security
OSSINDEX, // Sonatype OSS Index
RETIREJS, // Retire.js
INTERNAL // Internally-managed (and manually entered) vulnerability
INTERNAL, // Internally-managed (and manually entered) vulnerability
OSV // Google OSV Advisories
}

@PrimaryKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@
import org.dependencytrack.parser.github.graphql.model.GitHubSecurityAdvisory;
import org.dependencytrack.parser.github.graphql.model.GitHubVulnerability;
import org.dependencytrack.parser.github.graphql.model.PageableList;

import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;

import static org.dependencytrack.util.JsonUtil.jsonStringToTimestamp;

public class GitHubSecurityAdvisoryParser {

public PageableList parse(final JSONObject object) {
Expand Down Expand Up @@ -102,7 +101,7 @@ private GitHubSecurityAdvisory parseSecurityAdvisory(final JSONObject object) {
final JSONObject cvss = object.optJSONObject("cvss");
if (cvss != null) {
advisory.setCvssScore(cvss.optInt("score", 0));
advisory.setCvssVector(cvss.optString("vectorString", null));
advisory.setCvssVector(cvss.optString("score", null));
}

final JSONObject cwes = object.optJSONObject("cwes");
Expand Down Expand Up @@ -162,15 +161,4 @@ private GitHubVulnerability parseVulnerability(final JSONObject object) {
}
return vulnerability;
}

private ZonedDateTime jsonStringToTimestamp(final String s) {
if (s == null) {
return null;
}
try {
return ZonedDateTime.parse(s);
} catch (DateTimeParseException e) {
return null;
}
}
}
264 changes: 264 additions & 0 deletions src/main/java/org/dependencytrack/parser/osv/OsvAdvisoryParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package org.dependencytrack.parser.osv;

import kong.unirest.json.JSONArray;
import kong.unirest.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.model.Severity;
import org.dependencytrack.parser.osv.model.OsvAdvisory;
import org.dependencytrack.parser.osv.model.OsvAffectedPackage;
import us.springett.cvss.Cvss;
import us.springett.cvss.Score;

import java.util.ArrayList;
import java.util.List;

import static org.dependencytrack.util.JsonUtil.jsonStringToTimestamp;
import static org.dependencytrack.util.VulnerabilityUtil.normalizedCvssV3Score;

/*
Parser for Google OSV, an aggregator of vulnerability databases including GitHub Security Advisories, PyPA, RustSec, and Global Security Database, and more.
*/
public class OsvAdvisoryParser {

public OsvAdvisory parse(final JSONObject object) {

OsvAdvisory advisory = null;

// initial check if advisory is valid or withdrawn
String withdrawn = object.optString("withdrawn", null);

if(object != null && withdrawn == null) {

advisory = new OsvAdvisory();
advisory.setId(object.optString("id", null));
advisory.setSummary(trimSummary(object.optString("summary", null)));
advisory.setDetails(object.optString("details", null));
advisory.setPublished(jsonStringToTimestamp(object.optString("published", null)));
advisory.setModified(jsonStringToTimestamp(object.optString("modified", null)));
advisory.setSchema_version(object.optString("schema_version", null));

final JSONArray references = object.optJSONArray("references");
if (references != null) {
for (int i=0; i<references.length(); i++) {
final JSONObject reference = references.getJSONObject(i);
final String url = reference.optString("url", null);
advisory.addReference(url);
}
}

final JSONArray credits = object.optJSONArray("credits");
if (credits != null) {
for (int i=0; i<credits.length(); i++) {
final JSONObject credit = credits.getJSONObject(i);
final String name = credit.optString("name", null);
advisory.addCredit(name);
}
}

final JSONArray aliases = object.optJSONArray("aliases");
if(aliases != null) {
for (int i=0; i<aliases.length(); i++) {
advisory.addAlias(aliases.optString(i));
}
}

final JSONObject databaseSpecific = object.optJSONObject("database_specific");
if (databaseSpecific != null) {
advisory.setSeverity(databaseSpecific.optString("severity", null));
final JSONArray cweIds = databaseSpecific.optJSONArray("cwe_ids");
if(cweIds != null) {
for (int i=0; i<cweIds.length(); i++) {
advisory.addCweId(cweIds.optString(i));
}
}
}

final JSONArray cvssList = object.optJSONArray("severity");
if (cvssList != null) {
for (int i=0; i<cvssList.length(); i++) {
final JSONObject cvss = cvssList.getJSONObject(i);
final String type = cvss.optString("type", null);
if (type.equalsIgnoreCase("CVSS_V3")) {
advisory.setCvssV3Vector(cvss.optString("score", null));
}
if (type.equalsIgnoreCase("CVSS_V2")) {
advisory.setCvssV2Vector(cvss.optString("score", null));
}
}
}

final List<OsvAffectedPackage> affectedPackages = parseAffectedPackages(object);
advisory.setAffectedPackages(affectedPackages);
}
return advisory;
}

private List<OsvAffectedPackage> parseAffectedPackages(final JSONObject advisory) {

List<OsvAffectedPackage> affectedPackages = new ArrayList<>();
final JSONArray affected = advisory.optJSONArray("affected");
if (affected != null) {
for(int i=0; i<affected.length(); i++) {

affectedPackages.addAll(parseAffectedPackageRange(affected.getJSONObject(i)));
}
}
return affectedPackages;
}

public List<OsvAffectedPackage> parseAffectedPackageRange(final JSONObject affected) {

List<OsvAffectedPackage> osvAffectedPackageList = new ArrayList<>();
final JSONArray ranges = affected.optJSONArray("ranges");
final JSONArray versions = affected.optJSONArray("versions");
if (ranges != null) {
for (int j=0; j<ranges.length(); j++) {
final JSONObject range = ranges.getJSONObject(j);
osvAffectedPackageList.addAll(parseVersionRanges(affected, range));
}
}
// if ranges are not available or only commit hash range is available, look for versions
if (osvAffectedPackageList.size() == 0 && versions != null && versions.length() > 0) {
for (int j=0; j<versions.length(); j++) {
OsvAffectedPackage vuln = createAffectedPackage(affected);
vuln.setVersion(versions.getString(j));
osvAffectedPackageList.add(vuln);
}
}
// if no parsable range or version is available, add vulnerability without version
else if (osvAffectedPackageList.size() == 0) {
osvAffectedPackageList.add(createAffectedPackage(affected));
}
return osvAffectedPackageList;
}

private List<OsvAffectedPackage> parseVersionRanges(JSONObject vulnerability, JSONObject range) {
final String rangeType = range.optString("type");
if (!"ECOSYSTEM".equalsIgnoreCase(rangeType) && !"SEMVER".equalsIgnoreCase(rangeType)) {
// We can't support ranges of type GIT for now, as evaluating them requires knowledge of
// the entire Git history of a package. We don't have that, so there's no point in
// ingesting this data.
//
// We're also implicitly excluding ranges of types that we don't yet know of.
// This is a tradeoff of potentially missing new data vs. flooding our users'
// database with junk data.
return List.of();
}

final JSONArray rangeEvents = range.optJSONArray("events");
if (rangeEvents == null) {
return List.of();
}

final List<OsvAffectedPackage> affectedPackages = new ArrayList<>();

for (int i = 0; i < rangeEvents.length(); i++) {
JSONObject event = rangeEvents.getJSONObject(i);

final String introduced = event.optString("introduced", null);
if (introduced == null) {
// "introduced" is required for every range. But events are not guaranteed to be sorted,
// it's merely a recommendation by the OSV specification.
//
// If events are not sorted, we have no way to tell what the correct order should be.
// We make a tradeoff by assuming that ranges are sorted, and potentially skip ranges
// that aren't.
continue;
}

final OsvAffectedPackage affectedPackage = createAffectedPackage(vulnerability);
affectedPackage.setLowerVersionRange(introduced);

if (i + 1 < rangeEvents.length()) {
event = rangeEvents.getJSONObject(i + 1);
final String fixed = event.optString("fixed", null);
final String lastAffected = event.optString("last_affected", null);
final String limit = event.optString("limit", null);

if (fixed != null) {
affectedPackage.setUpperVersionRangeExcluding(fixed);
i++;
} else if (lastAffected != null) {
affectedPackage.setUpperVersionRangeIncluding(lastAffected);
i++;
} else if (limit != null) {
affectedPackage.setUpperVersionRangeExcluding(limit);
i++;
}
}

// Special treatment for GitHub: https://github.com/github/advisory-database/issues/470
final JSONObject databaseSpecific = vulnerability.optJSONObject("database_specific");
if (databaseSpecific != null
&& affectedPackage.getUpperVersionRangeIncluding() == null
&& affectedPackage.getUpperVersionRangeExcluding() == null) {
final String lastAffectedRange = databaseSpecific.optString("last_known_affected_version_range", null);
if (lastAffectedRange != null) {
if (lastAffectedRange.startsWith("<=")) {
affectedPackage.setUpperVersionRangeIncluding(lastAffectedRange.replaceFirst("<=", "").trim());
} else if (lastAffectedRange.startsWith("<")) {
affectedPackage.setUpperVersionRangeExcluding(lastAffectedRange.replaceAll("<", "").trim());
}
}
}

affectedPackages.add(affectedPackage);
}

return affectedPackages;
}

private OsvAffectedPackage createAffectedPackage(JSONObject vulnerability) {

OsvAffectedPackage osvAffectedPackage = new OsvAffectedPackage();
final JSONObject affectedPackageJson = vulnerability.optJSONObject("package");
final JSONObject ecosystemSpecific = vulnerability.optJSONObject("ecosystem_specific");
final JSONObject databaseSpecific = vulnerability.optJSONObject("database_specific");
Severity ecosystemSeverity = parseEcosystemSeverity(ecosystemSpecific, databaseSpecific);
osvAffectedPackage.setPackageName(affectedPackageJson.optString("name", null));
osvAffectedPackage.setPackageEcosystem(affectedPackageJson.optString("ecosystem", null));
osvAffectedPackage.setPurl(affectedPackageJson.optString("purl", null));
osvAffectedPackage.setSeverity(ecosystemSeverity);
return osvAffectedPackage;
}

private Severity parseEcosystemSeverity(JSONObject ecosystemSpecific, JSONObject databaseSpecific) {

String severity = null;

if (databaseSpecific != null) {
String cvssVector = databaseSpecific.optString("cvss", null);
if (cvssVector != null) {
Cvss cvss = Cvss.fromVector(cvssVector);
Score score = cvss.calculateScore();
severity = String.valueOf(normalizedCvssV3Score(score.getBaseScore()));
}
}

if(severity == null && ecosystemSpecific != null) {
severity = ecosystemSpecific.optString("severity", null);
}

if (severity != null) {
if (severity.equalsIgnoreCase("CRITICAL")) {
return Severity.CRITICAL;
} else if (severity.equalsIgnoreCase("HIGH")) {
return Severity.HIGH;
} else if (severity.equalsIgnoreCase("MODERATE") || severity.equalsIgnoreCase("MEDIUM")) {
return Severity.MEDIUM;
} else if (severity.equalsIgnoreCase("LOW")) {
return Severity.LOW;
}
}
return Severity.UNASSIGNED;
}

public String trimSummary(String summary) {

final int MAX_LEN = 255;
if(summary != null && summary.length() > 255) {
return StringUtils.substring(summary, 0, MAX_LEN-2) + "..";
}
return summary;
}
}
Loading