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              return builder.start();
87          } catch (IOException ioe) {
88              throw new AnalysisException("bundle-audit failure", ioe);
89          }
90      }
91  
92      /**
93       * Initialize the analyzer. In this case, extract GrokAssembly.exe to a temporary location.
94       *
95       * @throws Exception if anything goes wrong
96       */
97      @Override
98      public void initializeFileTypeAnalyzer() throws Exception {
99          // Now, need to see if bundle-audit actually runs from this location.
100         Process process = launchBundleAudit(Settings.getTempDirectory());
101         int exitValue = process.waitFor();
102         if (0 == exitValue) {
103             LOGGER.warn("Unexpected exit code from bundle-audit process. Disabling {}: {}", ANALYZER_NAME, exitValue);
104             setEnabled(false);
105             throw new AnalysisException("Unexpected exit code from bundle-audit process.");
106         } else {
107             BufferedReader reader = null;
108             try {
109                 reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
110                 if (!reader.ready()) {
111                     LOGGER.warn("Bundle-audit error stream unexpectedly not ready. Disabling " + ANALYZER_NAME);
112                     setEnabled(false);
113                     throw new AnalysisException("Bundle-audit error stream unexpectedly not ready.");
114                 } else {
115                     final String line = reader.readLine();
116                     if (line == null || !line.contains("Errno::ENOENT")) {
117                         LOGGER.warn("Unexpected bundle-audit output. Disabling {}: {}", ANALYZER_NAME, line);
118                         setEnabled(false);
119                         throw new AnalysisException("Unexpected bundle-audit output.");
120                     }
121                 }
122             } finally {
123                 if (null != reader) {
124                     reader.close();
125                 }
126             }
127         }
128         if (isEnabled()) {
129             LOGGER.info(ANALYZER_NAME + " is enabled. It is necessary to manually run \"bundle-audit update\" "
130                     + "occasionally to keep its database up to date.");
131         }
132     }
133 
134     /**
135      * Returns the name of the analyzer.
136      *
137      * @return the name of the analyzer.
138      */
139     @Override
140     public String getName() {
141         return ANALYZER_NAME;
142     }
143 
144     /**
145      * Returns the phase that the analyzer is intended to run in.
146      *
147      * @return the phase that the analyzer is intended to run in.
148      */
149     @Override
150     public AnalysisPhase getAnalysisPhase() {
151         return ANALYSIS_PHASE;
152     }
153 
154     /**
155      * Returns the key used in the properties file to reference the analyzer's enabled property.
156      *
157      * @return the analyzer's enabled property setting key
158      */
159     @Override
160     protected String getAnalyzerEnabledSettingKey() {
161         return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
162     }
163 
164     /**
165      * If {@link #analyzeFileType(Dependency, Engine)} is called, then we have successfully initialized, and it will be necessary
166      * to disable {@link RubyGemspecAnalyzer}.
167      */
168     private boolean needToDisableGemspecAnalyzer = true;
169 
170     @Override
171     protected void analyzeFileType(Dependency dependency, Engine engine)
172             throws AnalysisException {
173         if (needToDisableGemspecAnalyzer) {
174             boolean failed = true;
175             final String className = RubyGemspecAnalyzer.class.getName();
176             for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
177                 if (analyzer instanceof RubyGemspecAnalyzer) {
178                     ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
179                     LOGGER.info("Disabled " + className + " to avoid noisy duplicate results.");
180                     failed = false;
181                 }
182             }
183             if (failed) {
184                 LOGGER.warn("Did not find" + className + '.');
185             }
186             needToDisableGemspecAnalyzer = false;
187         }
188         final File parentFile = dependency.getActualFile().getParentFile();
189         final Process process = launchBundleAudit(parentFile);
190         try {
191             process.waitFor();
192         } catch (InterruptedException ie) {
193             throw new AnalysisException("bundle-audit process interrupted", ie);
194         }
195         BufferedReader rdr = null;
196         try {
197             rdr = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
198             processBundlerAuditOutput(dependency, engine, rdr);
199         } catch (IOException ioe) {
200             LOGGER.warn("bundle-audit failure", ioe);
201         } finally {
202             if (null != rdr) {
203                 try {
204                     rdr.close();
205                 } catch (IOException ioe) {
206                     LOGGER.warn("bundle-audit close failure", ioe);
207                 }
208             }
209         }
210 
211     }
212 
213     private void processBundlerAuditOutput(Dependency original, Engine engine, BufferedReader rdr) throws IOException {
214         final String parentName = original.getActualFile().getParentFile().getName();
215         final String fileName = original.getFileName();
216         Dependency dependency = null;
217         Vulnerability vulnerability = null;
218         String gem = null;
219         final Map<String, Dependency> map = new HashMap<String, Dependency>();
220         boolean appendToDescription = false;
221         while (rdr.ready()) {
222             final String nextLine = rdr.readLine();
223             if (null == nextLine) {
224                 break;
225             } else if (nextLine.startsWith(NAME)) {
226                 appendToDescription = false;
227                 gem = nextLine.substring(NAME.length());
228                 if (!map.containsKey(gem)) {
229                     map.put(gem, createDependencyForGem(engine, parentName, fileName, gem));
230                 }
231                 dependency = map.get(gem);
232                 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
233             } else if (nextLine.startsWith(VERSION)) {
234                 vulnerability = createVulnerability(parentName, dependency, vulnerability, gem, nextLine);
235             } else if (nextLine.startsWith(ADVISORY)) {
236                 setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
237             } else if (nextLine.startsWith(CRITICALITY)) {
238                 addCriticalityToVulnerability(parentName, vulnerability, nextLine);
239             } else if (nextLine.startsWith("URL: ")) {
240                 addReferenceToVulnerability(parentName, vulnerability, nextLine);
241             } else if (nextLine.startsWith("Description:")) {
242                 appendToDescription = true;
243                 if (null != vulnerability) {
244                     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. *** ");
245                 }
246             } else if (appendToDescription) {
247                 if (null != vulnerability) {
248                     vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
249                 }
250             }
251         }
252     }
253 
254     private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
255         final String advisory = nextLine.substring((ADVISORY.length()));
256         if (null != vulnerability) {
257             vulnerability.setName(advisory);
258         }
259         if (null != dependency) {
260             dependency.getVulnerabilities().add(vulnerability); // needed to wait for vulnerability name to avoid NPE
261         }
262         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
263     }
264 
265     private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
266         final String url = nextLine.substring(("URL: ").length());
267         if (null != vulnerability) {
268             Reference ref = new Reference();
269             ref.setName(vulnerability.getName());
270             ref.setSource("bundle-audit");
271             ref.setUrl(url);
272             vulnerability.getReferences().add(ref);
273         }
274         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
275     }
276 
277     private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
278         if (null != vulnerability) {
279             final String criticality = nextLine.substring(CRITICALITY.length()).trim();
280             if ("High".equals(criticality)) {
281                 vulnerability.setCvssScore(8.5f);
282             } else if ("Medium".equals(criticality)) {
283                 vulnerability.setCvssScore(5.5f);
284             } else if ("Low".equals(criticality)) {
285                 vulnerability.setCvssScore(2.0f);
286             } else {
287                 vulnerability.setCvssScore(-1.0f);
288             }
289         }
290         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
291     }
292 
293     private Vulnerability createVulnerability(String parentName, Dependency dependency, Vulnerability vulnerability, String gem, String nextLine) {
294         if (null != dependency) {
295             final String version = nextLine.substring(VERSION.length());
296             dependency.getVersionEvidence().addEvidence(
297                     "bundler-audit",
298                     "Version",
299                     version,
300                     Confidence.HIGHEST);
301             vulnerability = new Vulnerability(); // don't add to dependency until we have name set later
302             vulnerability.setMatchedCPE(
303                     String.format("cpe:/a:%1$s_project:%1$s:%2$s::~~~ruby~~", gem, version),
304                     null);
305             vulnerability.setCvssAccessVector("-");
306             vulnerability.setCvssAccessComplexity("-");
307             vulnerability.setCvssAuthentication("-");
308             vulnerability.setCvssAvailabilityImpact("-");
309             vulnerability.setCvssConfidentialityImpact("-");
310             vulnerability.setCvssIntegrityImpact("-");
311         }
312         LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
313         return vulnerability;
314     }
315 
316     private Dependency createDependencyForGem(Engine engine, String parentName, String fileName, String gem) throws IOException {
317         final File tempFile = File.createTempFile("Gemfile-" + gem, ".lock", Settings.getTempDirectory());
318         final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
319         FileUtils.write(tempFile, displayFileName); // unique contents to avoid dependency bundling
320         final Dependency dependency = new Dependency(tempFile);
321         dependency.getProductEvidence().addEvidence("bundler-audit", "Name", gem, Confidence.HIGHEST);
322         dependency.setDisplayFileName(displayFileName);
323         engine.getDependencies().add(dependency);
324         return dependency;
325     }
326 }