@nodesecure/scanner 5.0.1 → 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 +1 -1
- package/package.json +16 -16
- package/src/depWalker.js +17 -3
- package/src/manifest.js +32 -5
- package/src/npmRegistry.js +48 -2
- package/src/tarball.js +12 -3
- package/src/utils/semver.js +3 -2
- package/src/utils/warnings.js +1 -1
- package/types/scanner.d.ts +12 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodesecure/scanner",
|
|
3
|
-
"version": "5.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": ">=
|
|
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.
|
|
53
|
+
"@nodesecure/eslint-config": "^1.8.0",
|
|
54
54
|
"@slimio/is": "^2.0.0",
|
|
55
|
-
"@types/node": "^20.
|
|
56
|
-
"c8": "^
|
|
57
|
-
"dotenv": "^16.
|
|
55
|
+
"@types/node": "^20.10.0",
|
|
56
|
+
"c8": "^8.0.1",
|
|
57
|
+
"dotenv": "^16.3.1",
|
|
58
58
|
"eslint": "^8.37.0",
|
|
59
59
|
"get-folder-size": "^4.0.0",
|
|
60
|
-
"glob": "^10.3.
|
|
60
|
+
"glob": "^10.3.10",
|
|
61
61
|
"pkg-ok": "^3.0.0",
|
|
62
|
-
"sinon": "^
|
|
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.
|
|
70
|
-
"@nodesecure/js-x-ray": "^6.0
|
|
71
|
-
"@nodesecure/npm-registry-sdk": "^1.
|
|
69
|
+
"@nodesecure/i18n": "^3.4.0",
|
|
70
|
+
"@nodesecure/js-x-ray": "^6.2.0",
|
|
71
|
+
"@nodesecure/npm-registry-sdk": "^1.6.1",
|
|
72
72
|
"@nodesecure/ntlp": "^2.2.1",
|
|
73
73
|
"@nodesecure/vuln": "^1.7.0",
|
|
74
74
|
"@npm/types": "^1.0.2",
|
|
75
|
-
"@npmcli/arborist": "^
|
|
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
|
|
79
|
-
"itertools": "^2.1.
|
|
78
|
+
"combine-async-iterators": "^2.1.0",
|
|
79
|
+
"itertools": "^2.1.2",
|
|
80
80
|
"lodash.difference": "^4.5.0",
|
|
81
|
-
"pacote": "^
|
|
82
|
-
"semver": "^7.
|
|
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) {
|
|
@@ -333,7 +334,20 @@ 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
|
+
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
|
+
}
|
|
337
351
|
for (const [verStr, verDescriptor] of Object.entries(dependency.versions)) {
|
|
338
352
|
verDescriptor.flags.push(...addMissingVersionFlags(new Set(verDescriptor.flags), dependency));
|
|
339
353
|
|
|
@@ -352,7 +366,7 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
|
|
|
352
366
|
|
|
353
367
|
try {
|
|
354
368
|
const { warnings, flaggedAuthors } = await getDependenciesWarnings(dependencies);
|
|
355
|
-
payload.warnings = warnings;
|
|
369
|
+
payload.warnings = globalWarnings.concat(warnings);
|
|
356
370
|
payload.flaggedAuthors = flaggedAuthors;
|
|
357
371
|
payload.dependencies = Object.fromEntries(dependencies);
|
|
358
372
|
|
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
|
-
|
|
41
|
-
|
|
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
|
}
|
package/src/npmRegistry.js
CHANGED
|
@@ -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,
|
|
76
|
-
|
|
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, {
|
|
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);
|
package/src/utils/semver.js
CHANGED
|
@@ -50,12 +50,13 @@ export async function getCleanDependencyName([depName, range]) {
|
|
|
50
50
|
|
|
51
51
|
export function getSemVerWarning(value) {
|
|
52
52
|
return {
|
|
53
|
-
kind: "
|
|
53
|
+
kind: "zero-semver",
|
|
54
54
|
file: "package.json",
|
|
55
55
|
value,
|
|
56
56
|
location: null,
|
|
57
|
-
i18n: "sast_warnings.
|
|
57
|
+
i18n: "sast_warnings.zeroSemVer",
|
|
58
58
|
severity: "Information",
|
|
59
|
+
source: "Scanner",
|
|
59
60
|
experimental: false
|
|
60
61
|
};
|
|
61
62
|
}
|
package/src/utils/warnings.js
CHANGED
|
@@ -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
|
|
package/types/scanner.d.ts
CHANGED
|
@@ -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>;
|