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 {
397 dependency.setLicense(json.getJsonObject("license").getString("type"));
398 }
399 }
400 }
401
402
403
404
405
406
407
408 @Override
409 protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
410 if (!isEnabled() || !getFilesMatched()) {
411 this.setEnabled(false);
412 return;
413 }
414 if (searcher == null) {
415 LOGGER.debug("Initializing {}", getName());
416 try {
417 searcher = new NodeAuditSearch(getSettings());
418 } catch (MalformedURLException ex) {
419 setEnabled(false);
420 throw new InitializationException("The configured URL to NPM Audit API is malformed", ex);
421 }
422 try {
423 final Settings settings = engine.getSettings();
424 final boolean nodeEnabled = settings.getBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED);
425 if (!nodeEnabled) {
426 LOGGER.warn("The Node Package Analyzer has been disabled; the resulting report will only "
427 + "contain the known vulnerable dependency - not a bill of materials for the node project.");
428 }
429 } catch (InvalidSettingException ex) {
430 throw new InitializationException("Unable to read configuration settings", ex);
431 }
432 }
433 }
434
435
436
437
438
439
440
441
442
443
444
445
446
447 protected void processResults(final List<Advisory> advisories, Engine engine,
448 Dependency dependency, MultiValuedMap<String, String> dependencyMap)
449 throws CpeValidationException {
450 for (Advisory advisory : advisories) {
451
452 final Vulnerability vuln = new Vulnerability();
453 vuln.setDescription(advisory.getOverview());
454 vuln.setName(String.valueOf(advisory.getGhsaId()));
455 vuln.setUnscoredSeverity(advisory.getSeverity());
456 vuln.setCvssV3(advisory.getCvssV3());
457 vuln.setSource(Vulnerability.Source.NPM);
458 for (String cwe : advisory.getCwes()) {
459 vuln.addCwe(cwe);
460 }
461 if (advisory.getReferences() != null) {
462 final String[] references = advisory.getReferences().split("\\n");
463 for (String reference : references) {
464 if (reference.length() > 3) {
465 String url = reference.substring(2);
466 try {
467 new URL(url);
468 } catch (MalformedURLException ignored) {
469
470 url = null;
471 }
472 vuln.addReference("NPM Advisory reference: ", url == null ? reference : url, url);
473 }
474 }
475 }
476
477
478 final VulnerableSoftwareBuilder builder = new VulnerableSoftwareBuilder();
479 builder.part(Part.APPLICATION).product(advisory.getModuleName().replace(" ", "_"))
480 .version(advisory.getVulnerableVersions().replace(" ", ""));
481 final VulnerableSoftware vs = builder.build();
482 vuln.addVulnerableSoftware(vs);
483
484 String version = advisory.getVersion();
485 if (version == null && dependencyMap.containsKey(advisory.getModuleName())) {
486 version = determineVersionFromMap(advisory.getVulnerableVersions(), dependencyMap.get(advisory.getModuleName()));
487 }
488 final Dependency existing = findDependency(engine, advisory.getModuleName(), version);
489 if (existing == null) {
490 final Dependency nodeModule = createDependency(dependency, advisory.getModuleName(), version, "transitive");
491 nodeModule.addVulnerability(vuln);
492 engine.addDependency(nodeModule);
493 } else {
494 replaceOrAddVulnerability(existing, vuln);
495 }
496 }
497 }
498
499
500
501
502
503
504
505
506 protected void replaceOrAddVulnerability(Dependency dependency, Vulnerability vuln) {
507 final boolean found = vuln.getSource() == Vulnerability.Source.NPM
508 && dependency.getVulnerabilities().stream().anyMatch(existing -> {
509 return existing.getReferences().stream().anyMatch(ref -> {
510 return ref.getName() != null
511 && ref.getName().equals("https://nodesecurity.io/advisories/" + vuln.getName());
512 });
513 });
514 if (!found) {
515 dependency.addVulnerability(vuln);
516 }
517 }
518
519
520
521
522
523
524 protected NodeAuditSearch getSearcher() {
525 return searcher;
526 }
527
528
529
530
531
532
533
534
535
536
537 public static String determineVersionFromMap(String versionRange, Collection<String> availableVersions) {
538 if (availableVersions.size() == 1) {
539 return availableVersions.iterator().next();
540 }
541 for (String v : availableVersions) {
542 try {
543 final Semver version = new Semver(v);
544 if (version.satisfies(versionRange)) {
545 return v;
546 }
547 } catch (SemverException ex) {
548 LOGGER.debug("invalid semver: " + v);
549 }
550 }
551 return availableVersions.iterator().next();
552 }
553 }