1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
46
47
48
49
50 @ThreadSafe
51 public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {
52
53
54
55
56 private static final Logger LOGGER = LoggerFactory.getLogger(RubyBundleAuditAnalyzer.class);
57
58
59
60
61
62 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.RUBY;
63
64
65
66
67 private static final String ANALYZER_NAME = "Ruby Bundle Audit Analyzer";
68
69
70
71
72 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
73
74
75
76 private static final FileFilter FILTER = FileFilterBuilder.newInstance().addFilenames("Gemfile.lock").build();
77
78
79
80 public static final String NAME = "Name: ";
81
82
83
84 public static final String VERSION = "Version: ";
85
86
87
88 public static final String ADVISORY = "Advisory: ";
89
90
91
92 public static final String CVE = "CVE: ";
93
94
95
96 public static final String CRITICALITY = "Criticality: ";
97
98
99
100
101
102
103 private volatile boolean needToDisableGemspecAnalyzer = true;
104
105
106
107
108 @Override
109 protected FileFilter getFileFilter() {
110 return FILTER;
111 }
112
113
114
115
116
117
118 @Override
119 public String getName() {
120 return ANALYZER_NAME;
121 }
122
123
124
125
126
127
128 @Override
129 public AnalysisPhase getAnalysisPhase() {
130 return ANALYSIS_PHASE;
131 }
132
133
134
135
136
137
138
139 @Override
140 protected String getAnalyzerEnabledSettingKey() {
141 return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
142 }
143
144
145
146
147
148
149
150
151
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
196
197
198
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
243
244
245
246
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 }