@nodesecure/scanner 4.0.0 → 5.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/README.md +2 -2
- package/index.js +1 -1
- package/package.json +17 -20
- package/src/depWalker.js +366 -366
- package/src/manifest.js +9 -7
- package/src/npmRegistry.js +3 -5
- package/src/tarball.js +11 -2
- package/src/utils/index.js +1 -0
- package/src/utils/parseManifestAuthor.js +45 -0
- package/src/utils/semver.js +12 -0
- package/src/utils/warnings.js +14 -4
- package/types/scanner.d.ts +15 -4
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<img align="center" alt="# Nodesecure Scanner" src="https://user-images.githubusercontent.com/4438263/226018084-113c49e6-6c69-4baa-8f84-87e6d695be6d.jpg">
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-
[](https://github.com/NodeSecure/scanner/commit-activity)
|
|
4
|
+
[](https://github.com/NodeSecure/scanner/graphs/commit-activity)
|
|
5
5
|
[](https://api.securityscorecards.dev/projects/github.com/NodeSecure/scanner)
|
|
7
|
-
[](https://github.com/NodeSecure/scanner/blob/master/LICENSE)
|
|
8
8
|

|
|
9
9
|
|
|
10
10
|
⚡️ Run a static analysis of your module's dependencies.
|
package/index.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function cwd(location = process.cwd(), options = {}, logger = new L
|
|
|
38
38
|
return depWalker(JSON.parse(str), finalizedOptions, logger);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
export async function from(packageName, options, logger = new Logger()) {
|
|
41
|
+
export async function from(packageName, options = {}, logger = new Logger()) {
|
|
42
42
|
const registry = options.registry ? new URL(options.registry).toString() : getLocalRegistryURL();
|
|
43
43
|
|
|
44
44
|
logger.start(ScannerLoggerEvents.manifest.fetch);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodesecure/scanner",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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
|
|
13
|
+
"test:ci": "node --test test/**.spec.js test/**/*.spec.js",
|
|
14
|
+
"test-only": "glob -c \"node --test-reporter=spec --test\" \"./test/**/*.spec.js\"",
|
|
14
15
|
"coverage": "c8 -r html npm run test-only"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
@@ -48,39 +49,35 @@
|
|
|
48
49
|
},
|
|
49
50
|
"homepage": "https://github.com/NodeSecure/scanner#readme",
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@nodesecure/eslint-config": "^1.
|
|
52
|
+
"@nodesecure/eslint-config": "^1.7.0",
|
|
52
53
|
"@slimio/is": "^2.0.0",
|
|
53
|
-
"@
|
|
54
|
-
"
|
|
55
|
-
"@types/node": "^18.13.0",
|
|
56
|
-
"c8": "^7.12.0",
|
|
57
|
-
"cross-env": "^7.0.3",
|
|
54
|
+
"@types/node": "^20.4.5",
|
|
55
|
+
"c8": "^7.13.0",
|
|
58
56
|
"dotenv": "^16.0.3",
|
|
59
|
-
"eslint": "^8.
|
|
57
|
+
"eslint": "^8.37.0",
|
|
60
58
|
"get-folder-size": "^4.0.0",
|
|
59
|
+
"glob": "^10.3.4",
|
|
61
60
|
"pkg-ok": "^3.0.0",
|
|
62
|
-
"sinon": "^15.0.
|
|
63
|
-
"snap-shot-core": "^10.2.4"
|
|
64
|
-
"tape": "^5.6.1"
|
|
61
|
+
"sinon": "^15.0.3",
|
|
62
|
+
"snap-shot-core": "^10.2.4"
|
|
65
63
|
},
|
|
66
64
|
"dependencies": {
|
|
67
|
-
"@nodesecure/authors": "^1.0.
|
|
65
|
+
"@nodesecure/authors": "^1.0.2",
|
|
68
66
|
"@nodesecure/flags": "^2.4.0",
|
|
69
67
|
"@nodesecure/fs-walk": "^1.0.0",
|
|
70
|
-
"@nodesecure/i18n": "^3.
|
|
68
|
+
"@nodesecure/i18n": "^3.3.0",
|
|
71
69
|
"@nodesecure/js-x-ray": "^6.0.1",
|
|
72
|
-
"@nodesecure/npm-registry-sdk": "^1.
|
|
73
|
-
"@nodesecure/ntlp": "^2.2.
|
|
74
|
-
"@nodesecure/utils": "^1.0.0",
|
|
70
|
+
"@nodesecure/npm-registry-sdk": "^1.5.2",
|
|
71
|
+
"@nodesecure/ntlp": "^2.2.1",
|
|
75
72
|
"@nodesecure/vuln": "^1.7.0",
|
|
76
73
|
"@npm/types": "^1.0.2",
|
|
77
|
-
"@npmcli/arborist": "^6.2.
|
|
74
|
+
"@npmcli/arborist": "^6.2.6",
|
|
78
75
|
"@slimio/lock": "^1.0.0",
|
|
79
76
|
"builtins": "^5.0.1",
|
|
80
77
|
"combine-async-iterators": "^2.0.1",
|
|
81
|
-
"itertools": "^1.
|
|
78
|
+
"itertools": "^2.1.1",
|
|
82
79
|
"lodash.difference": "^4.5.0",
|
|
83
|
-
"pacote": "^15.
|
|
80
|
+
"pacote": "^15.1.1",
|
|
84
81
|
"semver": "^7.3.8"
|
|
85
82
|
},
|
|
86
83
|
"type": "module"
|
package/src/depWalker.js
CHANGED
|
@@ -1,366 +1,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 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 } 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
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Import Node.js Dependencies
|
|
2
|
-
import fs from "fs/promises";
|
|
3
|
-
import path from "path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
// Import
|
|
6
|
-
import {
|
|
5
|
+
// Import Internal Dependencies
|
|
6
|
+
import { parseAuthor } from "./utils/index.js";
|
|
7
7
|
|
|
8
8
|
// CONSTANTS
|
|
9
9
|
// PR welcome to contribute to this list!
|
|
@@ -16,8 +16,10 @@ const kNativeNpmPackages = new Set([
|
|
|
16
16
|
*/
|
|
17
17
|
const kUnsafeNpmScripts = new Set([
|
|
18
18
|
"install",
|
|
19
|
-
"preinstall",
|
|
20
|
-
"
|
|
19
|
+
"preinstall",
|
|
20
|
+
"postinstall",
|
|
21
|
+
"preuninstall",
|
|
22
|
+
"postuninstall"
|
|
21
23
|
]);
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -48,7 +50,7 @@ export async function readAnalyze(location) {
|
|
|
48
50
|
.some((pkg) => kNativeNpmPackages.has(pkg));
|
|
49
51
|
|
|
50
52
|
return {
|
|
51
|
-
author:
|
|
53
|
+
author: parseAuthor(author),
|
|
52
54
|
description,
|
|
53
55
|
engines,
|
|
54
56
|
repository,
|
package/src/npmRegistry.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// Import Third-party Dependencies
|
|
2
2
|
import semver from "semver";
|
|
3
|
-
import { parseManifestAuthor } from "@nodesecure/utils";
|
|
4
3
|
import { packument } from "@nodesecure/npm-registry-sdk";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
5
|
+
// Import Internal Dependencies
|
|
6
|
+
import { parseAuthor } from "./utils/index.js";
|
|
9
7
|
|
|
10
8
|
export async function packageMetadata(name, version, options) {
|
|
11
9
|
const { ref, logger } = options;
|
|
@@ -19,7 +17,7 @@ export async function packageMetadata(name, version, options) {
|
|
|
19
17
|
const lastVersion = pkg["dist-tags"].latest;
|
|
20
18
|
const lastUpdateAt = new Date(pkg.time[lastVersion]);
|
|
21
19
|
const metadata = {
|
|
22
|
-
author: parseAuthor(pkg.author)
|
|
20
|
+
author: parseAuthor(pkg.author),
|
|
23
21
|
homepage: pkg.homepage || null,
|
|
24
22
|
publishedCount: Object.values(pkg.versions).length,
|
|
25
23
|
lastVersion,
|
package/src/tarball.js
CHANGED
|
@@ -10,8 +10,13 @@ import ntlp from "@nodesecure/ntlp";
|
|
|
10
10
|
|
|
11
11
|
// Import Internal Dependencies
|
|
12
12
|
import {
|
|
13
|
-
getTarballComposition,
|
|
14
|
-
|
|
13
|
+
getTarballComposition,
|
|
14
|
+
isSensitiveFile,
|
|
15
|
+
filterDependencyKind,
|
|
16
|
+
analyzeDependencies,
|
|
17
|
+
booleanToFlags,
|
|
18
|
+
NPM_TOKEN,
|
|
19
|
+
getSemVerWarning
|
|
15
20
|
} from "./utils/index.js";
|
|
16
21
|
import * as manifest from "./manifest.js";
|
|
17
22
|
|
|
@@ -94,6 +99,10 @@ export async function scanDirOrArchive(name, version, options) {
|
|
|
94
99
|
|
|
95
100
|
ref.warnings.push(...fileAnalysisResults.flatMap((row) => row.warnings));
|
|
96
101
|
|
|
102
|
+
if (/^0(\.\d+)*$/.test(version)) {
|
|
103
|
+
ref.warnings.push(getSemVerWarning(version));
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
const dependencies = [...new Set(fileAnalysisResults.flatMap((row) => row.dependencies))];
|
|
98
107
|
const filesDependencies = [...new Set(fileAnalysisResults.flatMap((row) => row.filesDependencies))];
|
|
99
108
|
const tryDependencies = new Set(fileAnalysisResults.flatMap((row) => row.tryDependencies));
|
package/src/utils/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./filterDependencyKind.js";
|
|
|
10
10
|
export * from "./analyzeDependencies.js";
|
|
11
11
|
export * from "./booleanToFlags.js";
|
|
12
12
|
export * from "./addMissingVersionFlags.js";
|
|
13
|
+
export * from "./parseManifestAuthor.js";
|
|
13
14
|
|
|
14
15
|
export const NPM_TOKEN = typeof process.env.NODE_SECURE_TOKEN === "string" ?
|
|
15
16
|
{ token: process.env.NODE_SECURE_TOKEN } :
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function manifestAuthorRegex() {
|
|
2
|
+
return /^([^<(]+?)?[ \t]*(?:<([^>(]+?)>)?[ \t]*(?:\(([^)]+?)\)|$)/gm;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @see https://docs.npmjs.com/cli/v7/configuring-npm/package-json#people-fields-author-contributors
|
|
7
|
+
*/
|
|
8
|
+
export function parseManifestAuthor(manifestAuthorField) {
|
|
9
|
+
if (typeof manifestAuthorField !== "string") {
|
|
10
|
+
throw new TypeError("expected manifestAuthorField to be a string");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!/\w/.test(manifestAuthorField)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const match = manifestAuthorRegex().exec(manifestAuthorField);
|
|
18
|
+
if (!match) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const author = {
|
|
22
|
+
name: match[1]
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (let id = 2; id < match.length; id++) {
|
|
26
|
+
const val = match[id] || "";
|
|
27
|
+
|
|
28
|
+
if (val.includes("@")) {
|
|
29
|
+
author.email = val;
|
|
30
|
+
}
|
|
31
|
+
else if (val.includes("http")) {
|
|
32
|
+
author.url = val;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return author;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseAuthor(author) {
|
|
40
|
+
if (typeof author === "string") {
|
|
41
|
+
return parseManifestAuthor(author);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return !author || Object.keys(author).length === 0 ? null : author;
|
|
45
|
+
}
|
package/src/utils/semver.js
CHANGED
|
@@ -47,3 +47,15 @@ export async function getCleanDependencyName([depName, range]) {
|
|
|
47
47
|
|
|
48
48
|
return [`${depName}@${range}`, `${depName}@${depVer}`, isLatest];
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
export function getSemVerWarning(value) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "invalid-semver",
|
|
54
|
+
file: "package.json",
|
|
55
|
+
value,
|
|
56
|
+
location: null,
|
|
57
|
+
i18n: "sast_warnings.invalidSemVer",
|
|
58
|
+
severity: "Information",
|
|
59
|
+
experimental: false
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/utils/warnings.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
// Import Node.js Dependencies
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
1
4
|
// Import Third-party Dependencies
|
|
2
|
-
import
|
|
5
|
+
import * as i18n from "@nodesecure/i18n";
|
|
3
6
|
import { extractAllAuthors } from "@nodesecure/authors";
|
|
4
7
|
|
|
8
|
+
// Import Internal Dependencies
|
|
9
|
+
import { getDirNameFromUrl } from "./dirname.js";
|
|
10
|
+
|
|
11
|
+
i18n.extendFromSystemPath(
|
|
12
|
+
path.join(getDirNameFromUrl(import.meta.url), "..", "..", "i18n")
|
|
13
|
+
);
|
|
14
|
+
|
|
5
15
|
// CONSTANTS
|
|
6
|
-
const kDetectedDep = taggedString`The dependency '${0}' has been detected in the dependency Tree.`;
|
|
16
|
+
const kDetectedDep = i18n.taggedString`The dependency '${0}' has been detected in the dependency Tree.`;
|
|
7
17
|
const kFlaggedAuthors = [{
|
|
8
18
|
name: "marak",
|
|
9
19
|
email: "marak.squires@gmail.com"
|
|
10
20
|
}];
|
|
11
21
|
const kDependencyWarnMessage = Object.freeze({
|
|
12
|
-
"@scarf/scarf": await getToken("
|
|
13
|
-
iohook: await getToken("
|
|
22
|
+
"@scarf/scarf": await i18n.getToken("scanner.disable_scarf"),
|
|
23
|
+
iohook: await i18n.getToken("scanner.keylogging")
|
|
14
24
|
});
|
|
15
25
|
|
|
16
26
|
/**
|
package/types/scanner.d.ts
CHANGED
|
@@ -4,11 +4,22 @@ import { license as License } from "@nodesecure/ntlp";
|
|
|
4
4
|
import * as Vuln from "@nodesecure/vuln";
|
|
5
5
|
|
|
6
6
|
// Import Third-party Dependencies
|
|
7
|
-
import {
|
|
7
|
+
import { extractedAuthor } from "@nodesecure/authors";
|
|
8
8
|
|
|
9
9
|
export = Scanner;
|
|
10
10
|
|
|
11
11
|
declare namespace Scanner {
|
|
12
|
+
export interface Author {
|
|
13
|
+
name: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Maintainer {
|
|
19
|
+
name: string;
|
|
20
|
+
email: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export interface Publisher {
|
|
13
24
|
/**
|
|
14
25
|
* Publisher npm user name.
|
|
@@ -45,7 +56,7 @@ declare namespace Scanner {
|
|
|
45
56
|
/** Package description */
|
|
46
57
|
description: string;
|
|
47
58
|
/** Author of the package. This information is not trustable and can be empty. */
|
|
48
|
-
author:
|
|
59
|
+
author: Author | null;
|
|
49
60
|
engines: {
|
|
50
61
|
node?: string;
|
|
51
62
|
npm?: string;
|
|
@@ -109,13 +120,13 @@ declare namespace Scanner {
|
|
|
109
120
|
hasManyPublishers: boolean;
|
|
110
121
|
hasReceivedUpdateInOneYear: boolean;
|
|
111
122
|
/** Author of the package. This information is not trustable and can be empty. */
|
|
112
|
-
author:
|
|
123
|
+
author: Author | null;
|
|
113
124
|
/** Package home page */
|
|
114
125
|
homepage: string | null;
|
|
115
126
|
/**
|
|
116
127
|
* List of maintainers (list of people in the organization related to the package)
|
|
117
128
|
*/
|
|
118
|
-
maintainers:
|
|
129
|
+
maintainers: Maintainer[];
|
|
119
130
|
/**
|
|
120
131
|
* List of people who published this package
|
|
121
132
|
*/
|