OssIndexAnalyzer.java
/*
* This file is part of dependency-check-core.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) 2019 Jason Dillon. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;
import io.github.jeremylong.openvulnerability.client.nvd.CvssV2;
import io.github.jeremylong.openvulnerability.client.nvd.CvssV2Data;
import org.sonatype.ossindex.service.api.componentreport.ComponentReport;
import org.sonatype.ossindex.service.api.componentreport.ComponentReportVulnerability;
import org.sonatype.ossindex.service.api.cvss.Cvss2Severity;
import org.sonatype.ossindex.service.api.cvss.Cvss2Vector;
import org.sonatype.ossindex.service.api.cvss.CvssVector;
import org.sonatype.ossindex.service.api.cvss.CvssVectorFactory;
import org.sonatype.ossindex.service.client.OssindexClient;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.ossindex.OssindexClientFactory;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.Vulnerability;
import org.owasp.dependencycheck.dependency.VulnerableSoftware;
import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder;
import org.owasp.dependencycheck.dependency.naming.Identifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.exceptions.CpeValidationException;
import us.springett.parsers.cpe.values.Part;
import org.sonatype.goodies.packageurl.PackageUrl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.net.SocketTimeoutException;
import javax.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.owasp.dependencycheck.utils.CvssUtil;
import org.sonatype.goodies.packageurl.InvalidException;
import org.sonatype.ossindex.service.client.transport.Transport.TransportException;
/**
* Enrich dependency information from Sonatype OSS index.
*
* @author Jason Dillon
* @since 5.0.0
*/
public class OssIndexAnalyzer extends AbstractAnalyzer {
/**
* A reference to the logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(OssIndexAnalyzer.class);
/**
* A pattern to match CVE identifiers.
*/
private static final Pattern CVE_PATTERN = Pattern.compile("\\bCVE-\\d{4}-\\d{4,10}\\b");
/**
* The reference type.
*/
public static final String REFERENCE_TYPE = "OSSINDEX";
/**
* Fetched reports.
*/
private static Map<PackageUrl, ComponentReport> reports;
/**
* Lock to protect fetching state.
*/
private static final Object FETCH_MUTIX = new Object();
@Override
public String getName() {
return "Sonatype OSS Index Analyzer";
}
@Override
public AnalysisPhase getAnalysisPhase() {
return AnalysisPhase.FINDING_ANALYSIS_PHASE2;
}
@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_OSSINDEX_ENABLED;
}
/**
* Run without parallel support.
*
* @return false
*/
@Override
public boolean supportsParallelProcessing() {
return true;
}
@Override
protected void closeAnalyzer() throws Exception {
synchronized (FETCH_MUTIX) {
reports = null;
}
}
@Override
protected void analyzeDependency(final Dependency dependency, final Engine engine) throws AnalysisException {
// batch request component-reports for all dependencies
synchronized (FETCH_MUTIX) {
if (reports == null) {
try {
requestDelay();
reports = requestReports(engine.getDependencies());
} catch (TransportException ex) {
final String message = ex.getMessage();
final boolean warnOnly = getSettings().getBoolean(Settings.KEYS.ANALYZER_OSSINDEX_WARN_ONLY_ON_REMOTE_ERRORS, false);
this.setEnabled(false);
if (StringUtils.endsWith(message, "401")) {
LOG.error("Invalid credentials for the OSS Index, disabling the analyzer");
throw new AnalysisException("Invalid credentials provided for OSS Index", ex);
} else if (StringUtils.endsWith(message, "403")) {
LOG.error("OSS Index access forbidden, disabling the analyzer");
throw new AnalysisException("OSS Index access forbidden", ex);
} else if (StringUtils.endsWith(message, "429")) {
if (warnOnly) {
LOG.warn("OSS Index rate limit exceeded, disabling the analyzer", ex);
} else {
throw new AnalysisException("OSS Index rate limit exceeded, disabling the analyzer", ex);
}
} else if (warnOnly) {
LOG.warn("Error requesting component reports, disabling the analyzer", ex);
} else {
LOG.debug("Error requesting component reports, disabling the analyzer", ex);
throw new AnalysisException("Failed to request component-reports", ex);
}
} catch (SocketTimeoutException e) {
final boolean warnOnly = getSettings().getBoolean(Settings.KEYS.ANALYZER_OSSINDEX_WARN_ONLY_ON_REMOTE_ERRORS, false);
this.setEnabled(false);
if (warnOnly) {
LOG.warn("OSS Index socket timeout, disabling the analyzer", e);
} else {
LOG.debug("OSS Index socket timeout", e);
throw new AnalysisException("Failed to establish socket to OSS Index", e);
}
} catch (Exception e) {
LOG.debug("Error requesting component reports", e);
throw new AnalysisException("Failed to request component-reports", e);
}
}
// skip enrichment if we failed to fetch reports
if (reports != null) {
enrich(dependency);
}
}
}
/**
* Delays each request (thread) by the configured amount of seconds, if the
* configuration is present.
*/
private void requestDelay() throws InterruptedException {
final int delay = getSettings().getInt(Settings.KEYS.ANALYZER_OSSINDEX_REQUEST_DELAY, 0);
if (delay > 0) {
LOG.debug("Request delay: " + delay);
TimeUnit.SECONDS.sleep(delay);
}
}
/**
* Helper to complain if unable to parse Package-URL.
*
* @param value the url to parse
* @return the package url
*/
@Nullable
private PackageUrl parsePackageUrl(final String value) {
try {
return PackageUrl.parse(value);
} catch (InvalidException e) {
LOG.debug("Invalid Package-URL: {}", value, e);
return null;
}
}
/**
* Batch request component-reports for all dependencies.
*
* @param dependencies the collection of dependencies
* @return the map of dependency to OSS Index's component-report
* @throws Exception thrown if there is an exception requesting the report
*/
private Map<PackageUrl, ComponentReport> requestReports(final Dependency[] dependencies) throws Exception {
LOG.debug("Requesting component-reports for {} dependencies", dependencies.length);
// create requests for each dependency which has a PURL identifier
final List<PackageUrl> packages = new ArrayList<>();
Arrays.stream(dependencies).forEach(dependency -> dependency.getSoftwareIdentifiers().stream()
.filter(id -> id instanceof PurlIdentifier)
.map(id -> parsePackageUrl(id.getValue()))
.filter(id -> id != null && StringUtils.isNotBlank(id.getVersion()))
.forEach(packages::add));
// only attempt if we have been able to collect some packages
if (!packages.isEmpty()) {
try (OssindexClient client = newOssIndexClient()) {
LOG.debug("OSS Index Analyzer submitting: " + packages);
return client.requestComponentReports(packages);
}
}
LOG.warn("Unable to determine Package-URL identifiers for {} dependencies", dependencies.length);
return Collections.emptyMap();
}
OssindexClient newOssIndexClient() {
return OssindexClientFactory.create(getSettings());
}
/**
* Attempt to enrich given dependency with vulnerability details from OSS
* Index component-report.
*
* @param dependency the dependency to enrich
*/
void enrich(final Dependency dependency) {
LOG.debug("Enrich dependency: {}", dependency);
for (Identifier id : dependency.getSoftwareIdentifiers()) {
if (id instanceof PurlIdentifier) {
LOG.debug(" Package: {} -> {}", id, id.getConfidence());
final PackageUrl purl = parsePackageUrl(id.getValue());
if (purl != null && StringUtils.isNotBlank(purl.getVersion())) {
try {
final ComponentReport report = reports.get(purl);
if (report == null) {
LOG.debug("Missing component-report for: " + purl);
continue;
}
// expose the URL to the package details for report generation
id.setUrl(report.getReference().toString());
report.getVulnerabilities().stream()
.map((vuln) -> transform(report, vuln))
.forEachOrdered((v) -> {
final Vulnerability existing = dependency.getVulnerabilities().stream()
.filter(e -> e.getName().equals(v.getName())).findFirst()
.orElse(null);
if (existing != null) {
//TODO - can we enhance anything other than the references?
existing.getReferences().addAll(v.getReferences());
} else {
dependency.addVulnerability(v);
}
});
} catch (Exception e) {
LOG.warn("Failed to fetch component-report for: {}", purl, e);
}
}
}
}
}
/**
* Transform OSS Index component-report to ODC vulnerability.
*
* @param report the component report
* @param source the vulnerability from the report to transform
* @return the transformed vulnerability
*/
private Vulnerability transform(final ComponentReport report, final ComponentReportVulnerability source) {
final Vulnerability result = new Vulnerability();
result.setSource(Vulnerability.Source.OSSINDEX);
if (source.getCve() != null) {
result.setName(source.getCve());
} else {
String cve = null;
if (source.getTitle() != null) {
final Matcher matcher = CVE_PATTERN.matcher(source.getTitle());
if (matcher.find()) {
cve = matcher.group();
} else {
cve = source.getTitle();
}
}
if (cve == null && source.getReference() != null) {
final Matcher matcher = CVE_PATTERN.matcher(source.getReference().toString());
if (matcher.find()) {
cve = matcher.group();
}
}
result.setName(cve != null ? cve : source.getId());
}
result.setDescription(source.getDescription());
result.addCwe(source.getCwe());
final double cvssScore = source.getCvssScore() != null ? source.getCvssScore().doubleValue() : -1;
if (source.getCvssVector() != null) {
if (source.getCvssVector().startsWith("CVSS:3")) {
result.setCvssV3(CvssUtil.vectorToCvssV3(source.getCvssVector(), cvssScore));
} else {
// convert cvss details
final CvssVector cvssVector = CvssVectorFactory.create(source.getCvssVector());
final Map<String, String> metrics = cvssVector.getMetrics();
if (cvssVector instanceof Cvss2Vector) {
String tmp = metrics.get(Cvss2Vector.ACCESS_VECTOR);
CvssV2Data.AccessVectorType accessVector = null;
if (tmp != null) {
accessVector = CvssV2Data.AccessVectorType.fromValue(tmp);
}
tmp = metrics.get(Cvss2Vector.ACCESS_COMPLEXITY);
CvssV2Data.AccessComplexityType accessComplexity = null;
if (tmp != null) {
accessComplexity = CvssV2Data.AccessComplexityType.fromValue(tmp);
}
tmp = metrics.get(Cvss2Vector.AUTHENTICATION);
CvssV2Data.AuthenticationType authentication = null;
if (tmp != null) {
authentication = CvssV2Data.AuthenticationType.fromValue(tmp);
}
tmp = metrics.get(Cvss2Vector.CONFIDENTIALITY_IMPACT);
CvssV2Data.CiaType confidentialityImpact = null;
if (tmp != null) {
confidentialityImpact = CvssV2Data.CiaType.fromValue(tmp);
}
tmp = metrics.get(Cvss2Vector.INTEGRITY_IMPACT);
CvssV2Data.CiaType integrityImpact = null;
if (tmp != null) {
integrityImpact = CvssV2Data.CiaType.fromValue(tmp);
}
tmp = metrics.get(Cvss2Vector.AVAILABILITY_IMPACT);
CvssV2Data.CiaType availabilityImpact = null;
if (tmp != null) {
availabilityImpact = CvssV2Data.CiaType.fromValue(tmp);
}
final String severity = Cvss2Severity.of((float) cvssScore).name().toUpperCase();
final CvssV2Data cvssData = new CvssV2Data("2.0", source.getCvssVector(), accessVector,
accessComplexity, authentication, confidentialityImpact,
integrityImpact, availabilityImpact, cvssScore,
severity, null, null, null, null, null, null, null, null, null, null);
final CvssV2 cvssV2 = new CvssV2(null, null, cvssData, severity, null, null, null, null, null, null, null);
result.setCvssV2(cvssV2);
} else {
LOG.warn("Unsupported CVSS vector: {}", cvssVector);
result.setUnscoredSeverity(Double.toString(cvssScore));
}
}
} else {
LOG.debug("OSS has no vector for {}", result.getName());
result.setUnscoredSeverity(Double.toString(cvssScore));
}
// generate a reference to the vulnerability details on OSS Index
result.addReference(REFERENCE_TYPE, source.getTitle(), source.getReference().toString());
// generate references to other references reported by OSS Index
source.getExternalReferences().forEach(externalReference
-> result.addReference("OSSIndex", externalReference.toString(), externalReference.toString()));
// attach vulnerable software details as best we can
final PackageUrl purl = report.getCoordinates();
try {
final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder()
.part(Part.APPLICATION)
.vendor(purl.getNamespaceAsString())
.product(purl.getName())
.version(purl.getVersion());
// TODO: consider if we want/need to extract version-ranges to apply to vulnerable-software?
final VulnerableSoftware software = builder.build();
result.addVulnerableSoftware(software);
result.setMatchedVulnerableSoftware(software);
} catch (CpeValidationException e) {
LOG.warn("Unable to construct vulnerable-software for: {}", purl, e);
}
return result;
}
}