@nodesecure/scanner 5.0.1 → 5.2.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
 
@@ -78,7 +78,7 @@ interface Options {
78
78
  ## Contributors ✨
79
79
 
80
80
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
81
- [![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)
81
+ [![All Contributors](https://img.shields.io/badge/all_contributors-12-orange.svg?style=flat-square)](#contributors-)
82
82
  <!-- ALL-CONTRIBUTORS-BADGE:END -->
83
83
 
84
84
  Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@@ -102,6 +102,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
102
102
  <td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/ange-tekeu-a155811b4/"><img src="https://avatars.githubusercontent.com/u/35274201?v=4?s=100" width="100px;" alt="Ange TEKEU"/><br /><sub><b>Ange TEKEU</b></sub></a><br /><a href="https://github.com/NodeSecure/scanner/commits?author=tekeuange23" title="Code">💻</a></td>
103
103
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Kawacrepe"><img src="https://avatars.githubusercontent.com/u/40260517?v=4?s=100" width="100px;" alt="Vincent Dhennin"/><br /><sub><b>Vincent Dhennin</b></sub></a><br /><a href="https://github.com/NodeSecure/scanner/commits?author=Kawacrepe" title="Code">💻</a></td>
104
104
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/fabnguess"><img src="https://avatars.githubusercontent.com/u/72697416?v=4?s=100" width="100px;" alt="Kouadio Fabrice Nguessan"/><br /><sub><b>Kouadio Fabrice Nguessan</b></sub></a><br /><a href="#maintenance-fabnguess" title="Maintenance">🚧</a></td>
105
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/PierreDemailly"><img src="https://avatars.githubusercontent.com/u/39910767?v=4?s=100" width="100px;" alt="PierreDemailly"/><br /><sub><b>PierreDemailly</b></sub></a><br /><a href="https://github.com/NodeSecure/scanner/commits?author=PierreDemailly" title="Code">💻</a> <a href="https://github.com/NodeSecure/scanner/pulls?q=is%3Apr+reviewed-by%3APierreDemailly" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/NodeSecure/scanner/issues?q=author%3APierreDemailly" title="Bug reports">🐛</a> <a href="https://github.com/NodeSecure/scanner/commits?author=PierreDemailly" title="Tests">⚠️</a></td>
105
106
  </tr>
106
107
  </tbody>
107
108
  </table>
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@nodesecure/scanner",
3
- "version": "5.0.1",
3
+ "version": "5.2.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",
@@ -50,36 +50,36 @@
50
50
  },
51
51
  "homepage": "https://github.com/NodeSecure/scanner#readme",
52
52
  "devDependencies": {
53
- "@nodesecure/eslint-config": "^1.7.0",
53
+ "@nodesecure/eslint-config": "^1.8.0",
54
54
  "@slimio/is": "^2.0.0",
55
- "@types/node": "^20.4.5",
56
- "c8": "^7.13.0",
57
- "dotenv": "^16.0.3",
58
- "eslint": "^8.37.0",
55
+ "@types/node": "^20.10.0",
56
+ "c8": "^8.0.1",
57
+ "dotenv": "^16.3.1",
58
+ "eslint": "8.37.0",
59
59
  "get-folder-size": "^4.0.0",
60
- "glob": "^10.3.4",
60
+ "glob": "^10.3.10",
61
61
  "pkg-ok": "^3.0.0",
62
- "sinon": "^15.0.3",
62
+ "sinon": "^17.0.1",
63
63
  "snap-shot-core": "^10.2.4"
64
64
  },
65
65
  "dependencies": {
66
66
  "@nodesecure/authors": "^1.0.2",
67
67
  "@nodesecure/flags": "^2.4.0",
68
68
  "@nodesecure/fs-walk": "^1.0.0",
69
- "@nodesecure/i18n": "^3.3.0",
70
- "@nodesecure/js-x-ray": "^6.0.1",
71
- "@nodesecure/npm-registry-sdk": "^1.5.2",
69
+ "@nodesecure/i18n": "^3.5.0",
70
+ "@nodesecure/js-x-ray": "^6.3.0",
71
+ "@nodesecure/npm-registry-sdk": "^2.0.0",
72
72
  "@nodesecure/ntlp": "^2.2.1",
73
73
  "@nodesecure/vuln": "^1.7.0",
74
74
  "@npm/types": "^1.0.2",
75
- "@npmcli/arborist": "^6.2.6",
75
+ "@npmcli/arborist": "^7.2.1",
76
76
  "@slimio/lock": "^1.0.0",
77
77
  "builtins": "^5.0.1",
78
- "combine-async-iterators": "^2.0.1",
79
- "itertools": "^2.1.1",
78
+ "combine-async-iterators": "^2.1.0",
79
+ "itertools": "^2.1.2",
80
80
  "lodash.difference": "^4.5.0",
81
- "pacote": "^15.1.1",
82
- "semver": "^7.3.8"
81
+ "pacote": "^17.0.4",
82
+ "semver": "^7.5.4"
83
83
  },
84
84
  "type": "module"
85
85
  }
package/src/depWalker.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  NPM_TOKEN
20
20
  } from "./utils/index.js";
21
21
  import { scanDirOrArchive } from "./tarball.js";
22
- import { packageMetadata } from "./npmRegistry.js";
22
+ import { packageMetadata, manifestMetadata } from "./npmRegistry.js";
23
23
  import Dependency from "./class/dependency.class.js";
24
24
  import Logger from "./class/logger.class.js";
25
25
 
@@ -262,12 +262,13 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
262
262
  const rootDepsOptions = { maxDepth, exclude, usePackageLock, fullLockMode, includeDevDeps, location, registry };
263
263
  for await (const currentDep of getRootDependencies(manifest, rootDepsOptions)) {
264
264
  const { name, version, dev } = currentDep;
265
+
265
266
  const current = currentDep.exportAsPlainObject(name === manifest.name ? 0 : void 0);
266
267
  let proceedDependencyAnalysis = true;
267
268
 
268
269
  if (dependencies.has(name)) {
269
- // TODO: how to handle different metadata ?
270
270
  const dep = dependencies.get(name);
271
+ promisesToWait.push(manifestMetadata(name, version, dep.metadata));
271
272
 
272
273
  const currVersion = Object.keys(current.versions)[0];
273
274
  if (currVersion in dep.versions) {
@@ -292,7 +293,7 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
292
293
  logger.tick(ScannerLoggerEvents.analysis.tree);
293
294
 
294
295
  // There is no need to fetch 'N' times the npm metadata for the same package.
295
- if (fetchedMetadataPackages.has(name)) {
296
+ if (fetchedMetadataPackages.has(name) || !current.versions[version].existOnRemoteRegistry) {
296
297
  logger.tick(ScannerLoggerEvents.analysis.registry);
297
298
  }
298
299
  else {
@@ -333,7 +334,27 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
333
334
 
334
335
  // We do this because it "seem" impossible to link all dependencies in the first walk.
335
336
  // Because we are dealing with package only one time it may happen sometimes.
337
+ const globalWarnings = [];
336
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
+
344
+ const isEmptyPackage = dependencyVer.warnings
345
+ .some((warning) => warning.kind === "empty-package");
346
+ if (isEmptyPackage) {
347
+ globalWarnings.push(`${packageName}@${version} only contain a package.json file!`);
348
+ }
349
+
350
+ if (!("integrity" in dependencyVer) || dependencyVer.flags.includes("isGit")) {
351
+ continue;
352
+ }
353
+
354
+ if (dependencyVer.integrity !== integrity) {
355
+ globalWarnings.push(`${packageName}@${version} manifest & tarball integrity doesn't match!`);
356
+ }
357
+ }
337
358
  for (const [verStr, verDescriptor] of Object.entries(dependency.versions)) {
338
359
  verDescriptor.flags.push(...addMissingVersionFlags(new Set(verDescriptor.flags), dependency));
339
360
 
@@ -352,7 +373,7 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
352
373
 
353
374
  try {
354
375
  const { warnings, flaggedAuthors } = await getDependenciesWarnings(dependencies);
355
- payload.warnings = warnings;
376
+ payload.warnings = globalWarnings.concat(warnings);
356
377
  payload.flaggedAuthors = flaggedAuthors;
357
378
  payload.dependencies = Object.fromEntries(dependencies);
358
379
 
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,39 @@ 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
+ if (scriptValue.startsWith(kNodemodulesBinPrefix)) {
58
+ scripts[scriptName] = scriptValue.replaceAll(kNodemodulesBinPrefix, "");
59
+ }
60
+ }
61
+
62
+ const integrityObj = {
63
+ name,
64
+ version,
65
+ dependencies,
66
+ license,
67
+ scripts
68
+ };
69
+
70
+ const integrity = crypto
71
+ .createHash("sha256")
72
+ .update(JSON.stringify(integrityObj))
73
+ .digest("hex");
74
+
47
75
  const packageDeps = Object.keys(dependencies);
48
76
  const packageDevDeps = Object.keys(devDependencies);
49
77
  const hasNativePackage = [...packageDevDeps, ...packageDeps]
@@ -60,6 +88,7 @@ export async function readAnalyze(location) {
60
88
  packageDeps,
61
89
  packageDevDeps,
62
90
  nodejs: { imports },
63
- hasNativeElements: hasNativePackage || gypfile
91
+ hasNativeElements: hasNativePackage || gypfile,
92
+ integrity
64
93
  };
65
94
  }
@@ -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
- import { parseAuthor } from "./utils/index.js";
9
+ import { parseAuthor, getLinks } from "./utils/index.js";
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
+ }
7
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,18 +40,32 @@ 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);
48
+ const flags = ref.versions[version].flags;
31
49
  if (isOutdated) {
32
- ref.versions[version].flags.push("isOutdated");
50
+ flags.push("isOutdated");
33
51
  }
34
52
 
35
53
  const publishers = new Set();
36
54
  let searchForMaintainersInVersions = metadata.maintainers.length === 0;
37
55
  for (const ver of Object.values(pkg.versions).reverse()) {
56
+ const versionSpec = `${ver.name}:${ver.version}`;
57
+ if (packageSpec === versionSpec) {
58
+ if (ver.deprecated && !flags.includes("isDeprecated")) {
59
+ flags.push("isDeprecated");
60
+ }
61
+
62
+ metadata.integrity[ver.version] = getPackumentVersionIntegrity(
63
+ ver
64
+ );
65
+ }
66
+
38
67
  const { _npmUser: npmUser, version, maintainers = [] } = ver;
68
+
39
69
  const isNullOrUndefined = typeof npmUser === "undefined" || npmUser === null;
40
70
  if (isNullOrUndefined || !("name" in npmUser) || typeof npmUser.name !== "string") {
41
71
  continue;
@@ -61,6 +91,7 @@ export async function packageMetadata(name, version, options) {
61
91
  }
62
92
  }
63
93
 
94
+ Object.assign(ref.versions[version], { links: getLinks(pkg.versions[version]) });
64
95
  Object.assign(ref.metadata, metadata);
65
96
  }
66
97
  catch {
@@ -70,3 +101,25 @@ export async function packageMetadata(name, version, options) {
70
101
  logger.tick("registry");
71
102
  }
72
103
  }
104
+
105
+ function getPackumentVersionIntegrity(packumentVersion) {
106
+ const { name, version, dependencies = {}, license = "", scripts = {} } = packumentVersion;
107
+
108
+ // See https://github.com/npm/cli/issues/5234
109
+ if ("install" in dependencies && dependencies.install === "node-gyp rebuild") {
110
+ delete dependencies.install;
111
+ }
112
+
113
+ const integrityObj = {
114
+ name,
115
+ version,
116
+ dependencies,
117
+ license,
118
+ scripts
119
+ };
120
+
121
+ return crypto
122
+ .createHash("sha256")
123
+ .update(JSON.stringify(integrityObj))
124
+ .digest("hex");
125
+ }
package/src/tarball.js CHANGED
@@ -67,18 +67,42 @@ export async function scanDirOrArchive(name, version, options) {
67
67
  });
68
68
  await timers.setImmediate();
69
69
  }
70
+ else {
71
+ // Set links to an empty object because theses are generated only for NPM tarballs
72
+ Object.assign(ref, { links: {} });
73
+ }
70
74
 
71
75
  // Read the package.json at the root of the directory or archive.
72
76
  const {
73
77
  packageDeps,
74
78
  packageDevDeps,
75
- author, description, hasScript, hasNativeElements, nodejs,
76
- engines, repository, scripts
79
+ author,
80
+ description,
81
+ hasScript,
82
+ hasNativeElements,
83
+ nodejs,
84
+ engines,
85
+ repository,
86
+ scripts,
87
+ integrity
77
88
  } = await manifest.readAnalyze(dest);
78
- Object.assign(ref, { author, description, engines, repository, scripts });
89
+ Object.assign(ref, {
90
+ author, description, engines, repository, scripts, integrity
91
+ });
79
92
 
80
93
  // Get the composition of the (extracted) directory
81
94
  const { ext, files, size } = await getTarballComposition(dest);
95
+ if (files.length === 1 && files.includes("package.json")) {
96
+ ref.warnings.push({
97
+ kind: "empty-package",
98
+ location: null,
99
+ i18n: "sast_warnings.emptyPackage",
100
+ severity: "Critical",
101
+ source: "Scanner",
102
+ experimental: false
103
+ });
104
+ }
105
+
82
106
  ref.size = size;
83
107
  ref.composition.extensions.push(...ext);
84
108
  ref.composition.files.push(...files);
@@ -0,0 +1,31 @@
1
+ // CONSTANTS
2
+ const kVCSHosts = new Set(["github.com", "gitlab.com"]);
3
+
4
+ function getVCSRepositoryURL(link) {
5
+ try {
6
+ const url = new URL(link);
7
+ const { hostname, pathname } = url;
8
+
9
+ if (kVCSHosts.has(hostname) === false) {
10
+ return null;
11
+ }
12
+
13
+ const [owner, repo] = pathname.split("/").filter(Boolean).map((curr) => curr.replace(".git", ""));
14
+
15
+ return `https://${hostname}/${owner}/${repo}`;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export function getLinks(pkg) {
23
+ const homepage = pkg.homepage || null;
24
+ const repositoryUrl = pkg.repository?.url || null;
25
+
26
+ return {
27
+ npm: `https://www.npmjs.com/package/${pkg.name}/v/${pkg.version}`,
28
+ homepage,
29
+ repository: getVCSRepositoryURL(homepage) ?? getVCSRepositoryURL(repositoryUrl)
30
+ };
31
+ }
@@ -11,6 +11,7 @@ export * from "./analyzeDependencies.js";
11
11
  export * from "./booleanToFlags.js";
12
12
  export * from "./addMissingVersionFlags.js";
13
13
  export * from "./parseManifestAuthor.js";
14
+ export * from "./getLinks.js";
14
15
 
15
16
  export const NPM_TOKEN = typeof process.env.NODE_SECURE_TOKEN === "string" ?
16
17
  { token: process.env.NODE_SECURE_TOKEN } :
@@ -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
 
@@ -40,6 +40,15 @@ declare namespace Scanner {
40
40
  at: string;
41
41
  }
42
42
 
43
+ export interface DependencyLinks {
44
+ /** NPM Registry page */
45
+ npm: string;
46
+ /** Homepage URL */
47
+ homepage?: string;
48
+ /** VCS repository URL */
49
+ repository?: string;
50
+ }
51
+
43
52
  export interface DependencyVersion {
44
53
  /** Id of the package (useful for usedBy relation) */
45
54
  id: number;
@@ -104,6 +113,14 @@ declare namespace Scanner {
104
113
  * If the dependency is a GIT repository
105
114
  */
106
115
  gitUrl: null | string;
116
+ /**
117
+ * Version MD5 integrity hash
118
+ * Generated by the scanner to verify manifest/tarball confusion
119
+ *
120
+ * (Not supported on GIT dependency)
121
+ */
122
+ integrity?: string;
123
+ links: DependencyLinks;
107
124
  }
108
125
 
109
126
  export interface Dependency {
@@ -131,6 +148,11 @@ declare namespace Scanner {
131
148
  * List of people who published this package
132
149
  */
133
150
  publishers: Publisher[];
151
+ /**
152
+ * Version MD5 integrity hash
153
+ * Generated by the scanner to verify manifest/tarball confusion
154
+ */
155
+ integrity: Record<string, string>;
134
156
  }
135
157
  /** List of versions of this package available in the dependency tree (In the payload) */
136
158
  versions: Record<string, DependencyVersion>;