RubyBundleAuditAnalyzer.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) 2015 Institute for Defense Analyses. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.lang3.StringUtils;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.data.nvdcve.CveDB;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.processing.BundlerAuditProcessor;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.processing.ProcessReader;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.exceptions.CpeValidationException;
/**
* 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 = 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: ";
/**
* CVE.
*/
public static final String CVE = "CVE: ";
/**
* 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;
}
/**
* 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;
}
/**
* Launch bundle-audit.
*
* @param folder directory that contains bundle audit
* @param bundleAuditArgs the arguments to pass to bundle audit
* @return a handle to the process
* @throws AnalysisException thrown when there is an issue launching bundle
* audit
*/
private Process launchBundleAudit(File folder, List<String> bundleAuditArgs) throws AnalysisException {
if (!folder.isDirectory()) {
throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
}
final List<String> 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.getAbsolutePath() : "bundle-audit");
args.addAll(bundleAuditArgs);
final ProcessBuilder builder = new ProcessBuilder(args);
final String bundleAuditWorkingDirectoryPath = getSettings().getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_WORKING_DIRECTORY);
File bundleAuditWorkingDirectory = null;
if (bundleAuditWorkingDirectoryPath != null) {
bundleAuditWorkingDirectory = new File(bundleAuditWorkingDirectoryPath);
if (!bundleAuditWorkingDirectory.isDirectory()) {
LOGGER.warn("Supplied `bundleAuditWorkingDirectory` path is incorrect: {}",
bundleAuditWorkingDirectoryPath);
bundleAuditWorkingDirectory = null;
}
}
final File launchBundleAuditFromDirectory = bundleAuditWorkingDirectory != null ? bundleAuditWorkingDirectory : folder;
builder.directory(launchBundleAuditFromDirectory);
try {
LOGGER.info("Launching: {} from {}", args, launchBundleAuditFromDirectory);
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-checkException engine
* @throws InitializationException if anything goes wrong
*/
@Override
public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
if (engine != null) {
this.cvedb = engine.getDatabase();
}
String bundleAuditVersionDetails = null;
try {
final List<String> bundleAuditArgs = Collections.singletonList("version");
final Process process = launchBundleAudit(getSettings().getTempDirectory(), bundleAuditArgs);
try (ProcessReader processReader = new ProcessReader(process)) {
processReader.readAll();
final String error = processReader.getError();
if (error != null) {
LOGGER.warn("Warnings from bundle-audit {}", error);
}
bundleAuditVersionDetails = processReader.getOutput();
final int exitValue = process.exitValue();
if (exitValue != 0) {
setEnabled(false);
final String msg = String.format("bundle-audit execution failed - "
+ "exit code: %d; error: %s ", exitValue, error);
throw new InitializationException(msg);
}
}
} 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 (UnsupportedEncodingException ex) {
setEnabled(false);
throw new InitializationException("Unexpected bundle-audit encoding when "
+ "reading input stream.", ex);
} catch (IOException ex) {
setEnabled(false);
throw new InitializationException("Unable to read bundle-audit output.", ex);
} 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);
}
LOGGER.info("{} is enabled and is using bundle-audit with version details: {}. "
+ "Note: It is necessary to manually run \"bundle-audit update\" "
+ "occasionally to keep its database up to date.", ANALYZER_NAME,
bundleAuditVersionDetails);
}
/**
* Determines if the analyzer can analyze the given file type.
*
* @param dependency the dependency to determine if it can analyze
* @param engine the dependency-checkException 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;
}
}
needToDisableGemspecAnalyzer = false;
}
final File parentFile = dependency.getActualFile().getParentFile();
final List<String> bundleAuditArgs = Arrays.asList("check", "--verbose");
final Process process = launchBundleAudit(parentFile, bundleAuditArgs);
try (BundlerAuditProcessor processor = new BundlerAuditProcessor(dependency, engine);
ProcessReader processReader = new ProcessReader(process, processor)) {
processReader.readAll();
final String error = processReader.getError();
if (StringUtils.isNoneBlank(error)) {
LOGGER.warn("Warnings from bundle-audit {}", error);
}
final int exitValue = process.exitValue();
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);
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new AnalysisException("bundle-audit process interrupted", ie);
} catch (IOException | CpeValidationException ioe) {
LOGGER.warn("bundle-audit failure", ioe);
throw new AnalysisException("bunder-audit error: " + ioe.getMessage(), ioe);
}
}
}