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