@nodesecure/scanner 2.0.1 → 3.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/LICENSE +1 -1
- package/README.md +95 -94
- package/index.d.ts +2 -2
- package/index.js +10 -9
- 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/constants.js +13 -0
- package/src/depWalker.js +67 -110
- package/src/manifest.js +57 -0
- package/src/npmRegistry.js +68 -0
- package/src/tarball.js +85 -189
- 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/api.d.ts +5 -2
- package/types/logger.d.ts +17 -1
- package/types/scanner.d.ts +132 -33
package/src/depWalker.js
CHANGED
|
@@ -8,18 +8,21 @@ import os from "os";
|
|
|
8
8
|
import combineAsyncIterators from "combine-async-iterators";
|
|
9
9
|
import iter from "itertools";
|
|
10
10
|
import pacote from "pacote";
|
|
11
|
-
import semver from "semver";
|
|
12
11
|
import Arborist from "@npmcli/arborist";
|
|
13
12
|
import Lock from "@slimio/lock";
|
|
14
|
-
import { packument, getLocalRegistryURL } from "@nodesecure/npm-registry-sdk";
|
|
15
13
|
import * as vuln from "@nodesecure/vuln";
|
|
16
|
-
import {
|
|
14
|
+
import { getLocalRegistryURL } from "@nodesecure/npm-registry-sdk";
|
|
15
|
+
import { ScannerLoggerEvents } from "./constants.js";
|
|
17
16
|
|
|
18
17
|
// Import Internal Dependencies
|
|
19
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
mergeDependencies, getCleanDependencyName, getDependenciesWarnings, addMissingVersionFlags, isGitDependency,
|
|
20
|
+
NPM_TOKEN
|
|
21
|
+
} from "./utils/index.js";
|
|
20
22
|
import { scanDirOrArchive } from "./tarball.js";
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
+
import { packageMetadata } from "./npmRegistry.js";
|
|
24
|
+
import Dependency from "./class/dependency.class.js";
|
|
25
|
+
import Logger from "./class/logger.class.js";
|
|
23
26
|
|
|
24
27
|
const { version: packageVersion } = JSON.parse(
|
|
25
28
|
readFileSync(
|
|
@@ -27,20 +30,18 @@ const { version: packageVersion } = JSON.parse(
|
|
|
27
30
|
)
|
|
28
31
|
);
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
async function* searchDeepDependencies(packageName, gitURL, options) {
|
|
32
|
-
const isGit = typeof gitURL === "string";
|
|
33
|
+
export async function* searchDeepDependencies(packageName, gitURL, options) {
|
|
33
34
|
const { exclude, currDepth = 0, parent, maxDepth } = options;
|
|
34
35
|
|
|
35
|
-
const { name, version, deprecated, ...pkg } = await pacote.manifest(
|
|
36
|
-
...
|
|
36
|
+
const { name, version, deprecated, ...pkg } = await pacote.manifest(gitURL ?? packageName, {
|
|
37
|
+
...NPM_TOKEN,
|
|
37
38
|
registry: getLocalRegistryURL(),
|
|
38
39
|
cache: `${os.homedir()}/.npm`
|
|
39
40
|
});
|
|
40
41
|
const { dependencies, customResolvers } = mergeDependencies(pkg);
|
|
41
42
|
|
|
42
43
|
const current = new Dependency(name, version, parent);
|
|
43
|
-
|
|
44
|
+
gitURL !== null && current.isGit(gitURL);
|
|
44
45
|
current.addFlag("isDeprecated", deprecated === true);
|
|
45
46
|
current.addFlag("hasCustomResolver", customResolvers.size > 0);
|
|
46
47
|
current.addFlag("hasDependencies", dependencies.size > 0);
|
|
@@ -50,9 +51,9 @@ async function* searchDeepDependencies(packageName, gitURL, options) {
|
|
|
50
51
|
exclude, currDepth: currDepth + 1, parent: current, maxDepth
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
const gitDependencies = iter.filter(customResolvers.entries(), ([, valueStr]) => valueStr
|
|
54
|
+
const gitDependencies = iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr));
|
|
54
55
|
for (const [depName, valueStr] of gitDependencies) {
|
|
55
|
-
yield* searchDeepDependencies(depName, valueStr
|
|
56
|
+
yield* searchDeepDependencies(depName, valueStr, config);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
const depsNames = await Promise.all(iter.map(dependencies.entries(), getCleanDependencyName));
|
|
@@ -66,7 +67,7 @@ async function* searchDeepDependencies(packageName, gitURL, options) {
|
|
|
66
67
|
}
|
|
67
68
|
else {
|
|
68
69
|
exclude.set(cleanName, new Set([current.fullName]));
|
|
69
|
-
yield* searchDeepDependencies(fullName,
|
|
70
|
+
yield* searchDeepDependencies(fullName, null, config);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
}
|
|
@@ -74,7 +75,7 @@ async function* searchDeepDependencies(packageName, gitURL, options) {
|
|
|
74
75
|
yield current;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLockMode }) {
|
|
78
|
+
export async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLockMode }) {
|
|
78
79
|
const { version, integrity = to.integrity } = to.package;
|
|
79
80
|
|
|
80
81
|
const updatedVersion = version === "*" || typeof version === "undefined" ? "latest" : version;
|
|
@@ -82,7 +83,7 @@ async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLoc
|
|
|
82
83
|
|
|
83
84
|
if (fullLockMode) {
|
|
84
85
|
const { deprecated, _integrity, ...pkg } = await pacote.manifest(`${currentPackageName}@${updatedVersion}`, {
|
|
85
|
-
...
|
|
86
|
+
...NPM_TOKEN,
|
|
86
87
|
registry: getLocalRegistryURL(),
|
|
87
88
|
cache: `${os.homedir()}/.npm`
|
|
88
89
|
});
|
|
@@ -111,64 +112,7 @@ async function* deepReadEdges(currentPackageName, { to, parent, exclude, fullLoc
|
|
|
111
112
|
yield current;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
async function
|
|
115
|
-
const { ref, locker } = options;
|
|
116
|
-
const free = await locker.acquireOne();
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
const pkg = await packument(name);
|
|
120
|
-
|
|
121
|
-
const publishers = new Set();
|
|
122
|
-
const oneYearFromToday = new Date();
|
|
123
|
-
oneYearFromToday.setFullYear(oneYearFromToday.getFullYear() - 1);
|
|
124
|
-
|
|
125
|
-
ref.metadata.lastVersion = pkg["dist-tags"].latest;
|
|
126
|
-
if (semver.neq(version, ref.metadata.lastVersion)) {
|
|
127
|
-
ref[version].flags.push("isOutdated");
|
|
128
|
-
}
|
|
129
|
-
ref.metadata.publishedCount = Object.values(pkg.versions).length;
|
|
130
|
-
ref.metadata.lastUpdateAt = new Date(pkg.time[ref.metadata.lastVersion]);
|
|
131
|
-
ref.metadata.hasReceivedUpdateInOneYear = !(oneYearFromToday > ref.metadata.lastUpdateAt);
|
|
132
|
-
ref.metadata.homepage = pkg.homepage || null;
|
|
133
|
-
ref.metadata.maintainers = pkg.maintainers;
|
|
134
|
-
if (typeof pkg.author === "string") {
|
|
135
|
-
ref.metadata.author = parseManifestAuthor(pkg.author);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
ref.metadata.author = pkg.author;
|
|
139
|
-
}
|
|
140
|
-
const authorName = ref.metadata.author?.name ?? null;
|
|
141
|
-
|
|
142
|
-
for (const ver of Object.values(pkg.versions)) {
|
|
143
|
-
const { _npmUser: npmUser, version } = ver;
|
|
144
|
-
|
|
145
|
-
const isNullOrUndefined = typeof npmUser === "undefined" || npmUser === null;
|
|
146
|
-
if (isNullOrUndefined || !("name" in npmUser) || typeof npmUser.name !== "string") {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (authorName === null) {
|
|
151
|
-
ref.metadata.author.name = npmUser.name;
|
|
152
|
-
}
|
|
153
|
-
else if (npmUser.name !== ref.metadata.author.name) {
|
|
154
|
-
ref.metadata.hasManyPublishers = true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (!publishers.has(npmUser.name)) {
|
|
158
|
-
publishers.add(npmUser.name);
|
|
159
|
-
ref.metadata.publishers.push({ name: npmUser.name, version, at: new Date(pkg.time[version]) });
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
catch (err) {
|
|
164
|
-
// Ignore
|
|
165
|
-
}
|
|
166
|
-
finally {
|
|
167
|
-
free();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function* getRootDependencies(manifest, options) {
|
|
115
|
+
export async function* getRootDependencies(manifest, options) {
|
|
172
116
|
const { maxDepth = 4, exclude, usePackageLock, fullLockMode } = options;
|
|
173
117
|
|
|
174
118
|
const { dependencies, customResolvers } = mergeDependencies(manifest, void 0);
|
|
@@ -179,7 +123,7 @@ async function* getRootDependencies(manifest, options) {
|
|
|
179
123
|
let iterators;
|
|
180
124
|
if (usePackageLock) {
|
|
181
125
|
const arb = new Arborist({
|
|
182
|
-
...
|
|
126
|
+
...NPM_TOKEN,
|
|
183
127
|
registry: getLocalRegistryURL()
|
|
184
128
|
});
|
|
185
129
|
let tree;
|
|
@@ -191,15 +135,15 @@ async function* getRootDependencies(manifest, options) {
|
|
|
191
135
|
tree = await arb.loadVirtual();
|
|
192
136
|
}
|
|
193
137
|
|
|
194
|
-
iterators = iter.filter(tree.edgesOut.entries(), ([, { to }]) => !to.dev)
|
|
138
|
+
iterators = iter.filter(tree.edgesOut.entries(), ([, { to }]) => to !== null && !to.dev)
|
|
195
139
|
.map(([packageName, { to }]) => deepReadEdges(packageName, { to, parent, fullLockMode, exclude }));
|
|
196
140
|
}
|
|
197
141
|
else {
|
|
198
142
|
const configRef = { exclude, maxDepth, parent };
|
|
199
143
|
iterators = [
|
|
200
|
-
...iter.filter(customResolvers.entries(), ([, valueStr]) => valueStr
|
|
201
|
-
.map(([depName, valueStr]) => searchDeepDependencies(depName, valueStr
|
|
202
|
-
...iter.map(dependencies.entries(), ([name, ver]) => searchDeepDependencies(`${name}@${ver}`,
|
|
144
|
+
...iter.filter(customResolvers.entries(), ([, valueStr]) => isGitDependency(valueStr))
|
|
145
|
+
.map(([depName, valueStr]) => searchDeepDependencies(depName, valueStr, configRef)),
|
|
146
|
+
...iter.map(dependencies.entries(), ([name, ver]) => searchDeepDependencies(`${name}@${ver}`, null, configRef))
|
|
203
147
|
];
|
|
204
148
|
}
|
|
205
149
|
for await (const dep of combineAsyncIterators({}, ...iterators)) {
|
|
@@ -241,58 +185,67 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
|
|
|
241
185
|
const payload = {
|
|
242
186
|
id: tmpLocation.slice(-6),
|
|
243
187
|
rootDepencyName: manifest.name,
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
188
|
+
version: packageVersion,
|
|
189
|
+
vulnerabilityStrategy,
|
|
190
|
+
warnings: []
|
|
247
191
|
};
|
|
248
192
|
|
|
249
193
|
// We are dealing with an exclude Map to avoid checking a package more than one time in searchDeepDependencies
|
|
250
194
|
const exclude = new Map();
|
|
195
|
+
const dependencies = new Map();
|
|
251
196
|
|
|
252
197
|
{
|
|
253
|
-
logger
|
|
198
|
+
logger
|
|
199
|
+
.start(ScannerLoggerEvents.analysis.tree)
|
|
200
|
+
.start(ScannerLoggerEvents.analysis.tarball)
|
|
201
|
+
.start(ScannerLoggerEvents.analysis.registry);
|
|
202
|
+
const fetchedMetadataPackages = new Set();
|
|
254
203
|
const promisesToWait = [];
|
|
255
|
-
const rootDepsOptions = { maxDepth, exclude, usePackageLock, fullLockMode };
|
|
256
204
|
|
|
257
205
|
const tarballLocker = new Lock({ maxConcurrent: 5 });
|
|
258
|
-
|
|
259
|
-
metadataLocker.on("freeOne", () => logger.tick("registry"));
|
|
260
|
-
tarballLocker.on("freeOne", () => logger.tick("tarball"));
|
|
206
|
+
tarballLocker.on("freeOne", () => logger.tick(ScannerLoggerEvents.analysis.tarball));
|
|
261
207
|
|
|
208
|
+
const rootDepsOptions = { maxDepth, exclude, usePackageLock, fullLockMode };
|
|
262
209
|
for await (const currentDep of getRootDependencies(manifest, rootDepsOptions)) {
|
|
263
210
|
const { name, version } = currentDep;
|
|
264
211
|
const current = currentDep.exportAsPlainObject(name === manifest.name ? 0 : void 0);
|
|
265
212
|
let proceedDependencyAnalysis = true;
|
|
266
213
|
|
|
267
|
-
if (
|
|
214
|
+
if (dependencies.has(name)) {
|
|
268
215
|
// TODO: how to handle different metadata ?
|
|
269
|
-
const dep =
|
|
216
|
+
const dep = dependencies.get(name);
|
|
270
217
|
|
|
271
|
-
const currVersion = current.versions[0];
|
|
272
|
-
if (currVersion in dep) {
|
|
218
|
+
const currVersion = Object.keys(current.versions)[0];
|
|
219
|
+
if (currVersion in dep.versions) {
|
|
273
220
|
// The dependency has already entered the analysis
|
|
274
221
|
// This happens if the package is used by multiple packages in the tree
|
|
275
222
|
proceedDependencyAnalysis = false;
|
|
276
223
|
}
|
|
277
224
|
else {
|
|
278
|
-
dep[currVersion] = current[currVersion];
|
|
279
|
-
dep.versions.push(currVersion);
|
|
225
|
+
dep.versions[currVersion] = current.versions[currVersion];
|
|
280
226
|
}
|
|
281
227
|
}
|
|
282
228
|
else {
|
|
283
|
-
|
|
229
|
+
dependencies.set(name, current);
|
|
284
230
|
}
|
|
285
231
|
|
|
286
232
|
if (proceedDependencyAnalysis) {
|
|
287
|
-
logger.tick(
|
|
233
|
+
logger.tick(ScannerLoggerEvents.analysis.tree);
|
|
234
|
+
|
|
235
|
+
// There is no need to fetch 'N' times the npm metadata for the same package.
|
|
236
|
+
if (fetchedMetadataPackages.has(name)) {
|
|
237
|
+
logger.tick(ScannerLoggerEvents.analysis.registry);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
fetchedMetadataPackages.add(name);
|
|
241
|
+
promisesToWait.push(packageMetadata(name, version, {
|
|
242
|
+
ref: current,
|
|
243
|
+
logger
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
288
246
|
|
|
289
|
-
promisesToWait.push(fetchPackageMetadata(name, version, {
|
|
290
|
-
ref: current,
|
|
291
|
-
locker: metadataLocker,
|
|
292
|
-
logger
|
|
293
|
-
}));
|
|
294
247
|
promisesToWait.push(scanDirOrArchive(name, version, {
|
|
295
|
-
ref: current[version],
|
|
248
|
+
ref: current.versions[version],
|
|
296
249
|
tmpLocation: forceRootAnalysis && name === manifest.name ? null : tmpLocation,
|
|
297
250
|
locker: tarballLocker,
|
|
298
251
|
logger
|
|
@@ -300,17 +253,17 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
|
|
|
300
253
|
}
|
|
301
254
|
}
|
|
302
255
|
|
|
303
|
-
logger.end(
|
|
256
|
+
logger.end(ScannerLoggerEvents.analysis.tree);
|
|
304
257
|
|
|
305
258
|
// Wait for all extraction to be done!
|
|
306
259
|
await Promise.allSettled(promisesToWait);
|
|
307
260
|
await timers.setImmediate();
|
|
308
261
|
|
|
309
|
-
logger.end(
|
|
262
|
+
logger.end(ScannerLoggerEvents.analysis.tarball).end(ScannerLoggerEvents.analysis.registry);
|
|
310
263
|
}
|
|
311
264
|
|
|
312
265
|
const { hydratePayloadDependencies, strategy } = await vuln.setStrategy(vulnerabilityStrategy);
|
|
313
|
-
await hydratePayloadDependencies(
|
|
266
|
+
await hydratePayloadDependencies(dependencies, {
|
|
314
267
|
useStandardFormat: true
|
|
315
268
|
});
|
|
316
269
|
|
|
@@ -318,8 +271,10 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
|
|
|
318
271
|
|
|
319
272
|
// We do this because it "seem" impossible to link all dependencies in the first walk.
|
|
320
273
|
// Because we are dealing with package only one time it may happen sometimes.
|
|
321
|
-
for (const [packageName,
|
|
322
|
-
for (const verStr of
|
|
274
|
+
for (const [packageName, dependency] of dependencies) {
|
|
275
|
+
for (const [verStr, verDescriptor] of Object.entries(dependency.versions)) {
|
|
276
|
+
verDescriptor.flags.push(...addMissingVersionFlags(new Set(verDescriptor.flags), dependency));
|
|
277
|
+
|
|
323
278
|
const fullName = `${packageName}@${verStr}`;
|
|
324
279
|
const usedDeps = exclude.get(fullName) || new Set();
|
|
325
280
|
if (usedDeps.size === 0) {
|
|
@@ -330,18 +285,20 @@ export async function depWalker(manifest, options = {}, logger = new Logger()) {
|
|
|
330
285
|
for (const [name, version] of [...usedDeps].map((name) => name.split(" "))) {
|
|
331
286
|
usedBy[name] = version;
|
|
332
287
|
}
|
|
333
|
-
Object.assign(
|
|
288
|
+
Object.assign(verDescriptor.usedBy, usedBy);
|
|
334
289
|
}
|
|
335
290
|
}
|
|
336
291
|
|
|
337
292
|
try {
|
|
338
|
-
payload.warnings = getDependenciesWarnings(
|
|
339
|
-
payload.dependencies = Object.fromEntries(
|
|
293
|
+
payload.warnings = getDependenciesWarnings(dependencies);
|
|
294
|
+
payload.dependencies = Object.fromEntries(dependencies);
|
|
340
295
|
|
|
341
296
|
return payload;
|
|
342
297
|
}
|
|
343
298
|
finally {
|
|
344
299
|
await timers.setImmediate();
|
|
345
300
|
await fs.rm(tmpLocation, { recursive: true, force: true });
|
|
301
|
+
|
|
302
|
+
logger.emit(ScannerLoggerEvents.done);
|
|
346
303
|
}
|
|
347
304
|
}
|
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
|
+
}
|