org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of dependency-check-core Show documentation
Show all versions of dependency-check-core Show documentation
dependency-check-core is the engine and reporting tool used to identify and report if there are any known, publicly disclosed vulnerabilities in the scanned project's dependencies. The engine extracts meta-data from the dependencies and uses this to do fuzzy key-word matching against the Common Platfrom Enumeration (CPE), if any CPE identifiers are found the associated Common Vulnerability and Exposure (CVE) entries are added to the generated report.
/*
* 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) 2015 Institute for Defense Analyses. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import com.github.packageurl.PackageURLBuilder;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.io.FileUtils;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nvdcve.CveDB;
import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.CvssV2;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.Reference;
import org.owasp.dependencycheck.dependency.Vulnerability;
import org.owasp.dependencycheck.dependency.VulnerableSoftware;
import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder;
import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
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;
/**
* Used to analyze Ruby Bundler Gemspec.lock files utilizing the 3rd party
* bundle-audit tool.
*
* @author Dale Visser
*/
@ThreadSafe
public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {
/**
* The logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(RubyBundleAuditAnalyzer.class);
/**
* A descriptor for the type of dependencies processed or added by this
* analyzer.
*/
public static final String DEPENDENCY_ECOSYSTEM = "ruby";
/**
* The name of the analyzer.
*/
private static final String ANALYZER_NAME = "Ruby Bundle Audit Analyzer";
/**
* The phase that this analyzer is intended to run in.
*/
private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
/**
* The filter defining which files will be analyzed.
*/
private static final FileFilter FILTER = FileFilterBuilder.newInstance().addFilenames("Gemfile.lock").build();
/**
* Name.
*/
public static final String NAME = "Name: ";
/**
* Version.
*/
public static final String VERSION = "Version: ";
/**
* Advisory.
*/
public static final String ADVISORY = "Advisory: ";
/**
* Criticality.
*/
public static final String CRITICALITY = "Criticality: ";
/**
* The DAL.
*/
private CveDB cvedb = null;
/**
* If {@link #analyzeDependency(Dependency, Engine)} is called, then we have
* successfully initialized, and it will be necessary to disable
* {@link RubyGemspecAnalyzer}.
*/
private boolean needToDisableGemspecAnalyzer = true;
/**
* @return a filter that accepts files named Gemfile.lock
*/
@Override
protected FileFilter getFileFilter() {
return FILTER;
}
/**
* Launch bundle-audit.
*
* @param folder directory that contains bundle audit
* @return a handle to the process
* @throws AnalysisException thrown when there is an issue launching bundle
* audit
*/
private Process launchBundleAudit(File folder) throws AnalysisException {
if (!folder.isDirectory()) {
throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
}
final List args = new ArrayList<>();
final String bundleAuditPath = getSettings().getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH);
File bundleAudit = null;
if (bundleAuditPath != null) {
bundleAudit = new File(bundleAuditPath);
if (!bundleAudit.isFile()) {
LOGGER.warn("Supplied `bundleAudit` path is incorrect: {}", bundleAuditPath);
bundleAudit = null;
}
}
args.add(bundleAudit != null && bundleAudit.isFile() ? bundleAudit.getAbsolutePath() : "bundle-audit");
args.add("check");
args.add("--verbose");
final ProcessBuilder builder = new ProcessBuilder(args);
builder.directory(folder);
try {
LOGGER.info("Launching: {} from {}", args, folder);
return builder.start();
} catch (IOException ioe) {
throw new AnalysisException("bundle-audit initialization failure; this error can be ignored if you are not analyzing Ruby. "
+ "Otherwise ensure that bundle-audit is installed and the path to bundle audit is correctly specified", ioe);
}
}
/**
* Initialize the analyzer.
*
* @param engine a reference to the dependency-check engine
* @throws InitializationException if anything goes wrong
*/
@Override
public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
// Now, need to see if bundle-audit actually runs from this location.
if (engine != null) {
this.cvedb = engine.getDatabase();
}
final Process process;
try {
process = launchBundleAudit(getSettings().getTempDirectory());
} catch (AnalysisException ae) {
setEnabled(false);
final String msg = String.format("Exception from bundle-audit process: %s. Disabling %s", ae.getCause(), ANALYZER_NAME);
throw new InitializationException(msg, ae);
} catch (IOException ex) {
setEnabled(false);
throw new InitializationException("Unable to create temporary file, the Ruby Bundle Audit Analyzer will be disabled", ex);
}
final int exitValue;
try {
exitValue = process.waitFor();
} catch (InterruptedException ex) {
setEnabled(false);
final String msg = String.format("Bundle-audit process was interrupted. Disabling %s", ANALYZER_NAME);
Thread.currentThread().interrupt();
throw new InitializationException(msg);
}
if (0 == exitValue) {
setEnabled(false);
final String msg = String.format("Unexpected exit code from bundle-audit process. Disabling %s: %s", ANALYZER_NAME, exitValue);
throw new InitializationException(msg);
} else {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
if (!reader.ready()) {
LOGGER.warn("Bundle-audit error stream unexpectedly not ready. Disabling {}", ANALYZER_NAME);
setEnabled(false);
throw new InitializationException("Bundle-audit error stream unexpectedly not ready.");
} else {
final String line = reader.readLine();
if (line == null || !line.contains("Errno::ENOENT")) {
LOGGER.warn("Unexpected bundle-audit output. Disabling {}: {}", ANALYZER_NAME, line);
setEnabled(false);
throw new InitializationException("Unexpected bundle-audit output.");
}
}
} catch (UnsupportedEncodingException ex) {
setEnabled(false);
throw new InitializationException("Unexpected bundle-audit encoding.", ex);
} catch (IOException ex) {
setEnabled(false);
throw new InitializationException("Unable to read bundle-audit output.", ex);
}
}
if (isEnabled()) {
LOGGER.info("{} is enabled. It is necessary to manually run \"bundle-audit update\" "
+ "occasionally to keep its database up to date.", ANALYZER_NAME);
}
}
/**
* Returns the name of the analyzer.
*
* @return the name of the analyzer.
*/
@Override
public String getName() {
return ANALYZER_NAME;
}
/**
* Returns the phase that the analyzer is intended to run in.
*
* @return the phase that the analyzer is intended to run in.
*/
@Override
public AnalysisPhase getAnalysisPhase() {
return ANALYSIS_PHASE;
}
/**
* Returns the key used in the properties file to reference the analyzer's
* enabled property.
*
* @return the analyzer's enabled property setting key
*/
@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
}
/**
* Determines if the analyzer can analyze the given file type.
*
* @param dependency the dependency to determine if it can analyze
* @param engine the dependency-check engine
* @throws AnalysisException thrown if there is an analysis exception.
*/
@Override
protected void analyzeDependency(Dependency dependency, Engine engine)
throws AnalysisException {
if (needToDisableGemspecAnalyzer) {
boolean failed = true;
final String className = RubyGemspecAnalyzer.class.getName();
for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
if (analyzer instanceof RubyBundlerAnalyzer) {
((RubyBundlerAnalyzer) analyzer).setEnabled(false);
LOGGER.info("Disabled {} to avoid noisy duplicate results.", RubyBundlerAnalyzer.class.getName());
} else if (analyzer instanceof RubyGemspecAnalyzer) {
((RubyGemspecAnalyzer) analyzer).setEnabled(false);
LOGGER.info("Disabled {} to avoid noisy duplicate results.", className);
failed = false;
}
}
if (failed) {
LOGGER.warn("Did not find {}.", className);
}
needToDisableGemspecAnalyzer = false;
}
final File parentFile = dependency.getActualFile().getParentFile();
final Process process = launchBundleAudit(parentFile);
final int exitValue;
try {
exitValue = process.waitFor();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new AnalysisException("bundle-audit process interrupted", ie);
}
if (exitValue < 0 || exitValue > 1) {
final String msg = String.format("Unexpected exit code from bundle-audit process; exit code: %s", exitValue);
throw new AnalysisException(msg);
}
try {
try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
while (errReader.ready()) {
final String error = errReader.readLine();
LOGGER.warn(error);
}
}
try (BufferedReader rdr = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
processBundlerAuditOutput(dependency, engine, rdr);
}
} catch (IOException | CpeValidationException ioe) {
LOGGER.warn("bundle-audit failure", ioe);
}
}
/**
* Processes the bundler audit output.
*
* @param original the dependency
* @param engine the dependency-check engine
* @param rdr the reader of the report
* @throws IOException thrown if the report cannot be read
* @throws CpeValidationException if there is an error building the
* CPE/VulnerableSoftware object
*/
private void processBundlerAuditOutput(Dependency original, Engine engine, BufferedReader rdr) throws IOException, CpeValidationException {
final String parentName = original.getActualFile().getParentFile().getName();
final String fileName = original.getFileName();
final String filePath = original.getFilePath();
Dependency dependency = null;
Vulnerability vulnerability = null;
String gem = null;
final Map map = new HashMap<>();
boolean appendToDescription = false;
while (rdr.ready()) {
final String nextLine = rdr.readLine();
if (null == nextLine) {
break;
} else if (nextLine.startsWith(NAME)) {
appendToDescription = false;
gem = nextLine.substring(NAME.length());
if (!map.containsKey(gem)) {
map.put(gem, createDependencyForGem(engine, parentName, fileName, filePath, gem));
}
dependency = map.get(gem);
LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
} else if (nextLine.startsWith(VERSION)) {
vulnerability = createVulnerability(parentName, dependency, gem, nextLine);
} else if (nextLine.startsWith(ADVISORY)) {
setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
} else if (nextLine.startsWith(CRITICALITY)) {
addCriticalityToVulnerability(parentName, vulnerability, nextLine);
} else if (nextLine.startsWith("URL: ")) {
addReferenceToVulnerability(parentName, vulnerability, nextLine);
} else if (nextLine.startsWith("Description:")) {
appendToDescription = true;
if (null != vulnerability) {
vulnerability.setDescription("*** Vulnerability obtained from bundle-audit verbose report. "
+ "Title link may not work. CPE below is guessed. CVSS score is estimated (-1.0 "
+ " indicates unknown). See link below for full details. *** ");
}
} else if (appendToDescription && null != vulnerability) {
vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
}
}
}
/**
* Sets the vulnerability name.
*
* @param parentName the parent name
* @param dependency the dependency
* @param vulnerability the vulnerability
* @param nextLine the line to parse
*/
private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
final String advisory = nextLine.substring(ADVISORY.length());
if (null != vulnerability) {
vulnerability.setName(advisory);
}
if (null != dependency) {
dependency.addVulnerability(vulnerability);
}
LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
}
/**
* Adds a reference to the vulnerability.
*
* @param parentName the parent name
* @param vulnerability the vulnerability
* @param nextLine the line to parse
*/
private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
final String url = nextLine.substring("URL: ".length());
if (null != vulnerability) {
final Reference ref = new Reference();
ref.setName(vulnerability.getName());
ref.setSource("bundle-audit");
ref.setUrl(url);
vulnerability.getReferences().add(ref);
}
LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
}
/**
* Adds the criticality to the vulnerability
*
* @param parentName the parent name
* @param vulnerability the vulnerability
* @param nextLine the line to parse
*/
private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
if (null != vulnerability) {
final String criticality = nextLine.substring(CRITICALITY.length()).trim();
float score = -1.0f;
Vulnerability v = null;
if (cvedb != null) {
try {
v = cvedb.getVulnerability(vulnerability.getName());
} catch (DatabaseException ex) {
LOGGER.debug("Unable to look up vulnerability {}", vulnerability.getName());
}
}
if (v != null && (v.getCvssV2() != null || v.getCvssV3() != null)) {
if (v.getCvssV2() != null) {
vulnerability.setCvssV2(v.getCvssV2());
}
if (v.getCvssV3() != null) {
vulnerability.setCvssV3(v.getCvssV3());
}
} else {
if ("High".equalsIgnoreCase(criticality)) {
score = 8.5f;
} else if ("Medium".equalsIgnoreCase(criticality)) {
score = 5.5f;
} else if ("Low".equalsIgnoreCase(criticality)) {
score = 2.0f;
}
vulnerability.setCvssV2(new CvssV2(score, "-", "-", "-", "-", "-", "-", criticality));
}
}
LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
}
/**
* Creates a vulnerability.
*
* @param parentName the parent name
* @param dependency the dependency
* @param gem the gem name
* @param nextLine the line to parse
* @return the vulnerability
* @throws CpeValidationException thrown if there is an error building the
* CPE vulnerability object
*/
private Vulnerability createVulnerability(String parentName, Dependency dependency, String gem, String nextLine) throws CpeValidationException {
Vulnerability vulnerability = null;
if (null != dependency) {
final String version = nextLine.substring(VERSION.length());
dependency.addEvidence(EvidenceType.VERSION,
"bundler-audit",
"Version",
version,
Confidence.HIGHEST);
dependency.setVersion(version);
dependency.setName(gem);
try {
final PackageURL purl = PackageURLBuilder.aPackageURL().withType("gem").withName(dependency.getName())
.withVersion(dependency.getVersion()).build();
dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
} catch (MalformedPackageURLException ex) {
LOGGER.debug("Unable to build package url for python", ex);
final GenericIdentifier id = new GenericIdentifier("gem:" + dependency.getName() + "@" + dependency.getVersion(),
Confidence.HIGHEST);
dependency.addSoftwareIdentifier(id);
}
vulnerability = new Vulnerability(); // don't add to dependency until we have name set later
final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder();
final VulnerableSoftware vs = builder.part(Part.APPLICATION)
.vendor(gem)
.product(String.format("%s_project", gem))
.version(version).build();
vulnerability.addVulnerableSoftware(vs);
vulnerability.setMatchedVulnerableSoftware(vs);
vulnerability.setCvssV2(new CvssV2(-1, "-", "-", "-", "-", "-", "-", "unknown"));
}
LOGGER.debug("bundle-audit ({}): {}", parentName, nextLine);
return vulnerability;
}
/**
* Creates the dependency based off of the gem.
*
* @param engine the engine used for scanning
* @param parentName the gem parent
* @param fileName the file name
* @param filePath the file path
* @param gem the gem name
* @return the dependency to add
* @throws IOException thrown if a temporary gem file could not be written
*/
private Dependency createDependencyForGem(Engine engine, String parentName, String fileName, String filePath, String gem) throws IOException {
final File gemFile;
try {
gemFile = File.createTempFile(gem, "_Gemfile.lock", getSettings().getTempDirectory());
} catch (IOException ioe) {
throw new IOException("Unable to create temporary gem file");
}
final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
FileUtils.write(gemFile, displayFileName, Charset.defaultCharset()); // unique contents to avoid dependency bundling
final Dependency dependency = new Dependency(gemFile);
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
dependency.addEvidence(EvidenceType.PRODUCT, "bundler-audit", "Name", gem, Confidence.HIGHEST);
//TODO add package URL - note, this may require parsing the gemfile.lock and getting the version for each entry
dependency.setDisplayFileName(displayFileName);
dependency.setFileName(fileName);
dependency.setFilePath(filePath);
engine.addDependency(dependency);
return dependency;
}
}