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 return builder.start();
87 } catch (IOException ioe) {
88 throw new AnalysisException("bundle-audit failure", ioe);
89 }
90 }
91
92
93
94
95
96
97 @Override
98 public void initializeFileTypeAnalyzer() throws Exception {
99
100 Process process = launchBundleAudit(Settings.getTempDirectory());
101 int exitValue = process.waitFor();
102 if (0 == exitValue) {
103 LOGGER.warn("Unexpected exit code from bundle-audit process. Disabling {}: {}", ANALYZER_NAME, exitValue);
104 setEnabled(false);
105 throw new AnalysisException("Unexpected exit code from bundle-audit process.");
106 } else {
107 BufferedReader reader = null;
108 try {
109 reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
110 if (!reader.ready()) {
111 LOGGER.warn("Bundle-audit error stream unexpectedly not ready. Disabling " + ANALYZER_NAME);
112 setEnabled(false);
113 throw new AnalysisException("Bundle-audit error stream unexpectedly not ready.");
114 } else {
115 final String line = reader.readLine();
116 if (line == null || !line.contains("Errno::ENOENT")) {
117 LOGGER.warn("Unexpected bundle-audit output. Disabling {}: {}", ANALYZER_NAME, line);
118 setEnabled(false);
119 throw new AnalysisException("Unexpected bundle-audit output.");
120 }
121 }
122 } finally {
123 if (null != reader) {
124 reader.close();
125 }
126 }
127 }
128 if (isEnabled()) {
129 LOGGER.info(ANALYZER_NAME + " is enabled. It is necessary to manually run \"bundle-audit update\" "
130 + "occasionally to keep its database up to date.");
131 }
132 }
133
134
135
136
137
138
139 @Override
140 public String getName() {
141 return ANALYZER_NAME;
142 }
143
144
145
146
147
148
149 @Override
150 public AnalysisPhase getAnalysisPhase() {
151 return ANALYSIS_PHASE;
152 }
153
154
155
156
157
158
159 @Override
160 protected String getAnalyzerEnabledSettingKey() {
161 return Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED;
162 }
163
164
165
166
167
168 private boolean needToDisableGemspecAnalyzer = true;
169
170 @Override
171 protected void analyzeFileType(Dependency dependency, Engine engine)
172 throws AnalysisException {
173 if (needToDisableGemspecAnalyzer) {
174 boolean failed = true;
175 final String className = RubyGemspecAnalyzer.class.getName();
176 for (FileTypeAnalyzer analyzer : engine.getFileTypeAnalyzers()) {
177 if (analyzer instanceof RubyGemspecAnalyzer) {
178 ((RubyGemspecAnalyzer) analyzer).setEnabled(false);
179 LOGGER.info("Disabled " + className + " to avoid noisy duplicate results.");
180 failed = false;
181 }
182 }
183 if (failed) {
184 LOGGER.warn("Did not find" + className + '.');
185 }
186 needToDisableGemspecAnalyzer = false;
187 }
188 final File parentFile = dependency.getActualFile().getParentFile();
189 final Process process = launchBundleAudit(parentFile);
190 try {
191 process.waitFor();
192 } catch (InterruptedException ie) {
193 throw new AnalysisException("bundle-audit process interrupted", ie);
194 }
195 BufferedReader rdr = null;
196 try {
197 rdr = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
198 processBundlerAuditOutput(dependency, engine, rdr);
199 } catch (IOException ioe) {
200 LOGGER.warn("bundle-audit failure", ioe);
201 } finally {
202 if (null != rdr) {
203 try {
204 rdr.close();
205 } catch (IOException ioe) {
206 LOGGER.warn("bundle-audit close failure", ioe);
207 }
208 }
209 }
210
211 }
212
213 private void processBundlerAuditOutput(Dependency original, Engine engine, BufferedReader rdr) throws IOException {
214 final String parentName = original.getActualFile().getParentFile().getName();
215 final String fileName = original.getFileName();
216 Dependency dependency = null;
217 Vulnerability vulnerability = null;
218 String gem = null;
219 final Map<String, Dependency> map = new HashMap<String, Dependency>();
220 boolean appendToDescription = false;
221 while (rdr.ready()) {
222 final String nextLine = rdr.readLine();
223 if (null == nextLine) {
224 break;
225 } else if (nextLine.startsWith(NAME)) {
226 appendToDescription = false;
227 gem = nextLine.substring(NAME.length());
228 if (!map.containsKey(gem)) {
229 map.put(gem, createDependencyForGem(engine, parentName, fileName, gem));
230 }
231 dependency = map.get(gem);
232 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
233 } else if (nextLine.startsWith(VERSION)) {
234 vulnerability = createVulnerability(parentName, dependency, vulnerability, gem, nextLine);
235 } else if (nextLine.startsWith(ADVISORY)) {
236 setVulnerabilityName(parentName, dependency, vulnerability, nextLine);
237 } else if (nextLine.startsWith(CRITICALITY)) {
238 addCriticalityToVulnerability(parentName, vulnerability, nextLine);
239 } else if (nextLine.startsWith("URL: ")) {
240 addReferenceToVulnerability(parentName, vulnerability, nextLine);
241 } else if (nextLine.startsWith("Description:")) {
242 appendToDescription = true;
243 if (null != vulnerability) {
244 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. *** ");
245 }
246 } else if (appendToDescription) {
247 if (null != vulnerability) {
248 vulnerability.setDescription(vulnerability.getDescription() + nextLine + "\n");
249 }
250 }
251 }
252 }
253
254 private void setVulnerabilityName(String parentName, Dependency dependency, Vulnerability vulnerability, String nextLine) {
255 final String advisory = nextLine.substring((ADVISORY.length()));
256 if (null != vulnerability) {
257 vulnerability.setName(advisory);
258 }
259 if (null != dependency) {
260 dependency.getVulnerabilities().add(vulnerability);
261 }
262 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
263 }
264
265 private void addReferenceToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
266 final String url = nextLine.substring(("URL: ").length());
267 if (null != vulnerability) {
268 Reference ref = new Reference();
269 ref.setName(vulnerability.getName());
270 ref.setSource("bundle-audit");
271 ref.setUrl(url);
272 vulnerability.getReferences().add(ref);
273 }
274 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
275 }
276
277 private void addCriticalityToVulnerability(String parentName, Vulnerability vulnerability, String nextLine) {
278 if (null != vulnerability) {
279 final String criticality = nextLine.substring(CRITICALITY.length()).trim();
280 if ("High".equals(criticality)) {
281 vulnerability.setCvssScore(8.5f);
282 } else if ("Medium".equals(criticality)) {
283 vulnerability.setCvssScore(5.5f);
284 } else if ("Low".equals(criticality)) {
285 vulnerability.setCvssScore(2.0f);
286 } else {
287 vulnerability.setCvssScore(-1.0f);
288 }
289 }
290 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
291 }
292
293 private Vulnerability createVulnerability(String parentName, Dependency dependency, Vulnerability vulnerability, String gem, String nextLine) {
294 if (null != dependency) {
295 final String version = nextLine.substring(VERSION.length());
296 dependency.getVersionEvidence().addEvidence(
297 "bundler-audit",
298 "Version",
299 version,
300 Confidence.HIGHEST);
301 vulnerability = new Vulnerability();
302 vulnerability.setMatchedCPE(
303 String.format("cpe:/a:%1$s_project:%1$s:%2$s::~~~ruby~~", gem, version),
304 null);
305 vulnerability.setCvssAccessVector("-");
306 vulnerability.setCvssAccessComplexity("-");
307 vulnerability.setCvssAuthentication("-");
308 vulnerability.setCvssAvailabilityImpact("-");
309 vulnerability.setCvssConfidentialityImpact("-");
310 vulnerability.setCvssIntegrityImpact("-");
311 }
312 LOGGER.debug(String.format("bundle-audit (%s): %s", parentName, nextLine));
313 return vulnerability;
314 }
315
316 private Dependency createDependencyForGem(Engine engine, String parentName, String fileName, String gem) throws IOException {
317 final File tempFile = File.createTempFile("Gemfile-" + gem, ".lock", Settings.getTempDirectory());
318 final String displayFileName = String.format("%s%c%s:%s", parentName, File.separatorChar, fileName, gem);
319 FileUtils.write(tempFile, displayFileName);
320 final Dependency dependency = new Dependency(tempFile);
321 dependency.getProductEvidence().addEvidence("bundler-audit", "Name", gem, Confidence.HIGHEST);
322 dependency.setDisplayFileName(displayFileName);
323 engine.getDependencies().add(dependency);
324 return dependency;
325 }
326 }