Coverage Report - org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer
 
Classes in this File Line Coverage Branch Coverage Complexity
RubyBundleAuditAnalyzer
16%
38/227
4%
4/86
6.462
 
 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  
 
 20  
 import java.io.BufferedReader;
 21  
 import java.io.File;
 22  
 import java.io.FileFilter;
 23  
 import java.io.IOException;
 24  
 import java.io.InputStreamReader;
 25  
 import java.io.UnsupportedEncodingException;
 26  
 import java.nio.charset.Charset;
 27  
 import java.util.ArrayList;
 28  
 import java.util.HashMap;
 29  
 import java.util.List;
 30  
 import java.util.Map;
 31  
 
 32  
 import org.apache.commons.io.FileUtils;
 33  
 import org.owasp.dependencycheck.Engine;
 34  
 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
 35  
 import org.owasp.dependencycheck.data.nvdcve.CveDB;
 36  
 import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
 37  
 import org.owasp.dependencycheck.dependency.Confidence;
 38  
 import org.owasp.dependencycheck.dependency.Dependency;
 39  
 import org.owasp.dependencycheck.dependency.Reference;
 40  
 import org.owasp.dependencycheck.dependency.Vulnerability;
 41  
 import org.owasp.dependencycheck.exception.InitializationException;
 42  
 import org.owasp.dependencycheck.utils.FileFilterBuilder;
 43  
 import org.owasp.dependencycheck.utils.Settings;
 44  
 import org.slf4j.Logger;
 45  
 import org.slf4j.LoggerFactory;
 46  
 
 47  
 /**
 48  
  * Used to analyze Ruby Bundler Gemspec.lock files utilizing the 3rd party
 49  
  * bundle-audit tool.
 50  
  *
 51  
  * @author Dale Visser
 52  
  */
 53  
 @Experimental
 54  14
 public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {
 55  
 
 56  
     /**
 57  
      * The logger.
 58  
      */
 59  1
     private static final Logger LOGGER = LoggerFactory.getLogger(RubyBundleAuditAnalyzer.class);
 60  
 
 61  
     /**
 62  
      * The name of the analyzer.
 63  
      */
 64  
     private static final String ANALYZER_NAME = "Ruby Bundle Audit Analyzer";
 65  
 
 66  
     /**
 67  
      * The phase that this analyzer is intended to run in.
 68  
      */
 69  1
     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
 70  
     /**
 71  
      * The filter defining which files will be analyzed.
 72  
      */
 73  1
     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addFilenames("Gemfile.lock").build();
 74  
     /**
 75  
      * Name.
 76  
      */
 77  
     public static final String NAME = "Name: ";
 78  
     /**
 79  
      * Version.
 80  
      */
 81  
     public static final String VERSION = "Version: ";
 82  
     /**
 83  
      * Advisory.
 84  
      */
 85  
     public static final String ADVISORY = "Advisory: ";
 86  
     /**
 87  
      * Criticality.
 88  
      */
 89  
     public static final String CRITICALITY = "Criticality: ";
 90  
 
 91  
     /**
 92  
      * The DAL.
 93  
      */
 94  
     private CveDB cvedb;
 95  
 
 96  
     /**
 97  
      * @return a filter that accepts files named Gemfile.lock
 98  
      */
 99  
     @Override
 100  
     protected FileFilter getFileFilter() {
 101  859
         return FILTER;
 102  
     }
 103  
 
 104  
     /**
 105  
      * Launch bundle-audit.
 106  
      *
 107  
      * @param folder directory that contains bundle audit
 108  
      * @return a handle to the process
 109  
      * @throws AnalysisException thrown when there is an issue launching bundle
 110  
      * audit
 111  
      */
 112  
     private Process launchBundleAudit(File folder) throws AnalysisException {
 113  4
         if (!folder.isDirectory()) {
 114  0
             throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
 115  
         }
 116  4
         final List<String> args = new ArrayList<String>();
 117  4
         final String bundleAuditPath = Settings.getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH);
 118  4
         File bundleAudit = null;
 119  4
         if (bundleAuditPath != null) {
 120  4
             bundleAudit = new File(bundleAuditPath);
 121  4
             if (!bundleAudit.isFile()) {
 122  4
                 LOGGER.warn("Supplied `bundleAudit` path is incorrect: " + bundleAuditPath);
 123  4
                 bundleAudit = null;
 124  
             }
 125  
         }
 126  4
         args.add(bundleAudit != null && bundleAudit.isFile() ? bundleAudit.getAbsolutePath() : "bundle-audit");
 127  4
         args.add("check");
 128  4
         args.add("--verbose");
 129  4
         final ProcessBuilder builder = new ProcessBuilder(args);
 130  4
         builder.directory(folder);
 131  
         try {
 132  4
             LOGGER.info("Launching: " + args + " from " + folder);
 133  4
             return builder.start();
 134  4
         } catch (IOException ioe) {
 135  4
             throw new AnalysisException("bundle-audit failure", ioe);
 136  
         }
 137  
     }
 138  
 
 139  
     /**
 140  
      * Initialize the analyzer. In this case, extract GrokAssembly.exe to a
 141  
      * temporary location.
 142  
      *
 143  
      * @throws InitializationException if anything goes wrong
 144  
      */
 145  
     @Override
 146  
     public void initializeFileTypeAnalyzer() throws InitializationException {
 147  
         try {
 148  4
             cvedb = new CveDB();
 149  4
             cvedb.open();
 150  0
         } catch (DatabaseException ex) {
 151  0
             LOGGER.warn("Exception opening the database");
 152  0
             LOGGER.debug("error", ex);
 153  0
             setEnabled(false);
 154  0
             throw new InitializationException("Error connecting to the database", ex);
 155  4
         }
 156  
         // Now, need to see if bundle-audit actually runs from this location.
 157  4
         Process process = null;
 158  
         try {
 159  4
             process = launchBundleAudit(Settings.getTempDirectory());
 160  4
         } catch (AnalysisException ae) {
 161  
 
 162  4
             setEnabled(false);
 163  4
             cvedb.close();
 164  4
             cvedb = null;
 165  4
             final String msg = String.format("Exception from bundle-audit process: %s. Disabling %s", ae.getCause(), ANALYZER_NAME);
 166  4
             throw new InitializationException(msg, ae);
 167  0
         } catch (IOException ex) {
 168  0
             setEnabled(false);
 169  0
             throw new InitializationException("Unable to create temporary file, the Ruby Bundle Audit Analyzer will be disabled", ex);
 170  0
         }
 171  
 
 172  
         final int exitValue;
 173  
         try {
 174  0
             exitValue = process.waitFor();
 175  0
         } catch (InterruptedException ex) {
 176  0
             setEnabled(false);
 177  0
             final String msg = String.format("Bundle-audit process was interupted. Disabling %s", ANALYZER_NAME);
 178  0
             throw new InitializationException(msg);
 179  0
         }
 180  0
         if (0 == exitValue) {
 181  0
             setEnabled(false);
 182  0
             final String msg = String.format("Unexpected exit code from bundle-audit process. Disabling %s: %s", ANALYZER_NAME, exitValue);
 183  0
             throw new InitializationException(msg);
 184  
         } else {
 185  0
             BufferedReader reader = null;
 186  
             try {
 187  0
                 reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
 188  0
                 if (!reader.ready()) {
 189  0
                     LOGGER.warn("Bundle-audit error stream unexpectedly not ready. Disabling " + ANALYZER_NAME);
 190  0
                     setEnabled(false);
 191  0
                     throw new InitializationException("Bundle-audit error stream unexpectedly not ready.");
 192  
                 } else {
 193  0
                     final String line = reader.readLine();
 194  0
                     if (line == null || !line.contains("Errno::ENOENT")) {
 195  0
                         LOGGER.warn("Unexpected bundle-audit output. Disabling {}: {}", ANALYZER_NAME, line);
 196  0
                         setEnabled(false);
 197  0
                         throw new InitializationException("Unexpected bundle-audit output.");
 198  
                     }
 199  
                 }
 200  0
             } catch (UnsupportedEncodingException ex) {
 201  0
                 setEnabled(false);
 202  0
                 throw new InitializationException("Unexpected bundle-audit encoding.", ex);
 203  0
             } catch (IOException ex) {
 204  0
                 setEnabled(false);
 205  0
                 throw new InitializationException("Unable to read bundle-audit output.", ex);
 206  
             } finally {
 207  0
                 if (null != reader) {
 208  
                     try {
 209  0
                         reader.close();
 210  0
                     } catch (IOException ex) {
 211  0
                         LOGGER.debug("Error closing reader", ex);
 212  0
                     }
 213  
                 }
 214  
             }
 215  
         }
 216  
 
 217  0
         if (isEnabled()) {
 218  0
             LOGGER.info(ANALYZER_NAME + " is enabled. It is necessary to manually run \"bundle-audit update\" "
 219  
                     + "occasionally to keep its database up to date.");
 220  
         }
 221  0
     }
 222  
 
 223  
     /**
 224  
      * Returns the name of the analyzer.
 225  
      *
 226  
      * @return the name of the analyzer.
 227  
      */
 228  
     @Override
 229  
     public String getName() {
 230  17
         return ANALYZER_NAME;
 231  
     }
 232  
 
 233  
     /**
 234  
      * Returns the phase that the analyzer is intended to run in.
 235  
      *
 236  
      * @return the phase that the analyzer is intended to run in.
 237  
      */
 238  
     @Override
 239  
     public AnalysisPhase getAnalysisPhase() {
 240  6
         return ANALYSIS_PHASE;
 241  
     }
 242  
 
 243  
     /**
 244  
      * Returns the key used in the properties file to reference the analyzer's
 245  
      * enabled property.
 246  
      *
 247  
      * @return the analyzer's enabled property setting key
 248  
      */
 249  
     @Override
 250  
     protected String getAnalyzerEnabledSettingKey() {
 251  5
         return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
 252  
     }
 253  
 
 254  
     /**
 255  
      * If {@link #analyzeDependency(Dependency, Engine)} is called, then we have
 256  
      * successfully initialized, and it will be necessary to disable
 257  
      * {@link RubyGemspecAnalyzer}.
 258  
      */
 259  14
     private boolean needToDisableGemspecAnalyzer = true;
 260  
 
 261  
     /**
 262  
      * Determines if the analyzer can analyze the given file type.
 263  
      *
 264  
      * @param dependency the dependency to determine if it can analyze
 265  
      * @param engine the dependency-check engine
 266  
      * @throws AnalysisException thrown if there is an analysis exception.
 267  
      */
 268  
     @Override
 269  
     protected void analyzeDependency(Dependency dependency, Engine engine)
 270  
             throws AnalysisException {
 271  0
         if (needToDisableGemspecAnalyzer) {
 272  0
             boolean failed = true;
 273  0
             final String className = RubyGemspecAnalyzer.class.getName();
 274  0
             for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
 275  0
                 if (analyzer instanceof RubyBundlerAnalyzer) {
 276  0
                     ((RubyBundlerAnalyzer) analyzer).setEnabled(false);
 277  0
                     LOGGER.info("Disabled " + RubyBundlerAnalyzer.class.getName() + " to avoid noisy duplicate results.");
 278  0
                 } else if (analyzer instanceof RubyGemspecAnalyzer) {
 279  0
                     ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
 280  0
                     LOGGER.info("Disabled " + className + " to avoid noisy duplicate results.");
 281  0
                     failed = false;
 282  
                 }
 283  0
             }
 284  0
             if (failed) {
 285  0
                 LOGGER.warn("Did not find " + className + '.');
 286  
             }
 287  0
             needToDisableGemspecAnalyzer = false;
 288  
         }
 289  0
         final File parentFile = dependency.getActualFile().getParentFile();
 290  0
         final Process process = launchBundleAudit(parentFile);
 291  
         final int exitValue;
 292  
         try {
 293  0
             exitValue = process.waitFor();
 294  0
         } catch (InterruptedException ie) {
 295  0
             throw new AnalysisException("bundle-audit process interrupted", ie);
 296  0
         }
 297  0
         if (exitValue < 0 || exitValue > 1) {
 298  0
             final String msg = String.format("Unexpected exit code from bundle-audit process; exit code: %s", exitValue);
 299  0
             throw new AnalysisException(msg);
 300  
         }
 301  0
         BufferedReader rdr = null;
 302  0
         BufferedReader errReader = null;
 303  
         try {
 304  0
             errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
 305  0
             while (errReader.ready()) {
 306  0
                 final String error = errReader.readLine();
 307  0
                 LOGGER.warn(error);
 308  0
             }
 309  0
             rdr = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
 310  0
             processBundlerAuditOutput(dependency, engine, rdr);
 311  0
         } catch (IOException ioe) {
 312  0
             LOGGER.warn("bundle-audit failure", ioe);
 313  
         } finally {
 314  0
             if (errReader != null) {
 315  
                 try {
 316  0
                     errReader.close();
 317  0
                 } catch (IOException ioe) {
 318  0
                     LOGGER.warn("bundle-audit close failure", ioe);
 319  0
                 }
 320  
             }
 321  0
             if (null != rdr) {
 322  
                 try {
 323  0
                     rdr.close();
 324  0
                 } catch (IOException ioe) {
 325  0
                     LOGGER.warn("bundle-audit close failure", ioe);
 326  0
                 }
 327  
             }
 328  
         }
 329  
 
 330  0
     }
 331  
 
 332  
     /**
 333  
      * Processes the bundler audit output.
 334  
      *
 335  
      * @param original the dependency
 336  
      * @param engine the dependency-check engine
 337  
      * @param rdr the reader of the report
 338  
      * @throws IOException thrown if the report cannot be read.
 339  
      */
 340  
     private void processBundlerAuditOutput(Dependency original, Engine engine, BufferedReader rdr) throws IOException {
 341  0
         final String parentName = original.getActualFile().getParentFile().getName();
 342  0
         final String fileName = original.getFileName();
 343  0
         final String filePath = original.getFilePath();
 344  0
         Dependency dependency = null;
 345  0
         Vulnerability vulnerability = null;
 346  0
         String gem = null;
 347  0
         final Map<String, Dependency> map = new HashMap<String, Dependency>();
 348  0
         boolean appendToDescription = false;
 349  0
         while (rdr.ready()) {
 350  0
             final String nextLine = rdr.readLine();
 351  0
             if (null == nextLine) {
 352  0
                 break;
 353  0
             } else if (nextLine.startsWith(NAME)) {
 354  0
                 appendToDescription = false;
 355  0
                 gem = nextLine.substring(NAME.length());
 356  0
                 if (!map.containsKey(gem)) {
 357  0
                     map.put(gem, createDependencyForGem(engine, parentName, fileName, filePath, gem));
 358  
                 }
 359  0
                 dependency = map.get(gem);
 360  0
                 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 361  0
             } else if (nextLine.startsWith(VERSION)) {
 362  0
                 vulnerability = createVulnerability(parentName, dependency, gem, nextLine);
 363  0
             } else if (nextLine.startsWith(ADVISORY)) {
 364  0
                 setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
 365  0
             } else if (nextLine.startsWith(CRITICALITY)) {
 366  0
                 addCriticalityToVulnerability(parentName, vulnerability, nextLine);
 367  0
             } else if (nextLine.startsWith("URL: ")) {
 368  0
                 addReferenceToVulnerability(parentName, vulnerability, nextLine);
 369  0
             } else if (nextLine.startsWith("Description:")) {
 370  0
                 appendToDescription = true;
 371  0
                 if (null != vulnerability) {
 372  0
                     vulnerability.setDescription("*** Vulnerability obtained from bundle-audit verbose report. "
 373  
                             + "Title link may not work. CPE below is guessed. CVSS score is estimated (-1.0 "
 374  
                             + " indicates unknown). See link below for full details. *** ");
 375  
                 }
 376  0
             } else if (appendToDescription) {
 377  0
                 if (null != vulnerability) {
 378  0
                     vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
 379  
                 }
 380  
             }
 381  0
         }
 382  0
     }
 383  
 
 384  
     /**
 385  
      * Sets the vulnerability name.
 386  
      *
 387  
      * @param parentName the parent name
 388  
      * @param dependency the dependency
 389  
      * @param vulnerability the vulnerability
 390  
      * @param nextLine the line to parse
 391  
      */
 392  
     private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
 393  0
         final String advisory = nextLine.substring((ADVISORY.length()));
 394  0
         if (null != vulnerability) {
 395  0
             vulnerability.setName(advisory);
 396  
         }
 397  0
         if (null != dependency) {
 398  0
             dependency.getVulnerabilities().add(vulnerability); // needed to wait for vulnerability name to avoid NPE
 399  
         }
 400  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 401  0
     }
 402  
 
 403  
     /**
 404  
      * Adds a reference to the vulnerability.
 405  
      *
 406  
      * @param parentName the parent name
 407  
      * @param vulnerability the vulnerability
 408  
      * @param nextLine the line to parse
 409  
      */
 410  
     private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
 411  0
         final String url = nextLine.substring(("URL: ").length());
 412  0
         if (null != vulnerability) {
 413  0
             final Reference ref = new Reference();
 414  0
             ref.setName(vulnerability.getName());
 415  0
             ref.setSource("bundle-audit");
 416  0
             ref.setUrl(url);
 417  0
             vulnerability.getReferences().add(ref);
 418  
         }
 419  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 420  0
     }
 421  
 
 422  
     /**
 423  
      * Adds the criticality to the vulnerability
 424  
      *
 425  
      * @param parentName the parent name
 426  
      * @param vulnerability the vulnerability
 427  
      * @param nextLine the line to parse
 428  
      */
 429  
     private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
 430  0
         if (null != vulnerability) {
 431  0
             final String criticality = nextLine.substring(CRITICALITY.length()).trim();
 432  0
             float score = -1.0f;
 433  0
             Vulnerability v = null;
 434  
             try {
 435  0
                 v = cvedb.getVulnerability(vulnerability.getName());
 436  0
             } catch (DatabaseException ex) {
 437  0
                 LOGGER.debug("Unable to look up vulnerability {}", vulnerability.getName());
 438  0
             }
 439  0
             if (v != null) {
 440  0
                 score = v.getCvssScore();
 441  0
             } else if ("High".equalsIgnoreCase(criticality)) {
 442  0
                 score = 8.5f;
 443  0
             } else if ("Medium".equalsIgnoreCase(criticality)) {
 444  0
                 score = 5.5f;
 445  0
             } else if ("Low".equalsIgnoreCase(criticality)) {
 446  0
                 score = 2.0f;
 447  
             }
 448  0
             vulnerability.setCvssScore(score);
 449  
         }
 450  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 451  0
     }
 452  
 
 453  
     /**
 454  
      * Creates a vulnerability.
 455  
      *
 456  
      * @param parentName the parent name
 457  
      * @param dependency the dependency
 458  
      * @param gem the gem name
 459  
      * @param nextLine the line to parse
 460  
      * @return the vulnerability
 461  
      */
 462  
     private Vulnerability createVulnerability(String parentName, Dependency dependency, String gem, String nextLine) {
 463  0
         Vulnerability vulnerability = null;
 464  0
         if (null != dependency) {
 465  0
             final String version = nextLine.substring(VERSION.length());
 466  0
             dependency.getVersionEvidence().addEvidence(
 467  
                     "bundler-audit",
 468  
                     "Version",
 469  
                     version,
 470  
                     Confidence.HIGHEST);
 471  0
             vulnerability = new Vulnerability(); // don't add to dependency until we have name set later
 472  0
             vulnerability.setMatchedCPE(
 473  0
                     String.format("cpe:/a:%1$s_project:%1$s:%2$s::~~~ruby~~", gem, version),
 474  
                     null);
 475  0
             vulnerability.setCvssAccessVector("-");
 476  0
             vulnerability.setCvssAccessComplexity("-");
 477  0
             vulnerability.setCvssAuthentication("-");
 478  0
             vulnerability.setCvssAvailabilityImpact("-");
 479  0
             vulnerability.setCvssConfidentialityImpact("-");
 480  0
             vulnerability.setCvssIntegrityImpact("-");
 481  
         }
 482  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 483  0
         return vulnerability;
 484  
     }
 485  
 
 486  
     /**
 487  
      * Creates the dependency based off of the gem.
 488  
      *
 489  
      * @param engine the engine used for scanning
 490  
      * @param parentName the gem parent
 491  
      * @param fileName the file name
 492  
      * @param filePath the file path
 493  
      * @param gem the gem name
 494  
      * @return the dependency to add
 495  
      * @throws IOException thrown if a temporary gem file could not be written
 496  
      */
 497  
     private Dependency createDependencyForGem(Engine engine, String parentName, String fileName, String filePath, String gem) throws IOException {
 498  0
         final File gemFile = new File(Settings.getTempDirectory(), gem + "_Gemfile.lock");
 499  0
         if (!gemFile.createNewFile()) {
 500  0
             throw new IOException("Unable to create temporary gem file");
 501  
         }
 502  0
         final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
 503  
 
 504  0
         FileUtils.write(gemFile, displayFileName, Charset.defaultCharset()); // unique contents to avoid dependency bundling
 505  0
         final Dependency dependency = new Dependency(gemFile);
 506  0
         dependency.getProductEvidence().addEvidence("bundler-audit", "Name", gem, Confidence.HIGHEST);
 507  0
         dependency.setDisplayFileName(displayFileName);
 508  0
         dependency.setFileName(fileName);
 509  0
         dependency.setFilePath(filePath);
 510  0
         engine.getDependencies().add(dependency);
 511  0
         return dependency;
 512  
     }
 513  
 }