@nodesecure/scanner 2.1.0 → 3.1.1-rc.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Communauté JavaScript et Node.js Francophone
3
+ Copyright (c) 2021 NodeSecure
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -69,7 +69,7 @@ interface Options {
69
69
  ## Contributors ✨
70
70
 
71
71
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
72
- [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-)
72
+ [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-)
73
73
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
74
74
 
75
75
  Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -82,6 +82,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
82
82
  <td align="center"><a href="https://www.linkedin.com/in/thomas-gentilhomme/"><img src="https://avatars.githubusercontent.com/u/4438263?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Gentilhomme</b></sub></a><br /><a href="https://github.com/NodeSecure/scanner/commits?author=fraxken" title="Code">💻</a> <a href="https://github.com/NodeSecure/scanner/commits?author=fraxken" title="Documentation">📖</a> <a href="https://github.com/NodeSecure/scanner/pulls?q=is%3Apr+reviewed-by%3Afraxken" title="Reviewed Pull Requests">👀</a> <a href="#security-fraxken" title="Security">🛡️</a> <a href="https://github.com/NodeSecure/scanner/issues?q=author%3Afraxken" title="Bug reports">🐛</a></td>
83
83
  <td align="center"><a href="http://tonygo.dev"><img src="https://avatars.githubusercontent.com/u/22824417?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tony Gorez</b></sub></a><br /><a href="https://github.com/NodeSecure/scanner/commits?author=tony-go" title="Code">💻</a> <a href="https://github.com/NodeSecure/scanner/commits?author=tony-go" title="Documentation">📖</a> <a href="https://github.com/NodeSecure/scanner/pulls?q=is%3Apr+reviewed-by%3Atony-go" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/NodeSecure/scanner/issues?q=author%3Atony-go" title="Bug reports">🐛</a></td>
84
84
  <td align="center"><a href="https://mickaelcroquet.fr"><img src="https://avatars.githubusercontent.com/u/23740372?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Haze</b></sub></a><br /><a href="https://github.com/NodeSecure/scanner/commits?author=CroquetMickael" title="Code">💻</a></td>
85
+ <td align="center"><a href="https://github.com/mbalabash"><img src="https://avatars.githubusercontent.com/u/16868922?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Maksim Balabash</b></sub></a><br /><a href="https://github.com/NodeSecure/scanner/commits?author=mbalabash" title="Code">💻</a></td>
85
86
  </tr>
86
87
  </table>
87
88
 
package/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import Scanner from "./types/scanner";
2
- import { cwd, from, verify } from "./types/api";
2
+ import { cwd, from, verify, ScannerLoggerEvents } from "./types/api";
3
3
  import { depWalker } from "./types/walker";
4
4
  import { Logger, LoggerEventData } from "./types/logger";
5
5
  import tarball from "./types/tarball";
6
6
 
7
7
  export {
8
- cwd, from, verify,
8
+ cwd, from, verify, ScannerLoggerEvents,
9
9
  Scanner,
10
10
  Logger,
11
11
  LoggerEventData,
package/index.js CHANGED
@@ -10,8 +10,9 @@ import { getLocalRegistryURL } from "@nodesecure/npm-registry-sdk";
10
10
 
11
11
  // Import Internal Dependencies
12
12
  import { depWalker } from "./src/depWalker.js";
13
- import { constants } from "./src/utils/index.js";
14
- import Logger from "./src/logger.class.js";
13
+ import { NPM_TOKEN } from "./src/utils/index.js";
14
+ import { ScannerLoggerEvents } from "./src/constants.js";
15
+ import Logger from "./src/class/logger.class.js";
15
16
  import * as tarball from "./src/tarball.js";
16
17
 
17
18
  // CONSTANTS
@@ -20,20 +21,20 @@ const kDefaultCwdOptions = { forceRootAnalysis: true, usePackageLock: true };
20
21
  export async function cwd(cwd = process.cwd(), options = {}, logger = new Logger()) {
21
22
  const finalizedOptions = Object.assign({}, kDefaultCwdOptions, options);
22
23
 
23
- logger.start("readManifest");
24
+ logger.start(ScannerLoggerEvents.manifest.read);
24
25
  const packagePath = path.join(cwd, "package.json");
25
26
  const str = await fs.readFile(packagePath, "utf-8");
26
- logger.end("readManifest");
27
+ logger.end(ScannerLoggerEvents.manifest.read);
27
28
 
28
29
  return depWalker(JSON.parse(str), finalizedOptions, logger);
29
30
  }
30
31
 
31
32
  export async function from(packageName, options, logger = new Logger()) {
32
- logger.start("fetchManifest");
33
+ logger.start(ScannerLoggerEvents.manifest.fetch);
33
34
  const manifest = await pacote.manifest(packageName, {
34
- ...constants.NPM_TOKEN, registry: getLocalRegistryURL(), cache: `${os.homedir()}/.npm`
35
+ ...NPM_TOKEN, registry: getLocalRegistryURL(), cache: `${os.homedir()}/.npm`
35
36
  });
36
- logger.end("fetchManifest");
37
+ logger.end(ScannerLoggerEvents.manifest.fetch);
37
38
 
38
39
  return depWalker(manifest, options, logger);
39
40
  }
@@ -48,7 +49,7 @@ export async function verify(packageName = null) {
48
49
 
49
50
  try {
50
51
  await pacote.extract(packageName, dest, {
51
- ...constants.NPM_TOKEN, registry: getLocalRegistryURL(), cache: `${os.homedir()}/.npm`
52
+ ...NPM_TOKEN, registry: getLocalRegistryURL(), cache: `${os.homedir()}/.npm`
52
53
  });
53
54
 
54
55
  return await tarball.scanPackage(dest, packageName);
@@ -59,4 +60,4 @@ export async function verify(packageName = null) {
59
60
  }
60
61
  }
61
62
 
62
- export { depWalker, tarball, Logger };
63
+ export { depWalker, tarball, Logger, ScannerLoggerEvents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodesecure/scanner",
3
- "version": "2.1.0",
3
+ "version": "3.1.1-rc.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": {
@@ -10,7 +10,8 @@
10
10
  "lint": "eslint src test",
11
11
  "prepublishOnly": "pkg-ok",
12
12
  "test": "npm run lint && npm run test-only",
13
- "test-only": "cross-env NODE_OPTIONS=--experimental-vm-modules jest"
13
+ "test-only": "cross-env esm-tape-runner 'test/**/*.spec.js' | tap-monkey",
14
+ "coverage": "c8 -r html npm run test-only"
14
15
  },
15
16
  "files": [
16
17
  "src",
@@ -46,54 +47,36 @@
46
47
  "url": "https://github.com/NodeSecure/scanner/issues"
47
48
  },
48
49
  "homepage": "https://github.com/NodeSecure/scanner#readme",
49
- "jest": {
50
- "setupFilesAfterEnv": [
51
- "./jest.setup.js"
52
- ],
53
- "collectCoverage": true,
54
- "collectCoverageFrom": [
55
- "**/src/**/*.js"
56
- ],
57
- "testEnvironment": "node",
58
- "testMatch": [
59
- "**/test/**/*.js"
60
- ],
61
- "testPathIgnorePatterns": [
62
- "/test/fixtures/"
63
- ],
64
- "moduleNameMapper": {
65
- "^@nodesecure/npm-registry-sdk$": "@nodesecure/npm-registry-sdk/dist/index.js",
66
- "^@nodesecure/sec-literal$": "@nodesecure/sec-literal/src/index.js",
67
- "^estree-walker$": "estree-walker/src/index.js"
68
- }
69
- },
70
50
  "devDependencies": {
71
51
  "@nodesecure/eslint-config": "^1.3.0",
72
52
  "@slimio/is": "^1.5.1",
73
- "@types/jest": "^27.0.2",
74
- "@types/node": "^16.11.7",
53
+ "@small-tech/esm-tape-runner": "^1.0.3",
54
+ "@small-tech/tap-monkey": "^1.3.0",
55
+ "@types/node": "^16.11.10",
56
+ "c8": "^7.10.0",
75
57
  "cross-env": "^7.0.3",
76
58
  "dotenv": "^10.0.0",
77
- "eslint": "^8.2.0",
59
+ "eslint": "^8.3.0",
78
60
  "get-folder-size": "^3.1.0",
79
- "jest": "^27.3.1",
80
- "pkg-ok": "^2.3.1"
61
+ "pkg-ok": "^2.3.1",
62
+ "sinon": "^12.0.1",
63
+ "snap-shot-core": "^10.2.4",
64
+ "tape": "^5.3.2"
81
65
  },
82
66
  "dependencies": {
83
- "@nodesecure/flags": "^2.0.0",
67
+ "@nodesecure/flags": "^2.2.0",
84
68
  "@nodesecure/fs-walk": "^1.0.0",
85
- "@nodesecure/i18n": "^1.2.0",
86
- "@nodesecure/js-x-ray": "^4.0.1",
69
+ "@nodesecure/i18n": "^1.2.1",
70
+ "@nodesecure/js-x-ray": "^4.2.0",
87
71
  "@nodesecure/npm-registry-sdk": "^1.3.0",
88
- "@nodesecure/ntlp": "^2.0.0",
72
+ "@nodesecure/ntlp": "^2.1.0",
89
73
  "@nodesecure/utils": "^1.0.0",
90
- "@nodesecure/vuln": "^1.4.0",
74
+ "@nodesecure/vuln": "^1.4.1",
91
75
  "@npm/types": "^1.0.1",
92
- "@npmcli/arborist": "^4.0.4",
76
+ "@npmcli/arborist": "^4.1.0",
93
77
  "@slimio/lock": "^1.0.0",
94
78
  "builtins": "^4.0.0",
95
79
  "combine-async-iterators": "^2.0.1",
96
- "is-minified-code": "^2.0.0",
97
80
  "itertools": "^1.7.1",
98
81
  "lodash.difference": "^4.5.0",
99
82
  "pacote": "^12.0.2",
@@ -56,28 +56,29 @@ export default class Dependency {
56
56
  }
57
57
 
58
58
  return {
59
- [this.version]: {
60
- id: typeof customId === "number" ? customId : Dependency.currentId++,
61
- usedBy: this.parent,
62
- flags: this.flags,
63
- description: "",
64
- size: 0,
65
- author: {},
66
- warnings: this.warnings,
67
- composition: {
68
- extensions: [],
69
- files: [],
70
- minified: [],
71
- unused: [],
72
- missing: [],
73
- required_files: [],
74
- required_nodejs: [],
75
- required_thirdparty: []
76
- },
77
- license: "unkown license",
78
- gitUrl: this.gitUrl
59
+ versions: {
60
+ [this.version]: {
61
+ id: typeof customId === "number" ? customId : Dependency.currentId++,
62
+ usedBy: this.parent,
63
+ flags: this.flags,
64
+ description: "",
65
+ size: 0,
66
+ author: {},
67
+ warnings: this.warnings,
68
+ composition: {
69
+ extensions: [],
70
+ files: [],
71
+ minified: [],
72
+ unused: [],
73
+ missing: [],
74
+ required_files: [],
75
+ required_nodejs: [],
76
+ required_thirdparty: []
77
+ },
78
+ license: "unkown license",
79
+ gitUrl: this.gitUrl
80
+ }
79
81
  },
80
- versions: [this.version],
81
82
  vulnerabilities: [],
82
83
  metadata: {
83
84
  dependencyCount: this.dependencyCount,
File without changes
@@ -0,0 +1,13 @@
1
+
2
+ export const ScannerLoggerEvents = {
3
+ done: "depWalkerFinished",
4
+ analysis: {
5
+ tree: "walkTree",
6
+ tarball: "tarball",
7
+ registry: "registry"
8
+ },
9
+ manifest: {
10
+ read: "readManifest",
11
+ fetch: "fetchManifest"
12
+ }
13
+ };
package/src/depWalker.js CHANGED
@@ -8,18 +8,21 @@ import os from "os";
8
8
  import combineAsyncIterators from "combine-async-iterators";
9
9
  import iter from "itertools";
10
10
  import pacote from "pacote";
11
- import semver from "semver";
12
11
  import Arborist from "@npmcli/arborist";
13
12
  import Lock from "@slimio/lock";
14
- import { packument, getLocalRegistryURL } from "@nodesecure/npm-registry-sdk";
15
13
  import * as vuln from "@nodesecure/vuln";
16
- import { parseManifestAuthor } from "@nodesecure/utils";
14
+ import { getLocalRegistryURL } from "@nodesecure/npm-registry-sdk";
15
+ import { ScannerLoggerEvents } from "./constants.js";
17
16
 
18
17
  // Import Internal Dependencies
19
- import { mergeDependencies, constants, getCleanDependencyName, getDependenciesWarnings } from "./utils/index.js";
18
+ import {
19
+ mergeDependencies, getCleanDependencyName, getDependenciesWarnings, addMissingVersionFlags, isGitDependency,
20
+ NPM_TOKEN
21
+ } from "./utils/index.js";
20
22
  import { scanDirOrArchive } from "./tarball.js";
21
- import Dependency from "./dependency.class.js";
22
- import Logger from "./logger.class.js";
23
+ import { packageMetadata } from "./npmRegistry.js";
24
+ import Dependency from "./class/dependency.class.js";
25
+ import Logger from "./class/logger.class.js";
23
26
 
24
27
  const { version: packageVersion } = JSON.parse(
25
28
  readFileSync(
@@ -27,20 +30,18 @@ const { version: packageVersion } = JSON.parse(
27
30
  )
28
31
  );
29
32
 
30
-
31
- async function* searchDeepDependencies(packageName, gitURL, options) {
32
- const isGit = typeof gitURL === "string";
33
+ export async function* searchDeepDependencies(packageName, gitURL, options) {
33
34
  const { exclude, currDepth = 0, parent, maxDepth } = options;
34
35
 
35
- const { name, version, deprecated, ...pkg } = await pacote.manifest(isGit ? gitURL : packageName, {
36
- ...constants.NPM_TOKEN,
36
+ const { name, version, deprecated, ...pkg } = await pacote.manifest(gitURL ?? packageName, {
37
+ ...NPM_TOKEN,
37
38
  registry: getLocalRegistryURL(),
38
39
  cache: `${os.homedir()}/.npm`
39
40
  });
40
41
  const { dependencies, customResolvers } = mergeDependencies(pkg);
41
42
 
42
43
  const current = new Dependency(name, version, parent);
43
- isGit && current.isGit(gitURL);
44
+ gitURL !== null && current.isGit(gitURL);
44
45
  current.addFlag("isDeprecated", deprecated === true);
45
46
  current.addFlag("hasCustomResolver", customResolvers.size > 0);
46
47
  current.addFlag("hasDependencies", dependencies.size > 0);
@@ -50,9 +51,9 @@ async function* searchDeepDependencies(packageName, gitURL, options) {
50
51
  exclude, currDepth: currDepth + 1, parent: current, maxDepth
51
52
  };
52
53
 
53
- const gitDependencies = iter.filter(customResolvers.entries(), ([, valueStr]) => valueStr.startsWith("git+"));
54
+ const gitDependencies = iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr));
54
55
  for (const [depName, valueStr] of gitDependencies) {
55
- yield* searchDeepDependencies(depName, valueStr.slice(4), config);
56
+ yield* searchDeepDependencies(depName, valueStr, config);
56
57
  }
57
58
 
58
59
  const depsNames = await Promise.all(iter.map(dependencies.entries(), getCleanDependencyName));
@@ -66,7 +67,7 @@ async function* searchDeepDependencies(packageName, gitURL, options) {
66
67
  }
67
68
  else {
68
69
  exclude.set(cleanName, new Set([current.fullName]));
69
- yield* searchDeepDependencies(fullName, void 0, config);
70
+ yield* searchDeepDependencies(fullName, null, config);
70
71
  }
71
72
  }
72
73
  }
@@ -74,7 +75,7 @@ async function* searchDeepDependencies(packageName, gitURL, options) {
74
75
  yield current;
75
76
  }
76
77
 
77
- async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLockMode }) {
78
+ export async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLockMode }) {
78
79
  const { version, integrity = to.integrity } = to.package;
79
80
 
80
81
  const updatedVersion = version === "*" || typeof version === "undefined" ? "latest" : version;
@@ -82,7 +83,7 @@ async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLoc
82
83
 
83
84
  if (fullLockMode) {
84
85
  const { deprecated, _integrity, ...pkg } = await pacote.manifest(`${currentPackageName}@${updatedVersion}`, {
85
- ...constants.NPM_TOKEN,
86
+ ...NPM_TOKEN,
86
87
  registry: getLocalRegistryURL(),
87
88
  cache: `${os.homedir()}/.npm`
88
89
  });
@@ -111,64 +112,7 @@ async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLoc
111
112
  yield current;
112
113
  }
113
114
 
114
- async function fetchPackageMetadata(name, version, options) {
115
- const { ref, locker } = options;
116
- const free = await locker.acquireOne();
117
-
118
- try {
119
- const pkg = await packument(name);
120
-
121
- const publishers = new Set();
122
- const oneYearFromToday = new Date();
123
- oneYearFromToday.setFullYear(oneYearFromToday.getFullYear() - 1);
124
-
125
- ref.metadata.lastVersion = pkg["dist-tags"].latest;
126
- if (semver.neq(version, ref.metadata.lastVersion)) {
127
- ref[version].flags.push("isOutdated");
128
- }
129
- ref.metadata.publishedCount = Object.values(pkg.versions).length;
130
- ref.metadata.lastUpdateAt = new Date(pkg.time[ref.metadata.lastVersion]);
131
- ref.metadata.hasReceivedUpdateInOneYear = !(oneYearFromToday > ref.metadata.lastUpdateAt);
132
- ref.metadata.homepage = pkg.homepage || null;
133
- ref.metadata.maintainers = pkg.maintainers;
134
- if (typeof pkg.author === "string") {
135
- ref.metadata.author = parseManifestAuthor(pkg.author);
136
- }
137
- else {
138
- ref.metadata.author = pkg.author;
139
- }
140
- const authorName = ref.metadata.author?.name ?? null;
141
-
142
- for (const ver of Object.values(pkg.versions)) {
143
- const { _npmUser: npmUser, version } = ver;
144
-
145
- const isNullOrUndefined = typeof npmUser === "undefined" || npmUser === null;
146
- if (isNullOrUndefined || !("name" in npmUser) || typeof npmUser.name !== "string") {
147
- continue;
148
- }
149
-
150
- if (authorName === null) {
151
- ref.metadata.author.name = npmUser.name;
152
- }
153
- else if (npmUser.name !== ref.metadata.author.name) {
154
- ref.metadata.hasManyPublishers = true;
155
- }
156
-
157
- if (!publishers.has(npmUser.name)) {
158
- publishers.add(npmUser.name);
159
- ref.metadata.publishers.push({ name: npmUser.name, version, at: new Date(pkg.time[version]) });
160
- }
161
- }
162
- }
163
- catch (err) {
164
- // Ignore
165
- }
166
- finally {
167
- free();
168
- }
169
- }
170
-
171
- async function* getRootDependencies(manifest, options) {
115
+ export async function* getRootDependencies(manifest, options) {
172
116
  const { maxDepth = 4, exclude, usePackageLock, fullLockMode } = options;
173
117
 
174
118
  const { dependencies, customResolvers } = mergeDependencies(manifest, void 0);
@@ -179,7 +123,7 @@ async function* getRootDependencies(manifest, options) {
179
123
  let iterators;
180
124
  if (usePackageLock) {
181
125
  const arb = new Arborist({
182
- ...constants.NPM_TOKEN,
126
+ ...NPM_TOKEN,
183
127
  registry: getLocalRegistryURL()
184
128
  });
185
129
  let tree;
@@ -191,15 +135,15 @@ async function* getRootDependencies(manifest, options) {
191
135
  tree = await arb.loadVirtual();
192
136
  }
193
137
 
194
- iterators = iter.filter(tree.edgesOut.entries(), ([, { to }]) => !to.dev)
138
+ iterators = iter.filter(tree.edgesOut.entries(), ([, { to }]) => to !== null && !to.dev)
195
139
  .map(([packageName, { to }]) => deepReadEdges(packageName, { to, parent, fullLockMode, exclude }));
196
140
  }
197
141
  else {
198
142
  const configRef = { exclude, maxDepth, parent };
199
143
  iterators = [
200
- ...iter.filter(customResolvers.entries(), ([, valueStr]) => valueStr.startsWith("git+"))
201
- .map(([depName, valueStr]) => searchDeepDependencies(depName, valueStr.slice(4), configRef)),
202
- ...iter.map(dependencies.entries(), ([name, ver]) => searchDeepDependencies(`${name}@${ver}`, void 0, configRef))
144
+ ...iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr))
145
+ .map(([depName, valueStr]) => searchDeepDependencies(depName, valueStr, configRef)),
146
+ ...iter.map(dependencies.entries(), ([name, ver]) => searchDeepDependencies(`${name}@${ver}`, null, configRef))
203
147
  ];
204
148
  }
205
149
  for await (const dep of combineAsyncIterators({}, ...iterators)) {
@@ -221,26 +165,6 @@ async function* getRootDependencies(manifest, options) {
221
165
  yield parent;
222
166
  }
223
167
 
224
- function* addMissingVersionFlags(flags, descriptor) {
225
- const { metadata, vulnerabilities = [], versions } = descriptor;
226
-
227
- if (!metadata.hasReceivedUpdateInOneYear && flags.has("hasOutdatedDependency") && !flags.has("isDead")) {
228
- yield "isDead";
229
- }
230
- if (metadata.hasManyPublishers && !flags.has("hasManyPublishers")) {
231
- yield "hasManyPublishers";
232
- }
233
- if (metadata.hasChangedAuthor && !flags.has("hasChangedAuthor")) {
234
- yield "hasChangedAuthor";
235
- }
236
- if (vulnerabilities.length > 0 && !flags.has("hasVulnerabilities")) {
237
- yield "hasVulnerabilities";
238
- }
239
- if (versions.length > 1 && !flags.has("hasDuplicate")) {
240
- yield "hasDuplicate";
241
- }
242
- }
243
-
244
168
  /**
245
169
  * @param {*} manifest
246
170
  * @param {*} options
@@ -260,59 +184,68 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
260
184
 
261
185
  const payload = {
262
186
  id: tmpLocation.slice(-6),
263
- rootDepencyName: manifest.name,
264
- warnings: [],
265
- dependencies: new Map(),
266
- version: packageVersion
187
+ rootDependencyName: manifest.name,
188
+ scannerVersion: packageVersion,
189
+ vulnerabilityStrategy,
190
+ warnings: []
267
191
  };
268
192
 
269
193
  // We are dealing with an exclude Map to avoid checking a package more than one time in searchDeepDependencies
270
194
  const exclude = new Map();
195
+ const dependencies = new Map();
271
196
 
272
197
  {
273
- logger.start("walkTree").start("tarball").start("registry");
198
+ logger
199
+ .start(ScannerLoggerEvents.analysis.tree)
200
+ .start(ScannerLoggerEvents.analysis.tarball)
201
+ .start(ScannerLoggerEvents.analysis.registry);
202
+ const fetchedMetadataPackages = new Set();
274
203
  const promisesToWait = [];
275
- const rootDepsOptions = { maxDepth, exclude, usePackageLock, fullLockMode };
276
204
 
277
205
  const tarballLocker = new Lock({ maxConcurrent: 5 });
278
- const metadataLocker = new Lock({ maxConcurrent: 10 });
279
- metadataLocker.on("freeOne", () => logger.tick("registry"));
280
- tarballLocker.on("freeOne", () => logger.tick("tarball"));
206
+ tarballLocker.on("freeOne", () => logger.tick(ScannerLoggerEvents.analysis.tarball));
281
207
 
208
+ const rootDepsOptions = { maxDepth, exclude, usePackageLock, fullLockMode };
282
209
  for await (const currentDep of getRootDependencies(manifest, rootDepsOptions)) {
283
210
  const { name, version } = currentDep;
284
211
  const current = currentDep.exportAsPlainObject(name === manifest.name ? 0 : void 0);
285
212
  let proceedDependencyAnalysis = true;
286
213
 
287
- if (payload.dependencies.has(name)) {
214
+ if (dependencies.has(name)) {
288
215
  // TODO: how to handle different metadata ?
289
- const dep = payload.dependencies.get(name);
216
+ const dep = dependencies.get(name);
290
217
 
291
- const currVersion = current.versions[0];
292
- if (currVersion in dep) {
218
+ const currVersion = Object.keys(current.versions)[0];
219
+ if (currVersion in dep.versions) {
293
220
  // The dependency has already entered the analysis
294
221
  // This happens if the package is used by multiple packages in the tree
295
222
  proceedDependencyAnalysis = false;
296
223
  }
297
224
  else {
298
- dep[currVersion] = current[currVersion];
299
- dep.versions.push(currVersion);
225
+ dep.versions[currVersion] = current.versions[currVersion];
300
226
  }
301
227
  }
302
228
  else {
303
- payload.dependencies.set(name, current);
229
+ dependencies.set(name, current);
304
230
  }
305
231
 
306
232
  if (proceedDependencyAnalysis) {
307
- logger.tick("walkTree");
233
+ logger.tick(ScannerLoggerEvents.analysis.tree);
234
+
235
+ // There is no need to fetch 'N' times the npm metadata for the same package.
236
+ if (fetchedMetadataPackages.has(name)) {
237
+ logger.tick(ScannerLoggerEvents.analysis.registry);
238
+ }
239
+ else {
240
+ fetchedMetadataPackages.add(name);
241
+ promisesToWait.push(packageMetadata(name, version, {
242
+ ref: current,
243
+ logger
244
+ }));
245
+ }
308
246
 
309
- promisesToWait.push(fetchPackageMetadata(name, version, {
310
- ref: current,
311
- locker: metadataLocker,
312
- logger
313
- }));
314
247
  promisesToWait.push(scanDirOrArchive(name, version, {
315
- ref: current[version],
248
+ ref: current.versions[version],
316
249
  tmpLocation: forceRootAnalysis && name === manifest.name ? null : tmpLocation,
317
250
  locker: tarballLocker,
318
251
  logger
@@ -320,17 +253,17 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
320
253
  }
321
254
  }
322
255
 
323
- logger.end("walkTree");
256
+ logger.end(ScannerLoggerEvents.analysis.tree);
324
257
 
325
258
  // Wait for all extraction to be done!
326
259
  await Promise.allSettled(promisesToWait);
327
260
  await timers.setImmediate();
328
261
 
329
- logger.end("tarball").end("registry");
262
+ logger.end(ScannerLoggerEvents.analysis.tarball).end(ScannerLoggerEvents.analysis.registry);
330
263
  }
331
264
 
332
265
  const { hydratePayloadDependencies, strategy } = await vuln.setStrategy(vulnerabilityStrategy);
333
- await hydratePayloadDependencies(payload.dependencies, {
266
+ await hydratePayloadDependencies(dependencies, {
334
267
  useStandardFormat: true
335
268
  });
336
269
 
@@ -338,10 +271,9 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
338
271
 
339
272
  // We do this because it "seem" impossible to link all dependencies in the first walk.
340
273
  // Because we are dealing with package only one time it may happen sometimes.
341
- for (const [packageName, descriptor] of payload.dependencies) {
342
- for (const verStr of descriptor.versions) {
343
- const verDescriptor = descriptor[verStr];
344
- verDescriptor.flags.push(...addMissingVersionFlags(new Set(verDescriptor.flags), descriptor));
274
+ for (const [packageName, dependency] of dependencies) {
275
+ for (const [verStr, verDescriptor] of Object.entries(dependency.versions)) {
276
+ verDescriptor.flags.push(...addMissingVersionFlags(new Set(verDescriptor.flags), dependency));
345
277
 
346
278
  const fullName = `${packageName}@${verStr}`;
347
279
  const usedDeps = exclude.get(fullName) || new Set();
@@ -358,13 +290,15 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
358
290
  }
359
291
 
360
292
  try {
361
- payload.warnings = getDependenciesWarnings(payload.dependencies);
362
- payload.dependencies = Object.fromEntries(payload.dependencies);
293
+ payload.warnings = getDependenciesWarnings(dependencies);
294
+ payload.dependencies = Object.fromEntries(dependencies);
363
295
 
364
296
  return payload;
365
297
  }
366
298
  finally {
367
299
  await timers.setImmediate();
368
300
  await fs.rm(tmpLocation, { recursive: true, force: true });
301
+
302
+ logger.emit(ScannerLoggerEvents.done);
369
303
  }
370
304
  }
@@ -0,0 +1,57 @@
1
+ // Import Node.js Dependencies
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
+ // Import Third-party Dependencies
6
+ import { parseManifestAuthor } from "@nodesecure/utils";
7
+
8
+ // CONSTANTS
9
+ // PR welcome to contribute to this list!
10
+ const kNativeNpmPackages = new Set([
11
+ "node-gyp", "node-pre-gyp", "node-gyp-build", "node-addon-api"
12
+ ]);
13
+
14
+ /**
15
+ * @see https://www.nerdycode.com/prevent-npm-executing-scripts-security/
16
+ */
17
+ const kUnsafeNpmScripts = new Set([
18
+ "install",
19
+ "preinstall", "postinstall",
20
+ "preuninstall", "postuninstall"
21
+ ]);
22
+
23
+ /**
24
+ * @param {!string} location
25
+ * @returns {import("@npm/types").PackageJson}
26
+ */
27
+ export async function read(location) {
28
+ const packageStr = await fs.readFile(
29
+ path.join(location, "package.json"),
30
+ "utf-8"
31
+ );
32
+
33
+ return JSON.parse(packageStr);
34
+ }
35
+
36
+ // TODO: PR @npm/types to fix dependencies typo
37
+ export async function readAnalyze(location) {
38
+ const {
39
+ description = "", author = {}, scripts = {},
40
+ dependencies = {}, devDependencies = {}, gypfile = false
41
+ } = await read(location);
42
+
43
+ const packageDeps = Object.keys(dependencies);
44
+ const packageDevDeps = Object.keys(devDependencies);
45
+ const hasNativePackage = [...packageDevDeps, ...packageDeps]
46
+ .some((pkg) => kNativeNpmPackages.has(pkg));
47
+
48
+ return {
49
+ author: typeof author === "string" ? parseManifestAuthor(author) : author,
50
+ description,
51
+ hasScript: Object.keys(scripts)
52
+ .some((value) => kUnsafeNpmScripts.has(value.toLowerCase())),
53
+ packageDeps,
54
+ packageDevDeps,
55
+ hasNativeElements: hasNativePackage || gypfile
56
+ };
57
+ }