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