@nodesecure/scanner 2.0.0 → 3.0.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 +1 -1
- package/README.md +95 -94
- package/index.js +4 -4
- package/package.json +19 -36
- package/src/{dependency.class.js → class/dependency.class.js} +22 -21
- package/src/{logger.class.js → class/logger.class.js} +0 -0
- package/src/depWalker.js +298 -347
- package/src/manifest.js +57 -0
- package/src/npmRegistry.js +68 -0
- package/src/tarball.js +85 -188
- package/src/utils/addMissingVersionFlags.js +24 -0
- package/src/utils/analyzeDependencies.js +40 -0
- package/src/utils/booleanToFlags.js +12 -0
- package/src/utils/filterDependencyKind.js +44 -0
- package/src/utils/getPackageName.js +18 -2
- package/src/utils/getTarballComposition.js +8 -10
- package/src/utils/index.js +16 -14
- package/src/utils/isGitDependency.js +20 -0
- package/src/utils/isSensitiveFile.js +6 -0
- package/src/utils/mergeDependencies.js +26 -23
- package/src/utils/semver.js +3 -3
- package/types/logger.d.ts +2 -0
- package/types/scanner.d.ts +128 -32
package/src/manifest.js
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Import Third-party Dependencies
|
|
2
|
+
import semver from "semver";
|
|
3
|
+
import { parseManifestAuthor } from "@nodesecure/utils";
|
|
4
|
+
import { packument } from "@nodesecure/npm-registry-sdk";
|
|
5
|
+
|
|
6
|
+
export function parseAuthor(author) {
|
|
7
|
+
return typeof author === "string" ? parseManifestAuthor(author) : author;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function packageMetadata(name, version, options) {
|
|
11
|
+
const { ref, logger } = options;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const pkg = await packument(name);
|
|
15
|
+
|
|
16
|
+
const oneYearFromToday = new Date();
|
|
17
|
+
oneYearFromToday.setFullYear(oneYearFromToday.getFullYear() - 1);
|
|
18
|
+
|
|
19
|
+
const lastVersion = pkg["dist-tags"].latest;
|
|
20
|
+
const lastUpdateAt = new Date(pkg.time[lastVersion]);
|
|
21
|
+
const metadata = {
|
|
22
|
+
author: parseAuthor(pkg.author) ?? {},
|
|
23
|
+
homepage: pkg.homepage || null,
|
|
24
|
+
publishedCount: Object.values(pkg.versions).length,
|
|
25
|
+
lastVersion,
|
|
26
|
+
lastUpdateAt,
|
|
27
|
+
hasReceivedUpdateInOneYear: !(oneYearFromToday > lastUpdateAt),
|
|
28
|
+
maintainers: pkg.maintainers,
|
|
29
|
+
publishers: []
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const isOutdated = semver.neq(version, lastVersion);
|
|
33
|
+
if (isOutdated) {
|
|
34
|
+
ref.versions[version].flags.push("isOutdated");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const publishers = new Set();
|
|
38
|
+
for (const ver of Object.values(pkg.versions).reverse()) {
|
|
39
|
+
const { _npmUser: npmUser, version } = ver;
|
|
40
|
+
const isNullOrUndefined = typeof npmUser === "undefined" || npmUser === null;
|
|
41
|
+
if (isNullOrUndefined || !("name" in npmUser) || typeof npmUser.name !== "string") {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const authorName = metadata.author?.name ?? null;
|
|
46
|
+
if (authorName === null) {
|
|
47
|
+
metadata.author = npmUser;
|
|
48
|
+
}
|
|
49
|
+
else if (npmUser.name !== metadata.author.name) {
|
|
50
|
+
metadata.hasManyPublishers = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// TODO: add npmUser.email
|
|
54
|
+
if (!publishers.has(npmUser.name)) {
|
|
55
|
+
publishers.add(npmUser.name);
|
|
56
|
+
metadata.publishers.push({ ...npmUser, version, at: new Date(pkg.time[version]) });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Object.assign(ref.metadata, metadata);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// ignore
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
logger.tick("registry");
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/tarball.js
CHANGED
|
@@ -1,99 +1,63 @@
|
|
|
1
1
|
// Import Node.js Dependencies
|
|
2
|
-
import
|
|
3
|
-
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
4
3
|
import os from "os";
|
|
5
4
|
import timers from "timers/promises";
|
|
6
5
|
|
|
7
6
|
// Import Third-party Dependencies
|
|
8
|
-
import {
|
|
9
|
-
import difference from "lodash.difference";
|
|
10
|
-
import isMinified from "is-minified-code";
|
|
7
|
+
import { runASTAnalysisOnFile } from "@nodesecure/js-x-ray";
|
|
11
8
|
import pacote from "pacote";
|
|
12
9
|
import ntlp from "@nodesecure/ntlp";
|
|
13
|
-
import builtins from "builtins";
|
|
14
10
|
|
|
15
11
|
// Import Internal Dependencies
|
|
16
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
getTarballComposition, isSensitiveFile, filterDependencyKind, analyzeDependencies, booleanToFlags,
|
|
14
|
+
NPM_TOKEN
|
|
15
|
+
} from "./utils/index.js";
|
|
16
|
+
import * as manifest from "./manifest.js";
|
|
17
17
|
import { getLocalRegistryURL } from "@nodesecure/npm-registry-sdk";
|
|
18
18
|
|
|
19
19
|
// CONSTANTS
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
const kNativeCodeExtensions = new Set([".gyp", ".c", ".cpp", ".node", ".so", ".h"]);
|
|
21
|
+
const kJsExtname = new Set([".js", ".mjs", ".cjs"]);
|
|
22
|
+
|
|
23
|
+
export async function scanJavascriptFile(dest, file, packageName) {
|
|
24
|
+
const result = await runASTAnalysisOnFile(path.join(dest, file), { packageName });
|
|
25
|
+
|
|
26
|
+
const warnings = result.warnings.map((curr) => Object.assign({}, curr, { file }));
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
return {
|
|
29
|
+
file,
|
|
30
|
+
warnings,
|
|
31
|
+
isMinified: false,
|
|
32
|
+
tryDependencies: [],
|
|
33
|
+
dependencies: [],
|
|
34
|
+
filesDependencies: []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const { packages, files } = filterDependencyKind(result.dependencies, path.dirname(file));
|
|
38
38
|
|
|
39
39
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
file,
|
|
41
|
+
warnings,
|
|
42
|
+
isMinified: result.isMinified,
|
|
43
|
+
tryDependencies: [...result.dependencies.getDependenciesInTryStatement()],
|
|
44
|
+
dependencies: packages,
|
|
45
|
+
filesDependencies: files
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export async function scanFile(dest, file, options) {
|
|
47
|
-
const { name, ref } = options;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const str = await fs.readFile(join(dest, file), "utf-8");
|
|
51
|
-
const isMin = file.includes(".min") || isMinified(str);
|
|
52
|
-
|
|
53
|
-
const ASTAnalysis = runASTAnalysis(str, { isMinified: isMin });
|
|
54
|
-
ASTAnalysis.dependencies.removeByName(name);
|
|
55
|
-
|
|
56
|
-
const dependencies = [];
|
|
57
|
-
const filesDependencies = [];
|
|
58
|
-
for (const depName of ASTAnalysis.dependencies) {
|
|
59
|
-
if (depName.startsWith(".")) {
|
|
60
|
-
const indexName = DIRECT_PATH.has(depName) ? join(depName, "index.js") : join(dirname(file), depName);
|
|
61
|
-
filesDependencies.push(indexName);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
dependencies.push(depName);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const inTryDeps = [...ASTAnalysis.dependencies.getDependenciesInTryStatement()];
|
|
68
|
-
|
|
69
|
-
if (!ASTAnalysis.isOneLineRequire && isMin) {
|
|
70
|
-
ref.composition.minified.push(file);
|
|
71
|
-
}
|
|
72
|
-
ref.warnings.push(...ASTAnalysis.warnings.map((curr) => Object.assign({}, curr, { file })));
|
|
73
|
-
|
|
74
|
-
return { inTryDeps, dependencies, filesDependencies };
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
if (!("code" in error)) {
|
|
78
|
-
ref.warnings.push({ file, kind: "parsing-error", value: error.message, location: [[0, 0], [0, 0]] });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
49
|
export async function scanDirOrArchive(name, version, options) {
|
|
86
50
|
const { ref, tmpLocation, locker } = options;
|
|
87
51
|
|
|
88
52
|
const isNpmTarball = !(tmpLocation === null);
|
|
89
|
-
const dest = isNpmTarball ? join(tmpLocation, `${name}@${version}`) : process.cwd();
|
|
53
|
+
const dest = isNpmTarball ? path.join(tmpLocation, `${name}@${version}`) : process.cwd();
|
|
90
54
|
const free = await locker.acquireOne();
|
|
91
55
|
|
|
92
56
|
try {
|
|
93
57
|
// If this is an NPM tarball then we extract it on the disk with pacote.
|
|
94
58
|
if (isNpmTarball) {
|
|
95
59
|
await pacote.extract(ref.flags.includes("isGit") ? ref.gitUrl : `${name}@${version}`, dest, {
|
|
96
|
-
...
|
|
60
|
+
...NPM_TOKEN,
|
|
97
61
|
registry: getLocalRegistryURL(),
|
|
98
62
|
cache: `${os.homedir()}/.npm`
|
|
99
63
|
});
|
|
@@ -101,103 +65,67 @@ export async function scanDirOrArchive(name, version, options) {
|
|
|
101
65
|
}
|
|
102
66
|
|
|
103
67
|
// Read the package.json at the root of the directory or archive.
|
|
104
|
-
const { packageDeps, packageDevDeps,
|
|
68
|
+
const { packageDeps, packageDevDeps, author, description, hasScript, hasNativeElements } = await manifest.readAnalyze(dest);
|
|
69
|
+
ref.author = author;
|
|
70
|
+
ref.description = description;
|
|
105
71
|
|
|
106
72
|
// Get the composition of the (extracted) directory
|
|
107
73
|
const { ext, files, size } = await getTarballComposition(dest);
|
|
108
74
|
ref.size = size;
|
|
109
75
|
ref.composition.extensions.push(...ext);
|
|
110
76
|
ref.composition.files.push(...files);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
77
|
+
const hasBannedFile = files.some((path) => isSensitiveFile(path));
|
|
78
|
+
const hasNativeCode = hasNativeElements || files.some((file) => kNativeCodeExtensions.has(path.extname(file)));
|
|
114
79
|
|
|
115
80
|
// Search for minified and runtime dependencies
|
|
116
81
|
// Run a JS-X-Ray analysis on each JavaScript files of the project!
|
|
117
|
-
const
|
|
118
|
-
const fileAnalysisResults = await Promise.all(
|
|
82
|
+
const fileAnalysisRaw = await Promise.allSettled(
|
|
119
83
|
files
|
|
120
|
-
.filter((name) =>
|
|
121
|
-
.map((file) =>
|
|
84
|
+
.filter((name) => kJsExtname.has(path.extname(name)))
|
|
85
|
+
.map((file) => scanJavascriptFile(dest, file, name))
|
|
122
86
|
);
|
|
123
87
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
result.filesDependencies.forEach((dep) => filesDependencies.add(dep));
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Search for native code
|
|
131
|
-
{
|
|
132
|
-
const hasNativeFile = files.some((file) => NATIVE_CODE_EXTENSIONS.has(extname(file)));
|
|
133
|
-
const hasNativePackage = hasNativeFile ? null : [
|
|
134
|
-
...new Set([...packageDevDeps, ...(packageDeps || [])])
|
|
135
|
-
].some((pkg) => NATIVE_NPM_PACKAGES.has(pkg));
|
|
88
|
+
const fileAnalysisResults = fileAnalysisRaw
|
|
89
|
+
.filter((promiseSettledResult) => promiseSettledResult.status === "fulfilled")
|
|
90
|
+
.map((promiseSettledResult) => promiseSettledResult.value);
|
|
136
91
|
|
|
137
|
-
|
|
138
|
-
ref.flags.push("hasNativeCode");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
92
|
+
ref.warnings.push(...fileAnalysisResults.flatMap((row) => row.warnings));
|
|
141
93
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
94
|
+
const dependencies = [...new Set(fileAnalysisResults.flatMap((row) => row.dependencies))];
|
|
95
|
+
const filesDependencies = [...new Set(fileAnalysisResults.flatMap((row) => row.filesDependencies))];
|
|
96
|
+
const tryDependencies = new Set(fileAnalysisResults.flatMap((row) => row.tryDependencies));
|
|
97
|
+
const minifiedFiles = fileAnalysisResults.filter((row) => row.isMinified).flatMap((row) => row.file);
|
|
146
98
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.filter((name) => !name.startsWith("."))
|
|
151
|
-
.filter((name) => !NODE_CORE_LIBS.has(name))
|
|
152
|
-
.filter((name) => !packageDevDeps.includes(name))
|
|
153
|
-
.filter((name) => !inTryDeps.has(name));
|
|
154
|
-
ref.composition.required_thirdparty = thirdPartyDependencies;
|
|
99
|
+
const {
|
|
100
|
+
nodeDependencies, thirdPartyDependencies, missingDependencies, unusedDependencies, flags
|
|
101
|
+
} = analyzeDependencies(dependencies, { packageDeps, packageDevDeps, tryDependencies });
|
|
155
102
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
ref.composition.unused.push(...unusedDeps);
|
|
164
|
-
ref.composition.missing.push(...missingDeps);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
ref.composition.required_files = [...filesDependencies]
|
|
168
|
-
.filter((depName) => depName.trim() !== "")
|
|
169
|
-
// .map((depName) => {
|
|
170
|
-
// return files.includes(depName) ? depName : join(depName, "index.js");
|
|
171
|
-
// })
|
|
172
|
-
.map((depName) => (extname(depName) === "" ? `${depName}.js` : depName));
|
|
173
|
-
ref.composition.required_nodejs = required.filter((name) => NODE_CORE_LIBS.has(name));
|
|
174
|
-
|
|
175
|
-
if (ref.composition.minified.length > 0) {
|
|
176
|
-
ref.flags.push("hasMinifiedCode");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const hasExternalCapacity = ref.composition.required_nodejs
|
|
180
|
-
.some((depName) => constants.EXT_DEPS.has(depName));
|
|
181
|
-
if (hasExternalCapacity) {
|
|
182
|
-
ref.flags.push("hasExternalCapacity");
|
|
183
|
-
}
|
|
103
|
+
ref.composition.required_thirdparty = thirdPartyDependencies;
|
|
104
|
+
ref.composition.unused.push(...unusedDependencies);
|
|
105
|
+
ref.composition.missing.push(...missingDependencies);
|
|
106
|
+
ref.composition.required_files = filesDependencies;
|
|
107
|
+
ref.composition.required_nodejs = nodeDependencies;
|
|
108
|
+
ref.composition.minified = minifiedFiles;
|
|
184
109
|
|
|
185
110
|
// License
|
|
186
111
|
await timers.setImmediate();
|
|
187
112
|
const licenses = await ntlp(dest);
|
|
188
|
-
|
|
189
113
|
const uniqueLicenseIds = Array.isArray(licenses.uniqueLicenseIds) ? licenses.uniqueLicenseIds : [];
|
|
190
|
-
if (uniqueLicenseIds.length === 0) {
|
|
191
|
-
ref.flags.push("hasNoLicense");
|
|
192
|
-
}
|
|
193
|
-
if (licenses.hasMultipleLicenses) {
|
|
194
|
-
ref.flags.push("hasMultipleLicenses");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
114
|
ref.license = licenses;
|
|
198
115
|
ref.license.uniqueLicenseIds = uniqueLicenseIds;
|
|
116
|
+
|
|
117
|
+
ref.flags.push(...booleanToFlags({
|
|
118
|
+
...flags,
|
|
119
|
+
hasNoLicense: uniqueLicenseIds.length === 0,
|
|
120
|
+
hasMultipleLicenses: licenses.hasMultipleLicenses,
|
|
121
|
+
hasMinifiedCode: minifiedFiles.length > 0,
|
|
122
|
+
hasWarnings: ref.warnings.length > 0 && !ref.flags.includes("hasWarnings"),
|
|
123
|
+
hasBannedFile,
|
|
124
|
+
hasNativeCode,
|
|
125
|
+
hasScript
|
|
126
|
+
}));
|
|
199
127
|
}
|
|
200
|
-
catch
|
|
128
|
+
catch {
|
|
201
129
|
// Ignore
|
|
202
130
|
}
|
|
203
131
|
finally {
|
|
@@ -205,66 +133,35 @@ export async function scanDirOrArchive(name, version, options) {
|
|
|
205
133
|
}
|
|
206
134
|
}
|
|
207
135
|
|
|
208
|
-
async function readJSFile(dest, file) {
|
|
209
|
-
const str = await fs.readFile(join(dest, file), "utf-8");
|
|
210
|
-
|
|
211
|
-
return [file, str];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
136
|
export async function scanPackage(dest, packageName) {
|
|
215
|
-
|
|
216
|
-
let isProjectUsingESM = false;
|
|
217
|
-
let localPackageName = packageName;
|
|
218
|
-
{
|
|
219
|
-
const packageStr = await fs.readFile(join(dest, "package.json"), "utf-8");
|
|
220
|
-
const { type = "script", name } = JSON.parse(packageStr);
|
|
221
|
-
isProjectUsingESM = type === "module";
|
|
222
|
-
if (localPackageName === null) {
|
|
223
|
-
localPackageName = name;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
137
|
+
const { type = "script", name } = await manifest.read(dest);
|
|
226
138
|
|
|
227
|
-
// Get the tarball composition
|
|
228
139
|
await timers.setImmediate();
|
|
229
140
|
const { ext, files, size } = await getTarballComposition(dest);
|
|
141
|
+
ext.delete("");
|
|
230
142
|
|
|
231
143
|
// Search for runtime dependencies
|
|
232
144
|
const dependencies = Object.create(null);
|
|
233
|
-
const minified = [];
|
|
234
|
-
const warnings = [];
|
|
145
|
+
const [minified, warnings] = [[], []];
|
|
235
146
|
|
|
236
|
-
const JSFiles = files.filter((name) =>
|
|
237
|
-
const
|
|
238
|
-
|
|
147
|
+
const JSFiles = files.filter((name) => kJsExtname.has(path.extname(name)));
|
|
148
|
+
for (const file of JSFiles) {
|
|
149
|
+
const result = await runASTAnalysisOnFile(path.join(dest, file), {
|
|
150
|
+
packageName: packageName ?? name,
|
|
151
|
+
module: type === "module"
|
|
152
|
+
});
|
|
239
153
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const ASTAnalysis = runASTAnalysis(str, {
|
|
244
|
-
module: extname(file) === ".mjs" ? true : isProjectUsingESM
|
|
245
|
-
});
|
|
246
|
-
ASTAnalysis.dependencies.removeByName(localPackageName);
|
|
247
|
-
dependencies[file] = ASTAnalysis.dependencies.dependencies;
|
|
248
|
-
warnings.push(...ASTAnalysis.warnings.map((warn) => {
|
|
249
|
-
warn.file = file;
|
|
250
|
-
|
|
251
|
-
return warn;
|
|
252
|
-
}));
|
|
253
|
-
|
|
254
|
-
if (!ASTAnalysis.isOneLineRequire && !file.includes(".min") && isMinified(str)) {
|
|
255
|
-
minified.push(file);
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch (err) {
|
|
259
|
-
if (!Reflect.has(err, "code")) {
|
|
260
|
-
warnings.push({ file, kind: "parsing-error", value: err.message, location: [[0, 0], [0, 0]] });
|
|
261
|
-
}
|
|
154
|
+
warnings.push(...result.warnings.map((curr) => Object.assign({}, curr, { file })));
|
|
155
|
+
if (!result.ok) {
|
|
156
|
+
continue;
|
|
262
157
|
}
|
|
158
|
+
|
|
159
|
+
dependencies[file] = result.dependencies.dependencies;
|
|
160
|
+
result.isMinified && minified.push(file);
|
|
263
161
|
}
|
|
264
162
|
|
|
265
163
|
await timers.setImmediate();
|
|
266
164
|
const { uniqueLicenseIds, licenses } = await ntlp(dest);
|
|
267
|
-
ext.delete("");
|
|
268
165
|
|
|
269
166
|
return {
|
|
270
167
|
files: { list: files, extensions: [...ext], minified },
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Set<string>} flags
|
|
3
|
+
* @param {import("../../types/scanner").Dependency} descriptor
|
|
4
|
+
*/
|
|
5
|
+
export function* addMissingVersionFlags(flags, descriptor) {
|
|
6
|
+
const { metadata, vulnerabilities = [], versions } = descriptor;
|
|
7
|
+
const semverVersions = Object.keys(versions);
|
|
8
|
+
|
|
9
|
+
if (!metadata.hasReceivedUpdateInOneYear && flags.has("hasOutdatedDependency") && !flags.has("isDead")) {
|
|
10
|
+
yield "isDead";
|
|
11
|
+
}
|
|
12
|
+
if (metadata.hasManyPublishers && !flags.has("hasManyPublishers")) {
|
|
13
|
+
yield "hasManyPublishers";
|
|
14
|
+
}
|
|
15
|
+
if (metadata.hasChangedAuthor && !flags.has("hasChangedAuthor")) {
|
|
16
|
+
yield "hasChangedAuthor";
|
|
17
|
+
}
|
|
18
|
+
if (vulnerabilities.length > 0 && !flags.has("hasVulnerabilities")) {
|
|
19
|
+
yield "hasVulnerabilities";
|
|
20
|
+
}
|
|
21
|
+
if (semverVersions.length > 1 && !flags.has("hasDuplicate")) {
|
|
22
|
+
yield "hasDuplicate";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Import Third-party Dependencies
|
|
2
|
+
import difference from "lodash.difference";
|
|
3
|
+
import builtins from "builtins";
|
|
4
|
+
|
|
5
|
+
// Import Internal Dependencies
|
|
6
|
+
import { getPackageName } from "./getPackageName.js";
|
|
7
|
+
|
|
8
|
+
// CONSTANTS
|
|
9
|
+
const kNodeModules = new Set(builtins({ experimental: true }));
|
|
10
|
+
const kExternalModules = new Set(["http", "https", "net", "http2", "dgram", "child_process"]);
|
|
11
|
+
|
|
12
|
+
export function analyzeDependencies(dependencies, deps = {}) {
|
|
13
|
+
const { packageDeps, packageDevDeps, tryDependencies } = deps;
|
|
14
|
+
|
|
15
|
+
const thirdPartyDependencies = dependencies
|
|
16
|
+
.map((name) => (packageDeps.includes(name) ? name : getPackageName(name)))
|
|
17
|
+
.filter((name) => !name.startsWith("."))
|
|
18
|
+
.filter((name) => !kNodeModules.has(name))
|
|
19
|
+
.filter((name) => !packageDevDeps.includes(name))
|
|
20
|
+
.filter((name) => !tryDependencies.has(name));
|
|
21
|
+
|
|
22
|
+
const unusedDependencies = difference(
|
|
23
|
+
packageDeps.filter((name) => !name.startsWith("@types")),
|
|
24
|
+
thirdPartyDependencies
|
|
25
|
+
);
|
|
26
|
+
const missingDependencies = [...new Set(difference(thirdPartyDependencies, packageDeps))];
|
|
27
|
+
const nodeDependencies = dependencies.filter((name) => kNodeModules.has(name));
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
nodeDependencies,
|
|
31
|
+
thirdPartyDependencies,
|
|
32
|
+
unusedDependencies,
|
|
33
|
+
missingDependencies,
|
|
34
|
+
|
|
35
|
+
flags: {
|
|
36
|
+
hasExternalCapacity: nodeDependencies.some((depName) => kExternalModules.has(depName)),
|
|
37
|
+
hasMissingOrUnusedDependency: unusedDependencies.length > 0 || missingDependencies.length > 0
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Record<string, boolean>} flagsRecord
|
|
3
|
+
* @example
|
|
4
|
+
* console.log(...booleanToFlags({ hasScript: true })); // "hasScript"
|
|
5
|
+
*/
|
|
6
|
+
export function* booleanToFlags(flagsRecord) {
|
|
7
|
+
for (const [flagName, boolValue] of Object.entries(flagsRecord)) {
|
|
8
|
+
if (boolValue) {
|
|
9
|
+
yield flagName;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
// CONSTANTS
|
|
5
|
+
const kRelativeImportPath = new Set([".", "..", "./", "../"]);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @see https://nodejs.org/docs/latest/api/modules.html#file-modules
|
|
9
|
+
*
|
|
10
|
+
* @param {IterableIterator<string>} dependencies
|
|
11
|
+
* @param {!string} relativeFileLocation
|
|
12
|
+
*/
|
|
13
|
+
export function filterDependencyKind(dependencies, relativeFileLocation) {
|
|
14
|
+
const packages = [];
|
|
15
|
+
const files = [];
|
|
16
|
+
|
|
17
|
+
for (const moduleNameOrPath of dependencies) {
|
|
18
|
+
const firstChar = moduleNameOrPath.charAt(0);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @example
|
|
22
|
+
* require("..");
|
|
23
|
+
* require("/home/marco/foo.js");
|
|
24
|
+
*/
|
|
25
|
+
if (firstChar === "." || firstChar === "/") {
|
|
26
|
+
// Note: condition only possible for CJS
|
|
27
|
+
if (kRelativeImportPath.has(moduleNameOrPath)) {
|
|
28
|
+
files.push(path.join(moduleNameOrPath, "index.js"));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Note: we are speculating that the extension is .js (but it could be .json or .node)
|
|
32
|
+
const fixedFileName = path.extname(moduleNameOrPath) === "" ?
|
|
33
|
+
`${moduleNameOrPath}.js` : moduleNameOrPath;
|
|
34
|
+
|
|
35
|
+
files.push(path.join(relativeFileLocation, fixedFileName));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
packages.push(moduleNameOrPath);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { packages, files };
|
|
44
|
+
}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
// CONSTANTS
|
|
2
|
+
const kPackageSeparator = "/";
|
|
3
|
+
const kPackageOrgSymbol = "@";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @see https://github.com/npm/validate-npm-package-name#naming-rules
|
|
7
|
+
*
|
|
8
|
+
* @param {!string} name full package name
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* getPackageName("foo"); // foo
|
|
13
|
+
* getPackageName("foo/bar"); // foo
|
|
14
|
+
* getPackageName("@org/bar"); // @org/bar
|
|
15
|
+
*/
|
|
1
16
|
export function getPackageName(name) {
|
|
2
|
-
const parts = name.split(
|
|
17
|
+
const parts = name.split(kPackageSeparator);
|
|
3
18
|
|
|
4
|
-
|
|
19
|
+
// Note: only scoped package are allowed to start with @
|
|
20
|
+
return name.startsWith(kPackageOrgSymbol) ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
5
21
|
}
|
|
@@ -21,16 +21,14 @@ export async function getTarballComposition(tarballDir) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// ignore
|
|
33
|
-
}
|
|
24
|
+
const sizeUnfilteredResult = await Promise.allSettled([
|
|
25
|
+
...files.map((file) => fs.stat(file)),
|
|
26
|
+
...dirs.map((file) => fs.stat(file))
|
|
27
|
+
]);
|
|
28
|
+
const sizeAll = sizeUnfilteredResult
|
|
29
|
+
.filter((promiseSettledResult) => promiseSettledResult.status === "fulfilled")
|
|
30
|
+
.map((promiseSettledResult) => promiseSettledResult.value);
|
|
31
|
+
size += sizeAll.reduce((prev, curr) => prev + curr.size, 0);
|
|
34
32
|
|
|
35
33
|
return {
|
|
36
34
|
ext,
|