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.github.packageurl.MalformedPackageURLException;
21 import com.github.packageurl.PackageURL;
22 import com.github.packageurl.PackageURL.StandardTypes;
23 import com.github.packageurl.PackageURLBuilder;
24 import org.semver4j.Semver;
25 import org.semver4j.SemverException;
26 import org.owasp.dependencycheck.Engine;
27 import org.owasp.dependencycheck.data.nodeaudit.Advisory;
28 import org.owasp.dependencycheck.data.nodeaudit.NodeAuditSearch;
29 import org.owasp.dependencycheck.dependency.Confidence;
30 import org.owasp.dependencycheck.dependency.Dependency;
31 import org.owasp.dependencycheck.dependency.Vulnerability;
32 import org.owasp.dependencycheck.dependency.VulnerableSoftware;
33 import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder;
34 import org.owasp.dependencycheck.exception.InitializationException;
35 import org.owasp.dependencycheck.utils.InvalidSettingException;
36 import org.owasp.dependencycheck.utils.Settings;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39 import java.io.File;
40 import java.io.IOException;
41 import java.net.MalformedURLException;
42 import java.net.URL;
43 import java.util.Collection;
44 import java.util.List;
45 import java.util.Map;
46 import javax.annotation.concurrent.ThreadSafe;
47 import jakarta.json.Json;
48 import jakarta.json.JsonArray;
49 import jakarta.json.JsonObject;
50 import jakarta.json.JsonObjectBuilder;
51 import jakarta.json.JsonString;
52 import jakarta.json.JsonValue;
53 import jakarta.json.JsonValue.ValueType;
54 import org.apache.commons.collections4.MultiValuedMap;
55 import org.apache.commons.lang3.StringUtils;
56 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
57 import org.owasp.dependencycheck.analyzer.exception.UnexpectedAnalysisException;
58 import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
59 import org.owasp.dependencycheck.dependency.EvidenceType;
60 import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
61 import org.owasp.dependencycheck.dependency.naming.Identifier;
62 import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
63 import org.owasp.dependencycheck.utils.Checksum;
64 import us.springett.parsers.cpe.exceptions.CpeValidationException;
65 import us.springett.parsers.cpe.values.Part;
66
67
68
69
70
71
72
73 @ThreadSafe
74 public abstract class AbstractNpmAnalyzer extends AbstractFileTypeAnalyzer {
75
76
77
78
79 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractNpmAnalyzer.class);
80
81
82
83
84
85 public static final String NPM_DEPENDENCY_ECOSYSTEM = Ecosystem.NODEJS;
86
87
88
89 private static final String PACKAGE_JSON = "package.json";
90
91
92
93
94 private NodeAuditSearch searcher;
95
96
97
98
99
100
101
102
103 @Override
104 public boolean accept(File pathname) {
105 boolean accept = super.accept(pathname);
106 if (accept) {
107 try {
108 accept = shouldProcess(pathname);
109 } catch (AnalysisException ex) {
110 throw new UnexpectedAnalysisException(ex.getMessage(), ex.getCause());
111 }
112 }
113 return accept;
114 }
115
116
117
118
119
120
121
122
123
124
125
126 public static boolean shouldProcess(File pathname) throws AnalysisException {
127 try {
128
129 final String canonicalPath = pathname.getCanonicalPath();
130 if (canonicalPath.contains(File.separator + "node_modules" + File.separator)
131 || canonicalPath.contains(File.separator + "bower_components" + File.separator)) {
132 LOGGER.debug("Skipping analysis of node/bower module: {}", canonicalPath);
133 return false;
134 }
135 } catch (IOException ex) {
136 throw new AnalysisException("Unable to process dependency", ex);
137 }
138 return true;
139 }
140
141
142
143
144
145
146
147
148
149
150 protected Dependency createDependency(Dependency dependency, String name, String version, String scope) {
151 final Dependency nodeModule = new Dependency(new File(dependency.getActualFile() + "?" + name), true);
152 nodeModule.setEcosystem(NPM_DEPENDENCY_ECOSYSTEM);
153
154 nodeModule.setSha1sum(Checksum.getSHA1Checksum(String.format("%s:%s", name, version)));
155 nodeModule.setSha256sum(Checksum.getSHA256Checksum(String.format("%s:%s", name, version)));
156 nodeModule.setMd5sum(Checksum.getMD5Checksum(String.format("%s:%s", name, version)));
157 nodeModule.addEvidence(EvidenceType.PRODUCT, "package.json", "name", name, Confidence.HIGHEST);
158 nodeModule.addEvidence(EvidenceType.VENDOR, "package.json", "name", name, Confidence.HIGH);
159 if (!StringUtils.isBlank(version)) {
160 nodeModule.addEvidence(EvidenceType.VERSION, "package.json", "version", version, Confidence.HIGHEST);
161 nodeModule.setVersion(version);
162 }
163 if (dependency.getName() != null) {
164 nodeModule.addProjectReference(dependency.getName() + ": " + scope);
165 } else {
166 nodeModule.addProjectReference(dependency.getDisplayFileName() + ": " + scope);
167 }
168 nodeModule.setName(name);
169
170
171
172 Identifier id;
173 try {
174 final PackageURL purl = PackageURLBuilder.aPackageURL().withType(StandardTypes.NPM)
175 .withName(name).withVersion(version).build();
176 id = new PurlIdentifier(purl, Confidence.HIGHEST);
177 } catch (MalformedPackageURLException ex) {
178 LOGGER.debug("Unable to generate Purl - using a generic identifier instead " + ex.getMessage());
179 id = new GenericIdentifier(String.format("npm:%s@%s", dependency.getName(), version), Confidence.HIGHEST);
180 }
181 nodeModule.addSoftwareIdentifier(id);
182 return nodeModule;
183 }
184
185
186
187
188
189
190
191
192
193
194 protected void processPackage(Engine engine, Dependency dependency, JsonArray jsonArray, String depType) {
195 final JsonObjectBuilder builder = Json.createObjectBuilder();
196 jsonArray.getValuesAs(JsonString.class).forEach((str) -> builder.add(str.toString(), ""));
197 final JsonObject jsonObject = builder.build();
198 processPackage(engine, dependency, jsonObject, depType);
199 }
200
201
202
203
204
205
206
207
208
209
210 protected void processPackage(Engine engine, Dependency dependency, JsonObject jsonObject, String depType) {
211 for (int i = 0; i < jsonObject.size(); i++) {
212 jsonObject.forEach((name, value) -> {
213 String version = "";
214 if (value != null && value.getValueType() == ValueType.STRING) {
215 version = ((JsonString) value).getString();
216 }
217 final Dependency existing = findDependency(engine, name, version);
218 if (existing == null) {
219 final Dependency nodeModule = createDependency(dependency, name, version, depType);
220 engine.addDependency(nodeModule);
221 } else {
222 existing.addProjectReference(dependency.getName() + ": " + depType);
223 }
224 });
225 }
226 }
227
228
229
230
231
232
233
234
235
236
237
238 private static String addToEvidence(Dependency dep, EvidenceType t, JsonObject json, String key) {
239 String evidenceStr = null;
240 if (json.containsKey(key)) {
241 final JsonValue value = json.get(key);
242 if (value instanceof JsonString) {
243 evidenceStr = ((JsonString) value).getString();
244 dep.addEvidence(t, PACKAGE_JSON, key, evidenceStr, Confidence.HIGHEST);
245 } else if (value instanceof JsonObject) {
246 final JsonObject jsonObject = (JsonObject) value;
247 for (final Map.Entry<String, JsonValue> entry : jsonObject.entrySet()) {
248 final String property = entry.getKey();
249 final JsonValue subValue = entry.getValue();
250 if (subValue instanceof JsonString) {
251 evidenceStr = ((JsonString) subValue).getString();
252 dep.addEvidence(t, PACKAGE_JSON,
253 String.format("%s.%s", key, property),
254 evidenceStr,
255 Confidence.HIGHEST);
256 } else {
257 LOGGER.warn("JSON sub-value not string as expected: {}", subValue);
258 }
259 }
260 } else if (value instanceof JsonArray) {
261 final JsonArray jsonArray = (JsonArray) value;
262 jsonArray.forEach(entry -> {
263 if (entry instanceof JsonObject) {
264 ((JsonObject) entry).keySet().forEach(item -> {
265 final JsonValue v = ((JsonObject) entry).get(item);
266 if (v instanceof JsonString) {
267 final String eStr = ((JsonString) v).getString();
268 dep.addEvidence(t, PACKAGE_JSON,
269 String.format("%s.%s", key, item),
270 eStr,
271 Confidence.HIGHEST);
272 }
273 });
274 }
275 });
276 } else {
277 LOGGER.warn("JSON value not string or JSON object as expected: {}", value);
278 }
279 }
280 return evidenceStr;
281 }
282
283
284
285
286
287
288
289
290
291
292 protected Dependency findDependency(Engine engine, String name, String version) {
293 for (Dependency d : engine.getDependencies()) {
294 if (NPM_DEPENDENCY_ECOSYSTEM.equals(d.getEcosystem()) && name.equals(d.getName()) && version != null && d.getVersion() != null) {
295 final String dependencyVersion = d.getVersion();
296 if (DependencyBundlingAnalyzer.npmVersionsMatch(version, dependencyVersion)) {
297 return d;
298 }
299 }
300 }
301 return null;
302 }
303
304
305
306
307
308
309
310 public void gatherEvidence(final JsonObject json, Dependency dependency) {
311 String displayName = null;
312 if (json.containsKey("name")) {
313 final Object value = json.get("name");
314 if (value instanceof JsonString) {
315 final String valueString = ((JsonString) value).getString();
316 displayName = valueString;
317 dependency.setName(valueString);
318 dependency.setPackagePath(valueString);
319 dependency.addEvidence(EvidenceType.PRODUCT, PACKAGE_JSON, "name", valueString, Confidence.HIGHEST);
320 dependency.addEvidence(EvidenceType.VENDOR, PACKAGE_JSON, "name", valueString, Confidence.HIGHEST);
321 dependency.addEvidence(EvidenceType.VENDOR, PACKAGE_JSON, "name", valueString + "_project", Confidence.HIGHEST);
322 } else {
323 LOGGER.warn("JSON value not string as expected: {}", value);
324 }
325 }
326
327 final String desc = addToEvidence(dependency, EvidenceType.VENDOR, json, "description");
328 dependency.setDescription(desc);
329 String vendor = addToEvidence(dependency, EvidenceType.VENDOR, json, "author");
330 if (vendor == null) {
331 vendor = addToEvidence(dependency, EvidenceType.VENDOR, json, "maintainers");
332 } else {
333 addToEvidence(dependency, EvidenceType.VENDOR, json, "maintainers");
334 }
335 addToEvidence(dependency, EvidenceType.VENDOR, json, "homepage");
336 addToEvidence(dependency, EvidenceType.VENDOR, json, "bugs");
337
338 final String version = addToEvidence(dependency, EvidenceType.VERSION, json, "version");
339 if (version != null) {
340 displayName = String.format("%s:%s", displayName, version);
341 dependency.setVersion(version);
342 dependency.setPackagePath(displayName);
343 Identifier id;
344 try {
345 final PackageURL purl = PackageURLBuilder.aPackageURL()
346 .withType(StandardTypes.NPM).withName(dependency.getName()).withVersion(version).build();
347 id = new PurlIdentifier(purl, Confidence.HIGHEST);
348 } catch (MalformedPackageURLException ex) {
349 LOGGER.debug("Unable to generate Purl - using a generic identifier instead " + ex.getMessage());
350 id = new GenericIdentifier(String.format("npm:%s:%s", dependency.getName(), version), Confidence.HIGHEST);
351 }
352 dependency.addSoftwareIdentifier(id);
353 }
354 if (displayName != null) {
355 dependency.setDisplayFileName(displayName);
356 dependency.setPackagePath(displayName);
357 } else {
358 LOGGER.warn("Unable to determine package name or version for {}", dependency.getActualFilePath());
359 if (vendor != null && !vendor.isEmpty()) {
360 dependency.setDisplayFileName(String.format("%s package.json", vendor));
361 }
362 }
363
364 if (json.containsKey("license")) {
365 final Object value = json.get("license");
366 if (value instanceof JsonString) {
367 dependency.setLicense(json.getString("license"));
368 } else if (value instanceof JsonArray) {
369 final JsonArray array = (JsonArray) value;
370 final StringBuilder sb = new StringBuilder();
371 boolean addComma = false;
372 for (int x = 0; x < array.size(); x++) {
373 if (!array.isNull(x)) {
374 if (addComma) {
375 sb.append(", ");
376 } else {
377 addComma = true;
378 }
379 if (ValueType.STRING == array.get(x).getValueType()) {
380 sb.append(array.getString(x));
381 } else {
382 final JsonObject lo = array.getJsonObject(x);
383 if (lo.containsKey("type") && !lo.isNull("type")
384 && lo.containsKey("url") && !lo.isNull("url")) {
385 final String license = String.format("%s (%s)", lo.getString("type"), lo.getString("url"));
386 sb.append(license);
387 } else if (lo.containsKey("type") && !lo.isNull("type")) {
388 sb.append(lo.getString("type"));
389 } else if (lo.containsKey("url") && !lo.isNull("url")) {
390 sb.append(lo.getString("url"));
391 }
392 }
393 }
394 }
395 dependency.setLicense(sb.toString());
396 } else if (value instanceof JsonObject) {
397 final JsonObject object = (JsonObject) value;
398 if (object.containsKey("type") && !object.isNull("type")) {
399 dependency.setLicense(object.getString("type"));
400 }
401 }
402 }
403 }
404
405
406
407
408
409
410
411 @Override
412 protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
413 if (!isEnabled() || !getFilesMatched()) {
414 this.setEnabled(false);
415 return;
416 }
417 if (searcher == null) {
418 LOGGER.debug("Initializing {}", getName());
419 try {
420 searcher = new NodeAuditSearch(getSettings());
421 } catch (MalformedURLException ex) {
422 setEnabled(false);
423 throw new InitializationException("The configured URL to NPM Audit API is malformed", ex);
424 }
425 try {
426 final Settings settings = engine.getSettings();
427 final boolean nodeEnabled = settings.getBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED);
428 if (!nodeEnabled) {
429 LOGGER.warn("The Node Package Analyzer has been disabled; the resulting report will only "
430 + "contain the known vulnerable dependency - not a bill of materials for the node project.");
431 }
432 } catch (InvalidSettingException ex) {
433 throw new InitializationException("Unable to read configuration settings", ex);
434 }
435 }
436 }
437
438
439
440
441
442
443
444
445
446
447
448
449
450 protected void processResults(final List<Advisory> advisories, Engine engine,
451 Dependency dependency, MultiValuedMap<String, String> dependencyMap)
452 throws CpeValidationException {
453 for (Advisory advisory : advisories) {
454
455 final Vulnerability vuln = new Vulnerability();
456 vuln.setDescription(advisory.getOverview());
457 vuln.setName(String.valueOf(advisory.getGhsaId()));
458 vuln.setUnscoredSeverity(advisory.getSeverity());
459 vuln.setCvssV3(advisory.getCvssV3());
460 vuln.setSource(Vulnerability.Source.NPM);
461 for (String cwe : advisory.getCwes()) {
462 vuln.addCwe(cwe);
463 }
464 if (advisory.getReferences() != null) {
465 final String[] references = advisory.getReferences().split("\\n");
466 for (String reference : references) {
467 if (reference.length() > 3) {
468 String url = reference.substring(2);
469 try {
470 new URL(url);
471 } catch (MalformedURLException ignored) {
472
473 url = null;
474 }
475 vuln.addReference("NPM Advisory reference: ", url == null ? reference : url, url);
476 }
477 }
478 }
479
480
481 final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder();
482 builder.part(Part.APPLICATION).product(advisory.getModuleName().replace(" ", "_"))
483 .version(advisory.getVulnerableVersions().replace(" ", ""));
484 final VulnerableSoftware vs = builder.build();
485 vuln.addVulnerableSoftware(vs);
486
487 String version = advisory.getVersion();
488 if (version == null && dependencyMap.containsKey(advisory.getModuleName())) {
489 version = determineVersionFromMap(advisory.getVulnerableVersions(), dependencyMap.get(advisory.getModuleName()));
490 }
491 final Dependency existing = findDependency(engine, advisory.getModuleName(), version);
492 if (existing == null) {
493 final Dependency nodeModule = createDependency(dependency, advisory.getModuleName(), version, "transitive");
494 nodeModule.addVulnerability(vuln);
495 engine.addDependency(nodeModule);
496 } else {
497 replaceOrAddVulnerability(existing, vuln);
498 }
499 }
500 }
501
502
503
504
505
506
507
508
509 protected void replaceOrAddVulnerability(Dependency dependency, Vulnerability vuln) {
510 final boolean found = vuln.getSource() == Vulnerability.Source.NPM
511 && dependency.getVulnerabilities().stream().anyMatch(existing -> {
512 return existing.getReferences().stream().anyMatch(ref -> {
513 return ref.getName() != null
514 && ref.getName().equals("https://nodesecurity.io/advisories/" + vuln.getName());
515 });
516 });
517 if (!found) {
518 dependency.addVulnerability(vuln);
519 }
520 }
521
522
523
524
525
526
527 protected NodeAuditSearch getSearcher() {
528 return searcher;
529 }
530
531
532
533
534
535
536
537
538
539
540 public static String determineVersionFromMap(String versionRange, Collection<String> availableVersions) {
541 if (availableVersions.size() == 1) {
542 return availableVersions.iterator().next();
543 }
544 for (String v : availableVersions) {
545 try {
546 final Semver version = new Semver(v);
547 if (version.satisfies(versionRange)) {
548 return v;
549 }
550 } catch (SemverException ex) {
551 LOGGER.debug("invalid semver: " + v);
552 }
553 }
554 return availableVersions.iterator().next();
555 }
556 }