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.PackageURLBuilder;
23 import com.google.common.annotations.VisibleForTesting;
24 import com.h3xstream.retirejs.repo.JsLibraryResult;
25 import com.h3xstream.retirejs.repo.ScannerFacade;
26 import com.h3xstream.retirejs.repo.VulnerabilitiesRepository;
27 import com.h3xstream.retirejs.repo.VulnerabilitiesRepositoryLoader;
28 import org.apache.commons.io.IOUtils;
29 import org.apache.commons.lang3.StringUtils;
30 import org.apache.commons.validator.routines.UrlValidator;
31 import org.json.JSONException;
32 import org.jspecify.annotations.NonNull;
33 import org.jspecify.annotations.Nullable;
34 import org.owasp.dependencycheck.Engine;
35 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
36 import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
37 import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
38 import org.owasp.dependencycheck.data.update.RetireJSDataSource;
39 import org.owasp.dependencycheck.data.update.exception.UpdateException;
40 import org.owasp.dependencycheck.dependency.Confidence;
41 import org.owasp.dependencycheck.dependency.Dependency;
42 import org.owasp.dependencycheck.dependency.EvidenceType;
43 import org.owasp.dependencycheck.dependency.Reference;
44 import org.owasp.dependencycheck.dependency.Vulnerability;
45 import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
46 import org.owasp.dependencycheck.dependency.naming.Identifier;
47 import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
48 import org.owasp.dependencycheck.exception.InitializationException;
49 import org.owasp.dependencycheck.exception.WriteLockException;
50 import org.owasp.dependencycheck.utils.FileFilterBuilder;
51 import org.owasp.dependencycheck.utils.Settings;
52 import org.owasp.dependencycheck.utils.WriteLock;
53 import org.owasp.dependencycheck.utils.search.FileContentSearch;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57 import javax.annotation.concurrent.ThreadSafe;
58 import java.io.File;
59 import java.io.FileFilter;
60 import java.io.FileInputStream;
61 import java.io.IOException;
62 import java.io.InputStream;
63 import java.nio.file.Files;
64 import java.nio.file.StandardCopyOption;
65 import java.util.HashSet;
66 import java.util.LinkedHashMap;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.Objects;
70 import java.util.Optional;
71 import java.util.OptionalInt;
72 import java.util.Set;
73 import java.util.stream.Collectors;
74
75 import static org.owasp.dependencycheck.analyzer.RetireJsLibrary.KnownIdentifierTypes.CVE;
76 import static org.owasp.dependencycheck.analyzer.RetireJsLibrary.KnownIdentifierTypes.GITHUB_SECURITY_ADVISORY;
77 import static org.owasp.dependencycheck.analyzer.RetireJsLibrary.KnownIdentifierTypes.SECONDARY_NAME_TYPES;
78 import static org.owasp.dependencycheck.analyzer.RetireJsLibrary.KnownIdentifierTypes.SUMMARY;
79 import static org.owasp.dependencycheck.analyzer.RetireJsLibrary.KnownIdentifierTypes.singleEntry;
80 import static org.owasp.dependencycheck.analyzer.RetireJsLibrary.KnownIdentifierTypes.singleItem;
81
82
83
84
85
86
87
88
89
90
91 @ThreadSafe
92 public class RetireJsAnalyzer extends AbstractFileTypeAnalyzer {
93
94
95
96
97
98 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVASCRIPT;
99
100
101
102 private static final Logger LOGGER = LoggerFactory.getLogger(RetireJsAnalyzer.class);
103
104
105
106 private static final String ANALYZER_NAME = "RetireJS Analyzer";
107
108
109
110 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.FINDING_ANALYSIS;
111
112
113
114 private static final String[] EXTENSIONS = {"js"};
115
116
117
118 private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
119
120
121
122 private VulnerabilitiesRepository jsRepository;
123
124
125
126
127
128 private String[] filters = null;
129
130
131
132
133
134
135 @Override
136 protected FileFilter getFileFilter() {
137 return FILTER;
138 }
139
140
141
142
143
144
145
146
147 @Override
148 public boolean accept(File pathname) {
149 try {
150 final boolean accepted = super.accept(pathname);
151 if (accepted && !pathname.exists()) {
152
153 super.setFilesMatched(true);
154 return true;
155 }
156 if (accepted && filters != null && FileContentSearch.contains(pathname, filters)) {
157 return false;
158 }
159 return accepted;
160 } catch (IOException ex) {
161 LOGGER.warn("Error testing file {}", pathname, ex);
162 }
163 return false;
164 }
165
166
167
168
169
170
171 @Override
172 public void initialize(Settings settings) {
173 super.initialize(settings);
174 if (this.isEnabled()) {
175 this.filters = settings.getArray(Settings.KEYS.ANALYZER_RETIREJS_FILTERS);
176 }
177 }
178
179
180
181
182
183
184
185
186 @Override
187 protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
188
189
190
191
192
193
194
195 Log.set(Log.LEVEL_WARN);
196
197 File repoFile = tryRemoteFetchIfConfigured(engine);
198
199 try (WriteLock ignored = new WriteLock(getSettings(), true, repoFile.getName() + ".lock")) {
200 final File temp = getSettings().getTempDirectory();
201 final File tempRepo = new File(temp, repoFile.getName());
202 LOGGER.debug("copying RetireJS repo {} to {}", repoFile.toPath(), tempRepo.toPath());
203 Files.copy(repoFile.toPath(), tempRepo.toPath(), StandardCopyOption.REPLACE_EXISTING);
204 repoFile = tempRepo;
205 } catch (WriteLockException | IOException ex) {
206 this.setEnabled(false);
207 throw new InitializationException("Failed to copy the RetireJS repo", ex);
208 }
209 try (FileInputStream in = new FileInputStream(repoFile)) {
210 this.jsRepository = new VulnerabilitiesRepositoryLoader().loadFromInputStream(in);
211 } catch (JSONException ex) {
212 this.setEnabled(false);
213 throw new InitializationException("Failed to initialize the RetireJS repo: `" + repoFile
214 + "` appears to be malformed. Please delete the file or run the dependency-check purge "
215 + "command and re-try running dependency-check.", ex);
216 } catch (IOException ex) {
217 this.setEnabled(false);
218 throw new InitializationException("Failed to initialize the RetireJS repo", ex);
219 }
220 }
221
222 private File tryRemoteFetchIfConfigured(Engine engine) throws InitializationException {
223 RetireJSDataSource ds = new RetireJSDataSource();
224 try {
225 ds.update(engine);
226 return ds.validatedRepoFile();
227 } catch (UpdateException ex) {
228 this.setEnabled(false);
229 throw new InitializationException("Failed to initialize the RetireJS repo", ex);
230 }
231 }
232
233
234
235
236
237
238 @Override
239 public String getName() {
240 return ANALYZER_NAME;
241 }
242
243
244
245
246
247
248 @Override
249 public AnalysisPhase getAnalysisPhase() {
250 return ANALYSIS_PHASE;
251 }
252
253
254
255
256
257
258
259 @Override
260 protected String getAnalyzerEnabledSettingKey() {
261 return Settings.KEYS.ANALYZER_RETIREJS_ENABLED;
262 }
263
264
265
266
267
268
269
270
271 @Override
272 public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
273 if (dependency.isVirtual()) {
274 return;
275 }
276 try (InputStream fis = new FileInputStream(dependency.getActualFile())) {
277 final List<RetireJsLibrary> vulnerableLibraries = new ScannerFacade(jsRepository)
278 .scanScript(dependency.getActualFile().getAbsolutePath(), IOUtils.toByteArray(fis), 0)
279 .stream().map(RetireJsLibrary::adapt).collect(Collectors.toList());
280
281 if (vulnerableLibraries.isEmpty() && getSettings().getBoolean(Settings.KEYS.ANALYZER_RETIREJS_FILTER_NON_VULNERABLE, false)) {
282 engine.removeDependency(dependency);
283 return;
284 }
285
286 for (RetireJsLibrary lib : vulnerableLibraries) {
287 dependency.setName(lib.libraryName());
288 dependency.setVersion(lib.version());
289 dependency.addSoftwareIdentifier(lib.identifier());
290 dependency.addEvidence(EvidenceType.VERSION, "RetireJS", "version", lib.version(), Confidence.HIGH);
291 dependency.addEvidence(EvidenceType.PRODUCT, "RetireJS", "name", lib.libraryName(), Confidence.HIGH);
292 dependency.addEvidence(EvidenceType.VENDOR, "RetireJS", "name", lib.libraryName(), Confidence.HIGH);
293 dependency.addVulnerabilities(lib.vulnerabilities(cve -> engine.getDatabase().getVulnerability(cve)));
294 }
295 } catch (StackOverflowError ex) {
296 final String msg = String.format("An error occurred trying to analyze %s. "
297 + "To resolve this error please try increasing the Java stack size to "
298 + "8mb and re-run dependency-check:%n%n"
299 + "(win) : set JAVA_OPTS=\"-Xss8m\"%n"
300 + "(*nix): export JAVA_OPTS=\"-Xss8m\"%n%n",
301 dependency.getDisplayFileName());
302 throw new AnalysisException(msg, ex);
303 } catch (IOException | DatabaseException e) {
304 throw new AnalysisException(e);
305 }
306 }
307
308 @Override
309 protected void closeAnalyzer() throws Exception {
310 Log.set(Log.LEVEL_INFO);
311 }
312
313 @SuppressWarnings("SameParameterValue")
314 @VisibleForTesting
315 OptionalInt knownLibraryCountFor(String fileName) {
316 return jsRepository == null ? OptionalInt.empty() : OptionalInt.of(jsRepository.findByFilename(fileName).size());
317 }
318 }
319
320 class RetireJsLibrary {
321 private static final Logger LOGGER = LoggerFactory.getLogger(RetireJsLibrary.class);
322
323 private final JsLibraryResult result;
324
325 private RetireJsLibrary(JsLibraryResult result) {
326 this.result = result;
327 }
328
329 static RetireJsLibrary adapt(JsLibraryResult result) {
330 return new RetireJsLibrary(result);
331 }
332
333 String libraryName() {
334 return result.getLibrary().getName();
335 }
336
337 String version() {
338 return result.getDetectedVersion();
339 }
340
341 Identifier identifier() {
342 try {
343 return new PurlIdentifier(
344 PackageURLBuilder.aPackageURL()
345 .withType("javascript")
346 .withName(libraryName())
347 .withVersion(version())
348 .build(),
349 Confidence.HIGHEST);
350 } catch (MalformedPackageURLException ex) {
351 LOGGER.debug("Unable to build package url for retireJS; using generic identifier", ex);
352 return new GenericIdentifier(String.format("javascript:%s@%s", libraryName(), version()), Confidence.HIGHEST);
353 }
354 }
355
356 List<Vulnerability> vulnerabilities(KnownCveProvider knownCveProvider) {
357 List<Vulnerability> vulns = new RetireJsVulnerabilityIdentifiers(result.getVuln().getIdentifiers())
358 .toVulnerabilities(knownCveProvider, result.getVuln().getSeverity());
359
360 for (Vulnerability vuln : vulns) {
361 vuln.addReferences(infoReferences());
362 }
363 return vulns;
364 }
365
366 private @NonNull Set<Reference> infoReferences() {
367 return result.getVuln().getInfo().stream()
368 .map(info -> new Reference(info, "info", UrlValidator.getInstance().isValid(info) ? info : null))
369 .collect(Collectors.toSet());
370 }
371
372 @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
373 private class RetireJsVulnerabilityIdentifiers {
374
375 public static final int MAX_NAME_LENGTH = 100;
376
377
378 private final List<String> cveIds;
379 private final Optional<String> ghsaId;
380
381
382 private final Map<String, String> secondaryNameIds;
383 private final Optional<String> summary;
384
385 RetireJsVulnerabilityIdentifiers(Map<String, List<String>> rawIdentifiers) {
386
387 this.cveIds = Optional.ofNullable(rawIdentifiers.get(CVE)).orElse(List.of()).stream()
388 .map(StringUtils::trimToNull)
389 .filter(StringUtils::isNotEmpty)
390 .collect(Collectors.toList());
391
392
393 this.ghsaId = singleItem(rawIdentifiers.get(GITHUB_SECURITY_ADVISORY));
394 this.secondaryNameIds = SECONDARY_NAME_TYPES.stream()
395 .flatMap(type -> singleEntry(type, rawIdentifiers.get(type)).stream())
396 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new));
397
398
399 this.summary = singleItem(rawIdentifiers.get(SUMMARY));
400 }
401
402 List<Vulnerability> toVulnerabilities(KnownCveProvider cveProvider, String severity) {
403
404
405 List<Vulnerability> discoveredVulnerabilities = cveIds.stream()
406 .map(cveId -> cveProvider.optional(cveId).orElseGet(() -> retireJsVulnFor(cveId)))
407 .collect(Collectors.toList());
408
409
410
411 if (discoveredVulnerabilities.isEmpty()) {
412 discoveredVulnerabilities.add(retireJsVulnFor(vulnerabilityName()));
413 }
414
415
416 discoveredVulnerabilities.stream()
417 .filter(vuln -> Vulnerability.Source.RETIREJS.equals(vuln.getSource()))
418 .forEach(vuln -> {
419 vuln.setUnscoredSeverity(severity);
420 summary.ifPresent(vuln::setDescription);
421 vuln.addReferences(references());
422 });
423 return discoveredVulnerabilities;
424 }
425
426 private Vulnerability retireJsVulnFor(String name) {
427 final Vulnerability vuln = new Vulnerability(name);
428 vuln.setSource(Vulnerability.Source.RETIREJS);
429 return vuln;
430 }
431
432
433 private @NonNull String vulnerabilityName() {
434 if (!cveIds.isEmpty()) {
435 throw new IllegalStateException("vulnerability names for RetireJS vulnerabilities should be taken from the CVE ID");
436 }
437
438
439
440 return ghsaId
441 .or(() -> secondaryNameIds.entrySet().stream().findFirst().map(e -> libraryContextualName(e.getKey(), e.getValue())))
442 .or(() -> summary.filter(this::isSmallSingleLine))
443 .orElseGet(() -> "Vulnerability in " + libraryName());
444 }
445
446 private String libraryContextualName(String type, String id) {
447 return String.format("%s %s: %s", libraryName(), type, id);
448 }
449
450 private boolean isSmallSingleLine(String value) {
451 return value.length() <= MAX_NAME_LENGTH && value.lines().limit(2).count() == 1;
452 }
453
454 private Set<Reference> references() {
455 Set<Reference> references = new HashSet<>();
456
457 ghsaId.ifPresent(id -> references.add(new Reference(id, "ghsaId", null)));
458 secondaryNameIds.forEach((type, id) -> references.add(new Reference(id, type, null)));
459 return references;
460 }
461 }
462
463 @FunctionalInterface
464 interface KnownCveProvider {
465 @Nullable Vulnerability lookup(String cve);
466
467 default @NonNull Optional<Vulnerability> optional(String cve) {
468 return Optional.ofNullable(lookup(cve));
469 }
470 }
471
472
473
474
475
476
477
478
479
480
481 interface KnownIdentifierTypes {
482 String CVE = "CVE";
483 String GITHUB_SECURITY_ADVISORY = "githubID";
484 List<String> SECONDARY_NAME_TYPES = List.of("issue", "bug", "PR");
485 String SUMMARY = "summary";
486
487 static @NonNull Optional<String> singleItem(@Nullable List<String> identifiers) {
488 return Optional.ofNullable(identifiers)
489 .flatMap(s -> s.stream().map(StringUtils::trimToNull).filter(Objects::nonNull).findFirst());
490 }
491
492 static @NonNull Optional<Map.Entry<String, String>> singleEntry(@NonNull String type, @Nullable List<String> identifiers) {
493 return singleItem(identifiers).map(id -> Map.entry(type, id));
494 }
495 }
496 }