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