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.PackageURLBuilder;
23 import org.owasp.dependencycheck.Engine;
24 import org.owasp.dependencycheck.Engine.Mode;
25 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
26 import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
27 import org.owasp.dependencycheck.dependency.Confidence;
28 import org.owasp.dependencycheck.dependency.Dependency;
29 import org.owasp.dependencycheck.dependency.EvidenceType;
30 import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
31 import org.owasp.dependencycheck.exception.InitializationException;
32 import org.owasp.dependencycheck.utils.Checksum;
33 import org.owasp.dependencycheck.utils.FileFilterBuilder;
34 import org.owasp.dependencycheck.utils.InvalidSettingException;
35 import org.owasp.dependencycheck.utils.Settings;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import javax.annotation.concurrent.ThreadSafe;
40 import javax.json.Json;
41 import javax.json.JsonException;
42 import javax.json.JsonObject;
43 import javax.json.JsonReader;
44 import javax.json.JsonString;
45 import javax.json.JsonValue;
46 import java.io.File;
47 import java.io.FileFilter;
48 import java.io.IOException;
49 import java.nio.file.Files;
50 import java.nio.file.Paths;
51 import java.security.NoSuchAlgorithmException;
52 import java.util.Arrays;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Objects;
56
57
58
59
60
61
62
63 @ThreadSafe
64 public class NodePackageAnalyzer extends AbstractNpmAnalyzer {
65
66
67
68
69 private static final Logger LOGGER = LoggerFactory.getLogger(NodePackageAnalyzer.class);
70
71
72
73
74 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.NODEJS;
75
76
77
78 private static final String ANALYZER_NAME = "Node.js Package Analyzer";
79
80
81
82 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
83
84
85
86 public static final String PACKAGE_JSON = "package.json";
87
88
89
90 public static final String PACKAGE_LOCK_JSON = "package-lock.json";
91
92
93
94 public static final String SHRINKWRAP_JSON = "npm-shrinkwrap.json";
95
96
97
98 public static final String NODE_MODULES_DIRNAME = "node_modules";
99
100
101
102
103 private static final FileFilter PACKAGE_JSON_FILTER = FileFilterBuilder.newInstance()
104 .addFilenames(PACKAGE_JSON, PACKAGE_LOCK_JSON, SHRINKWRAP_JSON).build();
105
106
107
108
109
110
111 @Override
112 protected FileFilter getFileFilter() {
113 return PACKAGE_JSON_FILTER;
114 }
115
116
117
118
119
120
121
122
123 @Override
124 protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
125 if (engine.getMode() != Mode.EVIDENCE_COLLECTION) {
126 try {
127 final Settings settings = engine.getSettings();
128 final String[] tmp = settings.getArray(Settings.KEYS.ECOSYSTEM_SKIP_CPEANALYZER);
129 if (tmp != null) {
130 final List<String> skipEcosystems = Arrays.asList(tmp);
131 if (skipEcosystems.contains(DEPENDENCY_ECOSYSTEM)
132 && !settings.getBoolean(Settings.KEYS.ANALYZER_OSSINDEX_ENABLED)) {
133 if (!settings.getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_ENABLED)) {
134 final String msg = "Invalid Configuration: enabling the Node Package Analyzer without "
135 + "using the Node Audit Analyzer or OSS Index Analyzer is not supported.";
136 throw new InitializationException(msg);
137 } else if (!isNodeAuditEnabled(engine)) {
138 final String msg = "Missing package.lock or npm-shrinkwrap.lock file: Unable to scan a node "
139 + "project without a package-lock.json or npm-shrinkwrap.json.";
140 throw new InitializationException(msg);
141 }
142 } else if (skipEcosystems.contains(DEPENDENCY_ECOSYSTEM)
143 && !settings.getBoolean(Settings.KEYS.ANALYZER_NODE_AUDIT_ENABLED)) {
144 LOGGER.warn("Using only the OSS Index Analyzer with Node.js can result in many false positives "
145 + "- please enable the Node Audit Analyzer.");
146 }
147 }
148 } catch (InvalidSettingException ex) {
149 throw new InitializationException("Unable to read configuration settings", ex);
150 }
151 }
152 }
153
154
155
156
157
158
159 @Override
160 public String getName() {
161 return ANALYZER_NAME;
162 }
163
164
165
166
167
168
169 @Override
170 public AnalysisPhase getAnalysisPhase() {
171 return ANALYSIS_PHASE;
172 }
173
174
175
176
177
178
179
180 @Override
181 protected String getAnalyzerEnabledSettingKey() {
182 return Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED;
183 }
184
185
186
187
188
189
190
191
192 private boolean isNodeAuditEnabled(Engine engine) {
193 for (Analyzer a : engine.getAnalyzers()) {
194 if (a instanceof NodeAuditAnalyzer || a instanceof YarnAuditAnalyzer || a instanceof PnpmAuditAnalyzer) {
195 if (a.isEnabled()) {
196 try {
197 ((AbstractNpmAnalyzer) a).prepareFileTypeAnalyzer(engine);
198 } catch (InitializationException ex) {
199 LOGGER.debug("Error initializing the {}", a.getName());
200 }
201 }
202 return a.isEnabled();
203 }
204 }
205 return false;
206 }
207
208
209
210
211
212
213
214
215 private boolean noLockFileExists(File dependencyFile) {
216 final File lock = new File(dependencyFile.getParentFile(), "package-lock.json");
217 final File shrinkwrap = new File(dependencyFile.getParentFile(), "npm-shrinkwrap.json");
218 final File yarnLock = new File(dependencyFile.getParentFile(), "yarn.lock");
219 return !(lock.isFile() || shrinkwrap.isFile() || yarnLock.isFile());
220 }
221
222 @Override
223 protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
224 final File dependencyFile = dependency.getActualFile();
225 if (!dependencyFile.isFile() || dependencyFile.length() == 0 || !shouldProcess(dependencyFile)) {
226 return;
227 }
228 if (isNodeAuditEnabled(engine)
229 && !(PACKAGE_LOCK_JSON.equals(dependency.getFileName()) || SHRINKWRAP_JSON.equals(dependency.getFileName()))) {
230 engine.removeDependency(dependency);
231 }
232 if (noLockFileExists(dependency.getActualFile())) {
233 LOGGER.warn("No lock file exists - this will result in false negatives; please run `npm install --package-lock`");
234 }
235 final File baseDir = dependencyFile.getParentFile();
236 if (PACKAGE_JSON.equals(dependency.getFileName())) {
237 final File lockfile = new File(baseDir, PACKAGE_LOCK_JSON);
238 final File shrinkwrap = new File(baseDir, SHRINKWRAP_JSON);
239 if (shrinkwrap.exists() || lockfile.exists()) {
240 return;
241 }
242 } else if (PACKAGE_LOCK_JSON.equals(dependency.getFileName())) {
243 final File shrinkwrap = new File(baseDir, SHRINKWRAP_JSON);
244 if (shrinkwrap.exists()) {
245 return;
246 }
247 }
248 final File nodeModules = new File(baseDir, "node_modules");
249 if (!nodeModules.isDirectory()) {
250 LOGGER.warn("Analyzing `{}` - however, the node_modules directory does not exist. "
251 + "Please run `npm install` prior to running dependency-check", dependencyFile);
252 return;
253 }
254
255 try (JsonReader jsonReader = Json.createReader(Files.newInputStream(dependencyFile.toPath()))) {
256 final JsonObject json = jsonReader.readObject();
257 final String parentName = json.getString("name", "");
258 final String parentVersion = json.getString("version", "");
259 if (parentName.isEmpty()) {
260 return;
261 }
262 dependency.setName(parentName);
263 final String parentPackage;
264 if (!parentVersion.isEmpty()) {
265 dependency.setVersion(parentVersion);
266 parentPackage = String.format("%s:%s", parentName, parentVersion);
267 } else {
268 parentPackage = parentName;
269 }
270 processDependencies(json, baseDir, dependencyFile, parentPackage, engine);
271 } catch (JsonException e) {
272 LOGGER.warn("Failed to parse package.json file.", e);
273 } catch (IOException e) {
274 throw new AnalysisException("Problem occurred while reading dependency file.", e);
275 }
276 }
277
278
279
280
281
282
283
284
285
286
287
288 public static boolean shouldSkipDependency(String name, String version, boolean optional, boolean fileExist) {
289
290 if (Objects.nonNull(version) && version.startsWith("npm:")) {
291
292 LOGGER.warn("dependency skipped: package.json contain an alias for {} => {} npm audit doesn't "
293 + "support aliases", name, version.replace("npm:", ""));
294 return true;
295 }
296
297 if (optional && !fileExist) {
298 LOGGER.warn("dependency skipped: node module {} seems optional and not installed", name);
299 return true;
300 }
301
302
303
304 if (Objects.nonNull(version) && (version.startsWith("file:") || version.matches("^[.~]{0,2}/.*"))) {
305 LOGGER.warn("dependency skipped: package.json contain an local node_module for {} seems to be "
306 + "located {} npm audit doesn't support locally referenced modules",
307 name, version);
308 return true;
309 }
310
311
312 if ("".equals(name)) {
313 LOGGER.debug("Empty dependency of package-lock v2+ removed");
314 return true;
315 }
316
317 return false;
318 }
319
320
321
322
323
324
325
326
327
328
329
330 public static boolean shouldSkipDependency(String name, String version) {
331 return shouldSkipDependency(name, version, false, true);
332 }
333
334
335
336
337
338
339
340
341
342
343
344
345
346 private void processDependencies(JsonObject json, File baseDir, File rootFile,
347 String parentPackage, Engine engine) throws AnalysisException {
348 final boolean skipDev = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_SKIPDEV, false);
349 final JsonObject deps;
350 final File modulesRoot = new File(rootFile.getParentFile(), "node_modules");
351 final int lockJsonVersion = json.containsKey("lockfileVersion") ? json.getInt("lockfileVersion") : 1;
352 if (lockJsonVersion >= 2) {
353 deps = json.getJsonObject("packages");
354 } else if (json.containsKey("dependencies")) {
355 deps = json.getJsonObject("dependencies");
356 } else {
357 deps = null;
358 }
359
360 if (deps != null) {
361 for (Map.Entry<String, JsonValue> entry : deps.entrySet()) {
362 final String pathName = entry.getKey();
363 String name = pathName;
364 File base;
365
366 final int indexOfNodeModule = name.lastIndexOf(NODE_MODULES_DIRNAME + "/");
367 if (indexOfNodeModule >= 0) {
368 name = name.substring(indexOfNodeModule + NODE_MODULES_DIRNAME.length() + 1);
369 base = Paths.get(baseDir.getPath(), pathName).toFile();
370 } else {
371 base = Paths.get(baseDir.getPath(), "node_modules", name).toFile();
372 if (!base.isDirectory()) {
373 final File test = new File(modulesRoot, name);
374 if (test.isDirectory()) {
375 base = test;
376 }
377 }
378 }
379
380 final String version;
381 boolean optional = false;
382 boolean isDev = false;
383
384 final File f = new File(base, PACKAGE_JSON);
385 JsonObject jo = null;
386
387 if (entry.getValue() instanceof JsonObject) {
388 jo = (JsonObject) entry.getValue();
389
390
391
392 if (jo.getBoolean("link", false)) {
393 LOGGER.warn("Skipping `" + name + "` because it is a link dependency");
394 continue;
395 }
396
397 version = jo.getString("version", "");
398 optional = jo.getBoolean("optional", false);
399 isDev = jo.getBoolean("dev", false);
400 } else {
401 version = ((JsonString) entry.getValue()).getString();
402 }
403
404 if ((isDev && skipDev) || shouldSkipDependency(name, version, optional, f.exists())) {
405 continue;
406 }
407
408 if (null != jo && jo.containsKey("dependencies")) {
409 final String subPackageName = String.format("%s/%s:%s", parentPackage, name, version);
410 processDependencies(jo, base, rootFile, subPackageName, engine);
411 }
412
413 String ref = "";
414 final int slash = parentPackage.indexOf("/");
415 if (slash > 0) {
416 ref = parentPackage.substring(slash + 1);
417 }
418 final Dependency child = new Dependency(new File(rootFile + "?" + ref + "/" + name + ":" + version), true);
419 child.addProjectReference(parentPackage);
420 child.setEcosystem(DEPENDENCY_ECOSYSTEM);
421
422 if (f.exists()) {
423 try {
424
425 child.setMd5sum(Checksum.getMD5Checksum(f));
426 child.setSha1sum(Checksum.getSHA1Checksum(f));
427 child.setSha256sum(Checksum.getSHA256Checksum(f));
428 } catch (IOException | NoSuchAlgorithmException ex) {
429 LOGGER.debug("Error setting hashes:" + ex.getMessage(), ex);
430 }
431 try (JsonReader jr = Json.createReader(Files.newInputStream(f.toPath()))) {
432 final JsonObject childJson = jr.readObject();
433 gatherEvidence(childJson, child);
434 } catch (JsonException e) {
435 LOGGER.warn("Failed to parse package.json file from dependency.", e);
436 } catch (IOException e) {
437 throw new AnalysisException("Problem occurred while reading dependency file.", e);
438 }
439 } else {
440 LOGGER.warn("Unable to find node module: {}", f);
441
442 child.setSha1sum(Checksum.getSHA1Checksum(String.format("%s:%s", name, version)));
443 child.setSha256sum(Checksum.getSHA256Checksum(String.format("%s:%s", name, version)));
444 child.setMd5sum(Checksum.getMD5Checksum(String.format("%s:%s", name, version)));
445 child.addEvidence(EvidenceType.VENDOR, rootFile.getName(), "name", name, Confidence.HIGHEST);
446 child.addEvidence(EvidenceType.PRODUCT, rootFile.getName(), "name", name, Confidence.HIGHEST);
447 child.addEvidence(EvidenceType.VERSION, rootFile.getName(), "version", version, Confidence.HIGHEST);
448 child.setName(name);
449 child.setVersion(version);
450 final String packagePath = String.format("%s:%s", name, version);
451 child.setDisplayFileName(packagePath);
452 child.setPackagePath(packagePath);
453 try {
454 final PackageURL purl = PackageURLBuilder.aPackageURL().withType("npm").withName(name).withVersion(version).build();
455 final PurlIdentifier id = new PurlIdentifier(purl, Confidence.HIGHEST);
456 child.addSoftwareIdentifier(id);
457 } catch (MalformedPackageURLException ex) {
458 LOGGER.debug("Unable to build package url for `" + packagePath + "`", ex);
459 }
460 }
461 synchronized (this) {
462 final Dependency existing = findDependency(engine, name, version);
463 if (existing != null) {
464 if (existing.isVirtual()) {
465 DependencyMergingAnalyzer.mergeDependencies(child, existing, null);
466 engine.removeDependency(existing);
467 engine.addDependency(child);
468 } else {
469 DependencyBundlingAnalyzer.mergeDependencies(existing, child, null);
470 }
471 } else {
472 engine.addDependency(child);
473 }
474 }
475 }
476 }
477 }
478 }