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