dependency-radar 0.2.0 → 0.3.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 +176 -19
- package/dist/aggregator.js +1021 -406
- package/dist/cli.js +857 -56
- package/dist/generated/spdx.js +855 -0
- package/dist/license.js +324 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +88 -89
- package/dist/runners/importGraphRunner.js +11 -5
- package/dist/runners/npmAudit.js +81 -16
- package/dist/runners/npmLs.js +216 -15
- package/dist/runners/npmOutdated.js +115 -0
- package/dist/utils.js +159 -24
- package/package.json +23 -3
package/dist/aggregator.js
CHANGED
|
@@ -5,8 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.aggregateData = aggregateData;
|
|
7
7
|
const utils_1 = require("./utils");
|
|
8
|
+
const license_1 = require("./license");
|
|
8
9
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
10
12
|
const dependencyRadarVersion = (0, utils_1.getDependencyRadarVersion)();
|
|
11
13
|
async function getGitBranch(projectPath) {
|
|
12
14
|
var _a;
|
|
@@ -26,10 +28,10 @@ async function getGitBranch(projectPath) {
|
|
|
26
28
|
function findRootCauses(node, nodeMap, pkg) {
|
|
27
29
|
// If it's a direct dependency, it's its own root cause
|
|
28
30
|
if (isDirectDependency(node.name, pkg)) {
|
|
29
|
-
return [node.name];
|
|
31
|
+
return [{ name: node.name, version: node.version }];
|
|
30
32
|
}
|
|
31
33
|
// BFS up the parent chain to find all direct dependencies that lead to this
|
|
32
|
-
const rootCauses = new
|
|
34
|
+
const rootCauses = new Map();
|
|
33
35
|
const visited = new Set();
|
|
34
36
|
const queue = [...node.parents];
|
|
35
37
|
while (queue.length > 0) {
|
|
@@ -41,7 +43,7 @@ function findRootCauses(node, nodeMap, pkg) {
|
|
|
41
43
|
if (!parent)
|
|
42
44
|
continue;
|
|
43
45
|
if (isDirectDependency(parent.name, pkg)) {
|
|
44
|
-
rootCauses.
|
|
46
|
+
rootCauses.set(parent.key, { name: parent.name, version: parent.version });
|
|
45
47
|
}
|
|
46
48
|
else {
|
|
47
49
|
// Keep going up the chain
|
|
@@ -52,151 +54,175 @@ function findRootCauses(node, nodeMap, pkg) {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
return Array.from(rootCauses).sort()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
: 'github.com';
|
|
70
|
-
return `https://${host}/${cleaned}`;
|
|
71
|
-
}
|
|
72
|
-
// Handle git+https:// or git:// prefix
|
|
73
|
-
let normalized = url.replace(/^git\+/, '').replace(/^git:\/\//, 'https://');
|
|
74
|
-
// Handle git@host:user/repo.git SSH format
|
|
75
|
-
normalized = normalized.replace(/^git@([^:]+):(.+)$/, 'https://$1/$2');
|
|
76
|
-
// Remove .git suffix
|
|
77
|
-
normalized = normalized.replace(/\.git$/, '');
|
|
78
|
-
return normalized;
|
|
57
|
+
return Array.from(rootCauses.values()).sort((a, b) => {
|
|
58
|
+
const nameCompare = a.name.localeCompare(b.name);
|
|
59
|
+
if (nameCompare !== 0)
|
|
60
|
+
return nameCompare;
|
|
61
|
+
return a.version.localeCompare(b.version);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function formatProjectDir(projectPath) {
|
|
65
|
+
const home = os_1.default.homedir();
|
|
66
|
+
const relative = path_1.default.relative(home, projectPath);
|
|
67
|
+
if (relative && !relative.startsWith('..') && !path_1.default.isAbsolute(relative)) {
|
|
68
|
+
return `/${relative.split(path_1.default.sep).join('/')}`;
|
|
69
|
+
}
|
|
70
|
+
return projectPath;
|
|
79
71
|
}
|
|
80
72
|
async function aggregateData(input) {
|
|
81
|
-
var _a, _b, _c, _d, _e, _f;
|
|
82
|
-
const pkg = await (0, utils_1.readPackageJson)(input.projectPath);
|
|
83
|
-
const raw = {
|
|
84
|
-
audit: (_a = input.auditResult) === null || _a === void 0 ? void 0 : _a.data,
|
|
85
|
-
npmLs: (_b = input.npmLsResult) === null || _b === void 0 ? void 0 : _b.data,
|
|
86
|
-
importGraph: (_c = input.importGraphResult) === null || _c === void 0 ? void 0 : _c.data
|
|
87
|
-
};
|
|
88
|
-
const toolErrors = {};
|
|
89
|
-
if (input.auditResult && !input.auditResult.ok)
|
|
90
|
-
toolErrors['npm-audit'] = input.auditResult.error || 'unknown error';
|
|
91
|
-
if (input.npmLsResult && !input.npmLsResult.ok)
|
|
92
|
-
toolErrors['npm-ls'] = input.npmLsResult.error || 'unknown error';
|
|
93
|
-
if (input.importGraphResult && !input.importGraphResult.ok)
|
|
94
|
-
toolErrors['import-graph'] = input.importGraphResult.error || 'unknown error';
|
|
73
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
74
|
+
const pkg = input.pkgOverride || (await (0, utils_1.readPackageJson)(input.projectPath));
|
|
95
75
|
// Get git branch
|
|
96
76
|
const gitBranch = await getGitBranch(input.projectPath);
|
|
97
|
-
const nodeMap = buildNodeMap((
|
|
98
|
-
const vulnMap = parseVulnerabilities((
|
|
99
|
-
const importGraph = normalizeImportGraph((
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const maintenanceCache = new Map();
|
|
104
|
-
const runtimeCache = new Map();
|
|
77
|
+
const nodeMap = buildNodeMap((_a = input.npmLsResult) === null || _a === void 0 ? void 0 : _a.data);
|
|
78
|
+
const vulnMap = parseVulnerabilities((_b = input.auditResult) === null || _b === void 0 ? void 0 : _b.data);
|
|
79
|
+
const importGraph = normalizeImportGraph((_c = input.importGraphResult) === null || _c === void 0 ? void 0 : _c.data);
|
|
80
|
+
const usageResult = buildUsageSummary(importGraph, input.projectPath);
|
|
81
|
+
const outdatedById = buildOutdatedMap(input.outdatedResult);
|
|
82
|
+
const outdatedUnknownNames = new Set(((_d = input.outdatedResult) === null || _d === void 0 ? void 0 : _d.unknownNames) || []);
|
|
105
83
|
const packageMetaCache = new Map();
|
|
84
|
+
const resolvePaths = input.resolvePaths && input.resolvePaths.length > 0
|
|
85
|
+
? input.resolvePaths
|
|
86
|
+
: [input.projectPath];
|
|
106
87
|
const packageStatCache = new Map();
|
|
107
|
-
const dependencies =
|
|
88
|
+
const dependencies = {};
|
|
108
89
|
const licenseCache = new Map();
|
|
109
90
|
const nodeEngineRanges = [];
|
|
110
91
|
const nodes = Array.from(nodeMap.values());
|
|
111
|
-
|
|
112
|
-
|
|
92
|
+
let directCount = 0;
|
|
93
|
+
const MAX_TOP_ROOT_PACKAGES = 10; // cap to keep payload size predictable
|
|
94
|
+
const MAX_TOP_PARENT_PACKAGES = 5; // cap for direct parents to keep payload size predictable
|
|
113
95
|
for (const node of nodes) {
|
|
114
96
|
const direct = isDirectDependency(node.name, pkg);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
97
|
+
if (direct)
|
|
98
|
+
directCount += 1;
|
|
99
|
+
const cacheKey = `${node.name}@${node.version}`;
|
|
100
|
+
const cachedLicense = licenseCache.get(cacheKey);
|
|
101
|
+
const licenseSource = cachedLicense ||
|
|
102
|
+
(node.path
|
|
103
|
+
? (await (0, utils_1.readLicenseFromPackageDir)(node.path))
|
|
104
|
+
: (await (0, utils_1.readLicenseFromPackageJson)(node.name, resolvePaths, node.version))) ||
|
|
118
105
|
{ license: undefined };
|
|
119
|
-
if (!licenseCache.has(
|
|
120
|
-
licenseCache.set(
|
|
106
|
+
if (!licenseCache.has(cacheKey)) {
|
|
107
|
+
licenseCache.set(cacheKey, licenseSource);
|
|
121
108
|
}
|
|
122
109
|
const vulnerabilities = vulnMap.get(node.name) || emptyVulnSummary();
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const usage = buildUsageInfo(node.name, packageUsageCounts, pkg);
|
|
126
|
-
const maintenance = await resolveMaintenance(node.name, maintenanceCache, input.maintenanceEnabled, ++maintenanceIndex, totalDeps, input.onMaintenanceProgress);
|
|
127
|
-
if (!maintenanceCache.has(node.name)) {
|
|
128
|
-
maintenanceCache.set(node.name, maintenance);
|
|
129
|
-
}
|
|
130
|
-
const maintenanceRiskLevel = (0, utils_1.maintenanceRisk)(maintenance.lastPublished);
|
|
131
|
-
const runtimeData = classifyRuntime(node.key, pkg, nodeMap, runtimeCache);
|
|
110
|
+
const licenseInfo = buildLicenseInfo(licenseSource.license, licenseSource.licenseText);
|
|
111
|
+
const licenseRisk = (0, license_1.pickLicenseRisk)(licenseInfo.licenseIds);
|
|
132
112
|
// Calculate root causes (direct dependencies that cause this to be installed)
|
|
133
113
|
const rootCauses = findRootCauses(node, nodeMap, pkg);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
114
|
+
const packageInsights = await gatherPackageInsights(node.name, node.version, resolvePaths, packageMetaCache, packageStatCache);
|
|
115
|
+
if (packageInsights.nodeEngine) {
|
|
116
|
+
nodeEngineRanges.push(packageInsights.nodeEngine);
|
|
117
|
+
}
|
|
118
|
+
const scope = determineScope(node.name, direct, rootCauses, pkg);
|
|
119
|
+
const importUsage = usageResult.summary.get(node.name);
|
|
120
|
+
const runtimeImpact = usageResult.runtimeImpact.get(node.name);
|
|
121
|
+
const introduction = determineIntroduction(direct, rootCauses, runtimeImpact);
|
|
122
|
+
const parentIds = Array.from(node.parents).sort();
|
|
123
|
+
const origins = buildOrigins(rootCauses, parentIds, (_e = input.workspaceUsage) === null || _e === void 0 ? void 0 : _e.get(node.name), input.workspaceEnabled, MAX_TOP_ROOT_PACKAGES, MAX_TOP_PARENT_PACKAGES);
|
|
124
|
+
const execution = packageInsights.execution;
|
|
125
|
+
const id = node.key;
|
|
126
|
+
const upgrade = buildUpgradeBlock(packageInsights);
|
|
127
|
+
const outdated = resolveOutdated(node, direct, outdatedById, outdatedUnknownNames);
|
|
128
|
+
const subDeps = buildSubDeps(packageInsights.declaredDependencies, node);
|
|
129
|
+
// Group fields by reviewer question to keep the JSON readable and source-agnostic.
|
|
130
|
+
// Optional fields are only attached when meaningful to keep the payload sparse.
|
|
131
|
+
const upgradeRecord = {
|
|
132
|
+
nodeEngine: packageInsights.nodeEngine,
|
|
133
|
+
...(outdated ? { outdatedStatus: outdated.status } : {}),
|
|
134
|
+
...((outdated === null || outdated === void 0 ? void 0 : outdated.latestVersion) ? { latestVersion: outdated.latestVersion } : {}),
|
|
135
|
+
...((upgrade === null || upgrade === void 0 ? void 0 : upgrade.blockers) ? { blockers: upgrade.blockers } : {}),
|
|
136
|
+
...((upgrade === null || upgrade === void 0 ? void 0 : upgrade.blocksNodeMajor) ? { blocksNodeMajor: upgrade.blocksNodeMajor } : {})
|
|
137
|
+
};
|
|
138
|
+
dependencies[id] = {
|
|
139
|
+
package: {
|
|
140
|
+
id,
|
|
141
|
+
name: node.name,
|
|
142
|
+
version: node.version,
|
|
143
|
+
...(packageInsights.description ? { description: packageInsights.description } : {}),
|
|
144
|
+
deprecated: packageInsights.deprecated,
|
|
145
|
+
links: {
|
|
146
|
+
npm: `https://www.npmjs.com/package/${node.name}`,
|
|
147
|
+
...(((_f = packageInsights.links) === null || _f === void 0 ? void 0 : _f.repository) ? { repository: packageInsights.links.repository } : {}),
|
|
148
|
+
...(((_g = packageInsights.links) === null || _g === void 0 ? void 0 : _g.homepage) ? { homepage: packageInsights.links.homepage } : {}),
|
|
149
|
+
...(((_h = packageInsights.links) === null || _h === void 0 ? void 0 : _h.bugs) ? { bugs: packageInsights.links.bugs } : {})
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
compliance: {
|
|
153
|
+
license: licenseInfo.record,
|
|
154
|
+
licenseRisk
|
|
155
|
+
},
|
|
156
|
+
security: {
|
|
157
|
+
summary: {
|
|
158
|
+
critical: vulnerabilities.counts.critical,
|
|
159
|
+
high: vulnerabilities.counts.high,
|
|
160
|
+
moderate: vulnerabilities.counts.moderate,
|
|
161
|
+
low: vulnerabilities.counts.low,
|
|
162
|
+
highest: vulnerabilities.highestSeverity,
|
|
163
|
+
risk: vulnerabilities.risk
|
|
164
|
+
},
|
|
165
|
+
...(vulnerabilities.advisories && vulnerabilities.advisories.length > 0 ? { advisories: vulnerabilities.advisories } : {})
|
|
166
|
+
},
|
|
167
|
+
upgrade: upgradeRecord,
|
|
168
|
+
usage: {
|
|
169
|
+
direct,
|
|
170
|
+
scope,
|
|
171
|
+
depth: node.depth,
|
|
172
|
+
origins,
|
|
173
|
+
...(introduction ? { introduction } : {}),
|
|
174
|
+
...(runtimeImpact ? { runtimeImpact } : {}),
|
|
175
|
+
...(importUsage ? { importUsage } : {}),
|
|
176
|
+
tsTypes: packageInsights.tsTypes
|
|
177
|
+
},
|
|
178
|
+
graph: {
|
|
179
|
+
fanIn: node.parents.size,
|
|
180
|
+
fanOut: node.children.size,
|
|
181
|
+
...(subDeps ? { subDeps } : {})
|
|
182
|
+
},
|
|
183
|
+
...(execution ? { execution } : {})
|
|
184
|
+
};
|
|
177
185
|
}
|
|
178
|
-
dependencies.sort((a, b) => a.name.localeCompare(b.name));
|
|
179
|
-
const runtimeVersion = process.version.replace(/^v/, '');
|
|
180
|
-
const runtimeMajor = Number.parseInt(runtimeVersion.split('.')[0], 10);
|
|
181
186
|
const minRequiredMajor = deriveMinRequiredMajor(nodeEngineRanges);
|
|
187
|
+
const runtimeVersion = process.version;
|
|
188
|
+
const nodeVersion = process.versions.node;
|
|
189
|
+
const dependencyCount = nodes.length;
|
|
190
|
+
const transitiveCount = dependencyCount - directCount;
|
|
182
191
|
return {
|
|
192
|
+
schemaVersion: '1.2',
|
|
183
193
|
generatedAt: new Date().toISOString(),
|
|
184
|
-
projectPath: input.projectPath,
|
|
185
194
|
dependencyRadarVersion,
|
|
186
|
-
|
|
187
|
-
|
|
195
|
+
git: {
|
|
196
|
+
branch: gitBranch || ''
|
|
197
|
+
},
|
|
198
|
+
project: {
|
|
199
|
+
projectDir: formatProjectDir(input.projectPath)
|
|
200
|
+
},
|
|
188
201
|
environment: {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
202
|
+
nodeVersion,
|
|
203
|
+
runtimeVersion,
|
|
204
|
+
minRequiredMajor: minRequiredMajor !== null && minRequiredMajor !== void 0 ? minRequiredMajor : 0,
|
|
205
|
+
...(input.platform ? { platform: input.platform } : {}),
|
|
206
|
+
...(input.arch ? { arch: input.arch } : {}),
|
|
207
|
+
...(typeof input.ci === 'boolean' ? { ci: input.ci } : {}),
|
|
208
|
+
...(input.packageManagerField ? { packageManagerField: input.packageManagerField } : {}),
|
|
209
|
+
...(input.packageManager ? { packageManager: input.packageManager } : {}),
|
|
210
|
+
...(input.packageManagerVersion ? { packageManagerVersion: input.packageManagerVersion } : {}),
|
|
211
|
+
...(input.toolVersions ? { toolVersions: input.toolVersions } : {})
|
|
195
212
|
},
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
213
|
+
workspaces: {
|
|
214
|
+
enabled: input.workspaceEnabled,
|
|
215
|
+
...(input.workspaceType ? { type: input.workspaceType } : {}),
|
|
216
|
+
...(typeof input.workspacePackageCount === 'number'
|
|
217
|
+
? { packageCount: input.workspacePackageCount }
|
|
218
|
+
: {})
|
|
219
|
+
},
|
|
220
|
+
summary: {
|
|
221
|
+
dependencyCount,
|
|
222
|
+
directCount,
|
|
223
|
+
transitiveCount
|
|
224
|
+
},
|
|
225
|
+
dependencies
|
|
200
226
|
};
|
|
201
227
|
}
|
|
202
228
|
function deriveMinRequiredMajor(engineRanges) {
|
|
@@ -261,7 +287,7 @@ function parseMajorFromToken(token) {
|
|
|
261
287
|
const major = Number.parseInt(match[1], 10);
|
|
262
288
|
return Number.isNaN(major) ? undefined : major;
|
|
263
289
|
}
|
|
264
|
-
function buildNodeMap(lsData
|
|
290
|
+
function buildNodeMap(lsData) {
|
|
265
291
|
const map = new Map();
|
|
266
292
|
const traverse = (node, depth, parentKey, providedName) => {
|
|
267
293
|
const nodeName = (node === null || node === void 0 ? void 0 : node.name) || providedName;
|
|
@@ -277,7 +303,9 @@ function buildNodeMap(lsData, pkg) {
|
|
|
277
303
|
depth,
|
|
278
304
|
parents: new Set(parentKey ? [parentKey] : []),
|
|
279
305
|
children: new Set(),
|
|
280
|
-
|
|
306
|
+
childByName: new Map(),
|
|
307
|
+
dev: node.dev,
|
|
308
|
+
path: typeof node.path === 'string' ? node.path : undefined
|
|
281
309
|
});
|
|
282
310
|
}
|
|
283
311
|
else {
|
|
@@ -287,8 +315,12 @@ function buildNodeMap(lsData, pkg) {
|
|
|
287
315
|
existing.parents.add(parentKey);
|
|
288
316
|
if (existing.dev === undefined && node.dev !== undefined)
|
|
289
317
|
existing.dev = node.dev;
|
|
318
|
+
if (!existing.path && typeof node.path === 'string')
|
|
319
|
+
existing.path = node.path;
|
|
290
320
|
if (!existing.children)
|
|
291
321
|
existing.children = new Set();
|
|
322
|
+
if (!existing.childByName)
|
|
323
|
+
existing.childByName = new Map();
|
|
292
324
|
}
|
|
293
325
|
if (node.dependencies && typeof node.dependencies === 'object') {
|
|
294
326
|
Object.entries(node.dependencies).forEach(([depName, child]) => {
|
|
@@ -297,6 +329,7 @@ function buildNodeMap(lsData, pkg) {
|
|
|
297
329
|
const current = map.get(key);
|
|
298
330
|
if (current) {
|
|
299
331
|
current.children.add(childKey);
|
|
332
|
+
current.childByName.set(depName, childKey);
|
|
300
333
|
}
|
|
301
334
|
traverse(child, depth + 1, key, depName);
|
|
302
335
|
});
|
|
@@ -305,22 +338,152 @@ function buildNodeMap(lsData, pkg) {
|
|
|
305
338
|
if (lsData && lsData.dependencies) {
|
|
306
339
|
Object.entries(lsData.dependencies).forEach(([depName, child]) => traverse(child, 1, undefined, depName));
|
|
307
340
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
341
|
+
return map;
|
|
342
|
+
}
|
|
343
|
+
function buildOutdatedMap(outdatedResult) {
|
|
344
|
+
const map = new Map();
|
|
345
|
+
if (!outdatedResult || !Array.isArray(outdatedResult.entries))
|
|
346
|
+
return map;
|
|
347
|
+
for (const entry of outdatedResult.entries) {
|
|
348
|
+
if (!entry || typeof entry.name !== 'string' || typeof entry.currentVersion !== 'string')
|
|
349
|
+
continue;
|
|
350
|
+
const key = `${entry.name}@${entry.currentVersion}`;
|
|
351
|
+
if (entry.status === 'patch' || entry.status === 'minor' || entry.status === 'major') {
|
|
352
|
+
if (entry.latestVersion) {
|
|
353
|
+
map.set(key, { status: entry.status, latestVersion: entry.latestVersion });
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
map.set(key, { status: 'unknown' });
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
map.set(key, { status: 'unknown' });
|
|
321
361
|
}
|
|
322
362
|
return map;
|
|
323
363
|
}
|
|
364
|
+
function resolveOutdated(node, direct, outdatedById, unknownNames) {
|
|
365
|
+
const entry = outdatedById.get(node.key);
|
|
366
|
+
if (entry) {
|
|
367
|
+
if (entry.status === 'patch' || entry.status === 'minor' || entry.status === 'major') {
|
|
368
|
+
if (entry.latestVersion) {
|
|
369
|
+
return { status: entry.status, latestVersion: entry.latestVersion };
|
|
370
|
+
}
|
|
371
|
+
return { status: 'unknown' };
|
|
372
|
+
}
|
|
373
|
+
return { status: 'unknown' };
|
|
374
|
+
}
|
|
375
|
+
if (direct && unknownNames.has(node.name)) {
|
|
376
|
+
return { status: 'unknown' };
|
|
377
|
+
}
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
const GHSA_ID_REGEX = /GHSA-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}/i;
|
|
381
|
+
function extractGhsaId(value) {
|
|
382
|
+
if (typeof value !== 'string')
|
|
383
|
+
return undefined;
|
|
384
|
+
const match = value.match(GHSA_ID_REGEX);
|
|
385
|
+
return match ? match[0].toUpperCase() : undefined;
|
|
386
|
+
}
|
|
387
|
+
function extractNpmAdvisoryId(url) {
|
|
388
|
+
const match = url.match(/advisories\/(\d+)/i);
|
|
389
|
+
return match ? match[1] : undefined;
|
|
390
|
+
}
|
|
391
|
+
function resolveAdvisoryId(advisory, fallbackUrl) {
|
|
392
|
+
const ghsa = extractGhsaId(advisory === null || advisory === void 0 ? void 0 : advisory.github_advisory_id) ||
|
|
393
|
+
extractGhsaId(advisory === null || advisory === void 0 ? void 0 : advisory.ghsaId) ||
|
|
394
|
+
extractGhsaId(advisory === null || advisory === void 0 ? void 0 : advisory.ghsa_id) ||
|
|
395
|
+
extractGhsaId(advisory === null || advisory === void 0 ? void 0 : advisory.source) ||
|
|
396
|
+
extractGhsaId(advisory === null || advisory === void 0 ? void 0 : advisory.id) ||
|
|
397
|
+
extractGhsaId(advisory === null || advisory === void 0 ? void 0 : advisory.url) ||
|
|
398
|
+
(fallbackUrl ? extractGhsaId(fallbackUrl) : undefined);
|
|
399
|
+
if (ghsa)
|
|
400
|
+
return ghsa;
|
|
401
|
+
const url = typeof (advisory === null || advisory === void 0 ? void 0 : advisory.url) === 'string' ? advisory.url : undefined;
|
|
402
|
+
const npmId = url ? extractNpmAdvisoryId(url) : undefined;
|
|
403
|
+
if (npmId)
|
|
404
|
+
return npmId;
|
|
405
|
+
if ((advisory === null || advisory === void 0 ? void 0 : advisory.source) !== undefined)
|
|
406
|
+
return String(advisory.source);
|
|
407
|
+
if ((advisory === null || advisory === void 0 ? void 0 : advisory.id) !== undefined)
|
|
408
|
+
return String(advisory.id);
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
function resolveAdvisoryUrl(advisory, id) {
|
|
412
|
+
if (typeof (advisory === null || advisory === void 0 ? void 0 : advisory.url) === 'string' && advisory.url.trim())
|
|
413
|
+
return advisory.url.trim();
|
|
414
|
+
if (id) {
|
|
415
|
+
if (/^GHSA-/i.test(id))
|
|
416
|
+
return `https://github.com/advisories/${id}`;
|
|
417
|
+
if (/^\d+$/.test(id))
|
|
418
|
+
return `https://www.npmjs.com/advisories/${id}`;
|
|
419
|
+
}
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
function resolveFixAvailable(value, patchedVersions) {
|
|
423
|
+
if (typeof value === 'boolean')
|
|
424
|
+
return value;
|
|
425
|
+
if (value && typeof value === 'object')
|
|
426
|
+
return true;
|
|
427
|
+
if (typeof patchedVersions === 'string') {
|
|
428
|
+
const trimmed = patchedVersions.trim();
|
|
429
|
+
return Boolean(trimmed && trimmed !== '<0.0.0');
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
function normalizeAdvisoryRange(value) {
|
|
434
|
+
if (typeof value === 'string' && value.trim())
|
|
435
|
+
return value.trim();
|
|
436
|
+
return 'unknown';
|
|
437
|
+
}
|
|
438
|
+
function buildAdvisoryFromVia(via, item) {
|
|
439
|
+
var _a;
|
|
440
|
+
if (!via || typeof via !== 'object')
|
|
441
|
+
return undefined;
|
|
442
|
+
const id = resolveAdvisoryId(via, via.url) || resolveAdvisoryId(item, item === null || item === void 0 ? void 0 : item.url);
|
|
443
|
+
const title = typeof via.title === 'string' && via.title.trim()
|
|
444
|
+
? via.title.trim()
|
|
445
|
+
: typeof via.name === 'string' && via.name.trim()
|
|
446
|
+
? via.name.trim()
|
|
447
|
+
: 'Advisory';
|
|
448
|
+
const severity = normalizeSeverity(via.severity || (item === null || item === void 0 ? void 0 : item.severity));
|
|
449
|
+
const vulnerableRange = normalizeAdvisoryRange(via.range || via.vulnerable_versions || (item === null || item === void 0 ? void 0 : item.range));
|
|
450
|
+
const fixAvailable = resolveFixAvailable((_a = via.fixAvailable) !== null && _a !== void 0 ? _a : item === null || item === void 0 ? void 0 : item.fixAvailable, via.patched_versions);
|
|
451
|
+
const url = resolveAdvisoryUrl(via, id) || resolveAdvisoryUrl(item, id);
|
|
452
|
+
if (!id)
|
|
453
|
+
return undefined;
|
|
454
|
+
return {
|
|
455
|
+
id,
|
|
456
|
+
title,
|
|
457
|
+
severity,
|
|
458
|
+
vulnerableRange,
|
|
459
|
+
fixAvailable,
|
|
460
|
+
url: url || ''
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function buildAdvisoryFromLegacy(adv) {
|
|
464
|
+
if (!adv || typeof adv !== 'object')
|
|
465
|
+
return undefined;
|
|
466
|
+
const id = resolveAdvisoryId(adv, adv.url);
|
|
467
|
+
const title = typeof adv.title === 'string' && adv.title.trim()
|
|
468
|
+
? adv.title.trim()
|
|
469
|
+
: typeof adv.module_name === 'string' && adv.module_name.trim()
|
|
470
|
+
? adv.module_name.trim()
|
|
471
|
+
: 'Advisory';
|
|
472
|
+
const severity = normalizeSeverity(adv.severity);
|
|
473
|
+
const vulnerableRange = normalizeAdvisoryRange(adv.vulnerable_versions || adv.vulnerableRange || adv.range);
|
|
474
|
+
const fixAvailable = resolveFixAvailable(adv.fix_available, adv.patched_versions);
|
|
475
|
+
const url = resolveAdvisoryUrl(adv, id);
|
|
476
|
+
if (!id)
|
|
477
|
+
return undefined;
|
|
478
|
+
return {
|
|
479
|
+
id,
|
|
480
|
+
title,
|
|
481
|
+
severity,
|
|
482
|
+
vulnerableRange,
|
|
483
|
+
fixAvailable,
|
|
484
|
+
url: url || ''
|
|
485
|
+
};
|
|
486
|
+
}
|
|
324
487
|
function parseVulnerabilities(auditData) {
|
|
325
488
|
const map = new Map();
|
|
326
489
|
if (!auditData)
|
|
@@ -331,49 +494,88 @@ function parseVulnerabilities(auditData) {
|
|
|
331
494
|
}
|
|
332
495
|
return map.get(name);
|
|
333
496
|
};
|
|
497
|
+
const advisoryKeys = new Map();
|
|
498
|
+
const deferredViaStrings = [];
|
|
499
|
+
const addAdvisory = (name, advisory) => {
|
|
500
|
+
const entry = ensureEntry(name);
|
|
501
|
+
const key = `${advisory.id}|${advisory.vulnerableRange}`;
|
|
502
|
+
let keys = advisoryKeys.get(name);
|
|
503
|
+
if (!keys) {
|
|
504
|
+
keys = new Set();
|
|
505
|
+
advisoryKeys.set(name, keys);
|
|
506
|
+
}
|
|
507
|
+
if (keys.has(key))
|
|
508
|
+
return;
|
|
509
|
+
keys.add(key);
|
|
510
|
+
if (!entry.advisories)
|
|
511
|
+
entry.advisories = [];
|
|
512
|
+
entry.advisories.push(advisory);
|
|
513
|
+
};
|
|
514
|
+
// Advisories are disclosed findings from npm audit (not malware detection).
|
|
515
|
+
// Summary-only output loses evidence and is a data loss bug.
|
|
334
516
|
if (auditData.vulnerabilities) {
|
|
335
517
|
Object.values(auditData.vulnerabilities).forEach((item) => {
|
|
336
518
|
const name = item.name || 'unknown';
|
|
337
|
-
const severity = normalizeSeverity(item.severity);
|
|
338
|
-
const entry = ensureEntry(name);
|
|
339
|
-
entry.counts[severity] = (entry.counts[severity] || 0) + 1;
|
|
340
519
|
const viaList = Array.isArray(item.via) ? item.via : [];
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
520
|
+
let added = false;
|
|
521
|
+
for (const via of viaList) {
|
|
522
|
+
if (via && typeof via === 'object') {
|
|
523
|
+
const advisory = buildAdvisoryFromVia(via, item);
|
|
524
|
+
if (advisory) {
|
|
525
|
+
addAdvisory(name, advisory);
|
|
526
|
+
added = true;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const viaStrings = viaList.filter((via) => typeof via === 'string');
|
|
531
|
+
if (viaStrings.length > 0) {
|
|
532
|
+
deferredViaStrings.push({ name, via: viaStrings });
|
|
533
|
+
}
|
|
534
|
+
if (!added) {
|
|
535
|
+
const fallback = buildAdvisoryFromVia(item, item);
|
|
536
|
+
if (fallback)
|
|
537
|
+
addAdvisory(name, fallback);
|
|
538
|
+
}
|
|
356
539
|
});
|
|
357
540
|
}
|
|
358
541
|
if (auditData.advisories) {
|
|
359
542
|
Object.values(auditData.advisories).forEach((adv) => {
|
|
360
543
|
const name = adv.module_name || adv.module || 'unknown';
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
title: adv.title,
|
|
365
|
-
severity,
|
|
366
|
-
url: adv.url,
|
|
367
|
-
vulnerableRange: adv.vulnerable_versions,
|
|
368
|
-
fixAvailable: adv.fix_available,
|
|
369
|
-
paths: (adv.findings || []).flatMap((f) => f.paths || [])
|
|
370
|
-
});
|
|
371
|
-
entry.counts[severity] = (entry.counts[severity] || 0) + 1;
|
|
372
|
-
entry.highestSeverity = computeHighestSeverity(entry.counts);
|
|
544
|
+
const advisory = buildAdvisoryFromLegacy(adv);
|
|
545
|
+
if (advisory)
|
|
546
|
+
addAdvisory(name, advisory);
|
|
373
547
|
});
|
|
374
548
|
}
|
|
549
|
+
// One-level expansion: map string "via" references to their advisories without storing paths.
|
|
550
|
+
for (const entry of deferredViaStrings) {
|
|
551
|
+
for (const refName of entry.via) {
|
|
552
|
+
const referenced = map.get(refName);
|
|
553
|
+
if (referenced === null || referenced === void 0 ? void 0 : referenced.advisories) {
|
|
554
|
+
for (const advisory of referenced.advisories) {
|
|
555
|
+
addAdvisory(entry.name, advisory);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
375
560
|
map.forEach((entry) => {
|
|
376
|
-
|
|
561
|
+
const counts = { low: 0, moderate: 0, high: 0, critical: 0 };
|
|
562
|
+
if (entry.advisories) {
|
|
563
|
+
for (const advisory of entry.advisories) {
|
|
564
|
+
counts[advisory.severity] += 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
entry.counts = counts;
|
|
568
|
+
entry.highestSeverity = computeHighestSeverity(counts);
|
|
569
|
+
entry.risk = (0, utils_1.vulnRiskLevel)(counts);
|
|
570
|
+
if (entry.advisories && entry.advisories.length > 0) {
|
|
571
|
+
entry.advisories.sort((a, b) => {
|
|
572
|
+
const order = { critical: 4, high: 3, moderate: 2, low: 1 };
|
|
573
|
+
const diff = order[b.severity] - order[a.severity];
|
|
574
|
+
if (diff !== 0)
|
|
575
|
+
return diff;
|
|
576
|
+
return a.title.localeCompare(b.title);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
377
579
|
});
|
|
378
580
|
return map;
|
|
379
581
|
}
|
|
@@ -390,8 +592,8 @@ function normalizeSeverity(sev) {
|
|
|
390
592
|
function emptyVulnSummary() {
|
|
391
593
|
return {
|
|
392
594
|
counts: { low: 0, moderate: 0, high: 0, critical: 0 },
|
|
393
|
-
|
|
394
|
-
|
|
595
|
+
highestSeverity: 'none',
|
|
596
|
+
risk: 'green'
|
|
395
597
|
};
|
|
396
598
|
}
|
|
397
599
|
function computeHighestSeverity(counts) {
|
|
@@ -406,256 +608,405 @@ function computeHighestSeverity(counts) {
|
|
|
406
608
|
return 'none';
|
|
407
609
|
}
|
|
408
610
|
function normalizeImportGraph(data) {
|
|
409
|
-
if (data && typeof data === 'object' && data.
|
|
611
|
+
if (data && typeof data === 'object' && data.packages) {
|
|
410
612
|
return {
|
|
411
|
-
files: data.files || {},
|
|
412
613
|
packages: data.packages || {},
|
|
413
|
-
|
|
614
|
+
packageCounts: data.packageCounts || {}
|
|
414
615
|
};
|
|
415
616
|
}
|
|
416
|
-
return {
|
|
617
|
+
return { packages: {} };
|
|
417
618
|
}
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
return {
|
|
429
|
-
status: 'undeclared',
|
|
430
|
-
reason: 'Imported but not declared (may rely on transitive resolution; pnpm will usually break this)'
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
if (declared) {
|
|
434
|
-
return {
|
|
435
|
-
status: 'not-imported',
|
|
436
|
-
reason: 'Declared but never statically imported (may be used via tooling, scripts, or runtime plugins)'
|
|
437
|
-
};
|
|
619
|
+
function normalizeImportPath(file, projectPath) {
|
|
620
|
+
if (!file || typeof file !== 'string')
|
|
621
|
+
return undefined;
|
|
622
|
+
if (file.includes('node_modules'))
|
|
623
|
+
return undefined;
|
|
624
|
+
let relativePath = file;
|
|
625
|
+
if (path_1.default.isAbsolute(file)) {
|
|
626
|
+
relativePath = path_1.default.relative(projectPath, file);
|
|
438
627
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
628
|
+
if (!relativePath)
|
|
629
|
+
return undefined;
|
|
630
|
+
const trimmed = relativePath.replace(/^[.][\\/]/, '');
|
|
631
|
+
const normalized = trimmed.replace(/\\/g, '/');
|
|
632
|
+
if (!normalized || normalized.startsWith('..'))
|
|
633
|
+
return undefined;
|
|
634
|
+
if (normalized.includes('node_modules'))
|
|
635
|
+
return undefined;
|
|
636
|
+
return normalized;
|
|
443
637
|
}
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
638
|
+
function buildUsageSummary(graph, projectPath) {
|
|
639
|
+
var _a;
|
|
640
|
+
const summary = new Map();
|
|
641
|
+
const runtimeImpact = new Map();
|
|
642
|
+
const byDep = new Map();
|
|
643
|
+
const packages = graph.packages || {};
|
|
644
|
+
for (const [file, deps] of Object.entries(packages)) {
|
|
645
|
+
if (!Array.isArray(deps) || deps.length === 0)
|
|
646
|
+
continue;
|
|
647
|
+
const normalizedFile = normalizeImportPath(file, projectPath);
|
|
648
|
+
if (!normalizedFile)
|
|
649
|
+
continue;
|
|
650
|
+
const counts = ((_a = graph.packageCounts) === null || _a === void 0 ? void 0 : _a[file]) || {};
|
|
651
|
+
const uniqueDeps = new Set(deps.filter((dep) => typeof dep === 'string' && dep));
|
|
652
|
+
for (const dep of uniqueDeps) {
|
|
653
|
+
if (!byDep.has(dep))
|
|
654
|
+
byDep.set(dep, new Map());
|
|
655
|
+
const fileMap = byDep.get(dep);
|
|
656
|
+
const count = typeof counts[dep] === 'number' ? counts[dep] : 1;
|
|
657
|
+
fileMap.set(normalizedFile, count);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const [dep, fileMap] of byDep.entries()) {
|
|
661
|
+
const entries = Array.from(fileMap.entries()).map(([file, count]) => ({
|
|
662
|
+
file,
|
|
663
|
+
count,
|
|
664
|
+
depth: file.split('/').length,
|
|
665
|
+
isTest: isTestFile(file)
|
|
666
|
+
}));
|
|
667
|
+
// Rank: prefer non-test files, then higher import counts, then closer to root.
|
|
668
|
+
entries.sort((a, b) => {
|
|
669
|
+
if (a.isTest !== b.isTest)
|
|
670
|
+
return a.isTest ? 1 : -1;
|
|
671
|
+
if (b.count !== a.count)
|
|
672
|
+
return b.count - a.count;
|
|
673
|
+
if (a.depth !== b.depth)
|
|
674
|
+
return a.depth - b.depth;
|
|
675
|
+
return a.file.localeCompare(b.file);
|
|
452
676
|
});
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
packageHotness[name] = importers.size;
|
|
457
|
-
});
|
|
458
|
-
const declared = new Set([
|
|
459
|
-
...Object.keys(pkg.dependencies || {}),
|
|
460
|
-
...Object.keys(pkg.devDependencies || {})
|
|
461
|
-
]);
|
|
462
|
-
const undeclaredImports = Array.from(packageImporters.keys())
|
|
463
|
-
.filter((name) => !declared.has(name))
|
|
464
|
-
.sort();
|
|
465
|
-
return {
|
|
466
|
-
staticOnly: true,
|
|
467
|
-
notes: [
|
|
468
|
-
'Import analysis is static only.',
|
|
469
|
-
'Dynamic imports, runtime plugin loading, and tooling usage are not evaluated.'
|
|
470
|
-
],
|
|
471
|
-
packageHotness,
|
|
472
|
-
undeclaredImports,
|
|
473
|
-
unresolvedImports: graph.unresolvedImports || []
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
function buildImportInfo(graphData) {
|
|
477
|
-
if (!graphData || typeof graphData !== 'object')
|
|
478
|
-
return undefined;
|
|
479
|
-
const fanIn = {};
|
|
480
|
-
const fanOut = {};
|
|
481
|
-
Object.entries(graphData).forEach(([file, deps]) => {
|
|
482
|
-
fanOut[file] = Array.isArray(deps) ? deps.length : 0;
|
|
483
|
-
(deps || []).forEach((dep) => {
|
|
484
|
-
fanIn[dep] = (fanIn[dep] || 0) + 1;
|
|
677
|
+
summary.set(dep, {
|
|
678
|
+
fileCount: fileMap.size,
|
|
679
|
+
topFiles: entries.slice(0, 5).map((entry) => entry.file)
|
|
485
680
|
});
|
|
486
|
-
|
|
487
|
-
|
|
681
|
+
runtimeImpact.set(dep, determineRuntimeImpactFromFiles(Array.from(fileMap.keys())));
|
|
682
|
+
}
|
|
683
|
+
return { summary, runtimeImpact };
|
|
488
684
|
}
|
|
489
685
|
function isDirectDependency(name, pkg) {
|
|
490
|
-
return Boolean((pkg.dependencies && pkg.dependencies[name]) ||
|
|
686
|
+
return Boolean((pkg.dependencies && pkg.dependencies[name]) ||
|
|
687
|
+
(pkg.devDependencies && pkg.devDependencies[name]) ||
|
|
688
|
+
(pkg.optionalDependencies && pkg.optionalDependencies[name]));
|
|
689
|
+
}
|
|
690
|
+
function directScopeFromPackage(name, pkg) {
|
|
691
|
+
if (pkg.dependencies && pkg.dependencies[name])
|
|
692
|
+
return 'runtime';
|
|
693
|
+
if (pkg.devDependencies && pkg.devDependencies[name])
|
|
694
|
+
return 'dev';
|
|
695
|
+
if (pkg.optionalDependencies && pkg.optionalDependencies[name])
|
|
696
|
+
return 'optional';
|
|
697
|
+
if (pkg.peerDependencies && pkg.peerDependencies[name])
|
|
698
|
+
return 'peer';
|
|
699
|
+
return undefined;
|
|
491
700
|
}
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
return
|
|
495
|
-
if (!maintenanceEnabled) {
|
|
496
|
-
return { status: 'unknown', reason: 'maintenance checks disabled' };
|
|
701
|
+
function determineScope(name, direct, rootCauses, pkg) {
|
|
702
|
+
if (direct) {
|
|
703
|
+
return directScopeFromPackage(name, pkg) || 'runtime';
|
|
497
704
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const timestamps = Object.values(json || {}).filter((v) => typeof v === 'string');
|
|
504
|
-
const lastPublished = timestamps.sort().pop();
|
|
505
|
-
if (lastPublished) {
|
|
506
|
-
const risk = (0, utils_1.maintenanceRisk)(lastPublished);
|
|
507
|
-
const status = risk === 'green' ? 'active' : risk === 'amber' ? 'quiet' : risk === 'red' ? 'stale' : 'unknown';
|
|
508
|
-
return { lastPublished, status, reason: 'npm view time' };
|
|
509
|
-
}
|
|
510
|
-
return { status: 'unknown', reason: 'npm view returned no data' };
|
|
705
|
+
const scopes = new Set();
|
|
706
|
+
for (const root of rootCauses) {
|
|
707
|
+
const scope = directScopeFromPackage(root.name, pkg);
|
|
708
|
+
if (scope)
|
|
709
|
+
scopes.add(scope);
|
|
511
710
|
}
|
|
512
|
-
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
return
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
return result;
|
|
535
|
-
}
|
|
536
|
-
// Memoized recursion to inherit runtime class from parents; conservative for cycles.
|
|
537
|
-
const parentClasses = [];
|
|
538
|
-
const inProgress = cache.get(`__visiting__${nodeKey}`);
|
|
539
|
-
if (inProgress) {
|
|
540
|
-
const cycleFallback = { classification: 'build-time', reason: 'Dependency cycle; defaulting to build-time' };
|
|
541
|
-
cache.set(nodeKey, cycleFallback);
|
|
542
|
-
return cycleFallback;
|
|
543
|
-
}
|
|
544
|
-
cache.set(`__visiting__${nodeKey}`, { classification: 'build-time', reason: 'visiting' });
|
|
545
|
-
for (const parentKey of node.parents) {
|
|
546
|
-
const parent = map.get(parentKey);
|
|
547
|
-
if (!parent)
|
|
711
|
+
if (scopes.has('runtime'))
|
|
712
|
+
return 'runtime';
|
|
713
|
+
if (scopes.has('dev'))
|
|
714
|
+
return 'dev';
|
|
715
|
+
if (scopes.has('optional'))
|
|
716
|
+
return 'optional';
|
|
717
|
+
if (scopes.has('peer'))
|
|
718
|
+
return 'peer';
|
|
719
|
+
return 'runtime';
|
|
720
|
+
}
|
|
721
|
+
function buildSubDeps(declared, node) {
|
|
722
|
+
const out = {};
|
|
723
|
+
const entries = [
|
|
724
|
+
['dep', 'dep'],
|
|
725
|
+
['dev', 'dev'],
|
|
726
|
+
['opt', 'opt'],
|
|
727
|
+
['peer', 'peer']
|
|
728
|
+
];
|
|
729
|
+
for (const [declaredKey, outKey] of entries) {
|
|
730
|
+
const group = declared[declaredKey];
|
|
731
|
+
const names = Object.keys(group);
|
|
732
|
+
if (names.length === 0)
|
|
548
733
|
continue;
|
|
549
|
-
const
|
|
550
|
-
|
|
734
|
+
const bucket = {};
|
|
735
|
+
for (const name of names.sort()) {
|
|
736
|
+
const range = group[name];
|
|
737
|
+
const resolved = node.childByName.get(name) || null;
|
|
738
|
+
bucket[name] = [range, resolved];
|
|
739
|
+
}
|
|
740
|
+
if (Object.keys(bucket).length > 0) {
|
|
741
|
+
out[outKey] = bucket;
|
|
742
|
+
}
|
|
551
743
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
744
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
745
|
+
}
|
|
746
|
+
function buildOrigins(rootCauses, parentIds, workspaceList, workspaceEnabled, maxTopRoots, maxTopParents) {
|
|
747
|
+
const origins = {
|
|
748
|
+
rootPackageCount: rootCauses.length,
|
|
749
|
+
topRootPackages: rootCauses.slice(0, maxTopRoots),
|
|
750
|
+
parentPackageCount: parentIds.length,
|
|
751
|
+
topParentPackages: parentIds.slice(0, maxTopParents)
|
|
752
|
+
};
|
|
753
|
+
if (workspaceEnabled && workspaceList && workspaceList.length > 0) {
|
|
754
|
+
origins.workspaces = workspaceList;
|
|
556
755
|
}
|
|
557
|
-
|
|
558
|
-
|
|
756
|
+
return origins;
|
|
757
|
+
}
|
|
758
|
+
function isTestFile(file) {
|
|
759
|
+
return /(^|\/)(__tests__|__mocks__|test|tests)(\/|$)/.test(file) || /\.(test|spec)\./.test(file);
|
|
760
|
+
}
|
|
761
|
+
function isToolingFile(file) {
|
|
762
|
+
return /(^|\/)(eslint|prettier|stylelint|commitlint|lint-staged|husky)[^\/]*\./.test(file);
|
|
763
|
+
}
|
|
764
|
+
function isBuildFile(file) {
|
|
765
|
+
return /(^|\/)(webpack|rollup|vite|tsconfig|babel|swc|esbuild|parcel|gulpfile|gruntfile|postcss|tailwind)[^\/]*\./.test(file);
|
|
766
|
+
}
|
|
767
|
+
function determineRuntimeImpactFromFiles(files) {
|
|
768
|
+
const categories = new Set();
|
|
769
|
+
for (const file of files) {
|
|
770
|
+
if (isTestFile(file)) {
|
|
771
|
+
categories.add('testing');
|
|
772
|
+
}
|
|
773
|
+
else if (isToolingFile(file)) {
|
|
774
|
+
categories.add('tooling');
|
|
775
|
+
}
|
|
776
|
+
else if (isBuildFile(file)) {
|
|
777
|
+
categories.add('build');
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
categories.add('runtime');
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (categories.size === 0)
|
|
784
|
+
return 'runtime';
|
|
785
|
+
if (categories.size > 1)
|
|
786
|
+
return 'mixed';
|
|
787
|
+
return Array.from(categories)[0];
|
|
788
|
+
}
|
|
789
|
+
function buildLicenseInfo(declaredRaw, licenseText) {
|
|
790
|
+
const declaredValue = typeof declaredRaw === 'string' ? declaredRaw.trim() : '';
|
|
791
|
+
const hasDeclared = Boolean(declaredValue);
|
|
792
|
+
const declaredValidation = hasDeclared ? (0, license_1.validateSpdxExpression)(declaredValue) : undefined;
|
|
793
|
+
const inferred = licenseText ? (0, license_1.inferLicenseFromText)(licenseText) : undefined;
|
|
794
|
+
const record = {
|
|
795
|
+
status: 'unknown'
|
|
796
|
+
};
|
|
797
|
+
if (hasDeclared && declaredValidation) {
|
|
798
|
+
record.declared = {
|
|
799
|
+
spdxId: declaredValidation.normalized || declaredValue,
|
|
800
|
+
expression: declaredValidation.expression,
|
|
801
|
+
deprecated: declaredValidation.deprecated,
|
|
802
|
+
valid: declaredValidation.valid
|
|
803
|
+
};
|
|
804
|
+
if (declaredValidation.exceptions.length === 1) {
|
|
805
|
+
record.exception = declaredValidation.exceptions[0];
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (inferred) {
|
|
809
|
+
record.inferred = {
|
|
810
|
+
spdxId: inferred.spdxId,
|
|
811
|
+
confidence: inferred.confidence
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
if (hasDeclared && declaredValidation && !declaredValidation.valid) {
|
|
815
|
+
record.status = 'invalid-spdx';
|
|
816
|
+
}
|
|
817
|
+
else if ((declaredValidation === null || declaredValidation === void 0 ? void 0 : declaredValidation.valid) && inferred) {
|
|
818
|
+
record.status = declaredValidation.normalized === inferred.spdxId ? 'match' : 'mismatch';
|
|
819
|
+
}
|
|
820
|
+
else if (declaredValidation === null || declaredValidation === void 0 ? void 0 : declaredValidation.valid) {
|
|
821
|
+
record.status = 'declared-only';
|
|
822
|
+
}
|
|
823
|
+
else if (inferred) {
|
|
824
|
+
record.status = 'inferred-only';
|
|
559
825
|
}
|
|
560
826
|
else {
|
|
561
|
-
|
|
827
|
+
record.status = 'unknown';
|
|
562
828
|
}
|
|
563
|
-
|
|
564
|
-
|
|
829
|
+
if (declaredValidation === null || declaredValidation === void 0 ? void 0 : declaredValidation.valid) {
|
|
830
|
+
return { record, licenseIds: declaredValidation.licenseIds };
|
|
831
|
+
}
|
|
832
|
+
if (inferred) {
|
|
833
|
+
return { record, licenseIds: [inferred.spdxId] };
|
|
834
|
+
}
|
|
835
|
+
return { record, licenseIds: [] };
|
|
836
|
+
}
|
|
837
|
+
const TOOLING_PACKAGES = new Set([
|
|
838
|
+
'eslint',
|
|
839
|
+
'prettier',
|
|
840
|
+
'ts-node',
|
|
841
|
+
'typescript',
|
|
842
|
+
'babel',
|
|
843
|
+
'@babel/core',
|
|
844
|
+
'rollup',
|
|
845
|
+
'webpack',
|
|
846
|
+
'vite',
|
|
847
|
+
'parcel',
|
|
848
|
+
'swc',
|
|
849
|
+
'@swc/core',
|
|
850
|
+
'ts-jest',
|
|
851
|
+
'eslint-config-prettier',
|
|
852
|
+
'eslint-plugin-import',
|
|
853
|
+
'lint-staged',
|
|
854
|
+
'husky'
|
|
855
|
+
]);
|
|
856
|
+
const FRAMEWORK_PACKAGES = new Set([
|
|
857
|
+
'next',
|
|
858
|
+
'react-scripts',
|
|
859
|
+
'@angular/core',
|
|
860
|
+
'@angular/cli',
|
|
861
|
+
'vue',
|
|
862
|
+
'nuxt',
|
|
863
|
+
'svelte',
|
|
864
|
+
'@sveltejs/kit',
|
|
865
|
+
'gatsby',
|
|
866
|
+
'ember-cli',
|
|
867
|
+
'remix',
|
|
868
|
+
'expo'
|
|
869
|
+
]);
|
|
870
|
+
function isToolingPackage(name) {
|
|
871
|
+
if (TOOLING_PACKAGES.has(name))
|
|
872
|
+
return true;
|
|
873
|
+
if (name.startsWith('@typescript-eslint/'))
|
|
874
|
+
return true;
|
|
875
|
+
if (name.startsWith('eslint-'))
|
|
876
|
+
return true;
|
|
877
|
+
return false;
|
|
878
|
+
}
|
|
879
|
+
function isFrameworkPackage(name) {
|
|
880
|
+
return FRAMEWORK_PACKAGES.has(name);
|
|
881
|
+
}
|
|
882
|
+
// Heuristic-only classification for why a dependency exists. Kept deterministic and bounded.
|
|
883
|
+
function determineIntroduction(direct, rootCauses, runtimeImpact) {
|
|
884
|
+
const rootNames = rootCauses.map((root) => root.name);
|
|
885
|
+
if (direct)
|
|
886
|
+
return 'direct';
|
|
887
|
+
if (runtimeImpact === 'testing')
|
|
888
|
+
return 'testing';
|
|
889
|
+
if (rootNames.length > 0 && rootNames.every((root) => isToolingPackage(root)))
|
|
890
|
+
return 'tooling';
|
|
891
|
+
if (rootNames.some((root) => isFrameworkPackage(root)))
|
|
892
|
+
return 'framework';
|
|
893
|
+
if (rootNames.length > 0)
|
|
894
|
+
return 'transitive';
|
|
895
|
+
return 'unknown';
|
|
565
896
|
}
|
|
566
|
-
|
|
897
|
+
// Upgrade blockers derived only from local metadata (no external lookups).
|
|
898
|
+
function buildUpgradeBlock(insights) {
|
|
567
899
|
var _a;
|
|
568
|
-
const
|
|
900
|
+
const blockers = [];
|
|
901
|
+
if (insights.nodeEngine)
|
|
902
|
+
blockers.push('nodeEngine');
|
|
903
|
+
if (Object.keys(insights.declaredDependencies.peer).length > 0)
|
|
904
|
+
blockers.push('peerDependency');
|
|
905
|
+
if ((_a = insights.execution) === null || _a === void 0 ? void 0 : _a.native)
|
|
906
|
+
blockers.push('nativeBindings');
|
|
907
|
+
if (insights.deprecated)
|
|
908
|
+
blockers.push('deprecated');
|
|
909
|
+
if (blockers.length === 0)
|
|
910
|
+
return undefined;
|
|
911
|
+
return {
|
|
912
|
+
blocksNodeMajor: true,
|
|
913
|
+
blockers
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
async function gatherPackageInsights(name, version, resolvePaths, metaCache, statCache) {
|
|
917
|
+
var _a;
|
|
918
|
+
const meta = await loadPackageMeta(name, resolvePaths, metaCache, version);
|
|
919
|
+
if (!meta) {
|
|
920
|
+
return {
|
|
921
|
+
deprecated: false,
|
|
922
|
+
nodeEngine: null,
|
|
923
|
+
declaredDependencies: { dep: {}, dev: {}, peer: {}, opt: {} },
|
|
924
|
+
tsTypes: 'unknown'
|
|
925
|
+
};
|
|
926
|
+
}
|
|
569
927
|
const pkg = (meta === null || meta === void 0 ? void 0 : meta.pkg) || {};
|
|
570
928
|
const dir = meta === null || meta === void 0 ? void 0 : meta.dir;
|
|
571
929
|
const stats = dir ? await calculatePackageStats(dir, statCache) : undefined;
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
hasPeerDependencies: Object.keys(pkg.peerDependencies || {}).length > 0
|
|
930
|
+
const declaredDependencies = {
|
|
931
|
+
dep: normalizeDeclaredDeps(pkg.dependencies),
|
|
932
|
+
dev: normalizeDeclaredDeps(pkg.devDependencies),
|
|
933
|
+
peer: normalizeDeclaredDeps(pkg.peerDependencies),
|
|
934
|
+
opt: normalizeDeclaredDeps(pkg.optionalDependencies)
|
|
578
935
|
};
|
|
579
936
|
const scripts = pkg.scripts || {};
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
nativeBindings: Boolean((stats === null || stats === void 0 ? void 0 : stats.hasNativeBinary) || (stats === null || stats === void 0 ? void 0 : stats.hasBindingGyp) || scriptsContainNativeBuild(scripts)),
|
|
590
|
-
installScripts: hasInstallScripts(scripts)
|
|
591
|
-
};
|
|
592
|
-
const sizeFootprint = {
|
|
593
|
-
installedSize: (stats === null || stats === void 0 ? void 0 : stats.size) || 0,
|
|
594
|
-
fileCount: (stats === null || stats === void 0 ? void 0 : stats.files) || 0
|
|
595
|
-
};
|
|
596
|
-
const graph = {
|
|
597
|
-
fanIn,
|
|
598
|
-
fanOut,
|
|
599
|
-
dependedOnBy,
|
|
600
|
-
dependsOn
|
|
601
|
-
};
|
|
602
|
-
// Extract package links
|
|
603
|
-
const links = {
|
|
604
|
-
npm: `https://www.npmjs.com/package/${name}`
|
|
605
|
-
};
|
|
606
|
-
// Repository can be string or object with url
|
|
607
|
-
if (pkg.repository) {
|
|
608
|
-
if (typeof pkg.repository === 'string') {
|
|
609
|
-
links.repository = normalizeRepoUrl(pkg.repository);
|
|
610
|
-
}
|
|
611
|
-
else if (pkg.repository.url) {
|
|
612
|
-
links.repository = normalizeRepoUrl(pkg.repository.url);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
// Bugs can be string or object with url
|
|
616
|
-
if (pkg.bugs) {
|
|
617
|
-
if (typeof pkg.bugs === 'string') {
|
|
618
|
-
links.bugs = pkg.bugs;
|
|
619
|
-
}
|
|
620
|
-
else if (pkg.bugs.url) {
|
|
621
|
-
links.bugs = pkg.bugs.url;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
// Homepage is a simple string
|
|
625
|
-
if (pkg.homepage && typeof pkg.homepage === 'string') {
|
|
626
|
-
links.homepage = pkg.homepage;
|
|
627
|
-
}
|
|
937
|
+
const deprecated = Boolean(pkg.deprecated);
|
|
938
|
+
const nodeEngine = typeof ((_a = pkg.engines) === null || _a === void 0 ? void 0 : _a.node) === 'string' ? pkg.engines.node : null;
|
|
939
|
+
const description = typeof pkg.description === 'string' && pkg.description.trim()
|
|
940
|
+
? pkg.description.trim()
|
|
941
|
+
: undefined;
|
|
942
|
+
const hasDefinitelyTyped = await hasDefinitelyTypedPackage(name, resolvePaths, metaCache);
|
|
943
|
+
const tsTypes = determineTypes(pkg, (stats === null || stats === void 0 ? void 0 : stats.hasDts) || false, hasDefinitelyTyped);
|
|
944
|
+
const links = extractPackageLinks(pkg);
|
|
945
|
+
const execution = await deriveExecutionInfo(scripts, dir, stats);
|
|
628
946
|
return {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
links
|
|
947
|
+
deprecated,
|
|
948
|
+
nodeEngine,
|
|
949
|
+
description,
|
|
950
|
+
declaredDependencies,
|
|
951
|
+
links,
|
|
952
|
+
execution,
|
|
953
|
+
tsTypes
|
|
637
954
|
};
|
|
638
955
|
}
|
|
639
|
-
|
|
640
|
-
if (
|
|
641
|
-
return
|
|
956
|
+
function normalizeDeclaredDeps(source) {
|
|
957
|
+
if (!source || typeof source !== 'object')
|
|
958
|
+
return {};
|
|
959
|
+
const out = {};
|
|
960
|
+
for (const [name, range] of Object.entries(source)) {
|
|
961
|
+
if (typeof name !== 'string' || !name.trim())
|
|
962
|
+
continue;
|
|
963
|
+
if (typeof range !== 'string' || !range.trim())
|
|
964
|
+
continue;
|
|
965
|
+
out[name] = range.trim();
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
969
|
+
async function loadPackageMeta(name, resolvePaths, cache, version) {
|
|
970
|
+
const cacheKey = version ? `${name}@${version}` : name;
|
|
971
|
+
if (cache.has(cacheKey))
|
|
972
|
+
return cache.get(cacheKey);
|
|
642
973
|
try {
|
|
643
|
-
const pkgJsonPath =
|
|
974
|
+
const pkgJsonPath = await (0, utils_1.resolvePackageJsonPath)(name, resolvePaths, version);
|
|
975
|
+
if (!pkgJsonPath)
|
|
976
|
+
return undefined;
|
|
644
977
|
const pkgRaw = await promises_1.default.readFile(pkgJsonPath, 'utf8');
|
|
645
978
|
const pkg = JSON.parse(pkgRaw);
|
|
646
979
|
const meta = { pkg, dir: path_1.default.dirname(pkgJsonPath) };
|
|
647
|
-
cache.set(
|
|
980
|
+
cache.set(cacheKey, meta);
|
|
648
981
|
return meta;
|
|
649
982
|
}
|
|
650
983
|
catch (err) {
|
|
651
984
|
return undefined;
|
|
652
985
|
}
|
|
653
986
|
}
|
|
987
|
+
function toDefinitelyTypedPackageName(name) {
|
|
988
|
+
if (name.startsWith('@types/'))
|
|
989
|
+
return name;
|
|
990
|
+
if (name.startsWith('@')) {
|
|
991
|
+
const scoped = name.slice(1).split('/');
|
|
992
|
+
if (scoped.length < 2)
|
|
993
|
+
return undefined;
|
|
994
|
+
return `@types/${scoped[0]}__${scoped[1]}`;
|
|
995
|
+
}
|
|
996
|
+
return `@types/${name}`;
|
|
997
|
+
}
|
|
998
|
+
async function hasDefinitelyTypedPackage(name, resolvePaths, cache) {
|
|
999
|
+
if (name.startsWith('@types/'))
|
|
1000
|
+
return true;
|
|
1001
|
+
const typesName = toDefinitelyTypedPackageName(name);
|
|
1002
|
+
if (!typesName)
|
|
1003
|
+
return false;
|
|
1004
|
+
const meta = await loadPackageMeta(typesName, resolvePaths, cache);
|
|
1005
|
+
return Boolean(meta);
|
|
1006
|
+
}
|
|
654
1007
|
async function calculatePackageStats(dir, cache) {
|
|
655
1008
|
if (cache.has(dir))
|
|
656
1009
|
return cache.get(dir);
|
|
657
|
-
let size = 0;
|
|
658
|
-
let files = 0;
|
|
659
1010
|
let hasDts = false;
|
|
660
1011
|
let hasNativeBinary = false;
|
|
661
1012
|
let hasBindingGyp = false;
|
|
@@ -669,9 +1020,6 @@ async function calculatePackageStats(dir, cache) {
|
|
|
669
1020
|
await walk(full);
|
|
670
1021
|
}
|
|
671
1022
|
else if (entry.isFile()) {
|
|
672
|
-
const stat = await promises_1.default.stat(full);
|
|
673
|
-
size += stat.size;
|
|
674
|
-
files += 1;
|
|
675
1023
|
if (entry.name.endsWith('.d.ts'))
|
|
676
1024
|
hasDts = true;
|
|
677
1025
|
if (entry.name.endsWith('.node'))
|
|
@@ -687,33 +1035,300 @@ async function calculatePackageStats(dir, cache) {
|
|
|
687
1035
|
catch (err) {
|
|
688
1036
|
// best-effort; ignore inaccessible paths
|
|
689
1037
|
}
|
|
690
|
-
const result = {
|
|
1038
|
+
const result = { hasDts, hasNativeBinary, hasBindingGyp };
|
|
691
1039
|
cache.set(dir, result);
|
|
692
1040
|
return result;
|
|
693
1041
|
}
|
|
694
|
-
function
|
|
695
|
-
const typeField = pkg.type;
|
|
696
|
-
const hasModuleField = Boolean(pkg.module);
|
|
697
|
-
const hasExports = pkg.exports !== undefined;
|
|
698
|
-
const conditionalExports = typeof pkg.exports === 'object' && pkg.exports !== null;
|
|
699
|
-
let format = 'unknown';
|
|
700
|
-
if (typeField === 'module')
|
|
701
|
-
format = 'esm';
|
|
702
|
-
else if (typeField === 'commonjs')
|
|
703
|
-
format = 'commonjs';
|
|
704
|
-
else if (hasModuleField || hasExports)
|
|
705
|
-
format = 'dual';
|
|
706
|
-
else
|
|
707
|
-
format = 'commonjs';
|
|
708
|
-
return { format, conditionalExports };
|
|
709
|
-
}
|
|
710
|
-
function determineTypes(pkg, hasDts) {
|
|
1042
|
+
function determineTypes(pkg, hasDts, hasDefinitelyTyped) {
|
|
711
1043
|
const hasBundled = Boolean(pkg.types || pkg.typings || hasDts);
|
|
712
|
-
|
|
1044
|
+
if (hasBundled)
|
|
1045
|
+
return 'bundled';
|
|
1046
|
+
if (hasDefinitelyTyped)
|
|
1047
|
+
return 'definitelyTyped';
|
|
1048
|
+
return 'none';
|
|
713
1049
|
}
|
|
714
|
-
|
|
715
|
-
|
|
1050
|
+
const REPO_SHORTHAND_HOSTS = {
|
|
1051
|
+
github: 'github.com',
|
|
1052
|
+
gitlab: 'gitlab.com',
|
|
1053
|
+
bitbucket: 'bitbucket.org'
|
|
1054
|
+
};
|
|
1055
|
+
function normalizeUrl(raw) {
|
|
1056
|
+
const trimmed = raw.trim();
|
|
1057
|
+
if (!trimmed)
|
|
1058
|
+
return undefined;
|
|
1059
|
+
let url = trimmed.replace(/^git\+/, '');
|
|
1060
|
+
if (url.startsWith('ssh://')) {
|
|
1061
|
+
url = url.slice('ssh://'.length);
|
|
1062
|
+
if (url.startsWith('git@')) {
|
|
1063
|
+
const match = url.match(/^git@([^:]+):(.+)$/);
|
|
1064
|
+
if (match) {
|
|
1065
|
+
url = `https://${match[1]}/${match[2]}`;
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
url = `https://${url}`;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
url = `https://${url}`;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (url.startsWith('git@')) {
|
|
1076
|
+
const match = url.match(/^git@([^:]+):(.+)$/);
|
|
1077
|
+
if (match) {
|
|
1078
|
+
url = `https://${match[1]}/${match[2]}`;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
const shorthand = url.match(/^(github|gitlab|bitbucket):(.+)$/i);
|
|
1082
|
+
if (shorthand) {
|
|
1083
|
+
const host = REPO_SHORTHAND_HOSTS[shorthand[1].toLowerCase()];
|
|
1084
|
+
url = `https://${host}/${shorthand[2]}`;
|
|
1085
|
+
}
|
|
1086
|
+
if (url.startsWith('git://')) {
|
|
1087
|
+
url = `https://${url.slice('git://'.length)}`;
|
|
1088
|
+
}
|
|
1089
|
+
const hashIndex = url.indexOf('#');
|
|
1090
|
+
const hash = hashIndex === -1 ? '' : url.slice(hashIndex);
|
|
1091
|
+
const base = hashIndex === -1 ? url : url.slice(0, hashIndex);
|
|
1092
|
+
const cleaned = base.endsWith('.git') ? base.slice(0, -4) : base;
|
|
1093
|
+
return cleaned + hash;
|
|
716
1094
|
}
|
|
717
|
-
function
|
|
718
|
-
|
|
1095
|
+
function normalizeLinkValue(value) {
|
|
1096
|
+
if (!value)
|
|
1097
|
+
return undefined;
|
|
1098
|
+
if (typeof value === 'string')
|
|
1099
|
+
return normalizeUrl(value);
|
|
1100
|
+
if (typeof value === 'object' && typeof value.url === 'string') {
|
|
1101
|
+
return normalizeUrl(value.url);
|
|
1102
|
+
}
|
|
1103
|
+
return undefined;
|
|
1104
|
+
}
|
|
1105
|
+
function extractPackageLinks(pkg) {
|
|
1106
|
+
const repository = normalizeLinkValue(pkg === null || pkg === void 0 ? void 0 : pkg.repository);
|
|
1107
|
+
const homepage = normalizeLinkValue(pkg === null || pkg === void 0 ? void 0 : pkg.homepage);
|
|
1108
|
+
const bugs = normalizeLinkValue(pkg === null || pkg === void 0 ? void 0 : pkg.bugs);
|
|
1109
|
+
if (!repository && !homepage && !bugs)
|
|
1110
|
+
return undefined;
|
|
1111
|
+
return {
|
|
1112
|
+
...(repository ? { repository } : {}),
|
|
1113
|
+
...(homepage ? { homepage } : {}),
|
|
1114
|
+
...(bugs ? { bugs } : {})
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
const LIFECYCLE_HOOKS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
1118
|
+
const EXECUTION_SIGNAL_ORDER = [
|
|
1119
|
+
'network-access',
|
|
1120
|
+
'dynamic-exec',
|
|
1121
|
+
'child-process',
|
|
1122
|
+
'encoding',
|
|
1123
|
+
'obfuscated',
|
|
1124
|
+
'reads-env',
|
|
1125
|
+
'reads-home',
|
|
1126
|
+
'uses-ssh'
|
|
1127
|
+
];
|
|
1128
|
+
const INSTALL_SCRIPT_MAX_BYTES = 200000;
|
|
1129
|
+
const COMPLEXITY_THRESHOLD = 12;
|
|
1130
|
+
function collectLifecycleScripts(scripts) {
|
|
1131
|
+
const lifecycle = {};
|
|
1132
|
+
for (const hook of LIFECYCLE_HOOKS) {
|
|
1133
|
+
const cmd = scripts === null || scripts === void 0 ? void 0 : scripts[hook];
|
|
1134
|
+
if (typeof cmd === 'string' && cmd.trim().length > 0) {
|
|
1135
|
+
lifecycle[hook] = cmd.trim();
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
return lifecycle;
|
|
1139
|
+
}
|
|
1140
|
+
function scriptsContainNativeTooling(scripts) {
|
|
1141
|
+
return Object.values(scripts || {}).some((cmd) => typeof cmd === 'string' && /node-?gyp|node-pre-gyp|prebuild/i.test(cmd));
|
|
1142
|
+
}
|
|
1143
|
+
function scoreLifecycleScripts(lifecycleScripts) {
|
|
1144
|
+
const commands = Object.values(lifecycleScripts).filter((cmd) => typeof cmd === 'string');
|
|
1145
|
+
const combined = commands.join(' ');
|
|
1146
|
+
if (!combined)
|
|
1147
|
+
return 0;
|
|
1148
|
+
const lengthScore = Math.ceil(combined.length / 40);
|
|
1149
|
+
const andCount = (combined.match(/&&/g) || []).length;
|
|
1150
|
+
const orCount = (combined.match(/\|\|/g) || []).length;
|
|
1151
|
+
const semicolons = (combined.match(/;/g) || []).length;
|
|
1152
|
+
const pipeCount = Math.max(0, (combined.match(/\|/g) || []).length - orCount * 2);
|
|
1153
|
+
const inlineNodeExec = (combined.match(/\bnode\s+-[ep]\b/gi) || []).length;
|
|
1154
|
+
const inlineEval = (combined.match(/\beval\s*\(/g) || []).length;
|
|
1155
|
+
const inlineFunction = (combined.match(/new\s+Function\s*\(/g) || []).length;
|
|
1156
|
+
return lengthScore + (andCount + orCount + semicolons + pipeCount) * 2 + (inlineNodeExec + inlineEval + inlineFunction) * 5;
|
|
1157
|
+
}
|
|
1158
|
+
function tokenizeCommand(command) {
|
|
1159
|
+
var _a, _b;
|
|
1160
|
+
const tokens = [];
|
|
1161
|
+
const matcher = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
1162
|
+
let match;
|
|
1163
|
+
while ((match = matcher.exec(command))) {
|
|
1164
|
+
tokens.push((_b = (_a = match[1]) !== null && _a !== void 0 ? _a : match[2]) !== null && _b !== void 0 ? _b : match[3]);
|
|
1165
|
+
}
|
|
1166
|
+
return tokens;
|
|
1167
|
+
}
|
|
1168
|
+
function isNodeToken(token) {
|
|
1169
|
+
const base = path_1.default.basename(token).toLowerCase();
|
|
1170
|
+
return base === 'node' || base === 'node.exe';
|
|
1171
|
+
}
|
|
1172
|
+
function extractNodeScriptPath(command) {
|
|
1173
|
+
const tokens = tokenizeCommand(command);
|
|
1174
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1175
|
+
if (!isNodeToken(tokens[i]))
|
|
1176
|
+
continue;
|
|
1177
|
+
let idx = i + 1;
|
|
1178
|
+
while (idx < tokens.length) {
|
|
1179
|
+
const token = tokens[idx];
|
|
1180
|
+
if (token === '-e' || token === '-p' || token === '--eval') {
|
|
1181
|
+
return undefined;
|
|
1182
|
+
}
|
|
1183
|
+
if (token === '-r' || token === '--require') {
|
|
1184
|
+
idx += 2;
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
if (token.startsWith('-')) {
|
|
1188
|
+
idx += 1;
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const cleaned = token.replace(/[;|&]+$/, '');
|
|
1192
|
+
if (cleaned.includes('://'))
|
|
1193
|
+
return undefined;
|
|
1194
|
+
if (cleaned.endsWith('.js') || cleaned.endsWith('.cjs') || cleaned.endsWith('.mjs')) {
|
|
1195
|
+
return cleaned;
|
|
1196
|
+
}
|
|
1197
|
+
return undefined;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return undefined;
|
|
1201
|
+
}
|
|
1202
|
+
function findReferencedInstallScript(lifecycleScripts) {
|
|
1203
|
+
for (const hook of LIFECYCLE_HOOKS) {
|
|
1204
|
+
const command = lifecycleScripts[hook];
|
|
1205
|
+
if (!command)
|
|
1206
|
+
continue;
|
|
1207
|
+
const candidate = extractNodeScriptPath(command);
|
|
1208
|
+
if (candidate)
|
|
1209
|
+
return candidate;
|
|
1210
|
+
}
|
|
1211
|
+
return undefined;
|
|
1212
|
+
}
|
|
1213
|
+
async function readInstallScriptFile(scriptPath, packageDir) {
|
|
1214
|
+
const resolvedDir = path_1.default.resolve(packageDir);
|
|
1215
|
+
const resolvedPath = path_1.default.resolve(resolvedDir, scriptPath);
|
|
1216
|
+
if (!resolvedPath.startsWith(resolvedDir + path_1.default.sep))
|
|
1217
|
+
return undefined;
|
|
1218
|
+
try {
|
|
1219
|
+
const stat = await promises_1.default.stat(resolvedPath);
|
|
1220
|
+
if (!stat.isFile())
|
|
1221
|
+
return undefined;
|
|
1222
|
+
if (stat.size > INSTALL_SCRIPT_MAX_BYTES)
|
|
1223
|
+
return undefined;
|
|
1224
|
+
return await promises_1.default.readFile(resolvedPath, 'utf8');
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
return undefined;
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
function textHasAny(text, patterns) {
|
|
1231
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
1232
|
+
}
|
|
1233
|
+
function detectScriptSignals(text, signals) {
|
|
1234
|
+
// Signals are derived from static text inspection only: no code execution and no import walking.
|
|
1235
|
+
// They are NOT malware detection; they merely highlight review-worthy install-time behavior.
|
|
1236
|
+
// "network-access" surfaces install scripts that fetch remote resources (review for expected downloads; does NOT imply exfiltration).
|
|
1237
|
+
if (textHasAny(text, [/\bcurl\b/i, /\bwget\b/i, /https?:\/\//i, /\bfetch\s*\(/i, /\baxios\b/i, /node-fetch/i])) {
|
|
1238
|
+
signals.add('network-access');
|
|
1239
|
+
}
|
|
1240
|
+
// "reads-env" highlights environment access (does NOT imply exfiltration).
|
|
1241
|
+
if (textHasAny(text, [/\bprocess\.env\b/, /\bprintenv\b/i, /\benv\s*\|/i])) {
|
|
1242
|
+
signals.add('reads-env');
|
|
1243
|
+
}
|
|
1244
|
+
// "reads-home" highlights access to user home paths (does NOT imply credential theft).
|
|
1245
|
+
if (textHasAny(text, [/\$HOME\b/, /process\.env\.HOME\b/, /os\.homedir\s*\(/, /~\//])) {
|
|
1246
|
+
signals.add('reads-home');
|
|
1247
|
+
}
|
|
1248
|
+
// "uses-ssh" flags access to SSH-related paths (does NOT imply key exfiltration).
|
|
1249
|
+
if (textHasAny(text, [/\.ssh\b/i, /id_rsa\b/i, /known_hosts\b/i, /\.npmrc\b/i])) {
|
|
1250
|
+
signals.add('uses-ssh');
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function isObfuscated(text) {
|
|
1254
|
+
const lines = text.split(/\r?\n/);
|
|
1255
|
+
let longest = 0;
|
|
1256
|
+
for (const line of lines) {
|
|
1257
|
+
if (line.length > longest)
|
|
1258
|
+
longest = line.length;
|
|
1259
|
+
if (longest >= 4000)
|
|
1260
|
+
return true;
|
|
1261
|
+
}
|
|
1262
|
+
if (text.length >= 2000) {
|
|
1263
|
+
const nonWhitespace = text.replace(/\s/g, '');
|
|
1264
|
+
const ratio = nonWhitespace.length / text.length;
|
|
1265
|
+
if (ratio > 0.9)
|
|
1266
|
+
return true;
|
|
1267
|
+
}
|
|
1268
|
+
return /[A-Za-z0-9+/]{800,}={0,2}/.test(text);
|
|
1269
|
+
}
|
|
1270
|
+
function detectFileSignals(text, signals) {
|
|
1271
|
+
// Signals from a single directly-referenced JS file (no execution, no imports, no deep scanning).
|
|
1272
|
+
// These are review cues only and do NOT imply malicious intent.
|
|
1273
|
+
detectScriptSignals(text, signals);
|
|
1274
|
+
// "dynamic-exec" flags dynamic code execution APIs (does NOT imply malicious behavior).
|
|
1275
|
+
if (textHasAny(text, [/\beval\s*\(/, /new\s+Function\s*\(/, /\bvm\.runIn/i])) {
|
|
1276
|
+
signals.add('dynamic-exec');
|
|
1277
|
+
}
|
|
1278
|
+
// "child-process" flags process spawning (does NOT imply abuse).
|
|
1279
|
+
if (textHasAny(text, [/\bchild_process\.exec\b/, /\bspawn\s*\(/, /\bexecSync\s*\(/])) {
|
|
1280
|
+
signals.add('child-process');
|
|
1281
|
+
}
|
|
1282
|
+
// "encoding" flags explicit encode/decode flows (does NOT imply obfuscation intent).
|
|
1283
|
+
if (textHasAny(text, [/Buffer\.from\s*\(/, /\.toString\(\s*['"]base64['"]\s*\)/i, /\batob\s*\(/, /\bbtoa\s*\(/])) {
|
|
1284
|
+
signals.add('encoding');
|
|
1285
|
+
}
|
|
1286
|
+
// "obfuscated" flags minified or opaque install-time code (does NOT imply malware).
|
|
1287
|
+
if (isObfuscated(text)) {
|
|
1288
|
+
signals.add('obfuscated');
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function determineExecutionRisk(hasScripts, hasSignals, highComplexity, hooks) {
|
|
1292
|
+
const hasInstallHook = hooks.includes('install') || hooks.includes('postinstall');
|
|
1293
|
+
if (hasScripts && (hasSignals || (highComplexity && hasInstallHook)))
|
|
1294
|
+
return 'red';
|
|
1295
|
+
return 'amber';
|
|
1296
|
+
}
|
|
1297
|
+
async function deriveExecutionInfo(scripts, packageDir, stats) {
|
|
1298
|
+
const lifecycleScripts = collectLifecycleScripts(scripts);
|
|
1299
|
+
const hooks = LIFECYCLE_HOOKS.filter((hook) => Boolean(lifecycleScripts[hook]));
|
|
1300
|
+
const hasScripts = hooks.length > 0;
|
|
1301
|
+
const hasNative = Boolean((stats === null || stats === void 0 ? void 0 : stats.hasNativeBinary) || (stats === null || stats === void 0 ? void 0 : stats.hasBindingGyp) || scriptsContainNativeTooling(scripts));
|
|
1302
|
+
if (!hasScripts && !hasNative)
|
|
1303
|
+
return undefined;
|
|
1304
|
+
const signals = new Set();
|
|
1305
|
+
const combinedScripts = hooks.map((hook) => lifecycleScripts[hook]).join('\n');
|
|
1306
|
+
if (combinedScripts) {
|
|
1307
|
+
detectScriptSignals(combinedScripts, signals);
|
|
1308
|
+
}
|
|
1309
|
+
if (hasScripts && packageDir) {
|
|
1310
|
+
const referencedScript = findReferencedInstallScript(lifecycleScripts);
|
|
1311
|
+
if (referencedScript) {
|
|
1312
|
+
const fileContent = await readInstallScriptFile(referencedScript, packageDir);
|
|
1313
|
+
if (fileContent) {
|
|
1314
|
+
detectFileSignals(fileContent, signals);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const complexityScore = hasScripts ? scoreLifecycleScripts(lifecycleScripts) : 0;
|
|
1319
|
+
const complexity = complexityScore >= COMPLEXITY_THRESHOLD ? complexityScore : undefined;
|
|
1320
|
+
const signalList = EXECUTION_SIGNAL_ORDER.filter((signal) => signals.has(signal));
|
|
1321
|
+
const scriptsInfo = {
|
|
1322
|
+
hooks,
|
|
1323
|
+
...(complexity !== undefined ? { complexity } : {}),
|
|
1324
|
+
...(signalList.length > 0 ? { signals: signalList } : {})
|
|
1325
|
+
};
|
|
1326
|
+
const risk = determineExecutionRisk(hasScripts, signalList.length > 0, complexity !== undefined, hooks);
|
|
1327
|
+
const execution = { risk };
|
|
1328
|
+
// Native is surface description only; not a behavioral signal.
|
|
1329
|
+
if (hasNative)
|
|
1330
|
+
execution.native = true;
|
|
1331
|
+
if (hasScripts)
|
|
1332
|
+
execution.scripts = scriptsInfo;
|
|
1333
|
+
return execution;
|
|
719
1334
|
}
|