Skip to content

Commit

Permalink
Merge pull request #1703 from sahibamittal/google-osv-support
Browse files Browse the repository at this point in the history
Issue #931 : Support for Google OSV
  • Loading branch information
nscuro authored Jul 24, 2022
2 parents 8d7467d + 0c23fac commit e2a5ad4
Show file tree
Hide file tree
Showing 26 changed files with 2,231 additions and 23 deletions.
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

0 comments on commit e2a5ad4

Please sign in to comment.