RubyBundleAuditAnalyzer.java

  1. /*
  2.  * This file is part of dependency-check-core.
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *     http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  *
  16.  * Copyright (c) 2015 Institute for Defense Analyses. All Rights Reserved.
  17.  */
  18. package org.owasp.dependencycheck.analyzer;

  19. import java.io.File;
  20. import java.io.FileFilter;
  21. import java.io.IOException;
  22. import java.io.UnsupportedEncodingException;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.Collections;
  26. import java.util.List;
  27. import javax.annotation.concurrent.ThreadSafe;
  28. import org.apache.commons.lang3.StringUtils;

  29. import org.owasp.dependencycheck.Engine;
  30. import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
  31. import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
  32. import org.owasp.dependencycheck.data.nvdcve.CveDB;
  33. import org.owasp.dependencycheck.dependency.Dependency;
  34. import org.owasp.dependencycheck.exception.InitializationException;
  35. import org.owasp.dependencycheck.processing.BundlerAuditProcessor;
  36. import org.owasp.dependencycheck.utils.FileFilterBuilder;
  37. import org.owasp.dependencycheck.utils.processing.ProcessReader;
  38. import org.owasp.dependencycheck.utils.Settings;
  39. import org.slf4j.Logger;
  40. import org.slf4j.LoggerFactory;
  41. import us.springett.parsers.cpe.exceptions.CpeValidationException;

  42. /**
  43.  * Used to analyze Ruby Bundler Gemspec.lock files utilizing the 3rd party
  44.  * bundle-audit tool.
  45.  *
  46.  * @author Dale Visser
  47.  */
  48. @ThreadSafe
  49. public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {

  50.     /**
  51.      * The logger.
  52.      */
  53.     private static final Logger LOGGER = LoggerFactory.getLogger(RubyBundleAuditAnalyzer.class);

  54.     /**
  55.      * A descriptor for the type of dependencies processed or added by this
  56.      * analyzer.
  57.      */
  58.     public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.RUBY;

  59.     /**
  60.      * The name of the analyzer.
  61.      */
  62.     private static final String ANALYZER_NAME = "Ruby Bundle Audit Analyzer";

  63.     /**
  64.      * The phase that this analyzer is intended to run in.
  65.      */
  66.     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
  67.     /**
  68.      * The filter defining which files will be analyzed.
  69.      */
  70.     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addFilenames("Gemfile.lock").build();
  71.     /**
  72.      * Name.
  73.      */
  74.     public static final String NAME = "Name: ";
  75.     /**
  76.      * Version.
  77.      */
  78.     public static final String VERSION = "Version: ";
  79.     /**
  80.      * Advisory.
  81.      */
  82.     public static final String ADVISORY = "Advisory: ";
  83.     /**
  84.      * CVE.
  85.      */
  86.     public static final String CVE = "CVE: ";
  87.     /**
  88.      * Criticality.
  89.      */
  90.     public static final String CRITICALITY = "Criticality: ";

  91.     /**
  92.      * The DAL.
  93.      */
  94.     private CveDB cvedb = null;

  95.     /**
  96.      * If {@link #analyzeDependency(Dependency, Engine)} is called, then we have
  97.      * successfully initialized, and it will be necessary to disable
  98.      * {@link RubyGemspecAnalyzer}.
  99.      */
  100.     private boolean needToDisableGemspecAnalyzer = true;

  101.     /**
  102.      * @return a filter that accepts files named Gemfile.lock
  103.      */
  104.     @Override
  105.     protected FileFilter getFileFilter() {
  106.         return FILTER;
  107.     }

  108.     /**
  109.      * Returns the name of the analyzer.
  110.      *
  111.      * @return the name of the analyzer.
  112.      */
  113.     @Override
  114.     public String getName() {
  115.         return ANALYZER_NAME;
  116.     }

  117.     /**
  118.      * Returns the phase that the analyzer is intended to run in.
  119.      *
  120.      * @return the phase that the analyzer is intended to run in.
  121.      */
  122.     @Override
  123.     public AnalysisPhase getAnalysisPhase() {
  124.         return ANALYSIS_PHASE;
  125.     }

  126.     /**
  127.      * Returns the key used in the properties file to reference the analyzer's
  128.      * enabled property.
  129.      *
  130.      * @return the analyzer's enabled property setting key
  131.      */
  132.     @Override
  133.     protected String getAnalyzerEnabledSettingKey() {
  134.         return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
  135.     }

  136.     /**
  137.      * Launch bundle-audit.
  138.      *
  139.      * @param folder directory that contains bundle audit
  140.      * @param bundleAuditArgs the arguments to pass to bundle audit
  141.      * @return a handle to the process
  142.      * @throws AnalysisException thrown when there is an issue launching bundle
  143.      * audit
  144.      */
  145.     private Process launchBundleAudit(File folder, List<String> bundleAuditArgs) throws AnalysisException {
  146.         if (!folder.isDirectory()) {
  147.             throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
  148.         }
  149.         final List<String> args = new ArrayList<>();
  150.         final String bundleAuditPath = getSettings().getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH);
  151.         File bundleAudit = null;
  152.         if (bundleAuditPath != null) {
  153.             bundleAudit = new File(bundleAuditPath);
  154.             if (!bundleAudit.isFile()) {
  155.                 LOGGER.warn("Supplied `bundleAudit` path is incorrect: {}", bundleAuditPath);
  156.                 bundleAudit = null;
  157.             }
  158.         }
  159.         args.add(bundleAudit != null ? bundleAudit.getAbsolutePath() : "bundle-audit");
  160.         args.addAll(bundleAuditArgs);
  161.         final ProcessBuilder builder = new ProcessBuilder(args);

  162.         final String bundleAuditWorkingDirectoryPath = getSettings().getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_WORKING_DIRECTORY);
  163.         File bundleAuditWorkingDirectory = null;
  164.         if (bundleAuditWorkingDirectoryPath != null) {
  165.             bundleAuditWorkingDirectory = new File(bundleAuditWorkingDirectoryPath);
  166.             if (!bundleAuditWorkingDirectory.isDirectory()) {
  167.                 LOGGER.warn("Supplied `bundleAuditWorkingDirectory` path is incorrect: {}",
  168.                         bundleAuditWorkingDirectoryPath);
  169.                 bundleAuditWorkingDirectory = null;
  170.             }
  171.         }
  172.         final File launchBundleAuditFromDirectory = bundleAuditWorkingDirectory != null ? bundleAuditWorkingDirectory : folder;
  173.         builder.directory(launchBundleAuditFromDirectory);
  174.         try {
  175.             LOGGER.info("Launching: {} from {}", args, launchBundleAuditFromDirectory);
  176.             return builder.start();
  177.         } catch (IOException ioe) {
  178.             throw new AnalysisException("bundle-audit initialization failure; this error "
  179.                     + "can be ignored if you are not analyzing Ruby. Otherwise ensure that "
  180.                     + "bundle-audit is installed and the path to bundle audit is correctly "
  181.                     + "specified", ioe);
  182.         }
  183.     }

  184.     /**
  185.      * Initialize the analyzer.
  186.      *
  187.      * @param engine a reference to the dependency-checkException engine
  188.      * @throws InitializationException if anything goes wrong
  189.      */
  190.     @Override
  191.     public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
  192.         if (engine != null) {
  193.             this.cvedb = engine.getDatabase();
  194.         }
  195.         String bundleAuditVersionDetails = null;
  196.         try {
  197.             final List<String> bundleAuditArgs = Collections.singletonList("version");
  198.             final Process process = launchBundleAudit(getSettings().getTempDirectory(), bundleAuditArgs);
  199.             try (ProcessReader processReader = new ProcessReader(process)) {
  200.                 processReader.readAll();
  201.                 final String error = processReader.getError();
  202.                 if (error != null) {
  203.                     LOGGER.warn("Warnings from bundle-audit {}", error);
  204.                 }
  205.                 bundleAuditVersionDetails = processReader.getOutput();
  206.                 final int exitValue = process.exitValue();
  207.                 if (exitValue != 0) {
  208.                     setEnabled(false);
  209.                     final String msg = String.format("bundle-audit execution failed - "
  210.                             + "exit code: %d; error: %s ", exitValue, error);
  211.                     throw new InitializationException(msg);
  212.                 }
  213.             }
  214.         } catch (AnalysisException ae) {
  215.             setEnabled(false);
  216.             final String msg = String.format("Exception from bundle-audit process: %s. "
  217.                     + "Disabling %s", ae.getCause(), ANALYZER_NAME);
  218.             throw new InitializationException(msg, ae);
  219.         } catch (UnsupportedEncodingException ex) {
  220.             setEnabled(false);
  221.             throw new InitializationException("Unexpected bundle-audit encoding when "
  222.                     + "reading input stream.", ex);
  223.         } catch (IOException ex) {
  224.             setEnabled(false);
  225.             throw new InitializationException("Unable to read bundle-audit output.", ex);
  226.         } catch (InterruptedException ex) {
  227.             setEnabled(false);
  228.             final String msg = String.format("Bundle-audit process was interrupted. "
  229.                     + "Disabling %s", ANALYZER_NAME);
  230.             Thread.currentThread().interrupt();
  231.             throw new InitializationException(msg);
  232.         }
  233.         LOGGER.info("{} is enabled and is using bundle-audit with version details: {}. "
  234.                 + "Note: It is necessary to manually run \"bundle-audit update\" "
  235.                 + "occasionally to keep its database up to date.", ANALYZER_NAME,
  236.                 bundleAuditVersionDetails);
  237.     }

  238.     /**
  239.      * Determines if the analyzer can analyze the given file type.
  240.      *
  241.      * @param dependency the dependency to determine if it can analyze
  242.      * @param engine the dependency-checkException engine
  243.      * @throws AnalysisException thrown if there is an analysis exception.
  244.      */
  245.     @Override
  246.     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
  247.         if (needToDisableGemspecAnalyzer) {
  248.             boolean failed = true;
  249.             final String className = RubyGemspecAnalyzer.class.getName();
  250.             for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
  251.                 if (analyzer instanceof RubyBundlerAnalyzer) {
  252.                     ((RubyBundlerAnalyzer) analyzer).setEnabled(false);
  253.                     LOGGER.info("Disabled {} to avoid noisy duplicate results.",
  254.                             RubyBundlerAnalyzer.class.getName());
  255.                 } else if (analyzer instanceof RubyGemspecAnalyzer) {
  256.                     ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
  257.                     LOGGER.info("Disabled {} to avoid noisy duplicate results.", className);
  258.                     failed = false;
  259.                 }
  260.             }
  261.             needToDisableGemspecAnalyzer = false;
  262.         }
  263.         final File parentFile = dependency.getActualFile().getParentFile();
  264.         final List<String> bundleAuditArgs = Arrays.asList("check", "--verbose");

  265.         final Process process = launchBundleAudit(parentFile, bundleAuditArgs);
  266.         try (BundlerAuditProcessor processor = new BundlerAuditProcessor(dependency, engine);
  267.                 ProcessReader processReader = new ProcessReader(process, processor)) {

  268.             processReader.readAll();
  269.             final String error = processReader.getError();
  270.             if (StringUtils.isNoneBlank(error)) {
  271.                 LOGGER.warn("Warnings from bundle-audit {}", error);
  272.             }
  273.             final int exitValue = process.exitValue();
  274.             if (exitValue < 0 || exitValue > 1) {
  275.                 final String msg = String.format("Unexpected exit code from bundle-audit "
  276.                         + "process; exit code: %s", exitValue);
  277.                 throw new AnalysisException(msg);
  278.             }
  279.         } catch (InterruptedException ie) {
  280.             Thread.currentThread().interrupt();
  281.             throw new AnalysisException("bundle-audit process interrupted", ie);
  282.         } catch (IOException | CpeValidationException ioe) {
  283.             LOGGER.warn("bundle-audit failure", ioe);
  284.             throw new AnalysisException("bunder-audit error: " + ioe.getMessage(), ioe);
  285.         }
  286.     }
  287. }