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 jakarta.json.Json;
41 import jakarta.json.JsonException;
42 import jakarta.json.JsonObject;
43 import jakarta.json.JsonReader;
44 import jakarta.json.JsonString;
45 import jakarta.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 String message = "Error initializing the " + a.getName();
200 LOGGER.debug(message, ex);
201 }
202 }
203 return a.isEnabled();
204 }
205 }
206 return false;
207 }
208
209
210
211
212
213
214
215
216 private boolean noLockFileExists(File dependencyFile) {
217 final File lock = new File(dependencyFile.getParentFile(), "package-lock.json");
218 final File shrinkwrap = new File(dependencyFile.getParentFile(), "npm-shrinkwrap.json");
219 final File yarnLock = new File(dependencyFile.getParentFile(), "yarn.lock");
220 return !(lock.isFile() || shrinkwrap.isFile() || yarnLock.isFile());
221 }
222
223 @Override
224 protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
225 final File dependencyFile = dependency.getActualFile();
226 if (!dependencyFile.isFile() || dependencyFile.length() == 0 || !shouldProcess(dependencyFile)) {
227 return;
228 }
229 if (isNodeAuditEnabled(engine)
230 && !(PACKAGE_LOCK_JSON.equals(dependency.getFileName()) || SHRINKWRAP_JSON.equals(dependency.getFileName()))) {
231 engine.removeDependency(dependency);
232 }
233 if (noLockFileExists(dependency.getActualFile())) {
234 LOGGER.warn("No lock file exists - this will result in false negatives; please run `npm install --package-lock`");
235 }
236 final File baseDir = dependencyFile.getParentFile();
237 if (PACKAGE_JSON.equals(dependency.getFileName())) {
238 final File lockfile = new File(baseDir, PACKAGE_LOCK_JSON);
239 final File shrinkwrap = new File(baseDir, SHRINKWRAP_JSON);
240 if (shrinkwrap.exists() || lockfile.exists()) {
241 return;
242 }
243 } else if (PACKAGE_LOCK_JSON.equals(dependency.getFileName())) {
244 final File shrinkwrap = new File(baseDir, SHRINKWRAP_JSON);
245 if (shrinkwrap.exists()) {
246 return;
247 }
248 }
249 final File nodeModules = new File(baseDir, "node_modules");
250 if (!nodeModules.isDirectory()) {
251 LOGGER.warn("Analyzing `{}` - however, the node_modules directory does not exist. "
252 + "Please run `npm install` prior to running dependency-check", dependencyFile);
253 return;
254 }
255
256 try (JsonReader jsonReader = Json.createReader(Files.newInputStream(dependencyFile.toPath()))) {
257 final JsonObject json = jsonReader.readObject();
258 final String parentName = json.getString("name", "");
259 final String parentVersion = json.getString("version", "");
260 if (parentName.isEmpty()) {
261 return;
262 }
263 dependency.setName(parentName);
264 final String parentPackage;
265 if (!parentVersion.isEmpty()) {
266 dependency.setVersion(parentVersion);
267 parentPackage = String.format("%s:%s", parentName, parentVersion);
268 } else {
269 parentPackage = parentName;
270 }
271 processDependencies(json, baseDir, dependencyFile, parentPackage, engine);
272 } catch (JsonException e) {
273 LOGGER.warn("Failed to parse package.json file.", e);
274 } catch (IOException e) {
275 throw new AnalysisException("Problem occurred while reading dependency file.", e);
276 }
277 }
278
279
280
281
282
283
284
285
286
287
288
289 public static boolean shouldSkipDependency(String name, String version, boolean optional, boolean fileExist) {
290
291 if (Objects.nonNull(version) && version.startsWith("npm:")) {
292
293 LOGGER.warn("dependency skipped: package.json contain an alias for {} => {} npm audit doesn't "
294 + "support aliases", name, version.replace("npm:", ""));
295 return true;
296 }
297
298 if (optional && !fileExist) {
299 LOGGER.warn("dependency skipped: node module {} seems optional and not installed", name);
300 return true;
301 }
302
303
304
305 if (Objects.nonNull(version) && (version.startsWith("file:") || version.matches("^[.~]{0,2}/.*"))) {
306 LOGGER.warn("dependency skipped: package.json contain an local node_module for {} seems to be "
307 + "located {} npm audit doesn't support locally referenced modules",
308 name, version);
309 return true;
310 }
311
312
313 if ("".equals(name)) {
314 LOGGER.debug("Empty dependency of package-lock v2+ removed");
315 return true;
316 }
317
318 return false;
319 }
320
321
322
323
324
325
326
327
328
329
330
331 public static boolean shouldSkipDependency(String name, String version) {
332 return shouldSkipDependency(name, version, false, true);
333 }
334
335
336
337
338
339
340
341
342
343
344
345
346
347 private void processDependencies(JsonObject json, File baseDir, File rootFile,
348 String parentPackage, Engine engine) throws AnalysisException {
349 final boolean skipDev = getSettings().getBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_SKIPDEV, false);
350 final JsonObject deps;
351 final File modulesRoot = new File(rootFile.getParentFile(), "node_modules");
352 final int lockJsonVersion = json.containsKey("lockfileVersion") ? json.getInt("lockfileVersion") : 1;
353 if (lockJsonVersion >= 2) {
354 deps = json.getJsonObject("packages");
355 } else if (json.containsKey("dependencies")) {
356 deps = json.getJsonObject("dependencies");
357 } else {
358 deps = null;
359 }
360
361 if (deps != null) {
362 for (Map.Entry<String, JsonValue> entry : deps.entrySet()) {
363 final String pathName = entry.getKey();
364 String name = pathName;
365 File base;
366
367 final int indexOfNodeModule = name.lastIndexOf(NODE_MODULES_DIRNAME + "/");
368 if (indexOfNodeModule >= 0) {
369 name = name.substring(indexOfNodeModule + NODE_MODULES_DIRNAME.length() + 1);
370 base = Paths.get(baseDir.getPath(), pathName).toFile();
371 } else {
372 base = Paths.get(baseDir.getPath(), "node_modules", name).toFile();
373 if (!base.isDirectory()) {
374 final File test = new File(modulesRoot, name);
375 if (test.isDirectory()) {
376 base = test;
377 }
378 }
379 }
380
381 final String version;
382 boolean optional = false;
383 boolean isDev = false;
384
385 final File f = new File(base, PACKAGE_JSON);
386 JsonObject jo = null;
387
388 if (entry.getValue() instanceof JsonObject) {
389 jo = (JsonObject) entry.getValue();
390
391
392
393 if (jo.getBoolean("link", false)) {
394 LOGGER.warn("Skipping `" + name + "` because it is a link dependency");
395 continue;
396 }
397
398 version = jo.getString("version", "");
399 optional = jo.getBoolean("optional", false);
400 isDev = jo.getBoolean("dev", false);
401 } else {
402 version = ((JsonString) entry.getValue()).getString();
403 }
404
405 if ((isDev && skipDev) || shouldSkipDependency(name, version, optional, f.exists())) {
406 continue;
407 }
408
409 if (null != jo && jo.containsKey("dependencies")) {
410 final String subPackageName = String.format("%s/%s:%s", parentPackage, name, version);
411 processDependencies(jo, base, rootFile, subPackageName, engine);
412 }
413
414 String ref = "";
415 final int slash = parentPackage.indexOf("/");
416 if (slash > 0) {
417 ref = parentPackage.substring(slash + 1);
418 }
419 final Dependency child = new Dependency(new File(rootFile + "?" + ref + "/" + name + ":" + version), true);
420 child.addProjectReference(parentPackage);
421 child.setEcosystem(DEPENDENCY_ECOSYSTEM);
422
423 if (f.exists()) {
424 try {
425
426 child.setMd5sum(Checksum.getMD5Checksum(f));
427 child.setSha1sum(Checksum.getSHA1Checksum(f));
428 child.setSha256sum(Checksum.getSHA256Checksum(f));
429 } catch (IOException | NoSuchAlgorithmException ex) {
430 LOGGER.debug("Error setting hashes:" + ex.getMessage(), ex);
431 }
432 try (JsonReader jr = Json.createReader(Files.newInputStream(f.toPath()))) {
433 final JsonObject childJson = jr.readObject();
434 gatherEvidence(childJson, child);
435 } catch (JsonException e) {
436 LOGGER.warn("Failed to parse package.json file from dependency.", e);
437 } catch (IOException e) {
438 throw new AnalysisException("Problem occurred while reading dependency file.", e);
439 }
440 } else {
441 LOGGER.warn("Unable to find node module: {}", f);
442
443 child.setSha1sum(Checksum.getSHA1Checksum(String.format("%s:%s", name, version)));
444 child.setSha256sum(Checksum.getSHA256Checksum(String.format("%s:%s", name, version)));
445 child.setMd5sum(Checksum.getMD5Checksum(String.format("%s:%s", name, version)));
446 child.addEvidence(EvidenceType.VENDOR, rootFile.getName(), "name", name, Confidence.HIGHEST);
447 child.addEvidence(EvidenceType.PRODUCT, rootFile.getName(), "name", name, Confidence.HIGHEST);
448 child.addEvidence(EvidenceType.VERSION, rootFile.getName(), "version", version, Confidence.HIGHEST);
449 child.setName(name);
450 child.setVersion(version);
451 final String packagePath = String.format("%s:%s", name, version);
452 child.setDisplayFileName(packagePath);
453 child.setPackagePath(packagePath);
454 try {
455 final PackageURL purl = PackageURLBuilder.aPackageURL().withType("npm").withName(name).withVersion(version).build();
456 final PurlIdentifier id = new PurlIdentifier(purl, Confidence.HIGHEST);
457 child.addSoftwareIdentifier(id);
458 } catch (MalformedPackageURLException ex) {
459 LOGGER.debug("Unable to build package url for `" + packagePath + "`", ex);
460 }
461 }
462 synchronized (this) {
463 final Dependency existing = findDependency(engine, name, version);
464 if (existing != null) {
465 if (existing.isVirtual()) {
466 DependencyMergingAnalyzer.mergeDependencies(child, existing, null);
467 engine.removeDependency(existing);
468 engine.addDependency(child);
469 } else {
470 DependencyBundlingAnalyzer.mergeDependencies(existing, child, null);
471 }
472 } else {
473 engine.addDependency(child);
474 }
475 }
476 }
477 }
478 }
479 }