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