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