@nodesecure/scanner 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/sca
11
11
 
12
12
  ## Requirements
13
13
 
14
- - [Node.js](https://nodejs.org/en/) version 16 or higher
14
+ - [Node.js](https://nodejs.org/en/) version 18 or higher
15
15
 
16
16
  ## Getting Started
17
17
 
@@ -0,0 +1,6 @@
1
+ const scanner = {
2
+ disable_scarf: "This dependency could collect data against your consent so think to disable it with the env var: SCARF_ANALYTICS",
3
+ keylogging: "This dependency can retrieve your keyboard and mouse inputs. It can be used for 'keylogging' attacks/malwares."
4
+ };
5
+
6
+ export default { scanner };
package/i18n/french.js ADDED
@@ -0,0 +1,7 @@
1
+ const scanner = {
2
+ disable_scarf: "Cette dépendance peut récolter des données contre votre volonté, pensez donc à la désactiver en fournissant la variable d'environnement SCARF_ANALYTICS",
3
+ keylogging: "Cette dépendance peut obtenir vos entrées clavier ou de souris. Cette dépendance peut être utilisée en tant que 'keylogging' attacks/malwares."
4
+ };
5
+
6
+ export default { scanner };
7
+
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@nodesecure/scanner",
3
- "version": "5.0.0",
3
+ "version": "5.1.0",
4
4
  "description": "A package API to run a static analysis of your module's dependencies.",
5
5
  "exports": "./index.js",
6
6
  "engines": {
7
- "node": ">=16"
7
+ "node": ">=18"
8
8
  },
9
9
  "scripts": {
10
10
  "lint": "eslint src test",
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "src",
19
+ "i18n",
19
20
  "types",
20
21
  "index.js",
21
22
  "index.d.ts"
@@ -49,36 +50,36 @@
49
50
  },
50
51
  "homepage": "https://github.com/NodeSecure/scanner#readme",
51
52
  "devDependencies": {
52
- "@nodesecure/eslint-config": "^1.7.0",
53
+ "@nodesecure/eslint-config": "^1.8.0",
53
54
  "@slimio/is": "^2.0.0",
54
- "@types/node": "^20.4.5",
55
- "c8": "^7.13.0",
56
- "dotenv": "^16.0.3",
55
+ "@types/node": "^20.10.0",
56
+ "c8": "^8.0.1",
57
+ "dotenv": "^16.3.1",
57
58
  "eslint": "^8.37.0",
58
59
  "get-folder-size": "^4.0.0",
59
- "glob": "^10.3.4",
60
+ "glob": "^10.3.10",
60
61
  "pkg-ok": "^3.0.0",
61
- "sinon": "^15.0.3",
62
+ "sinon": "^17.0.1",
62
63
  "snap-shot-core": "^10.2.4"
63
64
  },
64
65
  "dependencies": {
65
66
  "@nodesecure/authors": "^1.0.2",
66
67
  "@nodesecure/flags": "^2.4.0",
67
68
  "@nodesecure/fs-walk": "^1.0.0",
68
- "@nodesecure/i18n": "^3.3.0",
69
- "@nodesecure/js-x-ray": "^6.0.1",
70
- "@nodesecure/npm-registry-sdk": "^1.5.2",
69
+ "@nodesecure/i18n": "^3.4.0",
70
+ "@nodesecure/js-x-ray": "^6.2.0",
71
+ "@nodesecure/npm-registry-sdk": "^1.6.1",
71
72
  "@nodesecure/ntlp": "^2.2.1",
72
73
  "@nodesecure/vuln": "^1.7.0",
73
74
  "@npm/types": "^1.0.2",
74
- "@npmcli/arborist": "^6.2.6",
75
+ "@npmcli/arborist": "^7.2.1",
75
76
  "@slimio/lock": "^1.0.0",
76
77
  "builtins": "^5.0.1",
77
- "combine-async-iterators": "^2.0.1",
78
- "itertools": "^2.1.1",
78
+ "combine-async-iterators": "^2.1.0",
79
+ "itertools": "^2.1.2",
79
80
  "lodash.difference": "^4.5.0",
80
- "pacote": "^15.1.1",
81
- "semver": "^7.3.8"
81
+ "pacote": "^17.0.4",
82
+ "semver": "^7.5.4"
82
83
  },
83
84
  "type": "module"
84
85
  }
@@ -13,11 +13,15 @@ export default class Dependency {
13
13
  this.alias = {};
14
14
 
15
15
  if (parent !== null) {
16
- parent.dependencyCount++;
16
+ parent.addChildren();
17
17
  }
18
18
  this.#parent = parent;
19
19
  }
20
20
 
21
+ addChildren() {
22
+ this.dependencyCount += 1;
23
+ }
24
+
21
25
  get fullName() {
22
26
  return `${this.name} ${this.version}`;
23
27
  }
package/src/depWalker.js CHANGED
@@ -1,366 +1,381 @@
1
- // Import Node.js Dependencies
2
- import path from "path";
3
- import { readFileSync, promises as fs } from "fs";
4
- import timers from "timers/promises";
5
- import os from "os";
6
-
7
- // Import Third-party Dependencies
8
- import combineAsyncIterators from "combine-async-iterators";
9
- import * as iter from "itertools";
10
- import pacote from "pacote";
11
- import Arborist from "@npmcli/arborist";
12
- import Lock from "@slimio/lock";
13
- import * as vuln from "@nodesecure/vuln";
14
- import { ScannerLoggerEvents } from "./constants.js";
15
-
16
- // Import Internal Dependencies
17
- import {
18
- mergeDependencies, getCleanDependencyName, getDependenciesWarnings, addMissingVersionFlags, isGitDependency,
19
- NPM_TOKEN
20
- } from "./utils/index.js";
21
- import { scanDirOrArchive } from "./tarball.js";
22
- import { packageMetadata } from "./npmRegistry.js";
23
- import Dependency from "./class/dependency.class.js";
24
- import Logger from "./class/logger.class.js";
25
-
26
- const { version: packageVersion } = JSON.parse(
27
- readFileSync(
28
- new URL(path.join("..", "package.json"), import.meta.url)
29
- )
30
- );
31
-
32
- export async function* searchDeepDependencies(packageName, gitURL, options) {
33
- const { exclude, currDepth = 0, parent, maxDepth, registry } = options;
34
-
35
- const { name, version, deprecated, ...pkg } = await pacote.manifest(gitURL ?? packageName, {
36
- ...NPM_TOKEN,
37
- registry,
38
- cache: `${os.homedir()}/.npm`
39
- });
40
- const { dependencies, customResolvers, alias } = mergeDependencies(pkg);
41
-
42
- const current = new Dependency(name, version, parent);
43
- current.alias = Object.fromEntries(alias);
44
-
45
- if (gitURL !== null) {
46
- current.isGit(gitURL);
47
- try {
48
- await pacote.manifest(`${name}@${version}`, {
49
- ...NPM_TOKEN,
50
- registry,
51
- cache: `${os.homedir()}/.npm`
52
- });
53
- }
54
- catch {
55
- current.existOnRemoteRegistry = false;
56
- }
57
- }
58
- current.addFlag("isDeprecated", deprecated === true);
59
- current.addFlag("hasCustomResolver", customResolvers.size > 0);
60
- current.addFlag("hasDependencies", dependencies.size > 0);
61
-
62
- if (currDepth !== maxDepth) {
63
- const config = {
64
- exclude, currDepth: currDepth + 1, parent: current, maxDepth, registry
65
- };
66
-
67
- const gitDependencies = iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr));
68
- for (const [depName, valueStr] of gitDependencies) {
69
- yield* searchDeepDependencies(depName, valueStr, config);
70
- }
71
-
72
- const depsNames = await Promise.all(iter.map(dependencies.entries(), getCleanDependencyName));
73
- for (const [fullName, cleanName, isLatest] of depsNames) {
74
- if (!isLatest) {
75
- current.addFlag("hasOutdatedDependency");
76
- }
77
-
78
- if (exclude.has(cleanName)) {
79
- exclude.get(cleanName).add(current.fullName);
80
- }
81
- else {
82
- exclude.set(cleanName, new Set([current.fullName]));
83
- yield* searchDeepDependencies(fullName, null, config);
84
- }
85
- }
86
- }
87
-
88
- yield current;
89
- }
90
-
91
- export async function* deepReadEdges(currentPackageName, options) {
92
- const { to, parent, exclude, fullLockMode, includeDevDeps, registry } = options;
93
- const { version, integrity = to.integrity } = to.package;
94
-
95
- const updatedVersion = version === "*" || typeof version === "undefined" ? "latest" : version;
96
- const current = new Dependency(currentPackageName, updatedVersion, parent);
97
- current.dev = to.dev;
98
-
99
- if (fullLockMode && !includeDevDeps) {
100
- const { deprecated, _integrity, ...pkg } = await pacote.manifest(`${currentPackageName}@${updatedVersion}`, {
101
- ...NPM_TOKEN,
102
- registry,
103
- cache: `${os.homedir()}/.npm`
104
- });
105
- const { customResolvers, alias } = mergeDependencies(pkg);
106
-
107
- current.alias = Object.fromEntries(alias);
108
- current.addFlag("hasValidIntegrity", _integrity === integrity);
109
- current.addFlag("isDeprecated");
110
- current.addFlag("hasCustomResolver", customResolvers.size > 0);
111
-
112
- if (isGitDependency(to.resolved)) {
113
- current.isGit(to.resolved);
114
- }
115
- }
116
- current.addFlag("hasDependencies", to.edgesOut.size > 0);
117
-
118
- for (const [packageName, { to: toNode }] of to.edgesOut) {
119
- if (toNode === null || (!includeDevDeps && toNode.dev)) {
120
- continue;
121
- }
122
- const cleanName = `${packageName}@${toNode.package.version}`;
123
-
124
- if (exclude.has(cleanName)) {
125
- exclude.get(cleanName).add(current.fullName);
126
- }
127
- else {
128
- exclude.set(cleanName, new Set([current.fullName]));
129
- yield* deepReadEdges(packageName, { parent: current, to: toNode, exclude, registry });
130
- }
131
- }
132
- yield current;
133
- }
134
-
135
- export async function* getRootDependencies(manifest, options) {
136
- const {
137
- maxDepth = 4, exclude,
138
- usePackageLock, fullLockMode, includeDevDeps,
139
- location,
140
- registry
141
- } = options;
142
-
143
- const { dependencies, customResolvers, alias } = mergeDependencies(manifest, void 0);
144
- const parent = new Dependency(manifest.name, manifest.version);
145
- parent.alias = Object.fromEntries(alias);
146
-
147
- try {
148
- await pacote.manifest(`${manifest.name}@${manifest.version}`, {
149
- ...NPM_TOKEN,
150
- registry,
151
- cache: `${os.homedir()}/.npm`
152
- });
153
- }
154
- catch {
155
- parent.existOnRemoteRegistry = false;
156
- }
157
- parent.addFlag("hasCustomResolver", customResolvers.size > 0);
158
- parent.addFlag("hasDependencies", dependencies.size > 0);
159
-
160
- let iterators;
161
- if (usePackageLock) {
162
- const arb = new Arborist({
163
- ...NPM_TOKEN,
164
- path: location,
165
- registry
166
- });
167
- let tree;
168
- try {
169
- await fs.access(path.join(location, "node_modules"));
170
- tree = await arb.loadActual();
171
- }
172
- catch {
173
- tree = await arb.loadVirtual();
174
- }
175
-
176
- iterators = [
177
- ...iter
178
- .filter(tree.edgesOut.entries(), ([, { to }]) => to !== null && (includeDevDeps ? true : (!to.dev || to.isWorkspace)))
179
- .map(([packageName, { to }]) => [packageName, to.isWorkspace ? to.target : to])
180
- .map(([packageName, to]) => deepReadEdges(packageName, {
181
- to,
182
- parent,
183
- fullLockMode,
184
- includeDevDeps,
185
- exclude,
186
- registry
187
- }))
188
- ];
189
- }
190
- else {
191
- const configRef = { exclude, maxDepth, parent, registry };
192
- iterators = [
193
- ...iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr))
194
- .map(([depName, valueStr]) => searchDeepDependencies(depName, valueStr, configRef)),
195
- ...iter.map(dependencies.entries(), ([name, ver]) => searchDeepDependencies(`${name}@${ver}`, null, configRef))
196
- ];
197
- }
198
- for await (const dep of combineAsyncIterators({}, ...iterators)) {
199
- yield dep;
200
- }
201
-
202
- // Add root dependencies to the exclude Map (because the parent is not handled by searchDeepDependencies)
203
- // if we skip this the code will fail to re-link properly dependencies in the following steps
204
- const depsName = await Promise.all(iter.map(dependencies.entries(), getCleanDependencyName));
205
- for (const [, fullRange, isLatest] of depsName) {
206
- if (!isLatest) {
207
- parent.addFlag("hasOutdatedDependency");
208
- }
209
- if (exclude.has(fullRange)) {
210
- exclude.get(fullRange).add(parent.fullName);
211
- }
212
- }
213
-
214
- yield parent;
215
- }
216
-
217
- /**
218
- * @param {*} manifest
219
- * @param {*} options
220
- * @param {Logger} logger
221
- */
222
- export async function depWalker(manifest, options = {}, logger = new Logger()) {
223
- const {
224
- forceRootAnalysis = false,
225
- usePackageLock = false,
226
- includeDevDeps = false,
227
- fullLockMode = false,
228
- maxDepth,
229
- location,
230
- vulnerabilityStrategy = vuln.strategies.NONE,
231
- registry
232
- } = options;
233
-
234
- // Create TMP directory
235
- const tmpLocation = await fs.mkdtemp(path.join(os.tmpdir(), "/"));
236
-
237
- const payload = {
238
- id: tmpLocation.slice(-6),
239
- rootDependencyName: manifest.name,
240
- scannerVersion: packageVersion,
241
- vulnerabilityStrategy,
242
- warnings: []
243
- };
244
-
245
- // We are dealing with an exclude Map to avoid checking a package more than one time in searchDeepDependencies
246
- const exclude = new Map();
247
- const dependencies = new Map();
248
-
249
- {
250
- logger
251
- .start(ScannerLoggerEvents.analysis.tree)
252
- .start(ScannerLoggerEvents.analysis.tarball)
253
- .start(ScannerLoggerEvents.analysis.registry);
254
- const fetchedMetadataPackages = new Set();
255
- const promisesToWait = [];
256
-
257
- const tarballLocker = new Lock({ maxConcurrent: 5 });
258
- tarballLocker.on("freeOne", () => logger.tick(ScannerLoggerEvents.analysis.tarball));
259
-
260
- const rootDepsOptions = { maxDepth, exclude, usePackageLock, fullLockMode, includeDevDeps, location, registry };
261
- for await (const currentDep of getRootDependencies(manifest, rootDepsOptions)) {
262
- const { name, version, dev } = currentDep;
263
- const current = currentDep.exportAsPlainObject(name === manifest.name ? 0 : void 0);
264
- let proceedDependencyAnalysis = true;
265
-
266
- if (dependencies.has(name)) {
267
- // TODO: how to handle different metadata ?
268
- const dep = dependencies.get(name);
269
-
270
- const currVersion = Object.keys(current.versions)[0];
271
- if (currVersion in dep.versions) {
272
- // The dependency has already entered the analysis
273
- // This happens if the package is used by multiple packages in the tree
274
- proceedDependencyAnalysis = false;
275
- }
276
- else {
277
- dep.versions[currVersion] = current.versions[currVersion];
278
- }
279
- }
280
- else {
281
- dependencies.set(name, current);
282
- }
283
-
284
- // If the dependency is a DevDependencies we ignore it.
285
- if (dev) {
286
- continue;
287
- }
288
-
289
- if (proceedDependencyAnalysis) {
290
- logger.tick(ScannerLoggerEvents.analysis.tree);
291
-
292
- // There is no need to fetch 'N' times the npm metadata for the same package.
293
- if (fetchedMetadataPackages.has(name)) {
294
- logger.tick(ScannerLoggerEvents.analysis.registry);
295
- }
296
- else {
297
- fetchedMetadataPackages.add(name);
298
- promisesToWait.push(packageMetadata(name, version, {
299
- ref: current,
300
- logger
301
- }));
302
- }
303
-
304
- promisesToWait.push(scanDirOrArchive(name, version, {
305
- ref: current.versions[version],
306
- location,
307
- tmpLocation: forceRootAnalysis && name === manifest.name ? null : tmpLocation,
308
- locker: tarballLocker,
309
- logger,
310
- registry
311
- }));
312
- }
313
- }
314
-
315
- logger.end(ScannerLoggerEvents.analysis.tree);
316
-
317
- // Wait for all extraction to be done!
318
- await Promise.allSettled(promisesToWait);
319
- await timers.setImmediate();
320
-
321
- logger.end(ScannerLoggerEvents.analysis.tarball).end(ScannerLoggerEvents.analysis.registry);
322
- }
323
-
324
- const { hydratePayloadDependencies, strategy } = await vuln.setStrategy(vulnerabilityStrategy);
325
- await hydratePayloadDependencies(dependencies, {
326
- useStandardFormat: true,
327
- path: location
328
- });
329
-
330
- payload.vulnerabilityStrategy = strategy;
331
-
332
- // We do this because it "seem" impossible to link all dependencies in the first walk.
333
- // Because we are dealing with package only one time it may happen sometimes.
334
- for (const [packageName, dependency] of dependencies) {
335
- for (const [verStr, verDescriptor] of Object.entries(dependency.versions)) {
336
- verDescriptor.flags.push(...addMissingVersionFlags(new Set(verDescriptor.flags), dependency));
337
-
338
- const fullName = `${packageName}@${verStr}`;
339
- const usedDeps = exclude.get(fullName) || new Set();
340
- if (usedDeps.size === 0) {
341
- continue;
342
- }
343
-
344
- const usedBy = Object.create(null);
345
- for (const [name, version] of [...usedDeps].map((name) => name.split(" "))) {
346
- usedBy[name] = version;
347
- }
348
- Object.assign(verDescriptor.usedBy, usedBy);
349
- }
350
- }
351
-
352
- try {
353
- const { warnings, flaggedAuthors } = await getDependenciesWarnings(dependencies);
354
- payload.warnings = warnings;
355
- payload.flaggedAuthors = flaggedAuthors;
356
- payload.dependencies = Object.fromEntries(dependencies);
357
-
358
- return payload;
359
- }
360
- finally {
361
- await timers.setImmediate();
362
- await fs.rm(tmpLocation, { recursive: true, force: true });
363
-
364
- logger.emit(ScannerLoggerEvents.done);
365
- }
366
- }
1
+ // Import Node.js Dependencies
2
+ import path from "path";
3
+ import { readFileSync, promises as fs } from "fs";
4
+ import timers from "timers/promises";
5
+ import os from "os";
6
+
7
+ // Import Third-party Dependencies
8
+ import combineAsyncIterators from "combine-async-iterators";
9
+ import * as iter from "itertools";
10
+ import pacote from "pacote";
11
+ import Arborist from "@npmcli/arborist";
12
+ import Lock from "@slimio/lock";
13
+ import * as vuln from "@nodesecure/vuln";
14
+ import { ScannerLoggerEvents } from "./constants.js";
15
+
16
+ // Import Internal Dependencies
17
+ import {
18
+ mergeDependencies, getCleanDependencyName, getDependenciesWarnings, addMissingVersionFlags, isGitDependency,
19
+ NPM_TOKEN
20
+ } from "./utils/index.js";
21
+ import { scanDirOrArchive } from "./tarball.js";
22
+ import { packageMetadata, manifestMetadata } from "./npmRegistry.js";
23
+ import Dependency from "./class/dependency.class.js";
24
+ import Logger from "./class/logger.class.js";
25
+
26
+ const { version: packageVersion } = JSON.parse(
27
+ readFileSync(
28
+ new URL(path.join("..", "package.json"), import.meta.url)
29
+ )
30
+ );
31
+
32
+ export async function* searchDeepDependencies(packageName, gitURL, options) {
33
+ const { exclude, currDepth = 0, parent, maxDepth, registry } = options;
34
+
35
+ const { name, version, deprecated, ...pkg } = await pacote.manifest(gitURL ?? packageName, {
36
+ ...NPM_TOKEN,
37
+ registry,
38
+ cache: `${os.homedir()}/.npm`
39
+ });
40
+ const { dependencies, customResolvers, alias } = mergeDependencies(pkg);
41
+
42
+ const current = new Dependency(name, version, parent);
43
+ current.alias = Object.fromEntries(alias);
44
+
45
+ if (gitURL !== null) {
46
+ current.isGit(gitURL);
47
+ try {
48
+ await pacote.manifest(`${name}@${version}`, {
49
+ ...NPM_TOKEN,
50
+ registry,
51
+ cache: `${os.homedir()}/.npm`
52
+ });
53
+ }
54
+ catch {
55
+ current.existOnRemoteRegistry = false;
56
+ }
57
+ }
58
+ current.addFlag("isDeprecated", deprecated === true);
59
+ current.addFlag("hasCustomResolver", customResolvers.size > 0);
60
+ current.addFlag("hasDependencies", dependencies.size > 0);
61
+
62
+ if (currDepth !== maxDepth) {
63
+ const config = {
64
+ exclude, currDepth: currDepth + 1, parent: current, maxDepth, registry
65
+ };
66
+
67
+ const gitDependencies = iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr));
68
+ for (const [depName, valueStr] of gitDependencies) {
69
+ yield* searchDeepDependencies(depName, valueStr, config);
70
+ }
71
+
72
+ const depsNames = await Promise.all(iter.map(dependencies.entries(), getCleanDependencyName));
73
+ for (const [fullName, cleanName, isLatest] of depsNames) {
74
+ if (!isLatest) {
75
+ current.addFlag("hasOutdatedDependency");
76
+ }
77
+
78
+ if (exclude.has(cleanName)) {
79
+ current.addChildren();
80
+ exclude.get(cleanName).add(current.fullName);
81
+ }
82
+ else {
83
+ exclude.set(cleanName, new Set([current.fullName]));
84
+ yield* searchDeepDependencies(fullName, null, config);
85
+ }
86
+ }
87
+ }
88
+
89
+ yield current;
90
+ }
91
+
92
+ export async function* deepReadEdges(currentPackageName, options) {
93
+ const { to, parent, exclude, fullLockMode, includeDevDeps, registry } = options;
94
+ const { version, integrity = to.integrity } = to.package;
95
+
96
+ const updatedVersion = version === "*" || typeof version === "undefined" ? "latest" : version;
97
+ const current = new Dependency(currentPackageName, updatedVersion, parent);
98
+ current.dev = to.dev;
99
+
100
+ if (fullLockMode && !includeDevDeps) {
101
+ const { deprecated, _integrity, ...pkg } = await pacote.manifest(`${currentPackageName}@${updatedVersion}`, {
102
+ ...NPM_TOKEN,
103
+ registry,
104
+ cache: `${os.homedir()}/.npm`
105
+ });
106
+ const { customResolvers, alias } = mergeDependencies(pkg);
107
+
108
+ current.alias = Object.fromEntries(alias);
109
+ current.addFlag("hasValidIntegrity", _integrity === integrity);
110
+ current.addFlag("isDeprecated");
111
+ current.addFlag("hasCustomResolver", customResolvers.size > 0);
112
+
113
+ if (isGitDependency(to.resolved)) {
114
+ current.isGit(to.resolved);
115
+ }
116
+ }
117
+ current.addFlag("hasDependencies", to.edgesOut.size > 0);
118
+
119
+ for (const [packageName, { to: toNode }] of to.edgesOut) {
120
+ if (toNode === null || (!includeDevDeps && toNode.dev)) {
121
+ continue;
122
+ }
123
+ const cleanName = `${packageName}@${toNode.package.version}`;
124
+
125
+ if (exclude.has(cleanName)) {
126
+ current.addChildren();
127
+ exclude.get(cleanName).add(current.fullName);
128
+ }
129
+ else {
130
+ exclude.set(cleanName, new Set([current.fullName]));
131
+ yield* deepReadEdges(packageName, { parent: current, to: toNode, exclude, registry });
132
+ }
133
+ }
134
+ yield current;
135
+ }
136
+
137
+ export async function* getRootDependencies(manifest, options) {
138
+ const {
139
+ maxDepth = 4, exclude,
140
+ usePackageLock, fullLockMode, includeDevDeps,
141
+ location,
142
+ registry
143
+ } = options;
144
+
145
+ const { dependencies, customResolvers, alias } = mergeDependencies(manifest, void 0);
146
+ const parent = new Dependency(manifest.name, manifest.version);
147
+ parent.alias = Object.fromEntries(alias);
148
+
149
+ try {
150
+ await pacote.manifest(`${manifest.name}@${manifest.version}`, {
151
+ ...NPM_TOKEN,
152
+ registry,
153
+ cache: `${os.homedir()}/.npm`
154
+ });
155
+ }
156
+ catch {
157
+ parent.existOnRemoteRegistry = false;
158
+ }
159
+ parent.addFlag("hasCustomResolver", customResolvers.size > 0);
160
+ parent.addFlag("hasDependencies", dependencies.size > 0);
161
+
162
+ let iterators;
163
+ if (usePackageLock) {
164
+ const arb = new Arborist({
165
+ ...NPM_TOKEN,
166
+ path: location,
167
+ registry
168
+ });
169
+ let tree;
170
+ try {
171
+ await fs.access(path.join(location, "node_modules"));
172
+ tree = await arb.loadActual();
173
+ }
174
+ catch {
175
+ tree = await arb.loadVirtual();
176
+ }
177
+
178
+ iterators = [
179
+ ...iter
180
+ .filter(tree.edgesOut.entries(), ([, { to }]) => to !== null && (includeDevDeps ? true : (!to.dev || to.isWorkspace)))
181
+ .map(([packageName, { to }]) => [packageName, to.isWorkspace ? to.target : to])
182
+ .map(([packageName, to]) => deepReadEdges(packageName, {
183
+ to,
184
+ parent,
185
+ fullLockMode,
186
+ includeDevDeps,
187
+ exclude,
188
+ registry
189
+ }))
190
+ ];
191
+ }
192
+ else {
193
+ const configRef = { exclude, maxDepth, parent, registry };
194
+ iterators = [
195
+ ...iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr))
196
+ .map(([depName, valueStr]) => searchDeepDependencies(depName, valueStr, configRef)),
197
+ ...iter.map(dependencies.entries(), ([name, ver]) => searchDeepDependencies(`${name}@${ver}`, null, configRef))
198
+ ];
199
+ }
200
+ for await (const dep of combineAsyncIterators({}, ...iterators)) {
201
+ yield dep;
202
+ }
203
+
204
+ // Add root dependencies to the exclude Map (because the parent is not handled by searchDeepDependencies)
205
+ // if we skip this the code will fail to re-link properly dependencies in the following steps
206
+ const depsName = await Promise.all(iter.map(dependencies.entries(), getCleanDependencyName));
207
+ for (const [, fullRange, isLatest] of depsName) {
208
+ if (!isLatest) {
209
+ parent.addFlag("hasOutdatedDependency");
210
+ }
211
+ if (exclude.has(fullRange)) {
212
+ exclude.get(fullRange).add(parent.fullName);
213
+ }
214
+ }
215
+
216
+ yield parent;
217
+ }
218
+
219
+ /**
220
+ * @param {*} manifest
221
+ * @param {*} options
222
+ * @param {Logger} logger
223
+ */
224
+ export async function depWalker(manifest, options = {}, logger = new Logger()) {
225
+ const {
226
+ forceRootAnalysis = false,
227
+ usePackageLock = false,
228
+ includeDevDeps = false,
229
+ fullLockMode = false,
230
+ maxDepth,
231
+ location,
232
+ vulnerabilityStrategy = vuln.strategies.NONE,
233
+ registry
234
+ } = options;
235
+
236
+ // Create TMP directory
237
+ const tmpLocation = await fs.mkdtemp(path.join(os.tmpdir(), "/"));
238
+
239
+ const payload = {
240
+ id: tmpLocation.slice(-6),
241
+ rootDependencyName: manifest.name,
242
+ scannerVersion: packageVersion,
243
+ vulnerabilityStrategy,
244
+ warnings: []
245
+ };
246
+
247
+ // We are dealing with an exclude Map to avoid checking a package more than one time in searchDeepDependencies
248
+ const exclude = new Map();
249
+ const dependencies = new Map();
250
+
251
+ {
252
+ logger
253
+ .start(ScannerLoggerEvents.analysis.tree)
254
+ .start(ScannerLoggerEvents.analysis.tarball)
255
+ .start(ScannerLoggerEvents.analysis.registry);
256
+ const fetchedMetadataPackages = new Set();
257
+ const promisesToWait = [];
258
+
259
+ const tarballLocker = new Lock({ maxConcurrent: 5 });
260
+ tarballLocker.on("freeOne", () => logger.tick(ScannerLoggerEvents.analysis.tarball));
261
+
262
+ const rootDepsOptions = { maxDepth, exclude, usePackageLock, fullLockMode, includeDevDeps, location, registry };
263
+ for await (const currentDep of getRootDependencies(manifest, rootDepsOptions)) {
264
+ const { name, version, dev } = currentDep;
265
+
266
+ const current = currentDep.exportAsPlainObject(name === manifest.name ? 0 : void 0);
267
+ let proceedDependencyAnalysis = true;
268
+
269
+ if (dependencies.has(name)) {
270
+ const dep = dependencies.get(name);
271
+ promisesToWait.push(manifestMetadata(name, version, dep.metadata));
272
+
273
+ const currVersion = Object.keys(current.versions)[0];
274
+ if (currVersion in dep.versions) {
275
+ // The dependency has already entered the analysis
276
+ // This happens if the package is used by multiple packages in the tree
277
+ proceedDependencyAnalysis = false;
278
+ }
279
+ else {
280
+ dep.versions[currVersion] = current.versions[currVersion];
281
+ }
282
+ }
283
+ else {
284
+ dependencies.set(name, current);
285
+ }
286
+
287
+ // If the dependency is a DevDependencies we ignore it.
288
+ if (dev) {
289
+ continue;
290
+ }
291
+
292
+ if (proceedDependencyAnalysis) {
293
+ logger.tick(ScannerLoggerEvents.analysis.tree);
294
+
295
+ // There is no need to fetch 'N' times the npm metadata for the same package.
296
+ if (fetchedMetadataPackages.has(name)) {
297
+ logger.tick(ScannerLoggerEvents.analysis.registry);
298
+ }
299
+ else {
300
+ fetchedMetadataPackages.add(name);
301
+ promisesToWait.push(packageMetadata(name, version, {
302
+ ref: current,
303
+ logger
304
+ }));
305
+ }
306
+
307
+ promisesToWait.push(scanDirOrArchive(name, version, {
308
+ ref: current.versions[version],
309
+ location,
310
+ tmpLocation: forceRootAnalysis && name === manifest.name ? null : tmpLocation,
311
+ locker: tarballLocker,
312
+ logger,
313
+ registry
314
+ }));
315
+ }
316
+ }
317
+
318
+ logger.end(ScannerLoggerEvents.analysis.tree);
319
+
320
+ // Wait for all extraction to be done!
321
+ await Promise.allSettled(promisesToWait);
322
+ await timers.setImmediate();
323
+
324
+ logger.end(ScannerLoggerEvents.analysis.tarball).end(ScannerLoggerEvents.analysis.registry);
325
+ }
326
+
327
+ const { hydratePayloadDependencies, strategy } = await vuln.setStrategy(vulnerabilityStrategy);
328
+ await hydratePayloadDependencies(dependencies, {
329
+ useStandardFormat: true,
330
+ path: location
331
+ });
332
+
333
+ payload.vulnerabilityStrategy = strategy;
334
+
335
+ // We do this because it "seem" impossible to link all dependencies in the first walk.
336
+ // Because we are dealing with package only one time it may happen sometimes.
337
+ const globalWarnings = [];
338
+ for (const [packageName, dependency] of dependencies) {
339
+ const metadataIntegrities = dependency.metadata?.integrity ?? {};
340
+
341
+ for (const [version, integrity] of Object.entries(metadataIntegrities)) {
342
+ const dependencyVer = dependency.versions[version];
343
+ if (!("integrity" in dependencyVer) || dependencyVer.flags.includes("isGit")) {
344
+ continue;
345
+ }
346
+
347
+ if (dependencyVer.integrity !== integrity) {
348
+ globalWarnings.push(`${packageName}@${version} manifest & tarball integrity doesn't match!`);
349
+ }
350
+ }
351
+ for (const [verStr, verDescriptor] of Object.entries(dependency.versions)) {
352
+ verDescriptor.flags.push(...addMissingVersionFlags(new Set(verDescriptor.flags), dependency));
353
+
354
+ const usedDeps = exclude.get(`${packageName}@${verStr}`) || new Set();
355
+ if (usedDeps.size === 0) {
356
+ continue;
357
+ }
358
+
359
+ const usedBy = Object.create(null);
360
+ for (const [name, version] of [...usedDeps].map((name) => name.split(" "))) {
361
+ usedBy[name] = version;
362
+ }
363
+ Object.assign(verDescriptor.usedBy, usedBy);
364
+ }
365
+ }
366
+
367
+ try {
368
+ const { warnings, flaggedAuthors } = await getDependenciesWarnings(dependencies);
369
+ payload.warnings = globalWarnings.concat(warnings);
370
+ payload.flaggedAuthors = flaggedAuthors;
371
+ payload.dependencies = Object.fromEntries(dependencies);
372
+
373
+ return payload;
374
+ }
375
+ finally {
376
+ await timers.setImmediate();
377
+ await fs.rm(tmpLocation, { recursive: true, force: true });
378
+
379
+ logger.emit(ScannerLoggerEvents.done);
380
+ }
381
+ }
package/src/manifest.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Import Node.js Dependencies
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import crypto from "node:crypto";
4
5
 
5
6
  // Import Internal Dependencies
6
7
  import { parseAuthor } from "./utils/index.js";
@@ -10,6 +11,7 @@ import { parseAuthor } from "./utils/index.js";
10
11
  const kNativeNpmPackages = new Set([
11
12
  "node-gyp", "node-pre-gyp", "node-gyp-build", "node-addon-api"
12
13
  ]);
14
+ const kNodemodulesBinPrefix = "node_modules/.bin/";
13
15
 
14
16
  /**
15
17
  * @see https://www.nerdycode.com/prevent-npm-executing-scripts-security/
@@ -24,7 +26,7 @@ const kUnsafeNpmScripts = new Set([
24
26
 
25
27
  /**
26
28
  * @param {!string} location
27
- * @returns {import("@npm/types").PackageJson}
29
+ * @returns {Promise<import("@npm/types").PackageJson>}
28
30
  */
29
31
  export async function read(location) {
30
32
  const packageStr = await fs.readFile(
@@ -37,13 +39,37 @@ export async function read(location) {
37
39
 
38
40
  export async function readAnalyze(location) {
39
41
  const {
40
- description = "", author = {}, scripts = {},
41
- dependencies = {}, devDependencies = {}, gypfile = false,
42
+ name,
43
+ version,
44
+ description = "",
45
+ author = {},
46
+ scripts = {},
47
+ dependencies = {},
48
+ devDependencies = {},
49
+ gypfile = false,
42
50
  engines = {},
43
51
  repository = {},
44
- imports = {}
52
+ imports = {},
53
+ license = ""
45
54
  } = await read(location);
46
55
 
56
+ for (const [scriptName, scriptValue] of Object.entries(scripts)) {
57
+ scripts[scriptName] = scriptValue.replaceAll(kNodemodulesBinPrefix, "");
58
+ }
59
+
60
+ const integrityObj = {
61
+ name,
62
+ version,
63
+ dependencies,
64
+ license,
65
+ scripts
66
+ };
67
+
68
+ const integrity = crypto
69
+ .createHash("sha256")
70
+ .update(JSON.stringify(integrityObj))
71
+ .digest("hex");
72
+
47
73
  const packageDeps = Object.keys(dependencies);
48
74
  const packageDevDeps = Object.keys(devDependencies);
49
75
  const hasNativePackage = [...packageDevDeps, ...packageDeps]
@@ -60,6 +86,7 @@ export async function readAnalyze(location) {
60
86
  packageDeps,
61
87
  packageDevDeps,
62
88
  nodejs: { imports },
63
- hasNativeElements: hasNativePackage || gypfile
89
+ hasNativeElements: hasNativePackage || gypfile,
90
+ integrity
64
91
  };
65
92
  }
@@ -1,12 +1,28 @@
1
+ // Import Node.js Dependencies
2
+ import crypto from "node:crypto";
3
+
1
4
  // Import Third-party Dependencies
2
5
  import semver from "semver";
3
- import { packument } from "@nodesecure/npm-registry-sdk";
6
+ import { packument, packumentVersion } from "@nodesecure/npm-registry-sdk";
4
7
 
5
8
  // Import Internal Dependencies
6
9
  import { parseAuthor } from "./utils/index.js";
7
10
 
11
+ export async function manifestMetadata(name, version, metadata) {
12
+ try {
13
+ const pkgVersion = await packumentVersion(name, version);
14
+
15
+ const integrity = getPackumentVersionIntegrity(pkgVersion);
16
+ metadata.integrity[version] = integrity;
17
+ }
18
+ catch {
19
+ // Ignore
20
+ }
21
+ }
22
+
8
23
  export async function packageMetadata(name, version, options) {
9
24
  const { ref, logger } = options;
25
+ const packageSpec = `${name}:${version}`;
10
26
 
11
27
  try {
12
28
  const pkg = await packument(name);
@@ -24,7 +40,8 @@ export async function packageMetadata(name, version, options) {
24
40
  lastUpdateAt,
25
41
  hasReceivedUpdateInOneYear: !(oneYearFromToday > lastUpdateAt),
26
42
  maintainers: pkg.maintainers ?? [],
27
- publishers: []
43
+ publishers: [],
44
+ integrity: {}
28
45
  };
29
46
 
30
47
  const isOutdated = semver.neq(version, lastVersion);
@@ -35,6 +52,13 @@ export async function packageMetadata(name, version, options) {
35
52
  const publishers = new Set();
36
53
  let searchForMaintainersInVersions = metadata.maintainers.length === 0;
37
54
  for (const ver of Object.values(pkg.versions).reverse()) {
55
+ const versionSpec = `${ver.name}:${ver.version}`;
56
+ if (packageSpec === versionSpec) {
57
+ metadata.integrity[ver.version] = getPackumentVersionIntegrity(
58
+ ver
59
+ );
60
+ }
61
+
38
62
  const { _npmUser: npmUser, version, maintainers = [] } = ver;
39
63
  const isNullOrUndefined = typeof npmUser === "undefined" || npmUser === null;
40
64
  if (isNullOrUndefined || !("name" in npmUser) || typeof npmUser.name !== "string") {
@@ -70,3 +94,25 @@ export async function packageMetadata(name, version, options) {
70
94
  logger.tick("registry");
71
95
  }
72
96
  }
97
+
98
+ function getPackumentVersionIntegrity(packumentVersion) {
99
+ const { name, version, dependencies = {}, license = "", scripts = {} } = packumentVersion;
100
+
101
+ // See https://github.com/npm/cli/issues/5234
102
+ if ("install" in dependencies && dependencies.install === "node-gyp rebuild") {
103
+ delete dependencies.install;
104
+ }
105
+
106
+ const integrityObj = {
107
+ name,
108
+ version,
109
+ dependencies,
110
+ license,
111
+ scripts
112
+ };
113
+
114
+ return crypto
115
+ .createHash("sha256")
116
+ .update(JSON.stringify(integrityObj))
117
+ .digest("hex");
118
+ }
package/src/tarball.js CHANGED
@@ -72,10 +72,19 @@ export async function scanDirOrArchive(name, version, options) {
72
72
  const {
73
73
  packageDeps,
74
74
  packageDevDeps,
75
- author, description, hasScript, hasNativeElements, nodejs,
76
- engines, repository, scripts
75
+ author,
76
+ description,
77
+ hasScript,
78
+ hasNativeElements,
79
+ nodejs,
80
+ engines,
81
+ repository,
82
+ scripts,
83
+ integrity
77
84
  } = await manifest.readAnalyze(dest);
78
- Object.assign(ref, { author, description, engines, repository, scripts });
85
+ Object.assign(ref, {
86
+ author, description, engines, repository, scripts, integrity
87
+ });
79
88
 
80
89
  // Get the composition of the (extracted) directory
81
90
  const { ext, files, size } = await getTarballComposition(dest);
@@ -50,12 +50,13 @@ export async function getCleanDependencyName([depName, range]) {
50
50
 
51
51
  export function getSemVerWarning(value) {
52
52
  return {
53
- kind: "invalid-semver",
53
+ kind: "zero-semver",
54
54
  file: "package.json",
55
55
  value,
56
56
  location: null,
57
- i18n: "sast_warnings.invalidSemVer",
57
+ i18n: "sast_warnings.zeroSemVer",
58
58
  severity: "Information",
59
+ source: "Scanner",
59
60
  experimental: false
60
61
  };
61
62
  }
@@ -8,7 +8,7 @@ import { extractAllAuthors } from "@nodesecure/authors";
8
8
  // Import Internal Dependencies
9
9
  import { getDirNameFromUrl } from "./dirname.js";
10
10
 
11
- i18n.extendFromSystemPath(
11
+ await i18n.extendFromSystemPath(
12
12
  path.join(getDirNameFromUrl(import.meta.url), "..", "..", "i18n")
13
13
  );
14
14
 
@@ -104,6 +104,13 @@ declare namespace Scanner {
104
104
  * If the dependency is a GIT repository
105
105
  */
106
106
  gitUrl: null | string;
107
+ /**
108
+ * Version MD5 integrity hash
109
+ * Generated by the scanner to verify manifest/tarball confusion
110
+ *
111
+ * (Not supported on GIT dependency)
112
+ */
113
+ integrity?: string;
107
114
  }
108
115
 
109
116
  export interface Dependency {
@@ -131,6 +138,11 @@ declare namespace Scanner {
131
138
  * List of people who published this package
132
139
  */
133
140
  publishers: Publisher[];
141
+ /**
142
+ * Version MD5 integrity hash
143
+ * Generated by the scanner to verify manifest/tarball confusion
144
+ */
145
+ integrity: Record<string, string>;
134
146
  }
135
147
  /** List of versions of this package available in the dependency tree (In the payload) */
136
148
  versions: Record<string, DependencyVersion>;