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.lang3.StringUtils;
21  import org.owasp.dependencycheck.Engine;
22  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
23  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
24  import org.owasp.dependencycheck.data.nvdcve.CveDB;
25  import org.owasp.dependencycheck.dependency.Dependency;
26  import org.owasp.dependencycheck.exception.InitializationException;
27  import org.owasp.dependencycheck.processing.BundlerAuditProcessor;
28  import org.owasp.dependencycheck.utils.FileFilterBuilder;
29  import org.owasp.dependencycheck.utils.Settings;
30  import org.owasp.dependencycheck.utils.processing.ProcessReader;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  import us.springett.parsers.cpe.exceptions.CpeValidationException;
34  
35  import javax.annotation.concurrent.ThreadSafe;
36  import java.io.File;
37  import java.io.FileFilter;
38  import java.io.IOException;
39  import java.util.ArrayList;
40  import java.util.Arrays;
41  import java.util.Collections;
42  import java.util.List;
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  @ThreadSafe
51  public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {
52  
53      /**
54       * The logger.
55       */
56      private static final Logger LOGGER = LoggerFactory.getLogger(RubyBundleAuditAnalyzer.class);
57  
58      /**
59       * A descriptor for the type of dependencies processed or added by this
60       * analyzer.
61       */
62      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.RUBY;
63  
64      /**
65       * The name of the analyzer.
66       */
67      private static final String ANALYZER_NAME = "Ruby Bundle Audit Analyzer";
68  
69      /**
70       * The phase that this analyzer is intended to run in.
71       */
72      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
73      /**
74       * The filter defining which files will be analyzed.
75       */
76      private static final FileFilter FILTER = FileFilterBuilder.newInstance().addFilenames("Gemfile.lock").build();
77      /**
78       * Name.
79       */
80      public static final String NAME = "Name: ";
81      /**
82       * Version.
83       */
84      public static final String VERSION = "Version: ";
85      /**
86       * Advisory.
87       */
88      public static final String ADVISORY = "Advisory: ";
89      /**
90       * CVE.
91       */
92      public static final String CVE = "CVE: ";
93      /**
94       * Criticality.
95       */
96      public static final String CRITICALITY = "Criticality: ";
97  
98      /**
99       * If {@link #analyzeDependency(Dependency, Engine)} is called, then we have
100      * successfully initialized, and it will be necessary to disable
101      * {@link RubyGemspecAnalyzer}.
102      */
103     private volatile boolean needToDisableGemspecAnalyzer = true;
104 
105     /**
106      * @return a filter that accepts files named Gemfile.lock
107      */
108     @Override
109     protected FileFilter getFileFilter() {
110         return FILTER;
111     }
112 
113     /**
114      * Returns the name of the analyzer.
115      *
116      * @return the name of the analyzer.
117      */
118     @Override
119     public String getName() {
120         return ANALYZER_NAME;
121     }
122 
123     /**
124      * Returns the phase that the analyzer is intended to run in.
125      *
126      * @return the phase that the analyzer is intended to run in.
127      */
128     @Override
129     public AnalysisPhase getAnalysisPhase() {
130         return ANALYSIS_PHASE;
131     }
132 
133     /**
134      * Returns the key used in the properties file to reference the analyzer's
135      * enabled property.
136      *
137      * @return the analyzer's enabled property setting key
138      */
139     @Override
140     protected String getAnalyzerEnabledSettingKey() {
141         return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
142     }
143 
144     /**
145      * Launch bundle-audit.
146      *
147      * @param folder directory that contains bundle audit
148      * @param bundleAuditArgs the arguments to pass to bundle audit
149      * @return a handle to the process
150      * @throws AnalysisException thrown when there is an issue launching bundle
151      * audit
152      */
153     private Process launchBundleAudit(File folder, List<String> bundleAuditArgs) throws AnalysisException {
154         if (!folder.isDirectory()) {
155             throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
156         }
157         final List<String> args = new ArrayList<>();
158         final String bundleAuditPath = getSettings().getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH);
159         File bundleAudit = null;
160         if (bundleAuditPath != null) {
161             bundleAudit = new File(bundleAuditPath);
162             if (!bundleAudit.isFile()) {
163                 LOGGER.warn("Supplied `bundleAudit` path is incorrect: {}", bundleAuditPath);
164                 bundleAudit = null;
165             }
166         }
167         args.add(bundleAudit != null ? bundleAudit.getAbsolutePath() : "bundle-audit");
168         args.addAll(bundleAuditArgs);
169         final ProcessBuilder builder = new ProcessBuilder(args);
170 
171         final String bundleAuditWorkingDirectoryPath = getSettings().getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_WORKING_DIRECTORY);
172         File bundleAuditWorkingDirectory = null;
173         if (bundleAuditWorkingDirectoryPath != null) {
174             bundleAuditWorkingDirectory = new File(bundleAuditWorkingDirectoryPath);
175             if (!bundleAuditWorkingDirectory.isDirectory()) {
176                 LOGGER.warn("Supplied `bundleAuditWorkingDirectory` path is incorrect: {}",
177                         bundleAuditWorkingDirectoryPath);
178                 bundleAuditWorkingDirectory = null;
179             }
180         }
181         final File launchBundleAuditFromDirectory = bundleAuditWorkingDirectory != null ? bundleAuditWorkingDirectory : folder;
182         builder.directory(launchBundleAuditFromDirectory);
183         try {
184             LOGGER.info("Launching: {} from {}", args, launchBundleAuditFromDirectory);
185             return builder.start();
186         } catch (IOException ioe) {
187             throw new AnalysisException("bundle-audit initialization failure; this error "
188                     + "can be ignored if you are not analyzing Ruby. Otherwise ensure that "
189                     + "bundle-audit is installed and the path to bundle audit is correctly "
190                     + "specified", ioe);
191         }
192     }
193 
194     /**
195      * Initialize the analyzer.
196      *
197      * @param engine a reference to the dependency-checkException engine
198      * @throws InitializationException if anything goes wrong
199      */
200     @Override
201     public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
202         String bundleAuditVersionDetails;
203         try {
204             final Process process = launchBundleAudit(getSettings().getTempDirectory(), List.of("version"));
205             try (ProcessReader processReader = new ProcessReader(process)) {
206                 processReader.readAll();
207                 final String error = processReader.getError();
208                 if (!StringUtils.isBlank(error)) {
209                     LOGGER.warn("Warnings from bundle-audit {}", error);
210                 }
211                 bundleAuditVersionDetails = processReader.getOutput();
212                 final int exitValue = process.exitValue();
213                 if (exitValue != 0) {
214                     setEnabled(false);
215                     final String msg = String.format("bundle-audit execution failed - "
216                             + "exit code: %d; error: %s ", exitValue, error);
217                     throw new InitializationException(msg);
218                 }
219             }
220         } catch (AnalysisException ae) {
221             setEnabled(false);
222             final String msg = String.format("Exception from bundle-audit process: %s. "
223                     + "Disabling %s", ae.getCause(), ANALYZER_NAME);
224             throw new InitializationException(msg, ae);
225         } catch (IOException ex) {
226             setEnabled(false);
227             throw new InitializationException("Unable to read bundle-audit output.", ex);
228         } catch (InterruptedException ex) {
229             setEnabled(false);
230             final String msg = String.format("Bundle-audit process was interrupted. "
231                     + "Disabling %s", ANALYZER_NAME);
232             Thread.currentThread().interrupt();
233             throw new InitializationException(msg);
234         }
235         LOGGER.info("{} is enabled and is using bundle-audit with version details: {}. "
236                 + "Note: It is necessary to manually run \"bundle-audit update\" "
237                 + "occasionally to keep its database up to date.", ANALYZER_NAME,
238                 bundleAuditVersionDetails);
239     }
240 
241     /**
242      * Determines if the analyzer can analyze the given file type.
243      *
244      * @param dependency the dependency to determine if it can analyze
245      * @param engine the dependency-checkException engine
246      * @throws AnalysisException thrown if there is an analysis exception.
247      */
248     @Override
249     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
250         if (needToDisableGemspecAnalyzer) {
251             for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
252                 if (analyzer instanceof RubyGemspecAnalyzer) {
253                     ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
254                     LOGGER.info("Disabled {} to avoid noisy duplicate results.", analyzer.getName());
255                 }
256             }
257             needToDisableGemspecAnalyzer = false;
258         }
259         final File parentFile = dependency.getActualFile().getParentFile();
260         final List<String> bundleAuditArgs = Arrays.asList("check", "--verbose");
261 
262         final Process process = launchBundleAudit(parentFile, bundleAuditArgs);
263         try (BundlerAuditProcessor processor = new BundlerAuditProcessor(dependency, engine);
264                 ProcessReader processReader = new ProcessReader(process, processor)) {
265 
266             processReader.readAll();
267             final String error = processReader.getError();
268             if (StringUtils.isNotBlank(error)) {
269                 LOGGER.warn("Warnings from bundle-audit {}", error);
270             }
271             final int exitValue = process.exitValue();
272             if (exitValue < 0 || exitValue > 1) {
273                 final String msg = String.format("Unexpected exit code from bundle-audit "
274                         + "process; exit code: %s", exitValue);
275                 throw new AnalysisException(msg);
276             }
277         } catch (InterruptedException ie) {
278             Thread.currentThread().interrupt();
279             throw new AnalysisException("bundle-audit process interrupted", ie);
280         } catch (IOException | CpeValidationException ioe) {
281             LOGGER.warn("bundle-audit failure", ioe);
282             throw new AnalysisException("bunder-audit error: " + ioe.getMessage(), ioe);
283         }
284     }
285 }