Coverage Report - org.owasp.dependencycheck.analyzer.RubyBundleAuditAnalyzer
 
Classes in this File Line Coverage Branch Coverage Complexity
RubyBundleAuditAnalyzer
16%
28/169
4%
3/68
4.615
 
 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 org.apache.commons.io.FileUtils;
 21  
 import org.owasp.dependencycheck.Engine;
 22  
 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
 23  
 import org.owasp.dependencycheck.dependency.Confidence;
 24  
 import org.owasp.dependencycheck.dependency.Dependency;
 25  
 import org.owasp.dependencycheck.dependency.Reference;
 26  
 import org.owasp.dependencycheck.dependency.Vulnerability;
 27  
 import org.owasp.dependencycheck.utils.FileFilterBuilder;
 28  
 import org.owasp.dependencycheck.utils.Settings;
 29  
 import org.slf4j.Logger;
 30  
 import org.slf4j.LoggerFactory;
 31  
 
 32  
 import java.io.*;
 33  
 import java.util.*;
 34  
 
 35  
 /**
 36  
  * Used to analyze Ruby Bundler Gemspec.lock files utilizing the 3rd party bundle-audit tool.
 37  
  *
 38  
  * @author Dale Visser
 39  
  */
 40  8
 public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {
 41  
 
 42  1
     private static final Logger LOGGER = LoggerFactory.getLogger(RubyBundleAuditAnalyzer.class);
 43  
 
 44  
     /**
 45  
      * The name of the analyzer.
 46  
      */
 47  
     private static final String ANALYZER_NAME = "Ruby Bundle Audit Analyzer";
 48  
 
 49  
     /**
 50  
      * The phase that this analyzer is intended to run in.
 51  
      */
 52  1
     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
 53  
 
 54  1
     private static final FileFilter FILTER
 55  1
             = FileFilterBuilder.newInstance().addFilenames("Gemfile.lock").build();
 56  
     public static final String NAME = "Name: ";
 57  
     public static final String VERSION = "Version: ";
 58  
     public static final String ADVISORY = "Advisory: ";
 59  
     public static final String CRITICALITY = "Criticality: ";
 60  
 
 61  
     /**
 62  
      * @return a filter that accepts files named Gemfile.lock
 63  
      */
 64  
     @Override
 65  
     protected FileFilter getFileFilter() {
 66  854
         return FILTER;
 67  
     }
 68  
 
 69  
     /**
 70  
      * Launch bundle-audit.
 71  
      *
 72  
      * @return a handle to the process
 73  
      */
 74  
     private Process launchBundleAudit(File folder) throws AnalysisException {
 75  2
         if (!folder.isDirectory()) {
 76  0
             throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
 77  
         }
 78  2
         final List<String> args = new ArrayList<String>();
 79  2
         final String bundleAuditPath = Settings.getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH);
 80  2
         args.add(null == bundleAuditPath ? "bundle-audit" : bundleAuditPath);
 81  2
         args.add("check");
 82  2
         args.add("--verbose");
 83  2
         final ProcessBuilder builder = new ProcessBuilder(args);
 84  2
         builder.directory(folder);
 85  
         try {
 86  2
                 LOGGER.info("Launching: " + args + " from " + folder);
 87  2
             return builder.start();
 88  2
         } catch (IOException ioe) {
 89  2
             throw new AnalysisException("bundle-audit failure", ioe);
 90  
         }
 91  
     }
 92  
 
 93  
     /**
 94  
      * Initialize the analyzer. In this case, extract GrokAssembly.exe to a temporary location.
 95  
      *
 96  
      * @throws Exception if anything goes wrong
 97  
      */
 98  
     @Override
 99  
     public void initializeFileTypeAnalyzer() throws Exception {
 100  
         // Now, need to see if bundle-audit actually runs from this location.
 101  2
             Process process = null;
 102  
             try {
 103  2
                 process = launchBundleAudit(Settings.getTempDirectory());
 104  
             }
 105  2
             catch(AnalysisException ae) {
 106  2
                     LOGGER.warn("Exception from bundle-audit process: {}. Disabling {}", ae.getCause(), ANALYZER_NAME);
 107  2
             setEnabled(false);
 108  2
             throw ae;
 109  0
             }
 110  
             
 111  0
         int exitValue = process.waitFor();
 112  0
         if (0 == exitValue) {
 113  0
             LOGGER.warn("Unexpected exit code from bundle-audit process. Disabling {}: {}", ANALYZER_NAME, exitValue);
 114  0
             setEnabled(false);
 115  0
             throw new AnalysisException("Unexpected exit code from bundle-audit process.");
 116  
         } else {
 117  0
             BufferedReader reader = null;
 118  
             try {
 119  0
                 reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
 120  0
                 if (!reader.ready()) {
 121  0
                     LOGGER.warn("Bundle-audit error stream unexpectedly not ready. Disabling " + ANALYZER_NAME);
 122  0
                     setEnabled(false);
 123  0
                     throw new AnalysisException("Bundle-audit error stream unexpectedly not ready.");
 124  
                 } else {
 125  0
                     final String line = reader.readLine();
 126  0
                     if (line == null || !line.contains("Errno::ENOENT")) {
 127  0
                         LOGGER.warn("Unexpected bundle-audit output. Disabling {}: {}", ANALYZER_NAME, line);
 128  0
                         setEnabled(false);
 129  0
                         throw new AnalysisException("Unexpected bundle-audit output.");
 130  
                     }
 131  
                 }
 132  
             } finally {
 133  0
                 if (null != reader) {
 134  0
                     reader.close();
 135  
                 }
 136  
             }
 137  
         }
 138  
             
 139  0
         if (isEnabled()) {
 140  0
             LOGGER.info(ANALYZER_NAME + " is enabled. It is necessary to manually run \"bundle-audit update\" "
 141  
                     + "occasionally to keep its database up to date.");
 142  
         }
 143  0
     }
 144  
 
 145  
     /**
 146  
      * Returns the name of the analyzer.
 147  
      *
 148  
      * @return the name of the analyzer.
 149  
      */
 150  
     @Override
 151  
     public String getName() {
 152  5
         return ANALYZER_NAME;
 153  
     }
 154  
 
 155  
     /**
 156  
      * Returns the phase that the analyzer is intended to run in.
 157  
      *
 158  
      * @return the phase that the analyzer is intended to run in.
 159  
      */
 160  
     @Override
 161  
     public AnalysisPhase getAnalysisPhase() {
 162  3
         return ANALYSIS_PHASE;
 163  
     }
 164  
 
 165  
     /**
 166  
      * Returns the key used in the properties file to reference the analyzer's enabled property.
 167  
      *
 168  
      * @return the analyzer's enabled property setting key
 169  
      */
 170  
     @Override
 171  
     protected String getAnalyzerEnabledSettingKey() {
 172  8
         return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
 173  
     }
 174  
 
 175  
     /**
 176  
      * If {@link #analyzeFileType(Dependency, Engine)} is called, then we have successfully initialized, and it will be necessary
 177  
      * to disable {@link RubyGemspecAnalyzer}.
 178  
      */
 179  8
     private boolean needToDisableGemspecAnalyzer = true;
 180  
 
 181  
     @Override
 182  
     protected void analyzeFileType(Dependency dependency, Engine engine)
 183  
             throws AnalysisException {
 184  0
         if (needToDisableGemspecAnalyzer) {
 185  0
             boolean failed = true;
 186  0
             final String className = RubyGemspecAnalyzer.class.getName();
 187  0
             for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
 188  0
                 if (analyzer instanceof RubyGemspecAnalyzer) {
 189  0
                     ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
 190  0
                     LOGGER.info("Disabled " + className + " to avoid noisy duplicate results.");
 191  0
                     failed = false;
 192  
                 }
 193  0
             }
 194  0
             if (failed) {
 195  0
                 LOGGER.warn("Did not find" + className + '.');
 196  
             }
 197  0
             needToDisableGemspecAnalyzer = false;
 198  
         }
 199  0
         final File parentFile = dependency.getActualFile().getParentFile();
 200  0
         final Process process = launchBundleAudit(parentFile);
 201  
         try {
 202  0
             process.waitFor();
 203  0
         } catch (InterruptedException ie) {
 204  0
             throw new AnalysisException("bundle-audit process interrupted", ie);
 205  0
         }
 206  0
         BufferedReader rdr = null;
 207  
         try {
 208  0
                 BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
 209  0
                 while(errReader.ready()) {
 210  0
                         String error = errReader.readLine();
 211  0
                         LOGGER.warn(error);
 212  0
                 }
 213  0
             rdr = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
 214  0
             processBundlerAuditOutput(dependency, engine, rdr);
 215  0
         } catch (IOException ioe) {
 216  0
             LOGGER.warn("bundle-audit failure", ioe);
 217  
         } finally {
 218  0
             if (null != rdr) {
 219  
                 try {
 220  0
                     rdr.close();
 221  0
                 } catch (IOException ioe) {
 222  0
                     LOGGER.warn("bundle-audit close failure", ioe);
 223  0
                 }
 224  
             }
 225  
         }
 226  
 
 227  0
     }
 228  
 
 229  
     private void processBundlerAuditOutput(Dependency original, Engine engine, BufferedReader rdr) throws IOException {
 230  0
         final String parentName = original.getActualFile().getParentFile().getName();
 231  0
         final String fileName = original.getFileName();
 232  0
         Dependency dependency = null;
 233  0
         Vulnerability vulnerability = null;
 234  0
         String gem = null;
 235  0
         final Map<String, Dependency> map = new HashMap<String, Dependency>();
 236  0
         boolean appendToDescription = false;
 237  0
         while (rdr.ready()) {
 238  0
             final String nextLine = rdr.readLine();
 239  0
             if (null == nextLine) {
 240  0
                 break;
 241  0
             } else if (nextLine.startsWith(NAME)) {
 242  0
                 appendToDescription = false;
 243  0
                 gem = nextLine.substring(NAME.length());
 244  0
                 if (!map.containsKey(gem)) {
 245  0
                     map.put(gem, createDependencyForGem(engine, parentName, fileName, gem));
 246  
                 }
 247  0
                 dependency = map.get(gem);
 248  0
                 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 249  0
             } else if (nextLine.startsWith(VERSION)) {
 250  0
                 vulnerability = createVulnerability(parentName, dependency, vulnerability, gem, nextLine);
 251  0
             } else if (nextLine.startsWith(ADVISORY)) {
 252  0
                 setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
 253  0
             } else if (nextLine.startsWith(CRITICALITY)) {
 254  0
                 addCriticalityToVulnerability(parentName, vulnerability, nextLine);
 255  0
             } else if (nextLine.startsWith("URL: ")) {
 256  0
                 addReferenceToVulnerability(parentName, vulnerability, nextLine);
 257  0
             } else if (nextLine.startsWith("Description:")) {
 258  0
                 appendToDescription = true;
 259  0
                 if (null != vulnerability) {
 260  0
                     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. *** ");
 261  
                 }
 262  0
             } else if (appendToDescription) {
 263  0
                 if (null != vulnerability) {
 264  0
                     vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
 265  
                 }
 266  
             }
 267  0
         }
 268  0
     }
 269  
 
 270  
     private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
 271  0
         final String advisory = nextLine.substring((ADVISORY.length()));
 272  0
         if (null != vulnerability) {
 273  0
             vulnerability.setName(advisory);
 274  
         }
 275  0
         if (null != dependency) {
 276  0
             dependency.getVulnerabilities().add(vulnerability); // needed to wait for vulnerability name to avoid NPE
 277  
         }
 278  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 279  0
     }
 280  
 
 281  
     private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
 282  0
         final String url = nextLine.substring(("URL: ").length());
 283  0
         if (null != vulnerability) {
 284  0
             Reference ref = new Reference();
 285  0
             ref.setName(vulnerability.getName());
 286  0
             ref.setSource("bundle-audit");
 287  0
             ref.setUrl(url);
 288  0
             vulnerability.getReferences().add(ref);
 289  
         }
 290  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 291  0
     }
 292  
 
 293  
     private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
 294  0
         if (null != vulnerability) {
 295  0
             final String criticality = nextLine.substring(CRITICALITY.length()).trim();
 296  0
             if ("High".equals(criticality)) {
 297  0
                 vulnerability.setCvssScore(8.5f);
 298  0
             } else if ("Medium".equals(criticality)) {
 299  0
                 vulnerability.setCvssScore(5.5f);
 300  0
             } else if ("Low".equals(criticality)) {
 301  0
                 vulnerability.setCvssScore(2.0f);
 302  
             } else {
 303  0
                 vulnerability.setCvssScore(-1.0f);
 304  
             }
 305  
         }
 306  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 307  0
     }
 308  
 
 309  
     private Vulnerability createVulnerability(String parentName, Dependency dependency, Vulnerability vulnerability, String gem, String nextLine) {
 310  0
         if (null != dependency) {
 311  0
             final String version = nextLine.substring(VERSION.length());
 312  0
             dependency.getVersionEvidence().addEvidence(
 313  
                     "bundler-audit",
 314  
                     "Version",
 315  
                     version,
 316  
                     Confidence.HIGHEST);
 317  0
             vulnerability = new Vulnerability(); // don't add to dependency until we have name set later
 318  0
             vulnerability.setMatchedCPE(
 319  0
                     String.format("cpe:/a:%1$s_project:%1$s:%2$s::~~~ruby~~", gem, version),
 320  
                     null);
 321  0
             vulnerability.setCvssAccessVector("-");
 322  0
             vulnerability.setCvssAccessComplexity("-");
 323  0
             vulnerability.setCvssAuthentication("-");
 324  0
             vulnerability.setCvssAvailabilityImpact("-");
 325  0
             vulnerability.setCvssConfidentialityImpact("-");
 326  0
             vulnerability.setCvssIntegrityImpact("-");
 327  
         }
 328  0
         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
 329  0
         return vulnerability;
 330  
     }
 331  
 
 332  
     private Dependency createDependencyForGem(Engine engine, String parentName, String fileName, String gem) throws IOException {
 333  0
         final File tempFile = File.createTempFile("Gemfile-" + gem, ".lock", Settings.getTempDirectory());
 334  0
         final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
 335  0
         FileUtils.write(tempFile, displayFileName); // unique contents to avoid dependency bundling
 336  0
         final Dependency dependency = new Dependency(tempFile);
 337  0
         dependency.getProductEvidence().addEvidence("bundler-audit", "Name", gem, Confidence.HIGHEST);
 338  0
         dependency.setDisplayFileName(displayFileName);
 339  0
         engine.getDependencies().add(dependency);
 340  0
         return dependency;
 341  
     }
 342  
 }