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 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  public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {
41  
42      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      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
53  
54      private static final FileFilter FILTER
55              = 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          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          if (!folder.isDirectory()) {
76              throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
77          }
78          final List<String> args = new ArrayList<String>();
79          final String bundleAuditPath = Settings.getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH);
80          args.add(null == bundleAuditPath ? "bundle-audit" : bundleAuditPath);
81          args.add("check");
82          args.add("--verbose");
83          final ProcessBuilder builder = new ProcessBuilder(args);
84          builder.directory(folder);
85          try {
86          	LOGGER.info("Launching: " + args + " from " + folder);
87              return builder.start();
88          } catch (IOException ioe) {
89              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     	Process process = null;
102     	try {
103 	        process = launchBundleAudit(Settings.getTempDirectory());
104     	}
105     	catch(AnalysisException ae) {
106     		LOGGER.warn("Exception from bundle-audit process: {}. Disabling {}", ae.getCause(), ANALYZER_NAME);
107             setEnabled(false);
108             throw ae;
109     	}
110     	
111         int exitValue = process.waitFor();
112         if (0 == exitValue) {
113             LOGGER.warn("Unexpected exit code from bundle-audit process. Disabling {}: {}", ANALYZER_NAME, exitValue);
114             setEnabled(false);
115             throw new AnalysisException("Unexpected exit code from bundle-audit process.");
116         } else {
117             BufferedReader reader = null;
118             try {
119                 reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
120                 if (!reader.ready()) {
121                     LOGGER.warn("Bundle-audit error stream unexpectedly not ready. Disabling " + ANALYZER_NAME);
122                     setEnabled(false);
123                     throw new AnalysisException("Bundle-audit error stream unexpectedly not ready.");
124                 } else {
125                     final String line = reader.readLine();
126                     if (line == null || !line.contains("Errno::ENOENT")) {
127                         LOGGER.warn("Unexpected bundle-audit output. Disabling {}: {}", ANALYZER_NAME, line);
128                         setEnabled(false);
129                         throw new AnalysisException("Unexpected bundle-audit output.");
130                     }
131                 }
132             } finally {
133                 if (null != reader) {
134                     reader.close();
135                 }
136             }
137         }
138     	
139         if (isEnabled()) {
140             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     }
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         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         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         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     private boolean needToDisableGemspecAnalyzer = true;
180 
181     @Override
182     protected void analyzeFileType(Dependency dependency, Engine engine)
183             throws AnalysisException {
184         if (needToDisableGemspecAnalyzer) {
185             boolean failed = true;
186             final String className = RubyGemspecAnalyzer.class.getName();
187             for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
188                 if (analyzer instanceof RubyGemspecAnalyzer) {
189                     ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
190                     LOGGER.info("Disabled " + className + " to avoid noisy duplicate results.");
191                     failed = false;
192                 }
193             }
194             if (failed) {
195                 LOGGER.warn("Did not find" + className + '.');
196             }
197             needToDisableGemspecAnalyzer = false;
198         }
199         final File parentFile = dependency.getActualFile().getParentFile();
200         final Process process = launchBundleAudit(parentFile);
201         try {
202             process.waitFor();
203         } catch (InterruptedException ie) {
204             throw new AnalysisException("bundle-audit process interrupted", ie);
205         }
206         BufferedReader rdr = null;
207         try {
208         	BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
209         	while(errReader.ready()) {
210         		String error = errReader.readLine();
211         		LOGGER.warn(error);
212         	}
213             rdr = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
214             processBundlerAuditOutput(dependency, engine, rdr);
215         } catch (IOException ioe) {
216             LOGGER.warn("bundle-audit failure", ioe);
217         } finally {
218             if (null != rdr) {
219                 try {
220                     rdr.close();
221                 } catch (IOException ioe) {
222                     LOGGER.warn("bundle-audit close failure", ioe);
223                 }
224             }
225         }
226 
227     }
228 
229     private void processBundlerAuditOutput(Dependency original, Engine engine, BufferedReader rdr) throws IOException {
230         final String parentName = original.getActualFile().getParentFile().getName();
231         final String fileName = original.getFileName();
232         Dependency dependency = null;
233         Vulnerability vulnerability = null;
234         String gem = null;
235         final Map<String, Dependency> map = new HashMap<String, Dependency>();
236         boolean appendToDescription = false;
237         while (rdr.ready()) {
238             final String nextLine = rdr.readLine();
239             if (null == nextLine) {
240                 break;
241             } else if (nextLine.startsWith(NAME)) {
242                 appendToDescription = false;
243                 gem = nextLine.substring(NAME.length());
244                 if (!map.containsKey(gem)) {
245                     map.put(gem, createDependencyForGem(engine, parentName, fileName, gem));
246                 }
247                 dependency = map.get(gem);
248                 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
249             } else if (nextLine.startsWith(VERSION)) {
250                 vulnerability = createVulnerability(parentName, dependency, vulnerability, gem, nextLine);
251             } else if (nextLine.startsWith(ADVISORY)) {
252                 setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
253             } else if (nextLine.startsWith(CRITICALITY)) {
254                 addCriticalityToVulnerability(parentName, vulnerability, nextLine);
255             } else if (nextLine.startsWith("URL: ")) {
256                 addReferenceToVulnerability(parentName, vulnerability, nextLine);
257             } else if (nextLine.startsWith("Description:")) {
258                 appendToDescription = true;
259                 if (null != vulnerability) {
260                     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             } else if (appendToDescription) {
263                 if (null != vulnerability) {
264                     vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
265                 }
266             }
267         }
268     }
269 
270     private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
271         final String advisory = nextLine.substring((ADVISORY.length()));
272         if (null != vulnerability) {
273             vulnerability.setName(advisory);
274         }
275         if (null != dependency) {
276             dependency.getVulnerabilities().add(vulnerability); // needed to wait for vulnerability name to avoid NPE
277         }
278         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
279     }
280 
281     private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
282         final String url = nextLine.substring(("URL: ").length());
283         if (null != vulnerability) {
284             Reference ref = new Reference();
285             ref.setName(vulnerability.getName());
286             ref.setSource("bundle-audit");
287             ref.setUrl(url);
288             vulnerability.getReferences().add(ref);
289         }
290         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
291     }
292 
293     private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
294         if (null != vulnerability) {
295             final String criticality = nextLine.substring(CRITICALITY.length()).trim();
296             if ("High".equals(criticality)) {
297                 vulnerability.setCvssScore(8.5f);
298             } else if ("Medium".equals(criticality)) {
299                 vulnerability.setCvssScore(5.5f);
300             } else if ("Low".equals(criticality)) {
301                 vulnerability.setCvssScore(2.0f);
302             } else {
303                 vulnerability.setCvssScore(-1.0f);
304             }
305         }
306         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
307     }
308 
309     private Vulnerability createVulnerability(String parentName, Dependency dependency, Vulnerability vulnerability, String gem, String nextLine) {
310         if (null != dependency) {
311             final String version = nextLine.substring(VERSION.length());
312             dependency.getVersionEvidence().addEvidence(
313                     "bundler-audit",
314                     "Version",
315                     version,
316                     Confidence.HIGHEST);
317             vulnerability = new Vulnerability(); // don't add to dependency until we have name set later
318             vulnerability.setMatchedCPE(
319                     String.format("cpe:/a:%1$s_project:%1$s:%2$s::~~~ruby~~", gem, version),
320                     null);
321             vulnerability.setCvssAccessVector("-");
322             vulnerability.setCvssAccessComplexity("-");
323             vulnerability.setCvssAuthentication("-");
324             vulnerability.setCvssAvailabilityImpact("-");
325             vulnerability.setCvssConfidentialityImpact("-");
326             vulnerability.setCvssIntegrityImpact("-");
327         }
328         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
329         return vulnerability;
330     }
331 
332     private Dependency createDependencyForGem(Engine engine, String parentName, String fileName, String gem) throws IOException {
333         final File tempFile = File.createTempFile("Gemfile-" + gem, ".lock", Settings.getTempDirectory());
334         final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
335         FileUtils.write(tempFile, displayFileName); // unique contents to avoid dependency bundling
336         final Dependency dependency = new Dependency(tempFile);
337         dependency.getProductEvidence().addEvidence("bundler-audit", "Name", gem, Confidence.HIGHEST);
338         dependency.setDisplayFileName(displayFileName);
339         engine.getDependencies().add(dependency);
340         return dependency;
341     }
342 }