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.
@@ -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 Set();
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.add(parent.name);
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
- * Normalize repository URLs to browsable HTTPS format.
59
- * Handles: git+https://, git://, github:user/repo, git@github.com:user/repo.git
60
- */
61
- function normalizeRepoUrl(url) {
62
- if (!url)
63
- return url;
64
- // Handle shorthand: github:user/repo or user/repo
65
- if (url.match(/^(github:|gitlab:|bitbucket:)?[\w-]+\/[\w.-]+$/)) {
66
- const cleaned = url.replace(/^(github:|gitlab:|bitbucket:)/, '');
67
- const host = url.startsWith('gitlab:') ? 'gitlab.com'
68
- : url.startsWith('bitbucket:') ? 'bitbucket.org'
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((_d = input.npmLsResult) === null || _d === void 0 ? void 0 : _d.data, pkg);
98
- const vulnMap = parseVulnerabilities((_e = input.auditResult) === null || _e === void 0 ? void 0 : _e.data);
99
- const importGraph = normalizeImportGraph((_f = input.importGraphResult) === null || _f === void 0 ? void 0 : _f.data);
100
- const importInfo = buildImportInfo(importGraph.files);
101
- const importAnalysis = buildImportAnalysis(importGraph, pkg);
102
- const packageUsageCounts = new Map(Object.entries(importAnalysis.packageHotness));
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
- const totalDeps = nodes.length;
112
- let maintenanceIndex = 0;
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
- const cachedLicense = licenseCache.get(node.name);
116
- const license = cachedLicense ||
117
- (await (0, utils_1.readLicenseFromPackageJson)(node.name, input.projectPath)) ||
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(node.name) && (license.license || license.licenseFile)) {
120
- licenseCache.set(node.name, license);
106
+ if (!licenseCache.has(cacheKey)) {
107
+ licenseCache.set(cacheKey, licenseSource);
121
108
  }
122
109
  const vulnerabilities = vulnMap.get(node.name) || emptyVulnSummary();
123
- const licenseRisk = (0, utils_1.licenseRiskLevel)(license.license);
124
- const vulnRisk = (0, utils_1.vulnRiskLevel)(vulnerabilities.counts);
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
- // Build dependedOnBy and dependsOn lists
135
- const dependedOnBy = Array.from(node.parents).map(key => {
136
- const parent = nodeMap.get(key);
137
- return parent ? parent.name : key.split('@')[0];
138
- });
139
- const dependsOn = Array.from(node.children).map(key => {
140
- const child = nodeMap.get(key);
141
- return child ? child.name : key.split('@')[0];
142
- });
143
- const packageInsights = await gatherPackageInsights(node.name, input.projectPath, packageMetaCache, packageStatCache, node.parents.size, node.children.size, dependedOnBy, dependsOn);
144
- if (packageInsights.identity.nodeEngine) {
145
- nodeEngineRanges.push(packageInsights.identity.nodeEngine);
146
- }
147
- dependencies.push({
148
- name: node.name,
149
- version: node.version,
150
- key: node.key,
151
- direct,
152
- transitive: !direct,
153
- depth: node.depth,
154
- parents: Array.from(node.parents),
155
- rootCauses,
156
- license,
157
- licenseRisk,
158
- vulnerabilities,
159
- vulnRisk,
160
- maintenance,
161
- maintenanceRisk: maintenanceRiskLevel,
162
- usage,
163
- identity: packageInsights.identity,
164
- dependencySurface: packageInsights.dependencySurface,
165
- sizeFootprint: packageInsights.sizeFootprint,
166
- buildPlatform: packageInsights.buildPlatform,
167
- moduleSystem: packageInsights.moduleSystem,
168
- typescript: packageInsights.typescript,
169
- graph: packageInsights.graph,
170
- links: packageInsights.links,
171
- importInfo,
172
- runtimeClass: runtimeData.classification,
173
- runtimeReason: runtimeData.reason,
174
- outdated: { status: 'unknown' },
175
- raw: {}
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
- gitBranch,
187
- maintenanceEnabled: input.maintenanceEnabled,
195
+ git: {
196
+ branch: gitBranch || ''
197
+ },
198
+ project: {
199
+ projectDir: formatProjectDir(input.projectPath)
200
+ },
188
201
  environment: {
189
- node: {
190
- runtimeVersion,
191
- runtimeMajor: Number.isNaN(runtimeMajor) ? 0 : runtimeMajor,
192
- minRequiredMajor,
193
- source: minRequiredMajor === undefined ? 'unknown' : 'dependency-engines'
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
- dependencies,
197
- toolErrors,
198
- raw,
199
- importAnalysis
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, pkg) {
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
- dev: node.dev
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
- else {
309
- const deps = Object.keys(pkg.dependencies || {});
310
- const devDeps = Object.keys(pkg.devDependencies || {});
311
- deps.forEach((name) => {
312
- const version = pkg.dependencies[name];
313
- const key = `${name}@${version}`;
314
- map.set(key, { name, version, key, depth: 1, parents: new Set(), children: new Set(), dev: false });
315
- });
316
- devDeps.forEach((name) => {
317
- const version = pkg.devDependencies[name];
318
- const key = `${name}@${version}`;
319
- map.set(key, { name, version, key, depth: 1, parents: new Set(), children: new Set(), dev: true });
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
- viaList
342
- .filter((v) => typeof v === 'object')
343
- .forEach((vul) => {
344
- const sev = normalizeSeverity(vul.severity) || severity;
345
- entry.items.push({
346
- title: vul.title || item.title || vul.name || name,
347
- severity: sev,
348
- url: vul.url,
349
- vulnerableRange: vul.range,
350
- fixAvailable: item.fixAvailable,
351
- paths: item.nodes
352
- });
353
- entry.counts[sev] = (entry.counts[sev] || 0) + 0; // already counted above
354
- });
355
- entry.highestSeverity = computeHighestSeverity(entry.counts);
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 severity = normalizeSeverity(adv.severity);
362
- const entry = ensureEntry(name);
363
- entry.items.push({
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
- entry.highestSeverity = computeHighestSeverity(entry.counts);
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
- items: [],
394
- highestSeverity: 'none'
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.files && data.packages) {
611
+ if (data && typeof data === 'object' && data.packages) {
410
612
  return {
411
- files: data.files || {},
412
613
  packages: data.packages || {},
413
- unresolvedImports: Array.isArray(data.unresolvedImports) ? data.unresolvedImports : []
614
+ packageCounts: data.packageCounts || {}
414
615
  };
415
616
  }
416
- return { files: data || {}, packages: {}, unresolvedImports: [] };
617
+ return { packages: {} };
417
618
  }
418
- function buildUsageInfo(name, packageUsageCounts, pkg) {
419
- const declared = Boolean((pkg.dependencies && pkg.dependencies[name]) || (pkg.devDependencies && pkg.devDependencies[name]));
420
- const importedCount = packageUsageCounts.get(name) || 0;
421
- if (importedCount > 0) {
422
- if (declared) {
423
- return {
424
- status: 'imported',
425
- reason: `Imported by ${importedCount} file${importedCount === 1 ? '' : 's'} (static analysis)`
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
- return {
440
- status: 'unknown',
441
- reason: 'Not statically imported; package is likely transitive or used dynamically'
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 buildImportAnalysis(graph, pkg) {
445
- const packageImporters = new Map();
446
- Object.entries(graph.packages || {}).forEach(([file, packages]) => {
447
- const unique = new Set(packages || []);
448
- unique.forEach((pkgName) => {
449
- if (!packageImporters.has(pkgName))
450
- packageImporters.set(pkgName, new Set());
451
- packageImporters.get(pkgName).add(file);
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
- const packageHotness = {};
455
- packageImporters.forEach((importers, name) => {
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
- return { files: graphData, fanIn, fanOut };
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]) || (pkg.devDependencies && pkg.devDependencies[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
- async function resolveMaintenance(name, cache, maintenanceEnabled, current, total, onProgress) {
493
- if (cache.has(name))
494
- return cache.get(name);
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
- onProgress === null || onProgress === void 0 ? void 0 : onProgress(current, total, name);
499
- try {
500
- await (0, utils_1.delay)(1000);
501
- const res = await (0, utils_1.runCommand)('npm', ['view', name, 'time', '--json']);
502
- const json = JSON.parse(res.stdout || '{}');
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
- catch (err) {
513
- return { status: 'unknown', reason: 'lookup failed' };
514
- }
515
- }
516
- function classifyRuntime(nodeKey, pkg, map, cache) {
517
- const cached = cache.get(nodeKey);
518
- if (cached)
519
- return cached;
520
- const node = map.get(nodeKey);
521
- if (!node) {
522
- const fallback = { classification: 'build-time', reason: 'Unknown node in dependency graph' };
523
- cache.set(nodeKey, fallback);
524
- return fallback;
525
- }
526
- if (pkg.dependencies && pkg.dependencies[node.name]) {
527
- const result = { classification: 'runtime', reason: 'Declared in dependencies' };
528
- cache.set(nodeKey, result);
529
- return result;
530
- }
531
- if (pkg.devDependencies && pkg.devDependencies[node.name]) {
532
- const result = { classification: 'dev-only', reason: 'Declared in devDependencies' };
533
- cache.set(nodeKey, result);
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 parentClass = classifyRuntime(parentKey, pkg, map, cache).classification;
550
- parentClasses.push(parentClass);
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
- cache.delete(`__visiting__${nodeKey}`);
553
- let result;
554
- if (parentClasses.includes('runtime')) {
555
- result = { classification: 'runtime', reason: 'Transitive of runtime dependency' };
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
- else if (parentClasses.includes('build-time')) {
558
- result = { classification: 'build-time', reason: 'Transitive of build-time dependency' };
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
- result = { classification: 'dev-only', reason: 'Transitive of dev-only dependency' };
827
+ record.status = 'unknown';
562
828
  }
563
- cache.set(nodeKey, result);
564
- return result;
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
- async function gatherPackageInsights(name, projectPath, metaCache, statCache, fanIn, fanOut, dependedOnBy, dependsOn) {
897
+ // Upgrade blockers derived only from local metadata (no external lookups).
898
+ function buildUpgradeBlock(insights) {
567
899
  var _a;
568
- const meta = await loadPackageMeta(name, projectPath, metaCache);
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 dependencySurface = {
573
- dependencies: Object.keys(pkg.dependencies || {}).length,
574
- devDependencies: Object.keys(pkg.devDependencies || {}).length,
575
- peerDependencies: Object.keys(pkg.peerDependencies || {}).length,
576
- optionalDependencies: Object.keys(pkg.optionalDependencies || {}).length,
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 identity = {
581
- deprecated: Boolean(pkg.deprecated),
582
- nodeEngine: typeof ((_a = pkg.engines) === null || _a === void 0 ? void 0 : _a.node) === 'string' ? pkg.engines.node : null,
583
- hasRepository: Boolean(pkg.repository),
584
- hasFunding: Boolean(pkg.funding)
585
- };
586
- const moduleSystem = determineModuleSystem(pkg);
587
- const typescript = determineTypes(pkg, (stats === null || stats === void 0 ? void 0 : stats.hasDts) || false);
588
- const buildPlatform = {
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
- identity,
630
- dependencySurface,
631
- sizeFootprint,
632
- buildPlatform,
633
- moduleSystem,
634
- typescript,
635
- graph,
636
- links
947
+ deprecated,
948
+ nodeEngine,
949
+ description,
950
+ declaredDependencies,
951
+ links,
952
+ execution,
953
+ tsTypes
637
954
  };
638
955
  }
639
- async function loadPackageMeta(name, projectPath, cache) {
640
- if (cache.has(name))
641
- return cache.get(name);
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 = require.resolve(path_1.default.join(name, 'package.json'), { paths: [projectPath] });
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(name, meta);
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 = { size, files, hasDts, hasNativeBinary, hasBindingGyp };
1038
+ const result = { hasDts, hasNativeBinary, hasBindingGyp };
691
1039
  cache.set(dir, result);
692
1040
  return result;
693
1041
  }
694
- function determineModuleSystem(pkg) {
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
- return { types: hasBundled ? 'bundled' : 'none' };
1044
+ if (hasBundled)
1045
+ return 'bundled';
1046
+ if (hasDefinitelyTyped)
1047
+ return 'definitelyTyped';
1048
+ return 'none';
713
1049
  }
714
- function scriptsContainNativeBuild(scripts) {
715
- return Object.values(scripts || {}).some((cmd) => typeof cmd === 'string' && /node-?gyp|node-pre-gyp/.test(cmd));
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 hasInstallScripts(scripts) {
718
- return ['preinstall', 'install', 'postinstall'].some((key) => typeof (scripts === null || scripts === void 0 ? void 0 : scripts[key]) === 'string' && scripts[key].trim().length > 0);
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
  }