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