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 com.esotericsoftware.minlog.Log;
21 import com.github.packageurl.MalformedPackageURLException;
22 import com.github.packageurl.PackageURL;
23 import com.github.packageurl.PackageURLBuilder;
24 import com.h3xstream.retirejs.repo.JsLibrary;
25 import com.h3xstream.retirejs.repo.JsLibraryResult;
26 import com.h3xstream.retirejs.repo.JsVulnerability;
27 import com.h3xstream.retirejs.repo.ScannerFacade;
28 import com.h3xstream.retirejs.repo.VulnerabilitiesRepository;
29 import com.h3xstream.retirejs.repo.VulnerabilitiesRepositoryLoader;
30 import org.apache.commons.lang3.StringUtils;
31 import org.apache.commons.validator.routines.UrlValidator;
32 import org.json.JSONException;
33 import org.owasp.dependencycheck.Engine;
34 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
35 import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
36 import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
37 import org.owasp.dependencycheck.data.update.RetireJSDataSource;
38 import org.owasp.dependencycheck.data.update.exception.UpdateException;
39 import org.owasp.dependencycheck.dependency.Confidence;
40 import org.owasp.dependencycheck.dependency.Dependency;
41 import org.owasp.dependencycheck.dependency.EvidenceType;
42 import org.owasp.dependencycheck.dependency.Reference;
43 import org.owasp.dependencycheck.dependency.Vulnerability;
44 import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
45 import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
46 import org.owasp.dependencycheck.exception.InitializationException;
47 import org.owasp.dependencycheck.exception.WriteLockException;
48 import org.owasp.dependencycheck.utils.FileFilterBuilder;
49 import org.owasp.dependencycheck.utils.Settings;
50 import org.owasp.dependencycheck.utils.WriteLock;
51 import org.owasp.dependencycheck.utils.search.FileContentSearch;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import javax.annotation.concurrent.ThreadSafe;
56 import java.io.File;
57 import java.io.FileFilter;
58 import java.io.FileInputStream;
59 import java.io.FileNotFoundException;
60 import java.io.IOException;
61 import java.io.InputStream;
62 import java.net.URL;
63 import java.nio.file.Files;
64 import java.util.ArrayList;
65 import java.util.List;
66 import java.util.Map;
67 import org.apache.commons.io.IOUtils;
68
69
70
71
72
73
74
75
76
77
78 @ThreadSafe
79 public class RetireJsAnalyzer extends AbstractFileTypeAnalyzer {
80
81
82
83
84
85 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVASCRIPT;
86
87
88
89 private static final Logger LOGGER = LoggerFactory.getLogger(RetireJsAnalyzer.class);
90
91
92
93 private static final String ANALYZER_NAME = "RetireJS Analyzer";
94
95
96
97 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.FINDING_ANALYSIS;
98
99
100
101 private static final String[] EXTENSIONS = {"js"};
102
103
104
105 private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
106
107
108
109 private VulnerabilitiesRepository jsRepository;
110
111
112
113
114
115 private String[] filters = null;
116
117
118
119
120
121
122 @SuppressWarnings("FieldMayBeFinal")
123 private boolean skipNonVulnerableInJAR = true;
124
125
126
127
128
129
130 @Override
131 protected FileFilter getFileFilter() {
132 return FILTER;
133 }
134
135
136
137
138
139
140
141
142 @Override
143 public boolean accept(File pathname) {
144 try {
145 final boolean accepted = super.accept(pathname);
146 if (accepted && !pathname.exists()) {
147
148 super.setFilesMatched(true);
149 return true;
150 }
151 if (accepted && filters != null && FileContentSearch.contains(pathname, filters)) {
152 return false;
153 }
154 return accepted;
155 } catch (IOException ex) {
156 LOGGER.warn(String.format("Error testing file %s", pathname), ex);
157 }
158 return false;
159 }
160
161
162
163
164
165
166 @Override
167 public void initialize(Settings settings) {
168 super.initialize(settings);
169 if (this.isEnabled()) {
170 this.filters = settings.getArray(Settings.KEYS.ANALYZER_RETIREJS_FILTERS);
171 }
172 }
173
174
175
176
177
178
179
180
181 @Override
182 protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
183
184
185
186
187
188
189
190 Log.set(Log.LEVEL_WARN);
191
192 File repoFile = null;
193 boolean repoEmpty = false;
194 try {
195 final String configuredUrl = getSettings().getString(Settings.KEYS.ANALYZER_RETIREJS_REPO_JS_URL, RetireJSDataSource.DEFAULT_JS_URL);
196 final URL url = new URL(configuredUrl);
197 final File filepath = new File(url.getPath());
198 repoFile = new File(getSettings().getDataDirectory(), filepath.getName());
199 if (!repoFile.isFile() || repoFile.length() <= 1L) {
200 LOGGER.warn("Retire JS repository is empty or missing - attempting to force the update");
201 repoEmpty = true;
202 getSettings().setBoolean(Settings.KEYS.ANALYZER_RETIREJS_FORCEUPDATE, true);
203 }
204 } catch (FileNotFoundException ex) {
205 this.setEnabled(false);
206 throw new InitializationException(String.format("RetireJS repo does not exist locally (%s)", repoFile), ex);
207 } catch (IOException ex) {
208 this.setEnabled(false);
209 throw new InitializationException("Failed to initialize the RetireJS", ex);
210 }
211
212 final boolean autoupdate = getSettings().getBoolean(Settings.KEYS.AUTO_UPDATE, true);
213 final boolean forceupdate = getSettings().getBoolean(Settings.KEYS.ANALYZER_RETIREJS_FORCEUPDATE, false);
214 if ((!autoupdate && forceupdate) || (autoupdate && repoEmpty)) {
215 final RetireJSDataSource ds = new RetireJSDataSource();
216 try {
217 ds.update(engine);
218 } catch (UpdateException ex) {
219 throw new InitializationException("Unable to initialize the Retire JS repository", ex);
220 }
221 }
222
223
224 try (WriteLock lock = new WriteLock(getSettings(), true, repoFile.getName() + ".lock")) {
225 final File temp = getSettings().getTempDirectory();
226 final File tempRepo = new File(temp, repoFile.getName());
227 LOGGER.debug("copying retireJs repo {} to {}", repoFile.toPath(), tempRepo.toPath());
228 Files.copy(repoFile.toPath(), tempRepo.toPath());
229 repoFile = tempRepo;
230 } catch (WriteLockException | IOException ex) {
231 this.setEnabled(false);
232 throw new InitializationException("Failed to copy the RetireJS repo", ex);
233 }
234 try (FileInputStream in = new FileInputStream(repoFile)) {
235 this.jsRepository = new VulnerabilitiesRepositoryLoader().loadFromInputStream(in);
236 } catch (JSONException ex) {
237 this.setEnabled(false);
238 throw new InitializationException("Failed to initialize the RetireJS repo: `" + repoFile
239 + "` appears to be malformed. Please delete the file or run the dependency-check purge "
240 + "command and re-try running dependency-check.", ex);
241 } catch (IOException ex) {
242 this.setEnabled(false);
243 throw new InitializationException("Failed to initialize the RetireJS repo", ex);
244 }
245 }
246
247
248
249
250
251
252 @Override
253 public String getName() {
254 return ANALYZER_NAME;
255 }
256
257
258
259
260
261
262 @Override
263 public AnalysisPhase getAnalysisPhase() {
264 return ANALYSIS_PHASE;
265 }
266
267
268
269
270
271
272
273 @Override
274 protected String getAnalyzerEnabledSettingKey() {
275 return Settings.KEYS.ANALYZER_RETIREJS_ENABLED;
276 }
277
278
279
280
281
282
283
284
285
286 @Override
287 public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
288 if (dependency.isVirtual()) {
289 return;
290 }
291 try (InputStream fis = new FileInputStream(dependency.getActualFile())) {
292 final byte[] fileContent = IOUtils.toByteArray(fis);
293 final ScannerFacade scanner = new ScannerFacade(jsRepository);
294 final List<JsLibraryResult> results;
295 try {
296 results = scanner.scanScript(dependency.getActualFile().getAbsolutePath(), fileContent, 0);
297 } catch (StackOverflowError ex) {
298 final String msg = String.format("An error occured trying to analyze %s. "
299 + "To resolve this error please try increasing the Java stack size to "
300 + "8mb and re-run dependency-check:%n%n"
301 + "(win) : set JAVA_OPTS=\"-Xss8192k\"%n"
302 + "(*nix): export JAVA_OPTS=\"-Xss8192k\"%n%n",
303 dependency.getDisplayFileName());
304 throw new AnalysisException(msg, ex);
305 }
306 if (results.size() > 0) {
307 for (JsLibraryResult libraryResult : results) {
308
309 final JsLibrary lib = libraryResult.getLibrary();
310 dependency.setName(lib.getName());
311 dependency.setVersion(libraryResult.getDetectedVersion());
312 try {
313 final PackageURL purl = PackageURLBuilder.aPackageURL().withType("javascript")
314 .withName(lib.getName()).withVersion(libraryResult.getDetectedVersion()).build();
315 dependency.addSoftwareIdentifier(new PurlIdentifier(purl, Confidence.HIGHEST));
316 } catch (MalformedPackageURLException ex) {
317 LOGGER.debug("Unable to build package url for retireJS", ex);
318 final GenericIdentifier id = new GenericIdentifier("javascript:" + lib.getName() + "@"
319 + libraryResult.getDetectedVersion(), Confidence.HIGHEST);
320 dependency.addSoftwareIdentifier(id);
321 }
322
323 dependency.addEvidence(EvidenceType.VERSION, "file", "version", libraryResult.getDetectedVersion(), Confidence.HIGH);
324 dependency.addEvidence(EvidenceType.PRODUCT, "file", "name", libraryResult.getLibrary().getName(), Confidence.HIGH);
325 dependency.addEvidence(EvidenceType.VENDOR, "file", "name", libraryResult.getLibrary().getName(), Confidence.HIGH);
326
327 final List<Vulnerability> vulns = new ArrayList<>();
328 final JsVulnerability jsVuln = libraryResult.getVuln();
329
330 if (jsVuln.getIdentifiers().containsKey("CVE") || jsVuln.getIdentifiers().containsKey("osvdb")) {
331
332
333
334
335
336
337 for (Map.Entry<String, List<String>> entry : jsVuln.getIdentifiers().entrySet()) {
338 final String key = entry.getKey();
339 final List<String> value = entry.getValue();
340 if ("CVE".equals(key)) {
341 for (String cve : value) {
342 Vulnerability vuln = engine.getDatabase().getVulnerability(StringUtils.trim(cve));
343 if (vuln == null) {
344
345
346
347 vuln = new Vulnerability();
348 vuln.setName(cve);
349 vuln.setUnscoredSeverity(jsVuln.getSeverity());
350 vuln.setSource(Vulnerability.Source.RETIREJS);
351 }
352 jsVuln.getInfo().stream().map((info) -> {
353 if (UrlValidator.getInstance().isValid(info)) {
354 return new Reference(info, "info", info);
355 }
356 return new Reference(info, "info", null);
357 }).forEach(vuln::addReference);
358 vulns.add(vuln);
359 }
360 } else if ("osvdb".equals(key)) {
361
362 value.forEach((osvdb) -> {
363 final Vulnerability vuln = new Vulnerability();
364 vuln.setName(osvdb);
365 vuln.setSource(Vulnerability.Source.RETIREJS);
366 vuln.setUnscoredSeverity(jsVuln.getSeverity());
367 jsVuln.getInfo().stream().map((info) -> {
368 if (UrlValidator.getInstance().isValid(info)) {
369 return new Reference(info, "info", info);
370 }
371 return new Reference(info, "info", null);
372 }).forEach(vuln::addReference);
373 vulns.add(vuln);
374 });
375 }
376 dependency.addVulnerabilities(vulns);
377 }
378
379 } else {
380 final Vulnerability individualVuln = new Vulnerability();
381
382
383 for (Map.Entry<String, List<String>> entry : jsVuln.getIdentifiers().entrySet()) {
384 final String key = entry.getKey();
385 final List<String> value = entry.getValue();
386
387 if (null != key) {
388 switch (key) {
389 case "summary":
390 if (null == individualVuln.getName()) {
391 individualVuln.setName(value.get(0));
392 }
393 individualVuln.setDescription(value.get(0));
394 break;
395 case "issue":
396 individualVuln.setName(libraryResult.getLibrary().getName() + " issue: " + value.get(0));
397 if (UrlValidator.getInstance().isValid(value.get(0))) {
398 individualVuln.addReference(key, key, value.get(0));
399 } else {
400 individualVuln.addReference(key, value.get(0), null);
401 }
402 break;
403 case "bug":
404 individualVuln.setName(libraryResult.getLibrary().getName() + " bug: " + value.get(0));
405 if (UrlValidator.getInstance().isValid(value.get(0))) {
406 individualVuln.addReference(key, key, value.get(0));
407 } else {
408 individualVuln.addReference(key, value.get(0), null);
409 }
410 break;
411 case "pr":
412 individualVuln.setName(libraryResult.getLibrary().getName() + " pr: " + value.get(0));
413 if (UrlValidator.getInstance().isValid(value.get(0))) {
414 individualVuln.addReference(key, key, value.get(0));
415 } else {
416 individualVuln.addReference(key, value.get(0), null);
417 }
418 break;
419
420 default:
421 if (UrlValidator.getInstance().isValid(value.get(0))) {
422 individualVuln.addReference(key, key, value.get(0));
423 } else {
424 individualVuln.addReference(key, value.get(0), null);
425 }
426 break;
427 }
428 }
429
430 }
431 if (StringUtils.isBlank(individualVuln.getName())) {
432 individualVuln.setName("Vulnerability in " + libraryResult.getLibrary().getName());
433 }
434 individualVuln.setSource(Vulnerability.Source.RETIREJS);
435 individualVuln.setUnscoredSeverity(jsVuln.getSeverity());
436 jsVuln.getInfo().stream().map((info) -> {
437 if (UrlValidator.getInstance().isValid(info)) {
438 return new Reference(info, "info", info);
439 }
440 return new Reference(info, "info", null);
441 }).forEach(individualVuln::addReference);
442
443 dependency.addVulnerability(individualVuln);
444 }
445 }
446 } else if (getSettings().getBoolean(Settings.KEYS.ANALYZER_RETIREJS_FILTER_NON_VULNERABLE, false)) {
447 engine.removeDependency(dependency);
448 }
449 } catch (IOException | DatabaseException e) {
450 throw new AnalysisException(e);
451 }
452 }
453
454 @Override
455 protected void closeAnalyzer() throws Exception {
456 Log.set(Log.LEVEL_INFO);
457 }
458 }