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.io.FileUtils;
21 import org.owasp.dependencycheck.Engine;
22 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
23 import org.owasp.dependencycheck.dependency.Confidence;
24 import org.owasp.dependencycheck.dependency.Dependency;
25 import org.owasp.dependencycheck.dependency.Reference;
26 import org.owasp.dependencycheck.dependency.Vulnerability;
27 import org.owasp.dependencycheck.utils.FileFilterBuilder;
28 import org.owasp.dependencycheck.utils.Settings;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 import java.io.*;
33 import java.util.*;
34
35
36
37
38
39
40 public class RubyBundleAuditAnalyzer extends AbstractFileTypeAnalyzer {
41
42 private static final Logger LOGGER = LoggerFactory.getLogger(RubyBundleAuditAnalyzer.class);
43
44
45
46
47 private static final String ANALYZER_NAME = "Ruby Bundle Audit Analyzer";
48
49
50
51
52 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_INFORMATION_COLLECTION;
53
54 private static final FileFilter FILTER
55 = FileFilterBuilder.newInstance().addFilenames("Gemfile.lock").build();
56 public static final String NAME = "Name: ";
57 public static final String VERSION = "Version: ";
58 public static final String ADVISORY = "Advisory: ";
59 public static final String CRITICALITY = "Criticality: ";
60
61
62
63
64 @Override
65 protected FileFilter getFileFilter() {
66 return FILTER;
67 }
68
69
70
71
72
73
74 private Process launchBundleAudit(File folder) throws AnalysisException {
75 if (!folder.isDirectory()) {
76 throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
77 }
78 final List<String> args = new ArrayList<String>();
79 final String bundleAuditPath = Settings.getString(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH);
80 args.add(null == bundleAuditPath ? "bundle-audit" : bundleAuditPath);
81 args.add("check");
82 args.add("--verbose");
83 final ProcessBuilder builder = new ProcessBuilder(args);
84 builder.directory(folder);
85 try {
86 LOGGER.info("Launching: " + args + " from " + folder);
87 return builder.start();
88 } catch (IOException ioe) {
89 throw new AnalysisException("bundle-audit failure", ioe);
90 }
91 }
92
93
94
95
96
97
98 @Override
99 public void initializeFileTypeAnalyzer() throws Exception {
100
101 Process process = null;
102 try {
103 process = launchBundleAudit(Settings.getTempDirectory());
104 }
105 catch(AnalysisException ae) {
106 LOGGER.warn("Exception from bundle-audit process: {}. Disabling {}", ae.getCause(), ANALYZER_NAME);
107 setEnabled(false);
108 throw ae;
109 }
110
111 int exitValue = process.waitFor();
112 if (0 == exitValue) {
113 LOGGER.warn("Unexpected exit code from bundle-audit process. Disabling {}: {}", ANALYZER_NAME, exitValue);
114 setEnabled(false);
115 throw new AnalysisException("Unexpected exit code from bundle-audit process.");
116 } else {
117 BufferedReader reader = null;
118 try {
119 reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
120 if (!reader.ready()) {
121 LOGGER.warn("Bundle-audit error stream unexpectedly not ready. Disabling " + ANALYZER_NAME);
122 setEnabled(false);
123 throw new AnalysisException("Bundle-audit error stream unexpectedly not ready.");
124 } else {
125 final String line = reader.readLine();
126 if (line == null || !line.contains("Errno::ENOENT")) {
127 LOGGER.warn("Unexpected bundle-audit output. Disabling {}: {}", ANALYZER_NAME, line);
128 setEnabled(false);
129 throw new AnalysisException("Unexpected bundle-audit output.");
130 }
131 }
132 } finally {
133 if (null != reader) {
134 reader.close();
135 }
136 }
137 }
138
139 if (isEnabled()) {
140 LOGGER.info(ANALYZER_NAME + " is enabled. It is necessary to manually run \"bundle-audit update\" "
141 + "occasionally to keep its database up to date.");
142 }
143 }
144
145
146
147
148
149
150 @Override
151 public String getName() {
152 return ANALYZER_NAME;
153 }
154
155
156
157
158
159
160 @Override
161 public AnalysisPhase getAnalysisPhase() {
162 return ANALYSIS_PHASE;
163 }
164
165
166
167
168
169
170 @Override
171 protected String getAnalyzerEnabledSettingKey() {
172 return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
173 }
174
175
176
177
178
179 private boolean needToDisableGemspecAnalyzer = true;
180
181 @Override
182 protected void analyzeFileType(Dependency dependency, Engine engine)
183 throws AnalysisException {
184 if (needToDisableGemspecAnalyzer) {
185 boolean failed = true;
186 final String className = RubyGemspecAnalyzer.class.getName();
187 for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
188 if (analyzer instanceof RubyGemspecAnalyzer) {
189 ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
190 LOGGER.info("Disabled " + className + " to avoid noisy duplicate results.");
191 failed = false;
192 }
193 }
194 if (failed) {
195 LOGGER.warn("Did not find" + className + '.');
196 }
197 needToDisableGemspecAnalyzer = false;
198 }
199 final File parentFile = dependency.getActualFile().getParentFile();
200 final Process process = launchBundleAudit(parentFile);
201 try {
202 process.waitFor();
203 } catch (InterruptedException ie) {
204 throw new AnalysisException("bundle-audit process interrupted", ie);
205 }
206 BufferedReader rdr = null;
207 try {
208 BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
209 while(errReader.ready()) {
210 String error = errReader.readLine();
211 LOGGER.warn(error);
212 }
213 rdr = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
214 processBundlerAuditOutput(dependency, engine, rdr);
215 } catch (IOException ioe) {
216 LOGGER.warn("bundle-audit failure", ioe);
217 } finally {
218 if (null != rdr) {
219 try {
220 rdr.close();
221 } catch (IOException ioe) {
222 LOGGER.warn("bundle-audit close failure", ioe);
223 }
224 }
225 }
226
227 }
228
229 private void processBundlerAuditOutput(Dependency original, Engine engine, BufferedReader rdr) throws IOException {
230 final String parentName = original.getActualFile().getParentFile().getName();
231 final String fileName = original.getFileName();
232 Dependency dependency = null;
233 Vulnerability vulnerability = null;
234 String gem = null;
235 final Map<String, Dependency> map = new HashMap<String, Dependency>();
236 boolean appendToDescription = false;
237 while (rdr.ready()) {
238 final String nextLine = rdr.readLine();
239 if (null == nextLine) {
240 break;
241 } else if (nextLine.startsWith(NAME)) {
242 appendToDescription = false;
243 gem = nextLine.substring(NAME.length());
244 if (!map.containsKey(gem)) {
245 map.put(gem, createDependencyForGem(engine, parentName, fileName, gem));
246 }
247 dependency = map.get(gem);
248 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
249 } else if (nextLine.startsWith(VERSION)) {
250 vulnerability = createVulnerability(parentName, dependency, vulnerability, gem, nextLine);
251 } else if (nextLine.startsWith(ADVISORY)) {
252 setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
253 } else if (nextLine.startsWith(CRITICALITY)) {
254 addCriticalityToVulnerability(parentName, vulnerability, nextLine);
255 } else if (nextLine.startsWith("URL: ")) {
256 addReferenceToVulnerability(parentName, vulnerability, nextLine);
257 } else if (nextLine.startsWith("Description:")) {
258 appendToDescription = true;
259 if (null != vulnerability) {
260 vulnerability.setDescription("*** Vulnerability obtained from bundle-audit verbose report. Title link may not work. CPE below is guessed. CVSS score is estimated (-1.0 indicates unknown). See link below for full details. *** ");
261 }
262 } else if (appendToDescription) {
263 if (null != vulnerability) {
264 vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
265 }
266 }
267 }
268 }
269
270 private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
271 final String advisory = nextLine.substring((ADVISORY.length()));
272 if (null != vulnerability) {
273 vulnerability.setName(advisory);
274 }
275 if (null != dependency) {
276 dependency.getVulnerabilities().add(vulnerability);
277 }
278 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
279 }
280
281 private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
282 final String url = nextLine.substring(("URL: ").length());
283 if (null != vulnerability) {
284 Reference ref = new Reference();
285 ref.setName(vulnerability.getName());
286 ref.setSource("bundle-audit");
287 ref.setUrl(url);
288 vulnerability.getReferences().add(ref);
289 }
290 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
291 }
292
293 private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
294 if (null != vulnerability) {
295 final String criticality = nextLine.substring(CRITICALITY.length()).trim();
296 if ("High".equals(criticality)) {
297 vulnerability.setCvssScore(8.5f);
298 } else if ("Medium".equals(criticality)) {
299 vulnerability.setCvssScore(5.5f);
300 } else if ("Low".equals(criticality)) {
301 vulnerability.setCvssScore(2.0f);
302 } else {
303 vulnerability.setCvssScore(-1.0f);
304 }
305 }
306 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
307 }
308
309 private Vulnerability createVulnerability(String parentName, Dependency dependency, Vulnerability vulnerability, String gem, String nextLine) {
310 if (null != dependency) {
311 final String version = nextLine.substring(VERSION.length());
312 dependency.getVersionEvidence().addEvidence(
313 "bundler-audit",
314 "Version",
315 version,
316 Confidence.HIGHEST);
317 vulnerability = new Vulnerability();
318 vulnerability.setMatchedCPE(
319 String.format("cpe:/a:%1$s_project:%1$s:%2$s::~~~ruby~~", gem, version),
320 null);
321 vulnerability.setCvssAccessVector("-");
322 vulnerability.setCvssAccessComplexity("-");
323 vulnerability.setCvssAuthentication("-");
324 vulnerability.setCvssAvailabilityImpact("-");
325 vulnerability.setCvssConfidentialityImpact("-");
326 vulnerability.setCvssIntegrityImpact("-");
327 }
328 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
329 return vulnerability;
330 }
331
332 private Dependency createDependencyForGem(Engine engine, String parentName, String fileName, String gem) throws IOException {
333 final File tempFile = File.createTempFile("Gemfile-" + gem, ".lock", Settings.getTempDirectory());
334 final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
335 FileUtils.write(tempFile, displayFileName);
336 final Dependency dependency = new Dependency(tempFile);
337 dependency.getProductEvidence().addEvidence("bundler-audit", "Name", gem, Confidence.HIGHEST);
338 dependency.setDisplayFileName(displayFileName);
339 engine.getDependencies().add(dependency);
340 return dependency;
341 }
342 }