@vibgrate/cli 0.1.1 → 0.1.3
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/DOCS.md +554 -0
- package/LICENSE +45 -0
- package/README.md +244 -0
- package/dist/{baseline-AENFLFQT.js → baseline-D5UDXOEJ.js} +2 -2
- package/dist/{chunk-OHAVLM6P.js → chunk-3X3ZMVHI.js} +1 -1
- package/dist/chunk-VXEZ7APL.js +3697 -0
- package/dist/cli.js +3 -3
- package/dist/index.d.ts +126 -0
- package/dist/index.js +1 -1
- package/package.json +5 -3
- package/dist/chunk-DLRBJYO6.js +0 -1077
|
@@ -0,0 +1,3697 @@
|
|
|
1
|
+
// src/utils/fs.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5
|
+
"node_modules",
|
|
6
|
+
".git",
|
|
7
|
+
".next",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
"out",
|
|
11
|
+
".turbo",
|
|
12
|
+
".cache",
|
|
13
|
+
"coverage",
|
|
14
|
+
"bin",
|
|
15
|
+
"obj",
|
|
16
|
+
".vs",
|
|
17
|
+
"packages",
|
|
18
|
+
"TestResults"
|
|
19
|
+
]);
|
|
20
|
+
async function findFiles(rootDir, predicate) {
|
|
21
|
+
const results = [];
|
|
22
|
+
async function walk(dir) {
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
26
|
+
entries = dirents.map((d) => ({
|
|
27
|
+
name: d.name,
|
|
28
|
+
isDirectory: d.isDirectory(),
|
|
29
|
+
isFile: d.isFile()
|
|
30
|
+
}));
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const e of entries) {
|
|
35
|
+
if (e.isDirectory) {
|
|
36
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
37
|
+
await walk(path.join(dir, e.name));
|
|
38
|
+
} else if (e.isFile && predicate(e.name)) {
|
|
39
|
+
results.push(path.join(dir, e.name));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
await walk(rootDir);
|
|
44
|
+
return results;
|
|
45
|
+
}
|
|
46
|
+
async function findPackageJsonFiles(rootDir) {
|
|
47
|
+
return findFiles(rootDir, (name) => name === "package.json");
|
|
48
|
+
}
|
|
49
|
+
async function findSolutionFiles(rootDir) {
|
|
50
|
+
return findFiles(rootDir, (name) => name.endsWith(".sln"));
|
|
51
|
+
}
|
|
52
|
+
async function findCsprojFiles(rootDir) {
|
|
53
|
+
return findFiles(rootDir, (name) => name.endsWith(".csproj"));
|
|
54
|
+
}
|
|
55
|
+
async function readJsonFile(filePath) {
|
|
56
|
+
const txt = await fs.readFile(filePath, "utf8");
|
|
57
|
+
return JSON.parse(txt);
|
|
58
|
+
}
|
|
59
|
+
async function readTextFile(filePath) {
|
|
60
|
+
return fs.readFile(filePath, "utf8");
|
|
61
|
+
}
|
|
62
|
+
async function pathExists(p) {
|
|
63
|
+
try {
|
|
64
|
+
await fs.access(p);
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function ensureDir(dir) {
|
|
71
|
+
await fs.mkdir(dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
async function writeJsonFile(filePath, data) {
|
|
74
|
+
await ensureDir(path.dirname(filePath));
|
|
75
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
76
|
+
}
|
|
77
|
+
async function writeTextFile(filePath, content) {
|
|
78
|
+
await ensureDir(path.dirname(filePath));
|
|
79
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/scoring/drift-score.ts
|
|
83
|
+
var DEFAULT_THRESHOLDS = {
|
|
84
|
+
failOnError: {
|
|
85
|
+
eolDays: 180,
|
|
86
|
+
frameworkMajorLag: 3,
|
|
87
|
+
dependencyTwoPlusPercent: 50
|
|
88
|
+
},
|
|
89
|
+
warn: {
|
|
90
|
+
frameworkMajorLag: 2,
|
|
91
|
+
dependencyTwoPlusPercent: 30
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
function clamp(val, min, max) {
|
|
95
|
+
return Math.min(max, Math.max(min, val));
|
|
96
|
+
}
|
|
97
|
+
function runtimeScore(projects) {
|
|
98
|
+
if (projects.length === 0) return 100;
|
|
99
|
+
const lags = projects.map((p) => p.runtimeMajorsBehind).filter((v) => v !== void 0);
|
|
100
|
+
if (lags.length === 0) return 100;
|
|
101
|
+
const maxLag = Math.max(...lags);
|
|
102
|
+
if (maxLag === 0) return 100;
|
|
103
|
+
if (maxLag === 1) return 80;
|
|
104
|
+
if (maxLag === 2) return 50;
|
|
105
|
+
if (maxLag === 3) return 20;
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
function frameworkScore(projects) {
|
|
109
|
+
const allFrameworks = projects.flatMap((p) => p.frameworks);
|
|
110
|
+
if (allFrameworks.length === 0) return 100;
|
|
111
|
+
const lags = allFrameworks.map((f) => f.majorsBehind).filter((v) => v !== null);
|
|
112
|
+
if (lags.length === 0) return 100;
|
|
113
|
+
const maxLag = Math.max(...lags);
|
|
114
|
+
const avgLag = lags.reduce((a, b) => a + b, 0) / lags.length;
|
|
115
|
+
const maxPenalty = Math.min(maxLag * 20, 100);
|
|
116
|
+
const avgPenalty = Math.min(avgLag * 15, 100);
|
|
117
|
+
return clamp(100 - (maxPenalty * 0.6 + avgPenalty * 0.4), 0, 100);
|
|
118
|
+
}
|
|
119
|
+
function dependencyScore(projects) {
|
|
120
|
+
let totalCurrent = 0;
|
|
121
|
+
let totalOne = 0;
|
|
122
|
+
let totalTwo = 0;
|
|
123
|
+
let totalUnknown = 0;
|
|
124
|
+
for (const p of projects) {
|
|
125
|
+
totalCurrent += p.dependencyAgeBuckets.current;
|
|
126
|
+
totalOne += p.dependencyAgeBuckets.oneBehind;
|
|
127
|
+
totalTwo += p.dependencyAgeBuckets.twoPlusBehind;
|
|
128
|
+
totalUnknown += p.dependencyAgeBuckets.unknown;
|
|
129
|
+
}
|
|
130
|
+
const total = totalCurrent + totalOne + totalTwo;
|
|
131
|
+
if (total === 0) return 100;
|
|
132
|
+
const currentPct = totalCurrent / total;
|
|
133
|
+
const onePct = totalOne / total;
|
|
134
|
+
const twoPct = totalTwo / total;
|
|
135
|
+
return clamp(Math.round(currentPct * 100 - onePct * 10 - twoPct * 40), 0, 100);
|
|
136
|
+
}
|
|
137
|
+
function eolScore(projects) {
|
|
138
|
+
let score = 100;
|
|
139
|
+
for (const p of projects) {
|
|
140
|
+
if (p.type === "node" && p.runtimeMajorsBehind !== void 0) {
|
|
141
|
+
if (p.runtimeMajorsBehind >= 3) score = Math.min(score, 0);
|
|
142
|
+
else if (p.runtimeMajorsBehind >= 2) score = Math.min(score, 30);
|
|
143
|
+
else if (p.runtimeMajorsBehind >= 1) score = Math.min(score, 70);
|
|
144
|
+
}
|
|
145
|
+
if (p.type === "dotnet" && p.runtimeMajorsBehind !== void 0) {
|
|
146
|
+
if (p.runtimeMajorsBehind >= 3) score = Math.min(score, 0);
|
|
147
|
+
else if (p.runtimeMajorsBehind >= 2) score = Math.min(score, 20);
|
|
148
|
+
else if (p.runtimeMajorsBehind >= 1) score = Math.min(score, 60);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return score;
|
|
152
|
+
}
|
|
153
|
+
function computeDriftScore(projects) {
|
|
154
|
+
const rs = runtimeScore(projects);
|
|
155
|
+
const fs5 = frameworkScore(projects);
|
|
156
|
+
const ds = dependencyScore(projects);
|
|
157
|
+
const es = eolScore(projects);
|
|
158
|
+
const score = Math.round(rs * 0.25 + fs5 * 0.25 + ds * 0.3 + es * 0.2);
|
|
159
|
+
let riskLevel;
|
|
160
|
+
if (score >= 70) riskLevel = "low";
|
|
161
|
+
else if (score >= 40) riskLevel = "moderate";
|
|
162
|
+
else riskLevel = "high";
|
|
163
|
+
return {
|
|
164
|
+
score,
|
|
165
|
+
riskLevel,
|
|
166
|
+
components: {
|
|
167
|
+
runtimeScore: rs,
|
|
168
|
+
frameworkScore: fs5,
|
|
169
|
+
dependencyScore: ds,
|
|
170
|
+
eolScore: es
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function generateFindings(projects, config) {
|
|
175
|
+
const thresholds = {
|
|
176
|
+
failOnError: { ...DEFAULT_THRESHOLDS.failOnError, ...config?.thresholds?.failOnError },
|
|
177
|
+
warn: { ...DEFAULT_THRESHOLDS.warn, ...config?.thresholds?.warn }
|
|
178
|
+
};
|
|
179
|
+
const findings = [];
|
|
180
|
+
for (const project of projects) {
|
|
181
|
+
if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 3) {
|
|
182
|
+
findings.push({
|
|
183
|
+
ruleId: "vibgrate/runtime-eol",
|
|
184
|
+
level: "error",
|
|
185
|
+
message: `${project.type === "node" ? "Node.js" : ".NET"} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}). Likely at or past EOL.`,
|
|
186
|
+
location: project.path
|
|
187
|
+
});
|
|
188
|
+
} else if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 2) {
|
|
189
|
+
findings.push({
|
|
190
|
+
ruleId: "vibgrate/runtime-lag",
|
|
191
|
+
level: "warning",
|
|
192
|
+
message: `${project.type === "node" ? "Node.js" : ".NET"} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}).`,
|
|
193
|
+
location: project.path
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
for (const fw of project.frameworks) {
|
|
197
|
+
if (fw.majorsBehind !== null && thresholds.failOnError.frameworkMajorLag !== void 0 && fw.majorsBehind >= thresholds.failOnError.frameworkMajorLag) {
|
|
198
|
+
findings.push({
|
|
199
|
+
ruleId: "vibgrate/framework-major-lag",
|
|
200
|
+
level: "error",
|
|
201
|
+
message: `${fw.name} is ${fw.majorsBehind} major versions behind (current: ${fw.currentVersion}, latest: ${fw.latestVersion}).`,
|
|
202
|
+
location: project.path
|
|
203
|
+
});
|
|
204
|
+
} else if (fw.majorsBehind !== null && thresholds.warn.frameworkMajorLag !== void 0 && fw.majorsBehind >= thresholds.warn.frameworkMajorLag) {
|
|
205
|
+
findings.push({
|
|
206
|
+
ruleId: "vibgrate/framework-major-lag",
|
|
207
|
+
level: "warning",
|
|
208
|
+
message: `${fw.name} is ${fw.majorsBehind} major versions behind (current: ${fw.currentVersion}, latest: ${fw.latestVersion}).`,
|
|
209
|
+
location: project.path
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const totalDeps = project.dependencyAgeBuckets.current + project.dependencyAgeBuckets.oneBehind + project.dependencyAgeBuckets.twoPlusBehind;
|
|
214
|
+
if (totalDeps > 0) {
|
|
215
|
+
const twoPlusPct = project.dependencyAgeBuckets.twoPlusBehind / totalDeps * 100;
|
|
216
|
+
if (thresholds.failOnError.dependencyTwoPlusPercent !== void 0 && twoPlusPct >= thresholds.failOnError.dependencyTwoPlusPercent) {
|
|
217
|
+
findings.push({
|
|
218
|
+
ruleId: "vibgrate/dependency-rot",
|
|
219
|
+
level: "error",
|
|
220
|
+
message: `${Math.round(twoPlusPct)}% of dependencies are 2+ major versions behind in ${project.name}.`,
|
|
221
|
+
location: project.path
|
|
222
|
+
});
|
|
223
|
+
} else if (thresholds.warn.dependencyTwoPlusPercent !== void 0 && twoPlusPct >= thresholds.warn.dependencyTwoPlusPercent) {
|
|
224
|
+
findings.push({
|
|
225
|
+
ruleId: "vibgrate/dependency-rot",
|
|
226
|
+
level: "warning",
|
|
227
|
+
message: `${Math.round(twoPlusPct)}% of dependencies are 2+ major versions behind in ${project.name}.`,
|
|
228
|
+
location: project.path
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const dep of project.dependencies) {
|
|
233
|
+
if (dep.majorsBehind !== null && dep.majorsBehind >= 3) {
|
|
234
|
+
findings.push({
|
|
235
|
+
ruleId: "vibgrate/dependency-major-lag",
|
|
236
|
+
level: "error",
|
|
237
|
+
message: `${dep.package} is ${dep.majorsBehind} major versions behind (spec: ${dep.currentSpec}, latest: ${dep.latestStable}).`,
|
|
238
|
+
location: project.path
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return findings;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/formatters/text.ts
|
|
247
|
+
import chalk from "chalk";
|
|
248
|
+
function formatText(artifact) {
|
|
249
|
+
const lines = [];
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
|
|
252
|
+
lines.push(chalk.bold.cyan("\u2551 Vibgrate Drift Report \u2551"));
|
|
253
|
+
lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
|
|
254
|
+
lines.push("");
|
|
255
|
+
const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
|
|
256
|
+
lines.push(chalk.bold(" Drift Score: ") + scoreColor.bold(`${artifact.drift.score}/100`));
|
|
257
|
+
lines.push(chalk.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
|
|
258
|
+
lines.push(chalk.bold(" Projects: ") + `${artifact.projects.length}`);
|
|
259
|
+
if (artifact.vcs) {
|
|
260
|
+
const vcsParts = [artifact.vcs.type];
|
|
261
|
+
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
262
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk.dim(artifact.vcs.shortSha));
|
|
263
|
+
lines.push(chalk.bold(" VCS: ") + vcsParts.join(" "));
|
|
264
|
+
}
|
|
265
|
+
lines.push("");
|
|
266
|
+
lines.push(chalk.bold.underline(" Score Breakdown"));
|
|
267
|
+
lines.push(` Runtime: ${scoreBar(artifact.drift.components.runtimeScore)}`);
|
|
268
|
+
lines.push(` Frameworks: ${scoreBar(artifact.drift.components.frameworkScore)}`);
|
|
269
|
+
lines.push(` Dependencies: ${scoreBar(artifact.drift.components.dependencyScore)}`);
|
|
270
|
+
lines.push(` EOL Risk: ${scoreBar(artifact.drift.components.eolScore)}`);
|
|
271
|
+
lines.push("");
|
|
272
|
+
for (const project of artifact.projects) {
|
|
273
|
+
lines.push(chalk.bold(` \u2500\u2500 ${project.name} `) + chalk.dim(`(${project.type}) ${project.path}`));
|
|
274
|
+
if (project.runtime) {
|
|
275
|
+
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk.green(" (current)");
|
|
276
|
+
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
277
|
+
}
|
|
278
|
+
if (project.targetFramework) {
|
|
279
|
+
lines.push(` Target: ${project.targetFramework}`);
|
|
280
|
+
}
|
|
281
|
+
if (project.frameworks.length > 0) {
|
|
282
|
+
lines.push(" Frameworks:");
|
|
283
|
+
for (const fw of project.frameworks) {
|
|
284
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk.green("current") : chalk.yellow(`${fw.majorsBehind} behind`) : chalk.dim("unknown");
|
|
285
|
+
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const b = project.dependencyAgeBuckets;
|
|
289
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
290
|
+
if (total > 0) {
|
|
291
|
+
lines.push(" Dependencies:");
|
|
292
|
+
lines.push(` ${chalk.green(`${b.current} current`)} ${chalk.yellow(`${b.oneBehind} 1-behind`)} ${chalk.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk.dim(`${b.unknown} unknown`)}`);
|
|
293
|
+
}
|
|
294
|
+
lines.push("");
|
|
295
|
+
}
|
|
296
|
+
if (artifact.findings.length > 0) {
|
|
297
|
+
lines.push(chalk.bold.underline(" Findings"));
|
|
298
|
+
for (const f of artifact.findings) {
|
|
299
|
+
const icon = f.level === "error" ? chalk.red("\u2716") : f.level === "warning" ? chalk.yellow("\u26A0") : chalk.blue("\u2139");
|
|
300
|
+
lines.push(` ${icon} ${f.message}`);
|
|
301
|
+
lines.push(chalk.dim(` ${f.ruleId} in ${f.location}`));
|
|
302
|
+
}
|
|
303
|
+
lines.push("");
|
|
304
|
+
}
|
|
305
|
+
if (artifact.delta !== void 0) {
|
|
306
|
+
const deltaStr = artifact.delta > 0 ? chalk.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk.red(`${artifact.delta}`) : chalk.dim("0");
|
|
307
|
+
lines.push(chalk.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
|
|
308
|
+
lines.push("");
|
|
309
|
+
}
|
|
310
|
+
if (artifact.extended) {
|
|
311
|
+
lines.push(...formatExtended(artifact.extended));
|
|
312
|
+
}
|
|
313
|
+
lines.push(chalk.dim(` Scanned at ${artifact.timestamp}`));
|
|
314
|
+
lines.push("");
|
|
315
|
+
return lines.join("\n");
|
|
316
|
+
}
|
|
317
|
+
function riskBadge(level) {
|
|
318
|
+
switch (level) {
|
|
319
|
+
case "low":
|
|
320
|
+
return chalk.bgGreen.black(" LOW ");
|
|
321
|
+
case "moderate":
|
|
322
|
+
return chalk.bgYellow.black(" MODERATE ");
|
|
323
|
+
case "high":
|
|
324
|
+
return chalk.bgRed.white(" HIGH ");
|
|
325
|
+
default:
|
|
326
|
+
return level;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function scoreBar(score) {
|
|
330
|
+
const width = 20;
|
|
331
|
+
const filled = Math.round(score / 100 * width);
|
|
332
|
+
const empty = width - filled;
|
|
333
|
+
const color = score >= 70 ? chalk.green : score >= 40 ? chalk.yellow : chalk.red;
|
|
334
|
+
return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + ` ${score}`;
|
|
335
|
+
}
|
|
336
|
+
var CATEGORY_LABELS = {
|
|
337
|
+
frontend: "Frontend",
|
|
338
|
+
metaFrameworks: "Meta-frameworks",
|
|
339
|
+
bundlers: "Bundlers",
|
|
340
|
+
css: "CSS / UI",
|
|
341
|
+
backend: "Backend",
|
|
342
|
+
orm: "ORM / Database",
|
|
343
|
+
testing: "Testing",
|
|
344
|
+
lintFormat: "Lint & Format",
|
|
345
|
+
apiMessaging: "API & Messaging",
|
|
346
|
+
observability: "Observability",
|
|
347
|
+
payment: "Payment",
|
|
348
|
+
auth: "Auth",
|
|
349
|
+
email: "Email",
|
|
350
|
+
cloud: "Cloud",
|
|
351
|
+
databases: "Databases",
|
|
352
|
+
messaging: "Messaging",
|
|
353
|
+
crm: "CRM & Comms",
|
|
354
|
+
storage: "Storage",
|
|
355
|
+
search: "Search & AI"
|
|
356
|
+
};
|
|
357
|
+
function formatExtended(ext) {
|
|
358
|
+
const lines = [];
|
|
359
|
+
if (ext.toolingInventory) {
|
|
360
|
+
const inv = ext.toolingInventory;
|
|
361
|
+
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
362
|
+
if (categories.length > 0) {
|
|
363
|
+
lines.push(chalk.bold.underline(" Tech Stack"));
|
|
364
|
+
for (const [cat, items] of categories) {
|
|
365
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
366
|
+
const names = items.map((i) => chalk.white(i.name)).join(chalk.dim(", "));
|
|
367
|
+
lines.push(` ${chalk.cyan(label)}: ${names}`);
|
|
368
|
+
}
|
|
369
|
+
lines.push("");
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (ext.serviceDependencies) {
|
|
373
|
+
const svc = ext.serviceDependencies;
|
|
374
|
+
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
375
|
+
if (categories.length > 0) {
|
|
376
|
+
lines.push(chalk.bold.underline(" Services & Integrations"));
|
|
377
|
+
for (const [cat, items] of categories) {
|
|
378
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
379
|
+
const names = items.map((i) => {
|
|
380
|
+
const ver = i.version ? chalk.dim(` ${i.version}`) : "";
|
|
381
|
+
return chalk.white(i.name) + ver;
|
|
382
|
+
}).join(chalk.dim(", "));
|
|
383
|
+
lines.push(` ${chalk.cyan(label)}: ${names}`);
|
|
384
|
+
}
|
|
385
|
+
lines.push("");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (ext.breakingChangeExposure) {
|
|
389
|
+
const bc = ext.breakingChangeExposure;
|
|
390
|
+
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
391
|
+
lines.push(chalk.bold.underline(" Breaking Change Exposure"));
|
|
392
|
+
const exposureColor = bc.exposureScore >= 40 ? chalk.red : bc.exposureScore >= 20 ? chalk.yellow : chalk.green;
|
|
393
|
+
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
394
|
+
if (bc.deprecatedPackages.length > 0) {
|
|
395
|
+
lines.push(` ${chalk.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk.dim(p)).join(", ")}`);
|
|
396
|
+
}
|
|
397
|
+
if (bc.legacyPolyfills.length > 0) {
|
|
398
|
+
lines.push(` ${chalk.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk.dim(p)).join(", ")}`);
|
|
399
|
+
}
|
|
400
|
+
if (bc.peerConflictsDetected) {
|
|
401
|
+
lines.push(` ${chalk.red("\u26A0")} Peer dependency conflicts detected`);
|
|
402
|
+
}
|
|
403
|
+
lines.push("");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
407
|
+
const ts = ext.tsModernity;
|
|
408
|
+
lines.push(chalk.bold.underline(" TypeScript"));
|
|
409
|
+
const parts = [];
|
|
410
|
+
parts.push(`v${ts.typescriptVersion}`);
|
|
411
|
+
if (ts.strict === true) parts.push(chalk.green("strict \u2714"));
|
|
412
|
+
else if (ts.strict === false) parts.push(chalk.yellow("strict \u2716"));
|
|
413
|
+
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
414
|
+
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
415
|
+
lines.push(` ${parts.join(chalk.dim(" \xB7 "))}`);
|
|
416
|
+
lines.push("");
|
|
417
|
+
}
|
|
418
|
+
if (ext.buildDeploy) {
|
|
419
|
+
const bd = ext.buildDeploy;
|
|
420
|
+
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
421
|
+
if (hasSomething) {
|
|
422
|
+
lines.push(chalk.bold.underline(" Build & Deploy"));
|
|
423
|
+
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
424
|
+
if (bd.docker.dockerfileCount > 0) {
|
|
425
|
+
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
426
|
+
}
|
|
427
|
+
if (bd.packageManagers.length > 0) lines.push(` Package Managers: ${bd.packageManagers.join(", ")}`);
|
|
428
|
+
if (bd.monorepoTools.length > 0) lines.push(` Monorepo: ${bd.monorepoTools.join(", ")}`);
|
|
429
|
+
if (bd.iac.length > 0) lines.push(` IaC: ${bd.iac.join(", ")}`);
|
|
430
|
+
lines.push("");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (ext.securityPosture) {
|
|
434
|
+
const sec = ext.securityPosture;
|
|
435
|
+
lines.push(chalk.bold.underline(" Security Posture"));
|
|
436
|
+
const checks = [];
|
|
437
|
+
checks.push(sec.lockfilePresent ? chalk.green("Lockfile \u2714") : chalk.red("Lockfile \u2716"));
|
|
438
|
+
checks.push(sec.gitignoreCoversEnv ? chalk.green(".env \u2714") : chalk.red(".env \u2716"));
|
|
439
|
+
checks.push(sec.gitignoreCoversNodeModules ? chalk.green("node_modules \u2714") : chalk.yellow("node_modules \u2716"));
|
|
440
|
+
if (sec.multipleLockfileTypes) checks.push(chalk.yellow("Multiple lockfiles \u26A0"));
|
|
441
|
+
if (sec.envFilesTracked) checks.push(chalk.red("Env files tracked \u2716"));
|
|
442
|
+
lines.push(` ${checks.join(chalk.dim(" \xB7 "))}`);
|
|
443
|
+
lines.push("");
|
|
444
|
+
}
|
|
445
|
+
if (ext.platformMatrix) {
|
|
446
|
+
const pm = ext.platformMatrix;
|
|
447
|
+
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
448
|
+
lines.push(chalk.bold.underline(" Platform"));
|
|
449
|
+
if (pm.nativeModules.length > 0) {
|
|
450
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk.dim(m)).join(", ")}`);
|
|
451
|
+
}
|
|
452
|
+
if (pm.osAssumptions.length > 0) {
|
|
453
|
+
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
454
|
+
}
|
|
455
|
+
lines.push("");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (ext.dependencyGraph) {
|
|
459
|
+
const dg = ext.dependencyGraph;
|
|
460
|
+
if (dg.lockfileType) {
|
|
461
|
+
lines.push(chalk.bold.underline(" Dependency Graph"));
|
|
462
|
+
lines.push(` ${dg.lockfileType}: ${chalk.white(`${dg.totalUnique}`)} unique, ${chalk.white(`${dg.totalInstalled}`)} installed`);
|
|
463
|
+
if (dg.duplicatedPackages.length > 0) {
|
|
464
|
+
lines.push(` ${chalk.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
465
|
+
}
|
|
466
|
+
if (dg.phantomDependencies.length > 0) {
|
|
467
|
+
lines.push(` ${chalk.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
468
|
+
}
|
|
469
|
+
lines.push("");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return lines;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/formatters/sarif.ts
|
|
476
|
+
function formatSarif(artifact) {
|
|
477
|
+
const rules = buildRules(artifact.findings);
|
|
478
|
+
const results = artifact.findings.map((f) => toSarifResult(f));
|
|
479
|
+
return {
|
|
480
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
481
|
+
version: "2.1.0",
|
|
482
|
+
runs: [
|
|
483
|
+
{
|
|
484
|
+
tool: {
|
|
485
|
+
driver: {
|
|
486
|
+
name: "vibgrate",
|
|
487
|
+
version: artifact.vibgrateVersion,
|
|
488
|
+
informationUri: "https://vibgrate.com",
|
|
489
|
+
rules
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
results,
|
|
493
|
+
invocations: [
|
|
494
|
+
{
|
|
495
|
+
executionSuccessful: true,
|
|
496
|
+
startTimeUtc: artifact.timestamp
|
|
497
|
+
}
|
|
498
|
+
]
|
|
499
|
+
}
|
|
500
|
+
]
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function buildRules(findings) {
|
|
504
|
+
const ruleIds = [...new Set(findings.map((f) => f.ruleId))];
|
|
505
|
+
return ruleIds.map((id) => {
|
|
506
|
+
const descriptions = {
|
|
507
|
+
"vibgrate/runtime-eol": {
|
|
508
|
+
id: "vibgrate/runtime-eol",
|
|
509
|
+
shortDescription: { text: "Runtime at or past end-of-life" },
|
|
510
|
+
helpUri: "https://vibgrate.com/rules/runtime-eol"
|
|
511
|
+
},
|
|
512
|
+
"vibgrate/runtime-lag": {
|
|
513
|
+
id: "vibgrate/runtime-lag",
|
|
514
|
+
shortDescription: { text: "Runtime major version lag" },
|
|
515
|
+
helpUri: "https://vibgrate.com/rules/runtime-lag"
|
|
516
|
+
},
|
|
517
|
+
"vibgrate/framework-major-lag": {
|
|
518
|
+
id: "vibgrate/framework-major-lag",
|
|
519
|
+
shortDescription: { text: "Framework major version behind latest" },
|
|
520
|
+
helpUri: "https://vibgrate.com/rules/framework-major-lag"
|
|
521
|
+
},
|
|
522
|
+
"vibgrate/dependency-rot": {
|
|
523
|
+
id: "vibgrate/dependency-rot",
|
|
524
|
+
shortDescription: { text: "High percentage of outdated dependencies" },
|
|
525
|
+
helpUri: "https://vibgrate.com/rules/dependency-rot"
|
|
526
|
+
},
|
|
527
|
+
"vibgrate/dependency-major-lag": {
|
|
528
|
+
id: "vibgrate/dependency-major-lag",
|
|
529
|
+
shortDescription: { text: "Individual dependency severely behind" },
|
|
530
|
+
helpUri: "https://vibgrate.com/rules/dependency-major-lag"
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
return descriptions[id] ?? {
|
|
534
|
+
id,
|
|
535
|
+
shortDescription: { text: id },
|
|
536
|
+
helpUri: "https://vibgrate.com"
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function toSarifResult(finding) {
|
|
541
|
+
return {
|
|
542
|
+
ruleId: finding.ruleId,
|
|
543
|
+
level: finding.level === "error" ? "error" : finding.level === "warning" ? "warning" : "note",
|
|
544
|
+
message: { text: finding.message },
|
|
545
|
+
locations: [
|
|
546
|
+
{
|
|
547
|
+
physicalLocation: {
|
|
548
|
+
artifactLocation: {
|
|
549
|
+
uri: finding.location
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
]
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// src/version.ts
|
|
558
|
+
import { createRequire } from "module";
|
|
559
|
+
var require2 = createRequire(import.meta.url);
|
|
560
|
+
var pkg = require2("../package.json");
|
|
561
|
+
var VERSION = pkg.version;
|
|
562
|
+
|
|
563
|
+
// src/commands/scan.ts
|
|
564
|
+
import * as path12 from "path";
|
|
565
|
+
import { Command } from "commander";
|
|
566
|
+
import chalk3 from "chalk";
|
|
567
|
+
|
|
568
|
+
// src/scanners/node-scanner.ts
|
|
569
|
+
import * as path2 from "path";
|
|
570
|
+
import * as semver2 from "semver";
|
|
571
|
+
|
|
572
|
+
// src/scanners/npm-cache.ts
|
|
573
|
+
import { spawn } from "child_process";
|
|
574
|
+
import * as semver from "semver";
|
|
575
|
+
function stableOnly(versions) {
|
|
576
|
+
return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
|
|
577
|
+
}
|
|
578
|
+
function maxStable(versions) {
|
|
579
|
+
const stable = stableOnly(versions);
|
|
580
|
+
if (stable.length === 0) return null;
|
|
581
|
+
return stable.sort(semver.rcompare)[0] ?? null;
|
|
582
|
+
}
|
|
583
|
+
async function npmViewJson(args, cwd) {
|
|
584
|
+
return new Promise((resolve4, reject) => {
|
|
585
|
+
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
586
|
+
cwd,
|
|
587
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
588
|
+
});
|
|
589
|
+
let out = "";
|
|
590
|
+
let err = "";
|
|
591
|
+
child.stdout.on("data", (d) => out += String(d));
|
|
592
|
+
child.stderr.on("data", (d) => err += String(d));
|
|
593
|
+
child.on("error", reject);
|
|
594
|
+
child.on("close", (code) => {
|
|
595
|
+
if (code !== 0) {
|
|
596
|
+
reject(new Error(`npm view ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const trimmed = out.trim();
|
|
600
|
+
if (!trimmed) {
|
|
601
|
+
resolve4(null);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
resolve4(JSON.parse(trimmed));
|
|
606
|
+
} catch {
|
|
607
|
+
resolve4(trimmed.replace(/^"|"$/g, ""));
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
var NpmCache = class {
|
|
613
|
+
constructor(cwd, sem) {
|
|
614
|
+
this.cwd = cwd;
|
|
615
|
+
this.sem = sem;
|
|
616
|
+
}
|
|
617
|
+
meta = /* @__PURE__ */ new Map();
|
|
618
|
+
get(pkg2) {
|
|
619
|
+
const existing = this.meta.get(pkg2);
|
|
620
|
+
if (existing) return existing;
|
|
621
|
+
const p = this.sem.run(async () => {
|
|
622
|
+
let latest = null;
|
|
623
|
+
try {
|
|
624
|
+
const dist = await npmViewJson([pkg2, "dist-tags"], this.cwd);
|
|
625
|
+
if (dist && typeof dist === "object" && typeof dist.latest === "string") {
|
|
626
|
+
latest = dist.latest;
|
|
627
|
+
}
|
|
628
|
+
} catch {
|
|
629
|
+
}
|
|
630
|
+
let versions = [];
|
|
631
|
+
try {
|
|
632
|
+
const v = await npmViewJson([pkg2, "versions"], this.cwd);
|
|
633
|
+
if (Array.isArray(v)) versions = v.map(String);
|
|
634
|
+
else if (typeof v === "string") versions = [v];
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
const stable = stableOnly(versions);
|
|
638
|
+
const latestStableOverall = maxStable(stable);
|
|
639
|
+
if (!latest && latestStableOverall) latest = latestStableOverall;
|
|
640
|
+
return { latest, stableVersions: stable, latestStableOverall };
|
|
641
|
+
});
|
|
642
|
+
this.meta.set(pkg2, p);
|
|
643
|
+
return p;
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
function isSemverSpec(spec) {
|
|
647
|
+
const s = spec.trim();
|
|
648
|
+
if (!s) return false;
|
|
649
|
+
if (s.startsWith("workspace:")) return false;
|
|
650
|
+
if (s.startsWith("file:")) return false;
|
|
651
|
+
if (s.startsWith("link:")) return false;
|
|
652
|
+
if (s.startsWith("git+")) return false;
|
|
653
|
+
if (s.includes("://")) return false;
|
|
654
|
+
if (s.startsWith("github:")) return false;
|
|
655
|
+
if (s === "*" || s.toLowerCase() === "latest") return true;
|
|
656
|
+
return semver.validRange(s) !== null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/scanners/node-scanner.ts
|
|
660
|
+
var KNOWN_FRAMEWORKS = {
|
|
661
|
+
// ── Frontend ──
|
|
662
|
+
"react": "React",
|
|
663
|
+
"react-dom": "React DOM",
|
|
664
|
+
"vue": "Vue",
|
|
665
|
+
"@angular/core": "Angular",
|
|
666
|
+
"svelte": "Svelte",
|
|
667
|
+
"solid-js": "Solid",
|
|
668
|
+
"preact": "Preact",
|
|
669
|
+
"lit": "Lit",
|
|
670
|
+
"qwik": "Qwik",
|
|
671
|
+
"htmx.org": "htmx",
|
|
672
|
+
"alpinejs": "Alpine.js",
|
|
673
|
+
"stimulus": "Stimulus",
|
|
674
|
+
// ── Meta-frameworks ──
|
|
675
|
+
"next": "Next.js",
|
|
676
|
+
"nuxt": "Nuxt",
|
|
677
|
+
"@remix-run/react": "Remix",
|
|
678
|
+
"@remix-run/node": "Remix (Node)",
|
|
679
|
+
"gatsby": "Gatsby",
|
|
680
|
+
"astro": "Astro",
|
|
681
|
+
"@sveltejs/kit": "SvelteKit",
|
|
682
|
+
"@analogjs/platform": "Analog",
|
|
683
|
+
"@tanstack/start": "TanStack Start",
|
|
684
|
+
// ── Backend ──
|
|
685
|
+
"express": "Express",
|
|
686
|
+
"fastify": "Fastify",
|
|
687
|
+
"@nestjs/core": "NestJS",
|
|
688
|
+
"hono": "Hono",
|
|
689
|
+
"koa": "Koa",
|
|
690
|
+
"@hapi/hapi": "Hapi",
|
|
691
|
+
"restify": "Restify",
|
|
692
|
+
"@elysiajs/eden": "Elysia",
|
|
693
|
+
"elysia": "Elysia",
|
|
694
|
+
"@adonisjs/core": "AdonisJS",
|
|
695
|
+
"moleculer": "Moleculer",
|
|
696
|
+
"@feathersjs/feathers": "Feathers",
|
|
697
|
+
"sails": "Sails",
|
|
698
|
+
// ── Language & Runtime ──
|
|
699
|
+
"typescript": "TypeScript",
|
|
700
|
+
// ── State Management ──
|
|
701
|
+
"redux": "Redux",
|
|
702
|
+
"@reduxjs/toolkit": "Redux Toolkit",
|
|
703
|
+
"zustand": "Zustand",
|
|
704
|
+
"mobx": "MobX",
|
|
705
|
+
"jotai": "Jotai",
|
|
706
|
+
"recoil": "Recoil",
|
|
707
|
+
"pinia": "Pinia",
|
|
708
|
+
"vuex": "Vuex",
|
|
709
|
+
"@tanstack/react-query": "TanStack Query",
|
|
710
|
+
"swr": "SWR",
|
|
711
|
+
"xstate": "XState",
|
|
712
|
+
"@ngrx/store": "NgRx",
|
|
713
|
+
// ── ORM & Database ──
|
|
714
|
+
"prisma": "Prisma",
|
|
715
|
+
"drizzle-orm": "Drizzle",
|
|
716
|
+
"typeorm": "TypeORM",
|
|
717
|
+
"sequelize": "Sequelize",
|
|
718
|
+
"@mikro-orm/core": "MikroORM",
|
|
719
|
+
"mongoose": "Mongoose",
|
|
720
|
+
"knex": "Knex",
|
|
721
|
+
"kysely": "Kysely",
|
|
722
|
+
"objection": "Objection.js",
|
|
723
|
+
// ── Bundlers ──
|
|
724
|
+
"vite": "Vite",
|
|
725
|
+
"webpack": "webpack",
|
|
726
|
+
"rollup": "Rollup",
|
|
727
|
+
"esbuild": "esbuild",
|
|
728
|
+
"parcel": "Parcel",
|
|
729
|
+
"turbo": "Turbo",
|
|
730
|
+
"tsup": "tsup",
|
|
731
|
+
"@swc/core": "SWC",
|
|
732
|
+
"bun": "Bun",
|
|
733
|
+
// ── Testing ──
|
|
734
|
+
"vitest": "Vitest",
|
|
735
|
+
"jest": "Jest",
|
|
736
|
+
"@playwright/test": "Playwright",
|
|
737
|
+
"cypress": "Cypress",
|
|
738
|
+
"mocha": "Mocha",
|
|
739
|
+
"ava": "AVA",
|
|
740
|
+
"storybook": "Storybook",
|
|
741
|
+
"@storybook/react": "Storybook"
|
|
742
|
+
};
|
|
743
|
+
async function scanNodeProjects(rootDir, npmCache) {
|
|
744
|
+
const packageJsonFiles = await findPackageJsonFiles(rootDir);
|
|
745
|
+
const results = [];
|
|
746
|
+
for (const pjPath of packageJsonFiles) {
|
|
747
|
+
try {
|
|
748
|
+
const scan = await scanOnePackageJson(pjPath, rootDir, npmCache);
|
|
749
|
+
results.push(scan);
|
|
750
|
+
} catch (e) {
|
|
751
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
752
|
+
console.error(`Error scanning ${pjPath}: ${msg}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return results;
|
|
756
|
+
}
|
|
757
|
+
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
758
|
+
const pj = await readJsonFile(packageJsonPath);
|
|
759
|
+
const absProjectPath = path2.dirname(packageJsonPath);
|
|
760
|
+
const projectPath = path2.relative(rootDir, absProjectPath) || ".";
|
|
761
|
+
const nodeEngine = pj.engines?.node ?? void 0;
|
|
762
|
+
let runtimeLatest;
|
|
763
|
+
let runtimeMajorsBehind;
|
|
764
|
+
if (nodeEngine) {
|
|
765
|
+
const latestLtsMajor = 22;
|
|
766
|
+
const parsed = semver2.minVersion(nodeEngine);
|
|
767
|
+
if (parsed) {
|
|
768
|
+
const currentMajor = semver2.major(parsed);
|
|
769
|
+
runtimeLatest = `${latestLtsMajor}.0.0`;
|
|
770
|
+
runtimeMajorsBehind = Math.max(0, latestLtsMajor - currentMajor);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const sections = [
|
|
774
|
+
{ name: "dependencies", deps: pj.dependencies },
|
|
775
|
+
{ name: "devDependencies", deps: pj.devDependencies },
|
|
776
|
+
{ name: "peerDependencies", deps: pj.peerDependencies },
|
|
777
|
+
{ name: "optionalDependencies", deps: pj.optionalDependencies }
|
|
778
|
+
];
|
|
779
|
+
const dependencies = [];
|
|
780
|
+
const frameworks = [];
|
|
781
|
+
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: 0 };
|
|
782
|
+
const depEntries = [];
|
|
783
|
+
for (const s of sections) {
|
|
784
|
+
if (!s.deps) continue;
|
|
785
|
+
for (const [pkg2, spec] of Object.entries(s.deps)) {
|
|
786
|
+
if (!isSemverSpec(spec)) continue;
|
|
787
|
+
depEntries.push({ pkg: pkg2, section: s.name, spec });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const metaPromises = depEntries.map(async (entry) => {
|
|
791
|
+
const meta = await npmCache.get(entry.pkg);
|
|
792
|
+
return { ...entry, meta };
|
|
793
|
+
});
|
|
794
|
+
const resolved = await Promise.all(metaPromises);
|
|
795
|
+
for (const { pkg: pkg2, section, spec, meta } of resolved) {
|
|
796
|
+
const resolvedVersion = meta.stableVersions.length > 0 ? semver2.maxSatisfying(meta.stableVersions, spec) ?? null : null;
|
|
797
|
+
const latestStable = meta.latestStableOverall;
|
|
798
|
+
let majorsBehind = null;
|
|
799
|
+
let drift = "unknown";
|
|
800
|
+
if (resolvedVersion && latestStable) {
|
|
801
|
+
const currentMajor = semver2.major(resolvedVersion);
|
|
802
|
+
const latestMajor = semver2.major(latestStable);
|
|
803
|
+
majorsBehind = latestMajor - currentMajor;
|
|
804
|
+
if (majorsBehind === 0) {
|
|
805
|
+
drift = semver2.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
|
|
806
|
+
} else {
|
|
807
|
+
drift = "major-behind";
|
|
808
|
+
}
|
|
809
|
+
if (majorsBehind === 0) buckets.current++;
|
|
810
|
+
else if (majorsBehind === 1) buckets.oneBehind++;
|
|
811
|
+
else buckets.twoPlusBehind++;
|
|
812
|
+
} else {
|
|
813
|
+
buckets.unknown++;
|
|
814
|
+
}
|
|
815
|
+
dependencies.push({
|
|
816
|
+
package: pkg2,
|
|
817
|
+
section,
|
|
818
|
+
currentSpec: spec,
|
|
819
|
+
resolvedVersion,
|
|
820
|
+
latestStable,
|
|
821
|
+
majorsBehind,
|
|
822
|
+
drift
|
|
823
|
+
});
|
|
824
|
+
if (pkg2 in KNOWN_FRAMEWORKS) {
|
|
825
|
+
frameworks.push({
|
|
826
|
+
name: KNOWN_FRAMEWORKS[pkg2],
|
|
827
|
+
currentVersion: resolvedVersion,
|
|
828
|
+
latestVersion: latestStable,
|
|
829
|
+
majorsBehind
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
dependencies.sort((a, b) => {
|
|
834
|
+
const order = { "major-behind": 0, "minor-behind": 1, "current": 2, "unknown": 3 };
|
|
835
|
+
const diff = (order[a.drift] ?? 9) - (order[b.drift] ?? 9);
|
|
836
|
+
if (diff !== 0) return diff;
|
|
837
|
+
return a.package.localeCompare(b.package);
|
|
838
|
+
});
|
|
839
|
+
return {
|
|
840
|
+
type: "node",
|
|
841
|
+
path: projectPath,
|
|
842
|
+
name: pj.name ?? path2.basename(absProjectPath),
|
|
843
|
+
runtime: nodeEngine,
|
|
844
|
+
runtimeLatest,
|
|
845
|
+
runtimeMajorsBehind,
|
|
846
|
+
frameworks,
|
|
847
|
+
dependencies,
|
|
848
|
+
dependencyAgeBuckets: buckets
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/scanners/dotnet-scanner.ts
|
|
853
|
+
import * as path3 from "path";
|
|
854
|
+
import { XMLParser } from "fast-xml-parser";
|
|
855
|
+
var parser = new XMLParser({
|
|
856
|
+
ignoreAttributes: false,
|
|
857
|
+
attributeNamePrefix: "@_"
|
|
858
|
+
});
|
|
859
|
+
var KNOWN_DOTNET_FRAMEWORKS = {
|
|
860
|
+
// ── ASP.NET Core & Web ──
|
|
861
|
+
"Microsoft.AspNetCore.App": "ASP.NET Core",
|
|
862
|
+
"Microsoft.AspNetCore.Mvc": "ASP.NET Core MVC",
|
|
863
|
+
"Microsoft.AspNetCore.Components": "Blazor",
|
|
864
|
+
"Microsoft.AspNetCore.Components.WebAssembly": "Blazor WASM",
|
|
865
|
+
"Microsoft.AspNetCore.SignalR": "SignalR",
|
|
866
|
+
"Microsoft.AspNetCore.OData": "OData",
|
|
867
|
+
"Microsoft.AspNetCore.Identity": "ASP.NET Identity",
|
|
868
|
+
"Microsoft.AspNetCore.Authentication.JwtBearer": "JWT Bearer Auth",
|
|
869
|
+
"Microsoft.AspNetCore.Diagnostics.HealthChecks": "Health Checks",
|
|
870
|
+
"Swashbuckle.AspNetCore": "Swashbuckle",
|
|
871
|
+
"NSwag.AspNetCore": "NSwag",
|
|
872
|
+
// ── Entity Framework & Data Access ──
|
|
873
|
+
"Microsoft.EntityFrameworkCore": "EF Core",
|
|
874
|
+
"Microsoft.EntityFrameworkCore.SqlServer": "EF Core SQL Server",
|
|
875
|
+
"Microsoft.EntityFrameworkCore.Sqlite": "EF Core SQLite",
|
|
876
|
+
"Microsoft.EntityFrameworkCore.Design": "EF Core Design",
|
|
877
|
+
"Microsoft.EntityFrameworkCore.Tools": "EF Core Tools",
|
|
878
|
+
"Npgsql.EntityFrameworkCore.PostgreSQL": "EF Core PostgreSQL",
|
|
879
|
+
"Pomelo.EntityFrameworkCore.MySql": "EF Core MySQL (Pomelo)",
|
|
880
|
+
"MongoDB.EntityFrameworkCore": "EF Core MongoDB",
|
|
881
|
+
"Dapper": "Dapper",
|
|
882
|
+
"Dapper.Contrib": "Dapper Contrib",
|
|
883
|
+
"NHibernate": "NHibernate",
|
|
884
|
+
"Npgsql": "Npgsql",
|
|
885
|
+
"MySqlConnector": "MySqlConnector",
|
|
886
|
+
"MongoDB.Driver": "MongoDB Driver",
|
|
887
|
+
"StackExchange.Redis": "StackExchange.Redis",
|
|
888
|
+
"Microsoft.Data.SqlClient": "SqlClient",
|
|
889
|
+
"Oracle.ManagedDataAccess.Core": "Oracle (Managed)",
|
|
890
|
+
"Cassandra": "Cassandra Driver",
|
|
891
|
+
"Neo4j.Driver": "Neo4j Driver",
|
|
892
|
+
"Marten": "Marten",
|
|
893
|
+
"LiteDB": "LiteDB",
|
|
894
|
+
"RavenDB.Client": "RavenDB",
|
|
895
|
+
// ── Hosting & Configuration ──
|
|
896
|
+
"Microsoft.Extensions.Hosting": ".NET Hosting",
|
|
897
|
+
"Microsoft.Extensions.DependencyInjection": ".NET DI",
|
|
898
|
+
"Microsoft.Extensions.Configuration": ".NET Configuration",
|
|
899
|
+
"Microsoft.Extensions.Logging": ".NET Logging",
|
|
900
|
+
"Microsoft.Extensions.Options": ".NET Options",
|
|
901
|
+
"Microsoft.Extensions.Caching.Memory": ".NET Memory Cache",
|
|
902
|
+
"Microsoft.Extensions.Caching.StackExchangeRedis": ".NET Redis Cache",
|
|
903
|
+
"Microsoft.Extensions.Http": ".NET HttpClientFactory",
|
|
904
|
+
"Microsoft.Extensions.Resilience": ".NET Resilience",
|
|
905
|
+
// ── CQRS & Mediator ──
|
|
906
|
+
"MediatR": "MediatR",
|
|
907
|
+
"Wolverine": "Wolverine",
|
|
908
|
+
"Brighter": "Brighter",
|
|
909
|
+
// ── Mapping ──
|
|
910
|
+
"AutoMapper": "AutoMapper",
|
|
911
|
+
"Mapster": "Mapster",
|
|
912
|
+
"Riok.Mapperly": "Mapperly",
|
|
913
|
+
// ── Validation ──
|
|
914
|
+
"FluentValidation": "FluentValidation",
|
|
915
|
+
"FluentValidation.AspNetCore": "FluentValidation ASP.NET",
|
|
916
|
+
// ── Serialization ──
|
|
917
|
+
"Newtonsoft.Json": "Newtonsoft.Json",
|
|
918
|
+
"System.Text.Json": "System.Text.Json",
|
|
919
|
+
"MessagePack": "MessagePack",
|
|
920
|
+
"protobuf-net": "protobuf-net",
|
|
921
|
+
"CsvHelper": "CsvHelper",
|
|
922
|
+
// ── Logging & Observability ──
|
|
923
|
+
"Serilog": "Serilog",
|
|
924
|
+
"Serilog.AspNetCore": "Serilog ASP.NET",
|
|
925
|
+
"Serilog.Sinks.Console": "Serilog Console",
|
|
926
|
+
"Serilog.Sinks.Seq": "Serilog Seq",
|
|
927
|
+
"Serilog.Sinks.File": "Serilog File",
|
|
928
|
+
"Serilog.Sinks.Elasticsearch": "Serilog Elasticsearch",
|
|
929
|
+
"NLog": "NLog",
|
|
930
|
+
"NLog.Web.AspNetCore": "NLog ASP.NET",
|
|
931
|
+
"log4net": "log4net",
|
|
932
|
+
"OpenTelemetry": "OpenTelemetry",
|
|
933
|
+
"OpenTelemetry.Extensions.Hosting": "OpenTelemetry Hosting",
|
|
934
|
+
"OpenTelemetry.Instrumentation.AspNetCore": "OpenTelemetry ASP.NET",
|
|
935
|
+
"OpenTelemetry.Exporter.Prometheus": "OpenTelemetry Prometheus",
|
|
936
|
+
"OpenTelemetry.Exporter.Jaeger": "OpenTelemetry Jaeger",
|
|
937
|
+
"OpenTelemetry.Exporter.OpenTelemetryProtocol": "OpenTelemetry OTLP",
|
|
938
|
+
"App.Metrics": "App.Metrics",
|
|
939
|
+
"prometheus-net": "Prometheus.NET",
|
|
940
|
+
"Elastic.Apm": "Elastic APM",
|
|
941
|
+
// ── Testing ──
|
|
942
|
+
"xunit": "xUnit",
|
|
943
|
+
"xunit.runner.visualstudio": "xUnit Runner",
|
|
944
|
+
"NUnit": "NUnit",
|
|
945
|
+
"NUnit3TestAdapter": "NUnit Adapter",
|
|
946
|
+
"MSTest.TestFramework": "MSTest",
|
|
947
|
+
"MSTest.TestAdapter": "MSTest Adapter",
|
|
948
|
+
"Moq": "Moq",
|
|
949
|
+
"NSubstitute": "NSubstitute",
|
|
950
|
+
"FakeItEasy": "FakeItEasy",
|
|
951
|
+
"FluentAssertions": "FluentAssertions",
|
|
952
|
+
"Shouldly": "Shouldly",
|
|
953
|
+
"Bogus": "Bogus",
|
|
954
|
+
"AutoFixture": "AutoFixture",
|
|
955
|
+
"WireMock.Net": "WireMock.Net",
|
|
956
|
+
"Testcontainers": "Testcontainers",
|
|
957
|
+
"Respawn": "Respawn",
|
|
958
|
+
"BenchmarkDotNet": "BenchmarkDotNet",
|
|
959
|
+
"coverlet.collector": "Coverlet",
|
|
960
|
+
"SpecFlow": "SpecFlow",
|
|
961
|
+
"TUnit": "TUnit",
|
|
962
|
+
"Verify.Xunit": "Verify",
|
|
963
|
+
"Snapshooter": "Snapshooter",
|
|
964
|
+
// ── Messaging & Event Bus ──
|
|
965
|
+
"MassTransit": "MassTransit",
|
|
966
|
+
"MassTransit.RabbitMQ": "MassTransit RabbitMQ",
|
|
967
|
+
"MassTransit.Azure.ServiceBus.Core": "MassTransit Azure SB",
|
|
968
|
+
"NServiceBus": "NServiceBus",
|
|
969
|
+
"RabbitMQ.Client": "RabbitMQ Client",
|
|
970
|
+
"Confluent.Kafka": "Confluent Kafka",
|
|
971
|
+
"Azure.Messaging.ServiceBus": "Azure Service Bus",
|
|
972
|
+
"Azure.Messaging.EventHubs": "Azure Event Hubs",
|
|
973
|
+
"Amazon.SQS": "AWS SQS",
|
|
974
|
+
"Amazon.SNS": "AWS SNS",
|
|
975
|
+
"Rebus": "Rebus",
|
|
976
|
+
"EasyNetQ": "EasyNetQ",
|
|
977
|
+
"SlimMessageBus": "SlimMessageBus",
|
|
978
|
+
"CAP": "DotNetCore.CAP",
|
|
979
|
+
// ── Cloud SDKs ──
|
|
980
|
+
"AWSSDK.Core": "AWS SDK Core",
|
|
981
|
+
"AWSSDK.S3": "AWS SDK S3",
|
|
982
|
+
"AWSSDK.SQS": "AWS SDK SQS",
|
|
983
|
+
"AWSSDK.DynamoDBv2": "AWS SDK DynamoDB",
|
|
984
|
+
"AWSSDK.Lambda": "AWS SDK Lambda",
|
|
985
|
+
"AWSSDK.SecretsManager": "AWS SDK Secrets Manager",
|
|
986
|
+
"AWSSDK.CloudWatch": "AWS SDK CloudWatch",
|
|
987
|
+
"Azure.Storage.Blobs": "Azure Blob Storage",
|
|
988
|
+
"Azure.Identity": "Azure Identity",
|
|
989
|
+
"Azure.Security.KeyVault.Secrets": "Azure Key Vault",
|
|
990
|
+
"Azure.Cosmos": "Azure Cosmos DB",
|
|
991
|
+
"Microsoft.Azure.Functions.Worker": "Azure Functions",
|
|
992
|
+
"Google.Cloud.Storage.V1": "GCP Storage",
|
|
993
|
+
"Google.Cloud.PubSub.V1": "GCP Pub/Sub",
|
|
994
|
+
"Google.Cloud.Firestore": "GCP Firestore",
|
|
995
|
+
// ── Auth & Identity ──
|
|
996
|
+
"Microsoft.Identity.Web": "Microsoft Identity Web",
|
|
997
|
+
"Microsoft.Identity.Client": "MSAL",
|
|
998
|
+
"IdentityServer4": "IdentityServer4",
|
|
999
|
+
"Duende.IdentityServer": "Duende IdentityServer",
|
|
1000
|
+
"Microsoft.AspNetCore.Authentication.OpenIdConnect": "OpenID Connect",
|
|
1001
|
+
"IdentityModel": "IdentityModel",
|
|
1002
|
+
// ── HTTP & API ──
|
|
1003
|
+
"Refit": "Refit",
|
|
1004
|
+
"RestSharp": "RestSharp",
|
|
1005
|
+
"Flurl.Http": "Flurl",
|
|
1006
|
+
"Polly": "Polly",
|
|
1007
|
+
"Polly.Extensions.Http": "Polly HTTP",
|
|
1008
|
+
"Microsoft.Extensions.Http.Polly": "HttpClient Polly",
|
|
1009
|
+
"Grpc.AspNetCore": "gRPC ASP.NET",
|
|
1010
|
+
"Grpc.Net.Client": "gRPC Client",
|
|
1011
|
+
"GraphQL.Server.All": "GraphQL Server",
|
|
1012
|
+
"HotChocolate.AspNetCore": "Hot Chocolate (GraphQL)",
|
|
1013
|
+
// ── Background Processing ──
|
|
1014
|
+
"Hangfire": "Hangfire",
|
|
1015
|
+
"Hangfire.Core": "Hangfire Core",
|
|
1016
|
+
"Hangfire.AspNetCore": "Hangfire ASP.NET",
|
|
1017
|
+
"Quartz": "Quartz.NET",
|
|
1018
|
+
"Quartz.Extensions.Hosting": "Quartz.NET Hosting",
|
|
1019
|
+
"Coravel": "Coravel",
|
|
1020
|
+
// ── File & Document ──
|
|
1021
|
+
"EPPlus": "EPPlus",
|
|
1022
|
+
"ClosedXML": "ClosedXML",
|
|
1023
|
+
"iTextSharp": "iTextSharp",
|
|
1024
|
+
"QuestPDF": "QuestPDF",
|
|
1025
|
+
"ImageSharp": "ImageSharp",
|
|
1026
|
+
"SixLabors.ImageSharp": "ImageSharp",
|
|
1027
|
+
// ── Feature Flags & Config ──
|
|
1028
|
+
"LaunchDarkly.ServerSdk": "LaunchDarkly",
|
|
1029
|
+
"Microsoft.FeatureManagement": "Feature Management",
|
|
1030
|
+
"Microsoft.FeatureManagement.AspNetCore": "Feature Management ASP.NET",
|
|
1031
|
+
// ── Microservices & Distributed ──
|
|
1032
|
+
"Dapr.Client": "Dapr",
|
|
1033
|
+
"Steeltoe.Discovery.ClientCore": "Steeltoe",
|
|
1034
|
+
"Ocelot": "Ocelot (API Gateway)",
|
|
1035
|
+
"Yarp.ReverseProxy": "YARP",
|
|
1036
|
+
// ── Real-time ──
|
|
1037
|
+
"Microsoft.AspNetCore.SignalR.Client": "SignalR Client"
|
|
1038
|
+
};
|
|
1039
|
+
var LATEST_DOTNET_MAJOR = 9;
|
|
1040
|
+
function parseTfmMajor(tfm) {
|
|
1041
|
+
const match = tfm.match(/^net(\d+)\.\d+$/);
|
|
1042
|
+
if (match?.[1]) return parseInt(match[1], 10);
|
|
1043
|
+
const coreMatch = tfm.match(/^netcoreapp(\d+)\.\d+$/);
|
|
1044
|
+
if (coreMatch?.[1]) return parseInt(coreMatch[1], 10);
|
|
1045
|
+
if (tfm.startsWith("netstandard")) return null;
|
|
1046
|
+
const fxMatch = tfm.match(/^net(\d)(\d+)?$/);
|
|
1047
|
+
if (fxMatch) return null;
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
function parseCsproj(xml, filePath) {
|
|
1051
|
+
const parsed = parser.parse(xml);
|
|
1052
|
+
const project = parsed?.Project;
|
|
1053
|
+
if (!project) {
|
|
1054
|
+
return { targetFrameworks: [], packageReferences: [], projectName: path3.basename(filePath, ".csproj") };
|
|
1055
|
+
}
|
|
1056
|
+
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1057
|
+
const targetFrameworks = [];
|
|
1058
|
+
for (const pg of propertyGroups) {
|
|
1059
|
+
if (pg.TargetFramework) {
|
|
1060
|
+
targetFrameworks.push(String(pg.TargetFramework));
|
|
1061
|
+
}
|
|
1062
|
+
if (pg.TargetFrameworks) {
|
|
1063
|
+
const tfms = String(pg.TargetFrameworks).split(";").map((s) => s.trim()).filter(Boolean);
|
|
1064
|
+
targetFrameworks.push(...tfms);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const itemGroups = Array.isArray(project.ItemGroup) ? project.ItemGroup : project.ItemGroup ? [project.ItemGroup] : [];
|
|
1068
|
+
const packageReferences = [];
|
|
1069
|
+
for (const ig of itemGroups) {
|
|
1070
|
+
const refs = Array.isArray(ig.PackageReference) ? ig.PackageReference : ig.PackageReference ? [ig.PackageReference] : [];
|
|
1071
|
+
for (const ref of refs) {
|
|
1072
|
+
const name = ref["@_Include"] ?? ref["@_include"] ?? "";
|
|
1073
|
+
const version = ref["@_Version"] ?? ref["@_version"] ?? ref.Version ?? "";
|
|
1074
|
+
if (name && version) {
|
|
1075
|
+
packageReferences.push({ name: String(name), version: String(version) });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1081
|
+
packageReferences,
|
|
1082
|
+
projectName: path3.basename(filePath, ".csproj")
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function scanDotnetProjects(rootDir) {
|
|
1086
|
+
const csprojFiles = await findCsprojFiles(rootDir);
|
|
1087
|
+
const slnFiles = await findSolutionFiles(rootDir);
|
|
1088
|
+
const slnCsprojPaths = /* @__PURE__ */ new Set();
|
|
1089
|
+
for (const slnPath of slnFiles) {
|
|
1090
|
+
try {
|
|
1091
|
+
const slnContent = await readTextFile(slnPath);
|
|
1092
|
+
const slnDir = path3.dirname(slnPath);
|
|
1093
|
+
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1094
|
+
let match;
|
|
1095
|
+
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1096
|
+
if (match[1]) {
|
|
1097
|
+
const csprojPath = path3.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1098
|
+
slnCsprojPaths.add(csprojPath);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
const allCsprojFiles = /* @__PURE__ */ new Set([...csprojFiles, ...slnCsprojPaths]);
|
|
1105
|
+
const results = [];
|
|
1106
|
+
for (const csprojPath of allCsprojFiles) {
|
|
1107
|
+
try {
|
|
1108
|
+
const scan = await scanOneCsproj(csprojPath, rootDir);
|
|
1109
|
+
results.push(scan);
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1112
|
+
console.error(`Error scanning ${csprojPath}: ${msg}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return results;
|
|
1116
|
+
}
|
|
1117
|
+
async function scanOneCsproj(csprojPath, rootDir) {
|
|
1118
|
+
const xml = await readTextFile(csprojPath);
|
|
1119
|
+
const data = parseCsproj(xml, csprojPath);
|
|
1120
|
+
const primaryTfm = data.targetFrameworks[0];
|
|
1121
|
+
let runtimeMajorsBehind;
|
|
1122
|
+
let targetFramework = primaryTfm;
|
|
1123
|
+
if (primaryTfm) {
|
|
1124
|
+
const major2 = parseTfmMajor(primaryTfm);
|
|
1125
|
+
if (major2 !== null) {
|
|
1126
|
+
runtimeMajorsBehind = Math.max(0, LATEST_DOTNET_MAJOR - major2);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const dependencies = data.packageReferences.map((ref) => ({
|
|
1130
|
+
package: ref.name,
|
|
1131
|
+
section: "dependencies",
|
|
1132
|
+
currentSpec: ref.version,
|
|
1133
|
+
resolvedVersion: ref.version,
|
|
1134
|
+
latestStable: null,
|
|
1135
|
+
// NuGet lookup not implemented in v1
|
|
1136
|
+
majorsBehind: null,
|
|
1137
|
+
drift: "unknown"
|
|
1138
|
+
}));
|
|
1139
|
+
const frameworks = [];
|
|
1140
|
+
for (const ref of data.packageReferences) {
|
|
1141
|
+
if (ref.name in KNOWN_DOTNET_FRAMEWORKS) {
|
|
1142
|
+
frameworks.push({
|
|
1143
|
+
name: KNOWN_DOTNET_FRAMEWORKS[ref.name],
|
|
1144
|
+
currentVersion: ref.version,
|
|
1145
|
+
latestVersion: null,
|
|
1146
|
+
majorsBehind: null
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
|
|
1151
|
+
return {
|
|
1152
|
+
type: "dotnet",
|
|
1153
|
+
path: path3.relative(rootDir, path3.dirname(csprojPath)) || ".",
|
|
1154
|
+
name: data.projectName,
|
|
1155
|
+
targetFramework,
|
|
1156
|
+
runtime: primaryTfm,
|
|
1157
|
+
runtimeLatest: `net${LATEST_DOTNET_MAJOR}.0`,
|
|
1158
|
+
runtimeMajorsBehind,
|
|
1159
|
+
frameworks,
|
|
1160
|
+
dependencies,
|
|
1161
|
+
dependencyAgeBuckets: buckets
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/utils/semaphore.ts
|
|
1166
|
+
var Semaphore = class {
|
|
1167
|
+
available;
|
|
1168
|
+
queue = [];
|
|
1169
|
+
constructor(max) {
|
|
1170
|
+
this.available = max;
|
|
1171
|
+
}
|
|
1172
|
+
async run(fn) {
|
|
1173
|
+
await this.acquire();
|
|
1174
|
+
try {
|
|
1175
|
+
return await fn();
|
|
1176
|
+
} finally {
|
|
1177
|
+
this.release();
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
acquire() {
|
|
1181
|
+
if (this.available > 0) {
|
|
1182
|
+
this.available--;
|
|
1183
|
+
return Promise.resolve();
|
|
1184
|
+
}
|
|
1185
|
+
return new Promise((resolve4) => this.queue.push(resolve4));
|
|
1186
|
+
}
|
|
1187
|
+
release() {
|
|
1188
|
+
const next = this.queue.shift();
|
|
1189
|
+
if (next) next();
|
|
1190
|
+
else this.available++;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
// src/config.ts
|
|
1195
|
+
import * as path4 from "path";
|
|
1196
|
+
import * as fs2 from "fs/promises";
|
|
1197
|
+
var CONFIG_FILES = [
|
|
1198
|
+
"vibgrate.config.ts",
|
|
1199
|
+
"vibgrate.config.js",
|
|
1200
|
+
"vibgrate.config.json"
|
|
1201
|
+
];
|
|
1202
|
+
var DEFAULT_CONFIG = {
|
|
1203
|
+
exclude: [],
|
|
1204
|
+
thresholds: {
|
|
1205
|
+
failOnError: {
|
|
1206
|
+
eolDays: 180,
|
|
1207
|
+
frameworkMajorLag: 3,
|
|
1208
|
+
dependencyTwoPlusPercent: 50
|
|
1209
|
+
},
|
|
1210
|
+
warn: {
|
|
1211
|
+
frameworkMajorLag: 2,
|
|
1212
|
+
dependencyTwoPlusPercent: 30
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
async function loadConfig(rootDir) {
|
|
1217
|
+
for (const file of CONFIG_FILES) {
|
|
1218
|
+
const configPath = path4.join(rootDir, file);
|
|
1219
|
+
if (await pathExists(configPath)) {
|
|
1220
|
+
if (file.endsWith(".json")) {
|
|
1221
|
+
const txt = await readTextFile(configPath);
|
|
1222
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(txt) };
|
|
1223
|
+
}
|
|
1224
|
+
try {
|
|
1225
|
+
const mod = await import(configPath);
|
|
1226
|
+
return { ...DEFAULT_CONFIG, ...mod.default ?? mod };
|
|
1227
|
+
} catch {
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return DEFAULT_CONFIG;
|
|
1232
|
+
}
|
|
1233
|
+
async function writeDefaultConfig(rootDir) {
|
|
1234
|
+
const configPath = path4.join(rootDir, "vibgrate.config.ts");
|
|
1235
|
+
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
1236
|
+
|
|
1237
|
+
const config: VibgrateConfig = {
|
|
1238
|
+
// exclude: ['legacy/**'],
|
|
1239
|
+
thresholds: {
|
|
1240
|
+
failOnError: {
|
|
1241
|
+
eolDays: 180,
|
|
1242
|
+
frameworkMajorLag: 3,
|
|
1243
|
+
dependencyTwoPlusPercent: 50,
|
|
1244
|
+
},
|
|
1245
|
+
warn: {
|
|
1246
|
+
frameworkMajorLag: 2,
|
|
1247
|
+
dependencyTwoPlusPercent: 30,
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
export default config;
|
|
1253
|
+
`;
|
|
1254
|
+
await fs2.writeFile(configPath, content, "utf8");
|
|
1255
|
+
return configPath;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/utils/vcs.ts
|
|
1259
|
+
import * as path5 from "path";
|
|
1260
|
+
import * as fs3 from "fs/promises";
|
|
1261
|
+
async function detectVcs(rootDir) {
|
|
1262
|
+
try {
|
|
1263
|
+
return await detectGit(rootDir);
|
|
1264
|
+
} catch {
|
|
1265
|
+
return { type: "unknown" };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function detectGit(rootDir) {
|
|
1269
|
+
const gitDir = await findGitDir(rootDir);
|
|
1270
|
+
if (!gitDir) {
|
|
1271
|
+
return { type: "unknown" };
|
|
1272
|
+
}
|
|
1273
|
+
const headPath = path5.join(gitDir, "HEAD");
|
|
1274
|
+
let headContent;
|
|
1275
|
+
try {
|
|
1276
|
+
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
1277
|
+
} catch {
|
|
1278
|
+
return { type: "unknown" };
|
|
1279
|
+
}
|
|
1280
|
+
let sha;
|
|
1281
|
+
let branch;
|
|
1282
|
+
if (headContent.startsWith("ref: ")) {
|
|
1283
|
+
const refPath = headContent.slice(5);
|
|
1284
|
+
branch = refPath.startsWith("refs/heads/") ? refPath.slice(11) : refPath;
|
|
1285
|
+
sha = await resolveRef(gitDir, refPath);
|
|
1286
|
+
} else if (/^[0-9a-f]{40}$/i.test(headContent)) {
|
|
1287
|
+
sha = headContent;
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
type: "git",
|
|
1291
|
+
sha: sha ?? void 0,
|
|
1292
|
+
shortSha: sha ? sha.slice(0, 7) : void 0,
|
|
1293
|
+
branch: branch ?? void 0
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
async function findGitDir(startDir) {
|
|
1297
|
+
let dir = path5.resolve(startDir);
|
|
1298
|
+
const root = path5.parse(dir).root;
|
|
1299
|
+
while (dir !== root) {
|
|
1300
|
+
const gitPath = path5.join(dir, ".git");
|
|
1301
|
+
try {
|
|
1302
|
+
const stat3 = await fs3.stat(gitPath);
|
|
1303
|
+
if (stat3.isDirectory()) {
|
|
1304
|
+
return gitPath;
|
|
1305
|
+
}
|
|
1306
|
+
if (stat3.isFile()) {
|
|
1307
|
+
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
1308
|
+
if (content.startsWith("gitdir: ")) {
|
|
1309
|
+
const resolved = path5.resolve(dir, content.slice(8));
|
|
1310
|
+
return resolved;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
} catch {
|
|
1314
|
+
}
|
|
1315
|
+
dir = path5.dirname(dir);
|
|
1316
|
+
}
|
|
1317
|
+
return null;
|
|
1318
|
+
}
|
|
1319
|
+
async function resolveRef(gitDir, refPath) {
|
|
1320
|
+
const loosePath = path5.join(gitDir, refPath);
|
|
1321
|
+
try {
|
|
1322
|
+
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
1323
|
+
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
1324
|
+
return sha;
|
|
1325
|
+
}
|
|
1326
|
+
} catch {
|
|
1327
|
+
}
|
|
1328
|
+
const packedPath = path5.join(gitDir, "packed-refs");
|
|
1329
|
+
try {
|
|
1330
|
+
const packed = await fs3.readFile(packedPath, "utf8");
|
|
1331
|
+
for (const line of packed.split("\n")) {
|
|
1332
|
+
if (line.startsWith("#") || line.startsWith("^")) continue;
|
|
1333
|
+
const parts = line.trim().split(" ");
|
|
1334
|
+
if (parts.length >= 2 && parts[1] === refPath) {
|
|
1335
|
+
return parts[0];
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
return void 0;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// src/ui/progress.ts
|
|
1344
|
+
import chalk2 from "chalk";
|
|
1345
|
+
var ROBOT = [
|
|
1346
|
+
chalk2.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk2.greenBright("\u279C"),
|
|
1347
|
+
chalk2.cyan(" \u256D\u2524") + chalk2.greenBright("\u25C9 \u25C9") + chalk2.cyan("\u251C\u256E"),
|
|
1348
|
+
chalk2.cyan(" \u2570\u2524") + chalk2.dim("\u2500\u2500\u2500") + chalk2.cyan("\u251C\u256F"),
|
|
1349
|
+
chalk2.cyan(" \u2570\u2500\u2500\u2500\u256F")
|
|
1350
|
+
];
|
|
1351
|
+
var BRAND = [
|
|
1352
|
+
chalk2.bold.white(" V I B G R A T E"),
|
|
1353
|
+
chalk2.dim(" Drift Intelligence Engine")
|
|
1354
|
+
];
|
|
1355
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1356
|
+
var ScanProgress = class {
|
|
1357
|
+
steps = [];
|
|
1358
|
+
stats = {
|
|
1359
|
+
projects: 0,
|
|
1360
|
+
dependencies: 0,
|
|
1361
|
+
frameworks: 0,
|
|
1362
|
+
findings: { warnings: 0, errors: 0, notes: 0 }
|
|
1363
|
+
};
|
|
1364
|
+
spinnerFrame = 0;
|
|
1365
|
+
timer = null;
|
|
1366
|
+
lastLineCount = 0;
|
|
1367
|
+
startTime = Date.now();
|
|
1368
|
+
isTTY;
|
|
1369
|
+
rootDir = "";
|
|
1370
|
+
constructor(rootDir) {
|
|
1371
|
+
this.isTTY = process.stderr.isTTY ?? false;
|
|
1372
|
+
this.rootDir = rootDir;
|
|
1373
|
+
}
|
|
1374
|
+
/** Register all steps up front */
|
|
1375
|
+
setSteps(steps) {
|
|
1376
|
+
this.steps = steps.map((s) => ({ ...s, status: "pending" }));
|
|
1377
|
+
if (this.isTTY) {
|
|
1378
|
+
this.startSpinner();
|
|
1379
|
+
}
|
|
1380
|
+
this.render();
|
|
1381
|
+
}
|
|
1382
|
+
/** Mark a step as active (currently running) */
|
|
1383
|
+
startStep(id) {
|
|
1384
|
+
const step = this.steps.find((s) => s.id === id);
|
|
1385
|
+
if (step) {
|
|
1386
|
+
step.status = "active";
|
|
1387
|
+
step.detail = void 0;
|
|
1388
|
+
step.count = void 0;
|
|
1389
|
+
}
|
|
1390
|
+
this.render();
|
|
1391
|
+
}
|
|
1392
|
+
/** Mark a step as completed */
|
|
1393
|
+
completeStep(id, detail, count) {
|
|
1394
|
+
const step = this.steps.find((s) => s.id === id);
|
|
1395
|
+
if (step) {
|
|
1396
|
+
step.status = "done";
|
|
1397
|
+
step.detail = detail;
|
|
1398
|
+
step.count = count;
|
|
1399
|
+
}
|
|
1400
|
+
this.render();
|
|
1401
|
+
}
|
|
1402
|
+
/** Mark a step as skipped */
|
|
1403
|
+
skipStep(id) {
|
|
1404
|
+
const step = this.steps.find((s) => s.id === id);
|
|
1405
|
+
if (step) {
|
|
1406
|
+
step.status = "skipped";
|
|
1407
|
+
step.detail = "disabled";
|
|
1408
|
+
}
|
|
1409
|
+
this.render();
|
|
1410
|
+
}
|
|
1411
|
+
/** Update live stats */
|
|
1412
|
+
updateStats(partial) {
|
|
1413
|
+
Object.assign(this.stats, partial);
|
|
1414
|
+
this.render();
|
|
1415
|
+
}
|
|
1416
|
+
/** Increment stats */
|
|
1417
|
+
addProjects(n) {
|
|
1418
|
+
this.stats.projects += n;
|
|
1419
|
+
this.render();
|
|
1420
|
+
}
|
|
1421
|
+
addDependencies(n) {
|
|
1422
|
+
this.stats.dependencies += n;
|
|
1423
|
+
this.render();
|
|
1424
|
+
}
|
|
1425
|
+
addFrameworks(n) {
|
|
1426
|
+
this.stats.frameworks += n;
|
|
1427
|
+
this.render();
|
|
1428
|
+
}
|
|
1429
|
+
addFindings(warnings, errors, notes) {
|
|
1430
|
+
this.stats.findings.warnings += warnings;
|
|
1431
|
+
this.stats.findings.errors += errors;
|
|
1432
|
+
this.stats.findings.notes += notes;
|
|
1433
|
+
this.render();
|
|
1434
|
+
}
|
|
1435
|
+
/** Stop the progress display and clear it */
|
|
1436
|
+
finish() {
|
|
1437
|
+
if (this.timer) {
|
|
1438
|
+
clearInterval(this.timer);
|
|
1439
|
+
this.timer = null;
|
|
1440
|
+
}
|
|
1441
|
+
if (this.isTTY) {
|
|
1442
|
+
this.clearLines();
|
|
1443
|
+
}
|
|
1444
|
+
const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
1445
|
+
const doneCount = this.steps.filter((s) => s.status === "done").length;
|
|
1446
|
+
process.stderr.write(
|
|
1447
|
+
chalk2.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
|
|
1448
|
+
|
|
1449
|
+
`)
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
// ── Internal rendering ──
|
|
1453
|
+
startSpinner() {
|
|
1454
|
+
this.timer = setInterval(() => {
|
|
1455
|
+
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
1456
|
+
this.render();
|
|
1457
|
+
}, 80);
|
|
1458
|
+
}
|
|
1459
|
+
clearLines() {
|
|
1460
|
+
if (this.lastLineCount > 0) {
|
|
1461
|
+
process.stderr.write(`\x1B[${this.lastLineCount}A`);
|
|
1462
|
+
for (let i = 0; i < this.lastLineCount; i++) {
|
|
1463
|
+
process.stderr.write("\x1B[2K\n");
|
|
1464
|
+
}
|
|
1465
|
+
process.stderr.write(`\x1B[${this.lastLineCount}A`);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
render() {
|
|
1469
|
+
if (!this.isTTY) {
|
|
1470
|
+
this.renderCI();
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
this.clearLines();
|
|
1474
|
+
const lines = [];
|
|
1475
|
+
lines.push("");
|
|
1476
|
+
lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
|
|
1477
|
+
lines.push(` ${ROBOT[1]} ${BRAND[1]}`);
|
|
1478
|
+
lines.push(` ${ROBOT[2]}`);
|
|
1479
|
+
lines.push(` ${ROBOT[3]} ${chalk2.dim(this.rootDir)}`);
|
|
1480
|
+
lines.push("");
|
|
1481
|
+
const totalSteps = this.steps.length;
|
|
1482
|
+
const doneSteps = this.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
|
|
1483
|
+
const pct = totalSteps > 0 ? Math.round(doneSteps / totalSteps * 100) : 0;
|
|
1484
|
+
const barWidth = 30;
|
|
1485
|
+
const filled = Math.round(doneSteps / Math.max(totalSteps, 1) * barWidth);
|
|
1486
|
+
const bar = chalk2.greenBright("\u2501".repeat(filled)) + chalk2.dim("\u254C".repeat(barWidth - filled));
|
|
1487
|
+
const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
1488
|
+
lines.push(` ${bar} ${chalk2.bold.white(`${pct}%`)} ${chalk2.dim(`${elapsed}s`)}`);
|
|
1489
|
+
lines.push("");
|
|
1490
|
+
for (const step of this.steps) {
|
|
1491
|
+
lines.push(this.renderStep(step));
|
|
1492
|
+
}
|
|
1493
|
+
lines.push("");
|
|
1494
|
+
lines.push(this.renderStats());
|
|
1495
|
+
lines.push("");
|
|
1496
|
+
const output = lines.join("\n") + "\n";
|
|
1497
|
+
process.stderr.write(output);
|
|
1498
|
+
this.lastLineCount = lines.length;
|
|
1499
|
+
}
|
|
1500
|
+
renderStep(step) {
|
|
1501
|
+
const spinner = SPINNER_FRAMES[this.spinnerFrame];
|
|
1502
|
+
let icon;
|
|
1503
|
+
let label;
|
|
1504
|
+
let detail = "";
|
|
1505
|
+
switch (step.status) {
|
|
1506
|
+
case "done":
|
|
1507
|
+
icon = chalk2.green("\u2714");
|
|
1508
|
+
label = chalk2.white(step.label);
|
|
1509
|
+
break;
|
|
1510
|
+
case "active":
|
|
1511
|
+
icon = chalk2.cyan(spinner);
|
|
1512
|
+
label = chalk2.bold.white(step.label);
|
|
1513
|
+
break;
|
|
1514
|
+
case "skipped":
|
|
1515
|
+
icon = chalk2.dim("\u25CC");
|
|
1516
|
+
label = chalk2.dim.strikethrough(step.label);
|
|
1517
|
+
break;
|
|
1518
|
+
default:
|
|
1519
|
+
icon = chalk2.dim("\u25CB");
|
|
1520
|
+
label = chalk2.dim(step.label);
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
if (step.detail) {
|
|
1524
|
+
detail = chalk2.dim(` \xB7 ${step.detail}`);
|
|
1525
|
+
}
|
|
1526
|
+
if (step.count !== void 0 && step.count > 0) {
|
|
1527
|
+
detail += chalk2.cyan(` (${step.count})`);
|
|
1528
|
+
}
|
|
1529
|
+
return ` ${icon} ${label}${detail}`;
|
|
1530
|
+
}
|
|
1531
|
+
renderStats() {
|
|
1532
|
+
const p = this.stats.projects;
|
|
1533
|
+
const d = this.stats.dependencies;
|
|
1534
|
+
const f = this.stats.frameworks;
|
|
1535
|
+
const w = this.stats.findings.warnings;
|
|
1536
|
+
const e = this.stats.findings.errors;
|
|
1537
|
+
const n = this.stats.findings.notes;
|
|
1538
|
+
const parts = [
|
|
1539
|
+
chalk2.bold.white(` ${p}`) + chalk2.dim(` project${p !== 1 ? "s" : ""}`),
|
|
1540
|
+
chalk2.white(`${d}`) + chalk2.dim(` dep${d !== 1 ? "s" : ""}`),
|
|
1541
|
+
chalk2.white(`${f}`) + chalk2.dim(` framework${f !== 1 ? "s" : ""}`)
|
|
1542
|
+
];
|
|
1543
|
+
const findingParts = [];
|
|
1544
|
+
if (e > 0) findingParts.push(chalk2.red(`${e} \u2716`));
|
|
1545
|
+
if (w > 0) findingParts.push(chalk2.yellow(`${w} \u26A0`));
|
|
1546
|
+
if (n > 0) findingParts.push(chalk2.blue(`${n} \u2139`));
|
|
1547
|
+
if (findingParts.length > 0) {
|
|
1548
|
+
parts.push(findingParts.join(chalk2.dim(" \xB7 ")));
|
|
1549
|
+
}
|
|
1550
|
+
return ` ${chalk2.dim("\u2503")} ${parts.join(chalk2.dim(" \u2502 "))}`;
|
|
1551
|
+
}
|
|
1552
|
+
/** Simple CI-friendly output (no ANSI rewriting) */
|
|
1553
|
+
lastCIStep = null;
|
|
1554
|
+
renderCI() {
|
|
1555
|
+
const active = this.steps.find((s) => s.status === "active");
|
|
1556
|
+
if (active && active.id !== this.lastCIStep) {
|
|
1557
|
+
this.lastCIStep = active.id;
|
|
1558
|
+
process.stderr.write(` \u25C9 ${active.label}...
|
|
1559
|
+
`);
|
|
1560
|
+
}
|
|
1561
|
+
for (const step of this.steps) {
|
|
1562
|
+
if (step.status === "done" && step.id !== this.lastCIStep) {
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// src/scanners/platform-matrix.ts
|
|
1569
|
+
import * as path6 from "path";
|
|
1570
|
+
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
1571
|
+
// Image / media processing
|
|
1572
|
+
"sharp",
|
|
1573
|
+
// libvips native bindings
|
|
1574
|
+
"canvas",
|
|
1575
|
+
// node-canvas (Cairo native)
|
|
1576
|
+
"jimp",
|
|
1577
|
+
// pure JS but optionally uses native
|
|
1578
|
+
"@napi-rs/image",
|
|
1579
|
+
// NAPI-RS image processing
|
|
1580
|
+
"imagemagick",
|
|
1581
|
+
// ImageMagick bindings
|
|
1582
|
+
"gm",
|
|
1583
|
+
// GraphicsMagick
|
|
1584
|
+
"fluent-ffmpeg",
|
|
1585
|
+
// FFmpeg bindings
|
|
1586
|
+
"ffmpeg-static",
|
|
1587
|
+
// bundled FFmpeg binary
|
|
1588
|
+
"puppeteer",
|
|
1589
|
+
// ships Chromium binary
|
|
1590
|
+
"playwright",
|
|
1591
|
+
// ships browser binaries
|
|
1592
|
+
"playwright-core",
|
|
1593
|
+
// browser binaries
|
|
1594
|
+
// Cryptography / security
|
|
1595
|
+
"bcrypt",
|
|
1596
|
+
// native bcrypt
|
|
1597
|
+
"argon2",
|
|
1598
|
+
// native argon2 hashing
|
|
1599
|
+
"sodium-native",
|
|
1600
|
+
// libsodium bindings
|
|
1601
|
+
"libsodium-wrappers",
|
|
1602
|
+
// libsodium WASM/native
|
|
1603
|
+
"node-forge",
|
|
1604
|
+
// mostly JS but linked to native in some envs
|
|
1605
|
+
"ssh2",
|
|
1606
|
+
// native SSH bindings (optional)
|
|
1607
|
+
"keytar",
|
|
1608
|
+
// OS keychain native bindings
|
|
1609
|
+
// Database drivers
|
|
1610
|
+
"better-sqlite3",
|
|
1611
|
+
// native SQLite
|
|
1612
|
+
"sqlite3",
|
|
1613
|
+
// native SQLite
|
|
1614
|
+
"pg-native",
|
|
1615
|
+
// PostgreSQL native bindings
|
|
1616
|
+
"oracledb",
|
|
1617
|
+
// Oracle native client
|
|
1618
|
+
"odbc",
|
|
1619
|
+
// native ODBC
|
|
1620
|
+
"ibm_db",
|
|
1621
|
+
// IBM DB2 native
|
|
1622
|
+
"couchbase",
|
|
1623
|
+
// native Couchbase SDK
|
|
1624
|
+
"rocksdb",
|
|
1625
|
+
// native RocksDB store
|
|
1626
|
+
"leveldown",
|
|
1627
|
+
// native LevelDB
|
|
1628
|
+
"lmdb",
|
|
1629
|
+
// native LMDB bindings
|
|
1630
|
+
// Compilation & build tools
|
|
1631
|
+
"node-gyp",
|
|
1632
|
+
// native build tool itself
|
|
1633
|
+
"node-pre-gyp",
|
|
1634
|
+
// native binary distribution
|
|
1635
|
+
"@mapbox/node-pre-gyp",
|
|
1636
|
+
// native binary distribution
|
|
1637
|
+
"prebuild",
|
|
1638
|
+
// prebuilt native bindings
|
|
1639
|
+
"prebuild-install",
|
|
1640
|
+
// prebuilt native installer
|
|
1641
|
+
"esbuild",
|
|
1642
|
+
// platform-specific Go binary
|
|
1643
|
+
"@swc/core",
|
|
1644
|
+
// platform-specific Rust binary
|
|
1645
|
+
"@rspack/core",
|
|
1646
|
+
// platform-specific Rust binary
|
|
1647
|
+
"@biomejs/biome",
|
|
1648
|
+
// platform-specific Rust binary
|
|
1649
|
+
"node-sass",
|
|
1650
|
+
// deprecated, native libsass
|
|
1651
|
+
"sass-embedded",
|
|
1652
|
+
// Dart Sass embedded binary
|
|
1653
|
+
"turbo",
|
|
1654
|
+
// Turborepo Go/Rust binary
|
|
1655
|
+
"@vercel/nft",
|
|
1656
|
+
// native file tracing (optional)
|
|
1657
|
+
// System / hardware access
|
|
1658
|
+
"fsevents",
|
|
1659
|
+
// macOS-only file watching
|
|
1660
|
+
"cpu-features",
|
|
1661
|
+
// CPU instruction detection
|
|
1662
|
+
"deasync",
|
|
1663
|
+
// native event loop control
|
|
1664
|
+
"usb",
|
|
1665
|
+
// USB device access
|
|
1666
|
+
"serialport",
|
|
1667
|
+
// serial port access
|
|
1668
|
+
"node-hid",
|
|
1669
|
+
// HID device access
|
|
1670
|
+
"i2c-bus",
|
|
1671
|
+
// I2C bus access
|
|
1672
|
+
"spi-device",
|
|
1673
|
+
// SPI bus access
|
|
1674
|
+
"node-bluetooth",
|
|
1675
|
+
// Bluetooth
|
|
1676
|
+
"mdns",
|
|
1677
|
+
// mDNS/Bonjour
|
|
1678
|
+
// Compression
|
|
1679
|
+
"snappy",
|
|
1680
|
+
// native Snappy compression
|
|
1681
|
+
"zstd-napi",
|
|
1682
|
+
// native Zstandard
|
|
1683
|
+
"lz4",
|
|
1684
|
+
// native LZ4
|
|
1685
|
+
"brotli",
|
|
1686
|
+
// native Brotli (older, node has built-in)
|
|
1687
|
+
// Regex / text
|
|
1688
|
+
"re2",
|
|
1689
|
+
// native RE2 regex engine
|
|
1690
|
+
"oniguruma",
|
|
1691
|
+
// native Oniguruma regex
|
|
1692
|
+
"vscode-oniguruma",
|
|
1693
|
+
// native Oniguruma (VS Code)
|
|
1694
|
+
"tree-sitter",
|
|
1695
|
+
// native parser generator
|
|
1696
|
+
"node-tree-sitter",
|
|
1697
|
+
// native Tree-sitter
|
|
1698
|
+
// XML / HTML
|
|
1699
|
+
"libxmljs",
|
|
1700
|
+
// native libxml2
|
|
1701
|
+
"libxmljs2",
|
|
1702
|
+
// native libxml2
|
|
1703
|
+
"node-expat",
|
|
1704
|
+
// native Expat XML parser
|
|
1705
|
+
"htmlparser2",
|
|
1706
|
+
// mostly JS, optional native
|
|
1707
|
+
// Networking / IPC
|
|
1708
|
+
"@grpc/grpc-js",
|
|
1709
|
+
// gRPC (JS but has native dep for HTTP/2)
|
|
1710
|
+
"grpc",
|
|
1711
|
+
// deprecated native gRPC
|
|
1712
|
+
"zeromq",
|
|
1713
|
+
// native ZeroMQ
|
|
1714
|
+
"nanomsg",
|
|
1715
|
+
// native nanomsg
|
|
1716
|
+
"unix-dgram",
|
|
1717
|
+
// native Unix sockets
|
|
1718
|
+
// Observability
|
|
1719
|
+
"dtrace-provider",
|
|
1720
|
+
// native DTrace
|
|
1721
|
+
"v8-profiler-next",
|
|
1722
|
+
// native V8 profiler
|
|
1723
|
+
"heapdump",
|
|
1724
|
+
// native V8 heap dump
|
|
1725
|
+
// Misc
|
|
1726
|
+
"farmhash",
|
|
1727
|
+
// native FarmHash
|
|
1728
|
+
"xxhash",
|
|
1729
|
+
// native xxHash
|
|
1730
|
+
"xxhash-addon",
|
|
1731
|
+
// native xxHash
|
|
1732
|
+
"iconv",
|
|
1733
|
+
// native iconv
|
|
1734
|
+
"ref-napi",
|
|
1735
|
+
// native FFI
|
|
1736
|
+
"ffi-napi",
|
|
1737
|
+
// native FFI
|
|
1738
|
+
"node-pty",
|
|
1739
|
+
// native pseudo-terminal
|
|
1740
|
+
"robotjs",
|
|
1741
|
+
// native desktop automation
|
|
1742
|
+
"electron",
|
|
1743
|
+
// ships Chromium + Node binary
|
|
1744
|
+
"xdg-open",
|
|
1745
|
+
// OS-specific
|
|
1746
|
+
"windows-process-tree"
|
|
1747
|
+
// Windows-only
|
|
1748
|
+
]);
|
|
1749
|
+
var OS_PATTERNS = [
|
|
1750
|
+
{ pattern: /\bcmd\.exe\b|\.bat\b|\.cmd\b/i, label: "windows-scripts" },
|
|
1751
|
+
{ pattern: /\bpowershell\b|\bpwsh\b/i, label: "powershell" },
|
|
1752
|
+
{ pattern: /\bbash\b|#!\/bin\/bash/i, label: "bash-scripts" },
|
|
1753
|
+
{ pattern: /\\\\/g, label: "backslash-paths" }
|
|
1754
|
+
];
|
|
1755
|
+
async function scanPlatformMatrix(rootDir) {
|
|
1756
|
+
const result = {
|
|
1757
|
+
dotnetTargetFrameworks: [],
|
|
1758
|
+
nativeModules: [],
|
|
1759
|
+
osAssumptions: [],
|
|
1760
|
+
dockerBaseImages: [],
|
|
1761
|
+
nodeVersionFiles: []
|
|
1762
|
+
};
|
|
1763
|
+
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
1764
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
1765
|
+
const osAssumptions = /* @__PURE__ */ new Set();
|
|
1766
|
+
for (const pjPath of pkgFiles) {
|
|
1767
|
+
try {
|
|
1768
|
+
const pj = await readJsonFile(pjPath);
|
|
1769
|
+
if (pj.engines?.node && !result.nodeEngines) result.nodeEngines = pj.engines.node;
|
|
1770
|
+
if (pj.engines?.npm && !result.npmEngines) result.npmEngines = pj.engines.npm;
|
|
1771
|
+
if (pj.engines?.pnpm && !result.pnpmEngines) {
|
|
1772
|
+
result.pnpmEngines = pj.engines.pnpm;
|
|
1773
|
+
}
|
|
1774
|
+
for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
1775
|
+
const deps = pj[section];
|
|
1776
|
+
if (deps) {
|
|
1777
|
+
for (const name of Object.keys(deps)) {
|
|
1778
|
+
allDeps.add(name);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const scripts = pj.scripts;
|
|
1783
|
+
if (scripts && typeof scripts === "object") {
|
|
1784
|
+
for (const val of Object.values(scripts)) {
|
|
1785
|
+
if (typeof val !== "string") continue;
|
|
1786
|
+
const firstToken = val.split(/\s/)[0] ?? "";
|
|
1787
|
+
for (const { pattern, label } of OS_PATTERNS) {
|
|
1788
|
+
if (pattern.test(firstToken)) {
|
|
1789
|
+
osAssumptions.add(label);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
} catch {
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
for (const dep of allDeps) {
|
|
1798
|
+
if (NATIVE_MODULE_PACKAGES.has(dep)) {
|
|
1799
|
+
result.nativeModules.push(dep);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
result.nativeModules.sort();
|
|
1803
|
+
result.osAssumptions = [...osAssumptions].sort();
|
|
1804
|
+
const csprojFiles = await findFiles(rootDir, (name) => name.endsWith(".csproj"));
|
|
1805
|
+
const tfms = /* @__PURE__ */ new Set();
|
|
1806
|
+
for (const csprojPath of csprojFiles) {
|
|
1807
|
+
try {
|
|
1808
|
+
const xml = await readTextFile(csprojPath);
|
|
1809
|
+
const tfMatch = xml.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
|
|
1810
|
+
if (tfMatch?.[1]) tfms.add(tfMatch[1]);
|
|
1811
|
+
const tfsMatch = xml.match(/<TargetFrameworks>(.*?)<\/TargetFrameworks>/);
|
|
1812
|
+
if (tfsMatch?.[1]) {
|
|
1813
|
+
for (const tfm of tfsMatch[1].split(";")) {
|
|
1814
|
+
if (tfm.trim()) tfms.add(tfm.trim());
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
} catch {
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
result.dotnetTargetFrameworks = [...tfms].sort();
|
|
1821
|
+
const dockerfiles = await findFiles(
|
|
1822
|
+
rootDir,
|
|
1823
|
+
(name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
|
|
1824
|
+
);
|
|
1825
|
+
const baseImages = /* @__PURE__ */ new Set();
|
|
1826
|
+
for (const df of dockerfiles) {
|
|
1827
|
+
try {
|
|
1828
|
+
const content = await readTextFile(df);
|
|
1829
|
+
for (const line of content.split("\n")) {
|
|
1830
|
+
const trimmed = line.trim();
|
|
1831
|
+
if (/^FROM\s+/i.test(trimmed)) {
|
|
1832
|
+
const parts = trimmed.split(/\s+/);
|
|
1833
|
+
if (parts[1] && !parts[1].startsWith("--")) {
|
|
1834
|
+
baseImages.add(parts[1]);
|
|
1835
|
+
} else if (parts[1]?.startsWith("--")) {
|
|
1836
|
+
const imageIdx = parts[1].includes("=") ? 2 : 3;
|
|
1837
|
+
if (parts[imageIdx]) baseImages.add(parts[imageIdx]);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
} catch {
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
result.dockerBaseImages = [...baseImages].sort();
|
|
1845
|
+
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
1846
|
+
if (await pathExists(path6.join(rootDir, file))) {
|
|
1847
|
+
result.nodeVersionFiles.push(file);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return result;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/scanners/dependency-risk.ts
|
|
1854
|
+
var DEPRECATED_PACKAGES = /* @__PURE__ */ new Set([
|
|
1855
|
+
"request",
|
|
1856
|
+
"node-sass",
|
|
1857
|
+
"tslint",
|
|
1858
|
+
"istanbul",
|
|
1859
|
+
"popper.js",
|
|
1860
|
+
"Left-pad",
|
|
1861
|
+
"left-pad",
|
|
1862
|
+
"bower",
|
|
1863
|
+
"grunt",
|
|
1864
|
+
"gulp",
|
|
1865
|
+
"coffee-script",
|
|
1866
|
+
"coffeescript",
|
|
1867
|
+
"merge",
|
|
1868
|
+
"nomnom",
|
|
1869
|
+
"optimist",
|
|
1870
|
+
"natives",
|
|
1871
|
+
"querystring",
|
|
1872
|
+
"domain-browser",
|
|
1873
|
+
"sys",
|
|
1874
|
+
"punycode"
|
|
1875
|
+
]);
|
|
1876
|
+
var NATIVE_MODULE_PACKAGES2 = /* @__PURE__ */ new Set([
|
|
1877
|
+
"sharp",
|
|
1878
|
+
"canvas",
|
|
1879
|
+
"bcrypt",
|
|
1880
|
+
"node-gyp",
|
|
1881
|
+
"fsevents",
|
|
1882
|
+
"better-sqlite3",
|
|
1883
|
+
"sqlite3",
|
|
1884
|
+
"leveldown",
|
|
1885
|
+
"sodium-native",
|
|
1886
|
+
"node-sass",
|
|
1887
|
+
"argon2",
|
|
1888
|
+
"usb",
|
|
1889
|
+
"serialport",
|
|
1890
|
+
"re2",
|
|
1891
|
+
"libxmljs",
|
|
1892
|
+
"libxmljs2",
|
|
1893
|
+
"cpu-features",
|
|
1894
|
+
"deasync",
|
|
1895
|
+
"farmhash",
|
|
1896
|
+
"grpc",
|
|
1897
|
+
"@grpc/grpc-js"
|
|
1898
|
+
]);
|
|
1899
|
+
function scanDependencyRisk(projects) {
|
|
1900
|
+
const deprecated = /* @__PURE__ */ new Set();
|
|
1901
|
+
const nativeModules = /* @__PURE__ */ new Set();
|
|
1902
|
+
let totalDeps = 0;
|
|
1903
|
+
for (const project of projects) {
|
|
1904
|
+
for (const dep of project.dependencies) {
|
|
1905
|
+
totalDeps++;
|
|
1906
|
+
if (DEPRECATED_PACKAGES.has(dep.package)) {
|
|
1907
|
+
deprecated.add(dep.package);
|
|
1908
|
+
}
|
|
1909
|
+
if (NATIVE_MODULE_PACKAGES2.has(dep.package)) {
|
|
1910
|
+
nativeModules.add(dep.package);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
deprecatedPackages: [...deprecated].sort(),
|
|
1916
|
+
nativeModulePackages: [...nativeModules].sort(),
|
|
1917
|
+
totalDependencies: totalDeps
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// src/scanners/dependency-graph.ts
|
|
1922
|
+
import * as path7 from "path";
|
|
1923
|
+
function parsePnpmLock(content) {
|
|
1924
|
+
const entries = [];
|
|
1925
|
+
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
1926
|
+
let match;
|
|
1927
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1928
|
+
if (match[1] && match[2]) {
|
|
1929
|
+
entries.push({ name: match[1], version: match[2] });
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return entries;
|
|
1933
|
+
}
|
|
1934
|
+
function parseNpmLock(content) {
|
|
1935
|
+
const entries = [];
|
|
1936
|
+
try {
|
|
1937
|
+
const lock = JSON.parse(content);
|
|
1938
|
+
if (lock.packages && typeof lock.packages === "object") {
|
|
1939
|
+
for (const [key, value] of Object.entries(lock.packages)) {
|
|
1940
|
+
if (key === "") continue;
|
|
1941
|
+
const v = value;
|
|
1942
|
+
if (v.version) {
|
|
1943
|
+
const name = key.replace(/^node_modules\//, "").replace(/.*node_modules\//, "");
|
|
1944
|
+
entries.push({ name, version: v.version });
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
} else if (lock.dependencies && typeof lock.dependencies === "object") {
|
|
1948
|
+
let walkDeps2 = function(deps) {
|
|
1949
|
+
for (const [name, data] of Object.entries(deps)) {
|
|
1950
|
+
if (data.version) entries.push({ name, version: data.version });
|
|
1951
|
+
if (data.dependencies) walkDeps2(data.dependencies);
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
var walkDeps = walkDeps2;
|
|
1955
|
+
walkDeps2(lock.dependencies);
|
|
1956
|
+
}
|
|
1957
|
+
} catch {
|
|
1958
|
+
}
|
|
1959
|
+
return entries;
|
|
1960
|
+
}
|
|
1961
|
+
function parseYarnLock(content) {
|
|
1962
|
+
const entries = [];
|
|
1963
|
+
const regex = /^"?(@?[^\s"@]+)@[^:]+:\s*\n\s+version\s+"([^"]+)"/gm;
|
|
1964
|
+
let match;
|
|
1965
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1966
|
+
if (match[1] && match[2]) {
|
|
1967
|
+
entries.push({ name: match[1], version: match[2] });
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return entries;
|
|
1971
|
+
}
|
|
1972
|
+
async function scanDependencyGraph(rootDir) {
|
|
1973
|
+
const result = {
|
|
1974
|
+
lockfileType: null,
|
|
1975
|
+
totalUnique: 0,
|
|
1976
|
+
totalInstalled: 0,
|
|
1977
|
+
duplicatedPackages: [],
|
|
1978
|
+
phantomDependencies: []
|
|
1979
|
+
};
|
|
1980
|
+
let entries = [];
|
|
1981
|
+
const pnpmLock = path7.join(rootDir, "pnpm-lock.yaml");
|
|
1982
|
+
const npmLock = path7.join(rootDir, "package-lock.json");
|
|
1983
|
+
const yarnLock = path7.join(rootDir, "yarn.lock");
|
|
1984
|
+
if (await pathExists(pnpmLock)) {
|
|
1985
|
+
result.lockfileType = "pnpm";
|
|
1986
|
+
const content = await readTextFile(pnpmLock);
|
|
1987
|
+
entries = parsePnpmLock(content);
|
|
1988
|
+
} else if (await pathExists(npmLock)) {
|
|
1989
|
+
result.lockfileType = "npm";
|
|
1990
|
+
const content = await readTextFile(npmLock);
|
|
1991
|
+
entries = parseNpmLock(content);
|
|
1992
|
+
} else if (await pathExists(yarnLock)) {
|
|
1993
|
+
result.lockfileType = "yarn";
|
|
1994
|
+
const content = await readTextFile(yarnLock);
|
|
1995
|
+
entries = parseYarnLock(content);
|
|
1996
|
+
}
|
|
1997
|
+
if (entries.length === 0) return result;
|
|
1998
|
+
const versionMap = /* @__PURE__ */ new Map();
|
|
1999
|
+
for (const entry of entries) {
|
|
2000
|
+
const existing = versionMap.get(entry.name);
|
|
2001
|
+
if (existing) {
|
|
2002
|
+
existing.add(entry.version);
|
|
2003
|
+
} else {
|
|
2004
|
+
versionMap.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
result.totalInstalled = entries.length;
|
|
2008
|
+
result.totalUnique = versionMap.size;
|
|
2009
|
+
const duplicated = [];
|
|
2010
|
+
for (const [name, versions] of versionMap) {
|
|
2011
|
+
if (versions.size > 1) {
|
|
2012
|
+
duplicated.push({
|
|
2013
|
+
name,
|
|
2014
|
+
versions: [...versions].sort(),
|
|
2015
|
+
consumers: versions.size
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
duplicated.sort((a, b) => b.versions.length - a.versions.length || a.name.localeCompare(b.name));
|
|
2020
|
+
result.duplicatedPackages = duplicated;
|
|
2021
|
+
const lockedNames = new Set(versionMap.keys());
|
|
2022
|
+
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
2023
|
+
const phantoms = /* @__PURE__ */ new Set();
|
|
2024
|
+
for (const pjPath of pkgFiles) {
|
|
2025
|
+
try {
|
|
2026
|
+
const pj = await readJsonFile(pjPath);
|
|
2027
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
2028
|
+
const deps = pj[section];
|
|
2029
|
+
if (!deps) continue;
|
|
2030
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
2031
|
+
const ver = typeof version === "string" ? version : "";
|
|
2032
|
+
if (!lockedNames.has(name) && !ver.startsWith("workspace:")) {
|
|
2033
|
+
phantoms.add(name);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
} catch {
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
result.phantomDependencies = [...phantoms].sort();
|
|
2041
|
+
return result;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// src/scanners/tooling-inventory.ts
|
|
2045
|
+
var CATEGORIES = {
|
|
2046
|
+
frontend: {
|
|
2047
|
+
"react": "React",
|
|
2048
|
+
"react-dom": "React DOM",
|
|
2049
|
+
"vue": "Vue",
|
|
2050
|
+
"@angular/core": "Angular",
|
|
2051
|
+
"svelte": "Svelte",
|
|
2052
|
+
"solid-js": "Solid",
|
|
2053
|
+
"qwik": "Qwik",
|
|
2054
|
+
"htmx.org": "htmx",
|
|
2055
|
+
"preact": "Preact",
|
|
2056
|
+
"lit": "Lit",
|
|
2057
|
+
"alpinejs": "Alpine.js",
|
|
2058
|
+
"stimulus": "Stimulus",
|
|
2059
|
+
"petite-vue": "petite-vue",
|
|
2060
|
+
"mithril": "Mithril",
|
|
2061
|
+
"inferno": "Inferno",
|
|
2062
|
+
"hyperapp": "Hyperapp",
|
|
2063
|
+
"marko": "Marko",
|
|
2064
|
+
"@builder.io/qwik": "Qwik (Builder)",
|
|
2065
|
+
"million": "Million",
|
|
2066
|
+
"ember-source": "Ember"
|
|
2067
|
+
},
|
|
2068
|
+
metaFrameworks: {
|
|
2069
|
+
"next": "Next.js",
|
|
2070
|
+
"nuxt": "Nuxt",
|
|
2071
|
+
"@analogjs/platform": "Analog",
|
|
2072
|
+
"astro": "Astro",
|
|
2073
|
+
"@remix-run/react": "Remix",
|
|
2074
|
+
"gatsby": "Gatsby",
|
|
2075
|
+
"@sveltejs/kit": "SvelteKit",
|
|
2076
|
+
"@tanstack/start": "TanStack Start",
|
|
2077
|
+
"@adonisjs/core": "AdonisJS",
|
|
2078
|
+
"@redwoodjs/core": "RedwoodJS",
|
|
2079
|
+
"blitz": "Blitz.js",
|
|
2080
|
+
"@solidjs/start": "SolidStart",
|
|
2081
|
+
"fresh": "Fresh (Deno)"
|
|
2082
|
+
},
|
|
2083
|
+
bundlers: {
|
|
2084
|
+
"vite": "Vite",
|
|
2085
|
+
"webpack": "webpack",
|
|
2086
|
+
"rollup": "Rollup",
|
|
2087
|
+
"esbuild": "esbuild",
|
|
2088
|
+
"parcel": "Parcel",
|
|
2089
|
+
"turbo": "Turbo",
|
|
2090
|
+
"tsup": "tsup",
|
|
2091
|
+
"unbuild": "unbuild",
|
|
2092
|
+
"@swc/core": "SWC",
|
|
2093
|
+
"bun": "Bun",
|
|
2094
|
+
"pkg": "pkg",
|
|
2095
|
+
"ncc": "ncc",
|
|
2096
|
+
"@vercel/ncc": "Vercel ncc",
|
|
2097
|
+
"microbundle": "microbundle",
|
|
2098
|
+
"tsc-watch": "tsc-watch",
|
|
2099
|
+
"ts-node": "ts-node",
|
|
2100
|
+
"tsx": "tsx",
|
|
2101
|
+
"jiti": "jiti",
|
|
2102
|
+
"@rspack/core": "Rspack",
|
|
2103
|
+
"farm": "Farm"
|
|
2104
|
+
},
|
|
2105
|
+
css: {
|
|
2106
|
+
"tailwindcss": "Tailwind CSS",
|
|
2107
|
+
"@mui/material": "MUI",
|
|
2108
|
+
"vuetify": "Vuetify",
|
|
2109
|
+
"bootstrap": "Bootstrap",
|
|
2110
|
+
"styled-components": "styled-components",
|
|
2111
|
+
"@emotion/react": "Emotion",
|
|
2112
|
+
"@chakra-ui/react": "Chakra UI",
|
|
2113
|
+
"sass": "Sass",
|
|
2114
|
+
"@mantine/core": "Mantine",
|
|
2115
|
+
"antd": "Ant Design",
|
|
2116
|
+
"@radix-ui/react-slot": "Radix UI",
|
|
2117
|
+
"@headlessui/react": "Headless UI",
|
|
2118
|
+
"daisyui": "DaisyUI",
|
|
2119
|
+
"@shadcn/ui": "shadcn/ui",
|
|
2120
|
+
"primereact": "PrimeReact",
|
|
2121
|
+
"primevue": "PrimeVue",
|
|
2122
|
+
"@nextui-org/react": "NextUI",
|
|
2123
|
+
"@ariakit/react": "Ariakit",
|
|
2124
|
+
"windstitch": "Windstitch",
|
|
2125
|
+
"vanilla-extract": "vanilla-extract",
|
|
2126
|
+
"@vanilla-extract/css": "vanilla-extract",
|
|
2127
|
+
"linaria": "Linaria",
|
|
2128
|
+
"stylex": "StyleX",
|
|
2129
|
+
"@stylexjs/stylex": "StyleX",
|
|
2130
|
+
"unocss": "UnoCSS",
|
|
2131
|
+
"postcss": "PostCSS",
|
|
2132
|
+
"autoprefixer": "Autoprefixer",
|
|
2133
|
+
"lightningcss": "Lightning CSS",
|
|
2134
|
+
"less": "Less",
|
|
2135
|
+
"stylus": "Stylus",
|
|
2136
|
+
"open-props": "Open Props",
|
|
2137
|
+
"@ark-ui/react": "Ark UI"
|
|
2138
|
+
},
|
|
2139
|
+
backend: {
|
|
2140
|
+
"express": "Express",
|
|
2141
|
+
"fastify": "Fastify",
|
|
2142
|
+
"@nestjs/core": "NestJS",
|
|
2143
|
+
"hono": "Hono",
|
|
2144
|
+
"koa": "Koa",
|
|
2145
|
+
"@hapi/hapi": "Hapi",
|
|
2146
|
+
"restify": "Restify",
|
|
2147
|
+
"elysia": "Elysia",
|
|
2148
|
+
"@elysiajs/eden": "Elysia Eden",
|
|
2149
|
+
"moleculer": "Moleculer",
|
|
2150
|
+
"@feathersjs/feathers": "Feathers",
|
|
2151
|
+
"sails": "Sails",
|
|
2152
|
+
"micro": "Micro",
|
|
2153
|
+
"polka": "Polka",
|
|
2154
|
+
"h3": "h3",
|
|
2155
|
+
"nitro": "Nitro",
|
|
2156
|
+
"@trpc/server": "tRPC Server",
|
|
2157
|
+
"@trpc/client": "tRPC Client",
|
|
2158
|
+
"middy": "Middy (Lambda)",
|
|
2159
|
+
"serverless": "Serverless Framework",
|
|
2160
|
+
"aws-lambda": "AWS Lambda",
|
|
2161
|
+
"@aws-sdk/client-lambda": "AWS Lambda SDK",
|
|
2162
|
+
"@cloudflare/workers-types": "Cloudflare Workers",
|
|
2163
|
+
"wrangler": "Wrangler"
|
|
2164
|
+
},
|
|
2165
|
+
orm: {
|
|
2166
|
+
"prisma": "Prisma",
|
|
2167
|
+
"@prisma/client": "Prisma Client",
|
|
2168
|
+
"drizzle-orm": "Drizzle",
|
|
2169
|
+
"typeorm": "TypeORM",
|
|
2170
|
+
"sequelize": "Sequelize",
|
|
2171
|
+
"knex": "Knex",
|
|
2172
|
+
"pg": "pg (PostgreSQL)",
|
|
2173
|
+
"mysql2": "mysql2",
|
|
2174
|
+
"mongodb": "MongoDB",
|
|
2175
|
+
"ioredis": "ioredis",
|
|
2176
|
+
"redis": "Redis",
|
|
2177
|
+
"better-sqlite3": "better-sqlite3",
|
|
2178
|
+
"@mikro-orm/core": "MikroORM",
|
|
2179
|
+
"mongoose": "Mongoose",
|
|
2180
|
+
"mssql": "mssql",
|
|
2181
|
+
"kysely": "Kysely",
|
|
2182
|
+
"objection": "Objection.js",
|
|
2183
|
+
"@planetscale/database": "PlanetScale",
|
|
2184
|
+
"@neondatabase/serverless": "Neon",
|
|
2185
|
+
"@libsql/client": "libSQL (Turso)",
|
|
2186
|
+
"@electric-sql/pglite": "PGlite",
|
|
2187
|
+
"sql.js": "sql.js",
|
|
2188
|
+
"oracledb": "Oracle DB",
|
|
2189
|
+
"cassandra-driver": "Cassandra",
|
|
2190
|
+
"neo4j-driver": "Neo4j",
|
|
2191
|
+
"@upstash/redis": "Upstash Redis",
|
|
2192
|
+
"@upstash/kafka": "Upstash Kafka",
|
|
2193
|
+
"dynamoose": "Dynamoose",
|
|
2194
|
+
"fauna": "Fauna",
|
|
2195
|
+
"faunadb": "FaunaDB",
|
|
2196
|
+
"@clickhouse/client": "ClickHouse",
|
|
2197
|
+
"influx": "InfluxDB",
|
|
2198
|
+
"slonik": "Slonik",
|
|
2199
|
+
"massive": "Massive.js"
|
|
2200
|
+
},
|
|
2201
|
+
testing: {
|
|
2202
|
+
"vitest": "Vitest",
|
|
2203
|
+
"jest": "Jest",
|
|
2204
|
+
"mocha": "Mocha",
|
|
2205
|
+
"@playwright/test": "Playwright",
|
|
2206
|
+
"cypress": "Cypress",
|
|
2207
|
+
"@testing-library/react": "Testing Library (React)",
|
|
2208
|
+
"@testing-library/vue": "Testing Library (Vue)",
|
|
2209
|
+
"@testing-library/svelte": "Testing Library (Svelte)",
|
|
2210
|
+
"@testing-library/jest-dom": "Testing Library DOM",
|
|
2211
|
+
"@testing-library/user-event": "Testing Library User Event",
|
|
2212
|
+
"chai": "Chai",
|
|
2213
|
+
"ava": "AVA",
|
|
2214
|
+
"tap": "node-tap",
|
|
2215
|
+
"supertest": "Supertest",
|
|
2216
|
+
"storybook": "Storybook",
|
|
2217
|
+
"@storybook/react": "Storybook (React)",
|
|
2218
|
+
"@storybook/vue3": "Storybook (Vue)",
|
|
2219
|
+
"msw": "Mock Service Worker",
|
|
2220
|
+
"nock": "Nock",
|
|
2221
|
+
"sinon": "Sinon",
|
|
2222
|
+
"faker": "Faker",
|
|
2223
|
+
"@faker-js/faker": "Faker.js",
|
|
2224
|
+
"testcontainers": "Testcontainers",
|
|
2225
|
+
"pact": "Pact",
|
|
2226
|
+
"@pact-foundation/pact": "Pact",
|
|
2227
|
+
"k6": "k6",
|
|
2228
|
+
"artillery": "Artillery",
|
|
2229
|
+
"autocannon": "Autocannon",
|
|
2230
|
+
"puppeteer": "Puppeteer",
|
|
2231
|
+
"webdriverio": "WebdriverIO",
|
|
2232
|
+
"nightwatch": "Nightwatch",
|
|
2233
|
+
"detox": "Detox (Mobile)",
|
|
2234
|
+
"jest-image-snapshot": "Image Snapshot",
|
|
2235
|
+
"happy-dom": "happy-dom",
|
|
2236
|
+
"jsdom": "jsdom",
|
|
2237
|
+
"c8": "c8 (coverage)",
|
|
2238
|
+
"nyc": "nyc (coverage)",
|
|
2239
|
+
"@vitest/coverage-v8": "Vitest Coverage"
|
|
2240
|
+
},
|
|
2241
|
+
lintFormat: {
|
|
2242
|
+
"eslint": "ESLint",
|
|
2243
|
+
"prettier": "Prettier",
|
|
2244
|
+
"stylelint": "Stylelint",
|
|
2245
|
+
"@biomejs/biome": "Biome",
|
|
2246
|
+
"oxlint": "oxlint",
|
|
2247
|
+
"tslint": "TSLint",
|
|
2248
|
+
"@typescript-eslint/parser": "typescript-eslint",
|
|
2249
|
+
"eslint-config-next": "ESLint Next.js",
|
|
2250
|
+
"eslint-config-prettier": "ESLint Prettier",
|
|
2251
|
+
"eslint-plugin-react": "ESLint React",
|
|
2252
|
+
"eslint-plugin-vue": "ESLint Vue",
|
|
2253
|
+
"husky": "Husky",
|
|
2254
|
+
"lint-staged": "lint-staged",
|
|
2255
|
+
"commitlint": "commitlint",
|
|
2256
|
+
"@commitlint/cli": "commitlint",
|
|
2257
|
+
"lefthook": "Lefthook",
|
|
2258
|
+
"cspell": "CSpell",
|
|
2259
|
+
"markdownlint": "markdownlint",
|
|
2260
|
+
"depcheck": "depcheck",
|
|
2261
|
+
"knip": "Knip",
|
|
2262
|
+
"madge": "Madge",
|
|
2263
|
+
"publint": "publint",
|
|
2264
|
+
"arethetypeswrong": "Are the Types Wrong",
|
|
2265
|
+
"sort-package-json": "sort-package-json"
|
|
2266
|
+
},
|
|
2267
|
+
apiMessaging: {
|
|
2268
|
+
"graphql": "GraphQL",
|
|
2269
|
+
"@grpc/grpc-js": "gRPC",
|
|
2270
|
+
"@trpc/server": "tRPC",
|
|
2271
|
+
"@connectrpc/connect": "Connect",
|
|
2272
|
+
"socket.io": "Socket.IO",
|
|
2273
|
+
"ws": "WebSocket (ws)",
|
|
2274
|
+
"@apollo/server": "Apollo Server",
|
|
2275
|
+
"@apollo/client": "Apollo Client",
|
|
2276
|
+
"urql": "URQL",
|
|
2277
|
+
"graphql-yoga": "GraphQL Yoga",
|
|
2278
|
+
"mercurius": "Mercurius",
|
|
2279
|
+
"type-graphql": "TypeGraphQL",
|
|
2280
|
+
"nexus": "Nexus",
|
|
2281
|
+
"pothos": "Pothos",
|
|
2282
|
+
"@graphql-codegen/cli": "GraphQL Codegen",
|
|
2283
|
+
"kafkajs": "Kafka.js",
|
|
2284
|
+
"amqplib": "AMQP (RabbitMQ)",
|
|
2285
|
+
"bullmq": "BullMQ",
|
|
2286
|
+
"bull": "Bull",
|
|
2287
|
+
"bee-queue": "Bee Queue",
|
|
2288
|
+
"agenda": "Agenda",
|
|
2289
|
+
"pg-boss": "pg-boss",
|
|
2290
|
+
"@temporalio/client": "Temporal",
|
|
2291
|
+
"inngest": "Inngest",
|
|
2292
|
+
"trigger.dev": "Trigger.dev",
|
|
2293
|
+
"nats": "NATS",
|
|
2294
|
+
"mqtt": "MQTT",
|
|
2295
|
+
"zeromq": "ZeroMQ",
|
|
2296
|
+
"sse-channel": "SSE Channel"
|
|
2297
|
+
},
|
|
2298
|
+
observability: {
|
|
2299
|
+
"@sentry/node": "Sentry (Node)",
|
|
2300
|
+
"@sentry/browser": "Sentry (Browser)",
|
|
2301
|
+
"@sentry/react": "Sentry (React)",
|
|
2302
|
+
"@sentry/nextjs": "Sentry (Next.js)",
|
|
2303
|
+
"@opentelemetry/api": "OpenTelemetry API",
|
|
2304
|
+
"@opentelemetry/sdk-node": "OpenTelemetry SDK",
|
|
2305
|
+
"@opentelemetry/auto-instrumentations-node": "OpenTelemetry Auto",
|
|
2306
|
+
"applicationinsights": "Application Insights",
|
|
2307
|
+
"pino": "Pino",
|
|
2308
|
+
"winston": "Winston",
|
|
2309
|
+
"dd-trace": "Datadog",
|
|
2310
|
+
"newrelic": "New Relic",
|
|
2311
|
+
"bunyan": "Bunyan",
|
|
2312
|
+
"@axiomhq/pino": "Axiom (Pino)",
|
|
2313
|
+
"loglevel": "loglevel",
|
|
2314
|
+
"consola": "consola",
|
|
2315
|
+
"@logtail/node": "Logtail",
|
|
2316
|
+
"elastic-apm-node": "Elastic APM",
|
|
2317
|
+
"prom-client": "Prometheus Client",
|
|
2318
|
+
"@google-cloud/trace-agent": "GCP Trace",
|
|
2319
|
+
"@google-cloud/logging": "GCP Logging",
|
|
2320
|
+
"lightstep-tracer": "Lightstep",
|
|
2321
|
+
"roarr": "Roarr",
|
|
2322
|
+
"cls-hooked": "CLS Hooked",
|
|
2323
|
+
"@baselime/node-opentelemetry": "Baselime",
|
|
2324
|
+
"highlight.run": "Highlight",
|
|
2325
|
+
"posthog-node": "PostHog",
|
|
2326
|
+
"@amplitude/node": "Amplitude",
|
|
2327
|
+
"mixpanel": "Mixpanel",
|
|
2328
|
+
"@segment/analytics-node": "Segment"
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
function scanToolingInventory(projects) {
|
|
2332
|
+
const result = {
|
|
2333
|
+
frontend: [],
|
|
2334
|
+
metaFrameworks: [],
|
|
2335
|
+
bundlers: [],
|
|
2336
|
+
css: [],
|
|
2337
|
+
backend: [],
|
|
2338
|
+
orm: [],
|
|
2339
|
+
testing: [],
|
|
2340
|
+
lintFormat: [],
|
|
2341
|
+
apiMessaging: [],
|
|
2342
|
+
observability: []
|
|
2343
|
+
};
|
|
2344
|
+
const packageVersions = /* @__PURE__ */ new Map();
|
|
2345
|
+
for (const project of projects) {
|
|
2346
|
+
for (const dep of project.dependencies) {
|
|
2347
|
+
if (!packageVersions.has(dep.package)) {
|
|
2348
|
+
packageVersions.set(dep.package, dep.resolvedVersion);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
for (const [category, packages] of Object.entries(CATEGORIES)) {
|
|
2353
|
+
const items = [];
|
|
2354
|
+
for (const [pkg2, displayName] of Object.entries(packages)) {
|
|
2355
|
+
if (packageVersions.has(pkg2)) {
|
|
2356
|
+
items.push({
|
|
2357
|
+
name: displayName,
|
|
2358
|
+
package: pkg2,
|
|
2359
|
+
version: packageVersions.get(pkg2) ?? null
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
2364
|
+
result[category] = items;
|
|
2365
|
+
}
|
|
2366
|
+
return result;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// src/scanners/build-deploy.ts
|
|
2370
|
+
import * as path8 from "path";
|
|
2371
|
+
var CI_FILES = {
|
|
2372
|
+
".github/workflows": "github-actions",
|
|
2373
|
+
".gitlab-ci.yml": "gitlab-ci",
|
|
2374
|
+
"azure-pipelines.yml": "azure-devops",
|
|
2375
|
+
"bitbucket-pipelines.yml": "bitbucket-pipelines",
|
|
2376
|
+
"Jenkinsfile": "jenkins",
|
|
2377
|
+
".circleci/config.yml": "circleci",
|
|
2378
|
+
".travis.yml": "travis-ci"
|
|
2379
|
+
};
|
|
2380
|
+
var RELEASE_PACKAGES = /* @__PURE__ */ new Set([
|
|
2381
|
+
"semantic-release",
|
|
2382
|
+
"@changesets/cli",
|
|
2383
|
+
"standard-version",
|
|
2384
|
+
"release-it",
|
|
2385
|
+
"auto",
|
|
2386
|
+
"lerna"
|
|
2387
|
+
]);
|
|
2388
|
+
var RELEASE_FILES = {
|
|
2389
|
+
".changeset": "changesets",
|
|
2390
|
+
".releaserc": "semantic-release",
|
|
2391
|
+
".releaserc.json": "semantic-release",
|
|
2392
|
+
".releaserc.yml": "semantic-release",
|
|
2393
|
+
"release.config.js": "semantic-release",
|
|
2394
|
+
"release.config.cjs": "semantic-release",
|
|
2395
|
+
"GitVersion.yml": "gitversion"
|
|
2396
|
+
};
|
|
2397
|
+
var MONOREPO_FILES = {
|
|
2398
|
+
"pnpm-workspace.yaml": "pnpm-workspaces",
|
|
2399
|
+
"lerna.json": "lerna",
|
|
2400
|
+
"nx.json": "nx",
|
|
2401
|
+
"turbo.json": "turbo",
|
|
2402
|
+
"rush.json": "rush"
|
|
2403
|
+
};
|
|
2404
|
+
var IAC_EXTENSIONS = {
|
|
2405
|
+
".tf": "terraform",
|
|
2406
|
+
".bicep": "bicep"
|
|
2407
|
+
};
|
|
2408
|
+
async function scanBuildDeploy(rootDir) {
|
|
2409
|
+
const result = {
|
|
2410
|
+
ci: [],
|
|
2411
|
+
ciWorkflowCount: 0,
|
|
2412
|
+
docker: { dockerfileCount: 0, baseImages: [] },
|
|
2413
|
+
iac: [],
|
|
2414
|
+
releaseTooling: [],
|
|
2415
|
+
packageManagers: [],
|
|
2416
|
+
monorepoTools: []
|
|
2417
|
+
};
|
|
2418
|
+
const ciSystems = /* @__PURE__ */ new Set();
|
|
2419
|
+
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
2420
|
+
const fullPath = path8.join(rootDir, file);
|
|
2421
|
+
if (await pathExists(fullPath)) {
|
|
2422
|
+
ciSystems.add(system);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
const ghWorkflowDir = path8.join(rootDir, ".github", "workflows");
|
|
2426
|
+
if (await pathExists(ghWorkflowDir)) {
|
|
2427
|
+
try {
|
|
2428
|
+
const files = await findFiles(
|
|
2429
|
+
ghWorkflowDir,
|
|
2430
|
+
(name) => name.endsWith(".yml") || name.endsWith(".yaml")
|
|
2431
|
+
);
|
|
2432
|
+
result.ciWorkflowCount = files.length;
|
|
2433
|
+
} catch {
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
result.ci = [...ciSystems].sort();
|
|
2437
|
+
const dockerfiles = await findFiles(
|
|
2438
|
+
rootDir,
|
|
2439
|
+
(name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
|
|
2440
|
+
);
|
|
2441
|
+
result.docker.dockerfileCount = dockerfiles.length;
|
|
2442
|
+
const baseImages = /* @__PURE__ */ new Set();
|
|
2443
|
+
for (const df of dockerfiles) {
|
|
2444
|
+
try {
|
|
2445
|
+
const content = await readTextFile(df);
|
|
2446
|
+
for (const line of content.split("\n")) {
|
|
2447
|
+
const trimmed = line.trim();
|
|
2448
|
+
if (/^FROM\s+/i.test(trimmed)) {
|
|
2449
|
+
const parts = trimmed.split(/\s+/);
|
|
2450
|
+
let imageIdx = 1;
|
|
2451
|
+
if (parts[1]?.startsWith("--")) {
|
|
2452
|
+
imageIdx = parts[1].includes("=") ? 2 : 3;
|
|
2453
|
+
}
|
|
2454
|
+
if (parts[imageIdx]) {
|
|
2455
|
+
baseImages.add(parts[imageIdx]);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
} catch {
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
result.docker.baseImages = [...baseImages].sort();
|
|
2463
|
+
const iacSystems = /* @__PURE__ */ new Set();
|
|
2464
|
+
for (const [ext, system] of Object.entries(IAC_EXTENSIONS)) {
|
|
2465
|
+
const files = await findFiles(rootDir, (name) => name.endsWith(ext));
|
|
2466
|
+
if (files.length > 0) iacSystems.add(system);
|
|
2467
|
+
}
|
|
2468
|
+
const cfnFiles = await findFiles(
|
|
2469
|
+
rootDir,
|
|
2470
|
+
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
2471
|
+
);
|
|
2472
|
+
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
2473
|
+
if (await pathExists(path8.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
2474
|
+
result.iac = [...iacSystems].sort();
|
|
2475
|
+
const releaseTools = /* @__PURE__ */ new Set();
|
|
2476
|
+
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
2477
|
+
if (await pathExists(path8.join(rootDir, file))) releaseTools.add(tool);
|
|
2478
|
+
}
|
|
2479
|
+
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
2480
|
+
for (const pjPath of pkgFiles) {
|
|
2481
|
+
try {
|
|
2482
|
+
const pj = await readJsonFile(pjPath);
|
|
2483
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
2484
|
+
const deps = pj[section];
|
|
2485
|
+
if (!deps) continue;
|
|
2486
|
+
for (const name of Object.keys(deps)) {
|
|
2487
|
+
if (RELEASE_PACKAGES.has(name)) releaseTools.add(name);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
} catch {
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
result.releaseTooling = [...releaseTools].sort();
|
|
2494
|
+
const lockfileMap = {
|
|
2495
|
+
"pnpm-lock.yaml": "pnpm",
|
|
2496
|
+
"package-lock.json": "npm",
|
|
2497
|
+
"yarn.lock": "yarn",
|
|
2498
|
+
"bun.lockb": "bun"
|
|
2499
|
+
};
|
|
2500
|
+
const managers = /* @__PURE__ */ new Set();
|
|
2501
|
+
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
2502
|
+
if (await pathExists(path8.join(rootDir, file))) managers.add(manager);
|
|
2503
|
+
}
|
|
2504
|
+
result.packageManagers = [...managers].sort();
|
|
2505
|
+
const monoTools = /* @__PURE__ */ new Set();
|
|
2506
|
+
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
2507
|
+
if (await pathExists(path8.join(rootDir, file))) monoTools.add(tool);
|
|
2508
|
+
}
|
|
2509
|
+
result.monorepoTools = [...monoTools].sort();
|
|
2510
|
+
return result;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
// src/scanners/ts-modernity.ts
|
|
2514
|
+
import * as path9 from "path";
|
|
2515
|
+
async function scanTsModernity(rootDir) {
|
|
2516
|
+
const result = {
|
|
2517
|
+
typescriptVersion: null,
|
|
2518
|
+
strict: null,
|
|
2519
|
+
noImplicitAny: null,
|
|
2520
|
+
strictNullChecks: null,
|
|
2521
|
+
module: null,
|
|
2522
|
+
moduleResolution: null,
|
|
2523
|
+
target: null,
|
|
2524
|
+
moduleType: null,
|
|
2525
|
+
exportsField: false
|
|
2526
|
+
};
|
|
2527
|
+
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
2528
|
+
let hasEsm = false;
|
|
2529
|
+
let hasCjs = false;
|
|
2530
|
+
for (const pjPath of pkgFiles) {
|
|
2531
|
+
try {
|
|
2532
|
+
const pj = await readJsonFile(pjPath);
|
|
2533
|
+
if (!result.typescriptVersion) {
|
|
2534
|
+
const tsVer = pj.devDependencies?.["typescript"] ?? pj.dependencies?.["typescript"];
|
|
2535
|
+
if (tsVer) {
|
|
2536
|
+
result.typescriptVersion = tsVer.replace(/^[\^~>=<\s]+/, "");
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
const typeField = pj.type;
|
|
2540
|
+
if (typeField === "module") hasEsm = true;
|
|
2541
|
+
else if (typeField === "commonjs") hasCjs = true;
|
|
2542
|
+
else if (!typeField) hasCjs = true;
|
|
2543
|
+
if (pj.exports) {
|
|
2544
|
+
result.exportsField = true;
|
|
2545
|
+
}
|
|
2546
|
+
} catch {
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
2550
|
+
else if (hasEsm) result.moduleType = "esm";
|
|
2551
|
+
else if (hasCjs) result.moduleType = "cjs";
|
|
2552
|
+
let tsConfigPath = path9.join(rootDir, "tsconfig.json");
|
|
2553
|
+
if (!await pathExists(tsConfigPath)) {
|
|
2554
|
+
const tsConfigs = await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
2555
|
+
if (tsConfigs.length > 0) {
|
|
2556
|
+
tsConfigPath = tsConfigs[0];
|
|
2557
|
+
} else {
|
|
2558
|
+
return result;
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
try {
|
|
2562
|
+
const raw = await readTextFile(tsConfigPath);
|
|
2563
|
+
const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
|
|
2564
|
+
const tsConfig = JSON.parse(stripped);
|
|
2565
|
+
const co = tsConfig?.compilerOptions;
|
|
2566
|
+
if (co) {
|
|
2567
|
+
if (typeof co.strict === "boolean") result.strict = co.strict;
|
|
2568
|
+
if (typeof co.noImplicitAny === "boolean") result.noImplicitAny = co.noImplicitAny;
|
|
2569
|
+
if (typeof co.strictNullChecks === "boolean") result.strictNullChecks = co.strictNullChecks;
|
|
2570
|
+
if (typeof co.module === "string") result.module = co.module;
|
|
2571
|
+
if (typeof co.moduleResolution === "string") result.moduleResolution = co.moduleResolution;
|
|
2572
|
+
if (typeof co.target === "string") result.target = co.target;
|
|
2573
|
+
}
|
|
2574
|
+
} catch {
|
|
2575
|
+
}
|
|
2576
|
+
return result;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// src/scanners/breaking-change.ts
|
|
2580
|
+
var DEPRECATED_PACKAGES2 = /* @__PURE__ */ new Set([
|
|
2581
|
+
// Fully deprecated / archived
|
|
2582
|
+
"request",
|
|
2583
|
+
// deprecated 2020, use undici/node fetch
|
|
2584
|
+
"request-promise",
|
|
2585
|
+
// deprecated with request
|
|
2586
|
+
"request-promise-native",
|
|
2587
|
+
// deprecated with request
|
|
2588
|
+
"moment",
|
|
2589
|
+
// deprecated, use date-fns / luxon / Temporal
|
|
2590
|
+
"node-sass",
|
|
2591
|
+
// deprecated, use sass (Dart Sass)
|
|
2592
|
+
"tslint",
|
|
2593
|
+
// archived, use eslint + typescript-eslint
|
|
2594
|
+
"aws-sdk",
|
|
2595
|
+
// AWS SDK v2 EOL, use @aws-sdk/* v3
|
|
2596
|
+
"babel-core",
|
|
2597
|
+
// replaced by @babel/core
|
|
2598
|
+
"babel-preset-env",
|
|
2599
|
+
// replaced by @babel/preset-env
|
|
2600
|
+
"babel-preset-react",
|
|
2601
|
+
// replaced by @babel/preset-react
|
|
2602
|
+
"babel-loader",
|
|
2603
|
+
// 7.x deprecated, must pair with @babel/core
|
|
2604
|
+
"core-js",
|
|
2605
|
+
// v2 deprecated (v3 ok but signals old config)
|
|
2606
|
+
"istanbul",
|
|
2607
|
+
// replaced by nyc / c8
|
|
2608
|
+
"istanbul-instrumenter-loader",
|
|
2609
|
+
// deprecated with istanbul
|
|
2610
|
+
"left-pad",
|
|
2611
|
+
// meme package, use String.padStart
|
|
2612
|
+
"popper.js",
|
|
2613
|
+
// deprecated, use @popperjs/core
|
|
2614
|
+
"create-react-class",
|
|
2615
|
+
// deprecated React pattern
|
|
2616
|
+
"react-addons-css-transition-group",
|
|
2617
|
+
// deprecated React addon
|
|
2618
|
+
"react-addons-test-utils",
|
|
2619
|
+
// use react-dom/test-utils
|
|
2620
|
+
"@types/express-serve-static-core",
|
|
2621
|
+
// now shipped with express
|
|
2622
|
+
"enzyme",
|
|
2623
|
+
// unmaintained, use Testing Library or Vitest
|
|
2624
|
+
"enzyme-adapter-react-16",
|
|
2625
|
+
// unmaintained
|
|
2626
|
+
"enzyme-adapter-react-17",
|
|
2627
|
+
// unmaintained
|
|
2628
|
+
"react-hot-loader",
|
|
2629
|
+
// deprecated, use React Fast Refresh
|
|
2630
|
+
"react-loadable",
|
|
2631
|
+
// unmaintained, use React.lazy
|
|
2632
|
+
"react-router-dom-v5-compat",
|
|
2633
|
+
// migration shim
|
|
2634
|
+
"redux-thunk",
|
|
2635
|
+
// bundled into @reduxjs/toolkit since v1.5
|
|
2636
|
+
"redux-saga",
|
|
2637
|
+
// declining, consider RTK Query
|
|
2638
|
+
"recompose",
|
|
2639
|
+
// archived, use hooks
|
|
2640
|
+
"classnames",
|
|
2641
|
+
// works but clsx is faster & smaller
|
|
2642
|
+
"glamor",
|
|
2643
|
+
// deprecated CSS-in-JS
|
|
2644
|
+
"radium",
|
|
2645
|
+
// deprecated CSS-in-JS
|
|
2646
|
+
"material-ui",
|
|
2647
|
+
// replaced by @mui/material
|
|
2648
|
+
"@material-ui/core",
|
|
2649
|
+
// replaced by @mui/material
|
|
2650
|
+
// Unmaintained / abandoned
|
|
2651
|
+
"bower",
|
|
2652
|
+
// dead package manager
|
|
2653
|
+
"grunt",
|
|
2654
|
+
// superseded by npm scripts / modern bundlers
|
|
2655
|
+
"gulp",
|
|
2656
|
+
// declining, modern bundlers preferred
|
|
2657
|
+
"browserify",
|
|
2658
|
+
// superseded by modern bundlers
|
|
2659
|
+
"coffee-script",
|
|
2660
|
+
// CoffeeScript 1.x deprecated
|
|
2661
|
+
"coffeescript",
|
|
2662
|
+
// declining
|
|
2663
|
+
"jade",
|
|
2664
|
+
// renamed to pug
|
|
2665
|
+
"nomnom",
|
|
2666
|
+
// deprecated CLI parser
|
|
2667
|
+
"optimist",
|
|
2668
|
+
// deprecated CLI parser, use yargs/commander
|
|
2669
|
+
"minimist",
|
|
2670
|
+
// unmaintained, use mri or parseArgs
|
|
2671
|
+
"colors",
|
|
2672
|
+
// supply chain compromised, use chalk/picocolors
|
|
2673
|
+
"faker",
|
|
2674
|
+
// supply chain compromised, use @faker-js/faker
|
|
2675
|
+
"event-stream",
|
|
2676
|
+
// supply chain incident
|
|
2677
|
+
"ua-parser-js",
|
|
2678
|
+
// had supply chain incident
|
|
2679
|
+
"caniuse-db",
|
|
2680
|
+
// replaced by caniuse-lite
|
|
2681
|
+
"circular-json",
|
|
2682
|
+
// deprecated, use flatted
|
|
2683
|
+
"mkdirp",
|
|
2684
|
+
// Node has fs.mkdir recursive since v10
|
|
2685
|
+
"rimraf",
|
|
2686
|
+
// Node has fs.rm recursive since v14
|
|
2687
|
+
"glob",
|
|
2688
|
+
// consider fast-glob or Node fs.glob
|
|
2689
|
+
"swig",
|
|
2690
|
+
// abandoned template engine
|
|
2691
|
+
"dustjs-linkedin",
|
|
2692
|
+
// abandoned template engine
|
|
2693
|
+
"hogan.js",
|
|
2694
|
+
// abandoned template engine
|
|
2695
|
+
"passport-local-mongoose",
|
|
2696
|
+
// low maintenance
|
|
2697
|
+
// Known breaking-change magnets
|
|
2698
|
+
"@angular/http",
|
|
2699
|
+
// removed in Angular 15+
|
|
2700
|
+
"rxjs-compat",
|
|
2701
|
+
// migration shim for RxJS 5→6
|
|
2702
|
+
"protractor",
|
|
2703
|
+
// deprecated by Angular team
|
|
2704
|
+
"karma",
|
|
2705
|
+
// deprecated, Vitest/Jest preferred
|
|
2706
|
+
"karma-jasmine",
|
|
2707
|
+
// deprecated with Karma
|
|
2708
|
+
"jasmine"
|
|
2709
|
+
// declining in usage
|
|
2710
|
+
]);
|
|
2711
|
+
var LEGACY_POLYFILLS = /* @__PURE__ */ new Set([
|
|
2712
|
+
// Built-in fetch & web APIs (Node 18+)
|
|
2713
|
+
"node-fetch",
|
|
2714
|
+
// native fetch since Node 18
|
|
2715
|
+
"cross-fetch",
|
|
2716
|
+
// native fetch since Node 18
|
|
2717
|
+
"isomorphic-fetch",
|
|
2718
|
+
// native fetch since Node 18
|
|
2719
|
+
"whatwg-fetch",
|
|
2720
|
+
// native fetch since Node 18
|
|
2721
|
+
"abort-controller",
|
|
2722
|
+
// native AbortController since Node 15
|
|
2723
|
+
"form-data",
|
|
2724
|
+
// native FormData since Node 18
|
|
2725
|
+
"formdata-polyfill",
|
|
2726
|
+
// native FormData since Node 18
|
|
2727
|
+
"web-streams-polyfill",
|
|
2728
|
+
// native ReadableStream since Node 18
|
|
2729
|
+
"whatwg-url",
|
|
2730
|
+
// native URL since Node 10
|
|
2731
|
+
"url-parse",
|
|
2732
|
+
// native URL since Node 10
|
|
2733
|
+
"domexception",
|
|
2734
|
+
// native DOMException since Node 17
|
|
2735
|
+
"abortcontroller-polyfill",
|
|
2736
|
+
// native since Node 15
|
|
2737
|
+
// Built-in Node modules shimmed for browser
|
|
2738
|
+
"querystring",
|
|
2739
|
+
// URLSearchParams preferred, native in Node
|
|
2740
|
+
"string_decoder",
|
|
2741
|
+
// native TextDecoder preferred
|
|
2742
|
+
"buffer",
|
|
2743
|
+
// native Buffer in Node, Uint8Array in browser
|
|
2744
|
+
"events",
|
|
2745
|
+
// native EventTarget preferred
|
|
2746
|
+
"path-browserify",
|
|
2747
|
+
// browser shim
|
|
2748
|
+
"stream-browserify",
|
|
2749
|
+
// browser shim
|
|
2750
|
+
"stream-http",
|
|
2751
|
+
// browser shim
|
|
2752
|
+
"https-browserify",
|
|
2753
|
+
// browser shim
|
|
2754
|
+
"os-browserify",
|
|
2755
|
+
// browser shim
|
|
2756
|
+
"crypto-browserify",
|
|
2757
|
+
// native Web Crypto API
|
|
2758
|
+
"assert",
|
|
2759
|
+
// native assert in Node, console.assert in browser
|
|
2760
|
+
"util",
|
|
2761
|
+
// Node native, deprecations in browser bundles
|
|
2762
|
+
"process",
|
|
2763
|
+
// shimmed, usually unnecessary
|
|
2764
|
+
"timers-browserify",
|
|
2765
|
+
// native timers
|
|
2766
|
+
"tty-browserify",
|
|
2767
|
+
// rarely needed
|
|
2768
|
+
"vm-browserify",
|
|
2769
|
+
// rarely needed
|
|
2770
|
+
"domain-browser",
|
|
2771
|
+
// domains deprecated in Node
|
|
2772
|
+
"punycode",
|
|
2773
|
+
// native in URL since Node 10
|
|
2774
|
+
"readable-stream",
|
|
2775
|
+
// Node streams polyfill, usually unnecessary
|
|
2776
|
+
// ES / language polyfills for ES2015+ (Node 18+ has all)
|
|
2777
|
+
"es6-promise",
|
|
2778
|
+
// native Promise since Node 4
|
|
2779
|
+
"promise-polyfill",
|
|
2780
|
+
// native Promise
|
|
2781
|
+
"es6-symbol",
|
|
2782
|
+
// native Symbol since Node 4
|
|
2783
|
+
"es6-map",
|
|
2784
|
+
// native Map since Node 4
|
|
2785
|
+
"es6-set",
|
|
2786
|
+
// native Set since Node 4
|
|
2787
|
+
"es6-weak-map",
|
|
2788
|
+
// native WeakMap since Node 4
|
|
2789
|
+
"es6-iterator",
|
|
2790
|
+
// native iterators
|
|
2791
|
+
"object-assign",
|
|
2792
|
+
// native Object.assign since Node 4
|
|
2793
|
+
"object.assign",
|
|
2794
|
+
// same
|
|
2795
|
+
"array.prototype.find",
|
|
2796
|
+
// native since ES2015
|
|
2797
|
+
"array.prototype.findindex",
|
|
2798
|
+
// native since ES2015
|
|
2799
|
+
"array.prototype.flat",
|
|
2800
|
+
// native since Node 11
|
|
2801
|
+
"array.prototype.flatmap",
|
|
2802
|
+
// native since Node 11
|
|
2803
|
+
"array-includes",
|
|
2804
|
+
// native Array.includes since Node 6
|
|
2805
|
+
"string.prototype.startswith",
|
|
2806
|
+
// native since ES2015
|
|
2807
|
+
"string.prototype.endswith",
|
|
2808
|
+
// native since ES2015
|
|
2809
|
+
"string.prototype.includes",
|
|
2810
|
+
// native since ES2015
|
|
2811
|
+
"string.prototype.padstart",
|
|
2812
|
+
// native since Node 8
|
|
2813
|
+
"string.prototype.padend",
|
|
2814
|
+
// native since Node 8
|
|
2815
|
+
"string.prototype.matchall",
|
|
2816
|
+
// native since Node 12
|
|
2817
|
+
"string.prototype.replaceall",
|
|
2818
|
+
// native since Node 15
|
|
2819
|
+
"string.prototype.trimstart",
|
|
2820
|
+
// native since Node 10
|
|
2821
|
+
"string.prototype.trimend",
|
|
2822
|
+
// native since Node 10
|
|
2823
|
+
"string.prototype.at",
|
|
2824
|
+
// native since Node 16.6
|
|
2825
|
+
"object.entries",
|
|
2826
|
+
// native since Node 7
|
|
2827
|
+
"object.values",
|
|
2828
|
+
// native since Node 7
|
|
2829
|
+
"object.fromentries",
|
|
2830
|
+
// native since Node 12
|
|
2831
|
+
"globalthis",
|
|
2832
|
+
// native globalThis since Node 12
|
|
2833
|
+
"symbol-observable",
|
|
2834
|
+
// TC39 withdrawn
|
|
2835
|
+
"setimmediate",
|
|
2836
|
+
// Node-only, setTimeout(fn, 0) preferred
|
|
2837
|
+
"regenerator-runtime",
|
|
2838
|
+
// async/await native since Node 8
|
|
2839
|
+
"@babel/polyfill",
|
|
2840
|
+
// deprecated, use core-js directly
|
|
2841
|
+
"whatwg-encoding",
|
|
2842
|
+
// native TextEncoder/TextDecoder
|
|
2843
|
+
"text-encoding",
|
|
2844
|
+
// native TextEncoder/TextDecoder since Node 11
|
|
2845
|
+
"encoding",
|
|
2846
|
+
// native TextEncoder/TextDecoder
|
|
2847
|
+
"unorm",
|
|
2848
|
+
// native String.normalize since Node 0.12
|
|
2849
|
+
"number.isnan",
|
|
2850
|
+
// native Number.isNaN since Node 0.12
|
|
2851
|
+
"is-nan",
|
|
2852
|
+
// native Number.isNaN
|
|
2853
|
+
"has-symbols",
|
|
2854
|
+
// native Symbol detection
|
|
2855
|
+
"has",
|
|
2856
|
+
// native Object.hasOwn since Node 16.9
|
|
2857
|
+
"hasown",
|
|
2858
|
+
// shim for Object.hasOwn
|
|
2859
|
+
"safe-buffer",
|
|
2860
|
+
// Buffer.from available since Node 5.10
|
|
2861
|
+
"safer-buffer"
|
|
2862
|
+
// Buffer.from available since Node 5.10
|
|
2863
|
+
]);
|
|
2864
|
+
function scanBreakingChangeExposure(projects) {
|
|
2865
|
+
const deprecated = /* @__PURE__ */ new Set();
|
|
2866
|
+
const legacyPolyfills = /* @__PURE__ */ new Set();
|
|
2867
|
+
let peerConflictsDetected = false;
|
|
2868
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
2869
|
+
for (const project of projects) {
|
|
2870
|
+
for (const dep of project.dependencies) {
|
|
2871
|
+
allDeps.add(dep.package);
|
|
2872
|
+
if (DEPRECATED_PACKAGES2.has(dep.package)) {
|
|
2873
|
+
deprecated.add(dep.package);
|
|
2874
|
+
}
|
|
2875
|
+
if (LEGACY_POLYFILLS.has(dep.package)) {
|
|
2876
|
+
legacyPolyfills.add(dep.package);
|
|
2877
|
+
}
|
|
2878
|
+
if (dep.section === "peerDependencies" && dep.majorsBehind !== null && dep.majorsBehind >= 2) {
|
|
2879
|
+
peerConflictsDetected = true;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
let score = 0;
|
|
2884
|
+
score += Math.min(deprecated.size * 10, 40);
|
|
2885
|
+
score += Math.min(legacyPolyfills.size * 5, 30);
|
|
2886
|
+
score += peerConflictsDetected ? 20 : 0;
|
|
2887
|
+
score = Math.min(score, 100);
|
|
2888
|
+
return {
|
|
2889
|
+
deprecatedPackages: [...deprecated].sort(),
|
|
2890
|
+
legacyPolyfills: [...legacyPolyfills].sort(),
|
|
2891
|
+
peerConflictsDetected,
|
|
2892
|
+
exposureScore: score
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// src/scanners/file-hotspots.ts
|
|
2897
|
+
import * as fs4 from "fs/promises";
|
|
2898
|
+
import * as path10 from "path";
|
|
2899
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
2900
|
+
"node_modules",
|
|
2901
|
+
".git",
|
|
2902
|
+
".next",
|
|
2903
|
+
"dist",
|
|
2904
|
+
"build",
|
|
2905
|
+
"out",
|
|
2906
|
+
".turbo",
|
|
2907
|
+
".cache",
|
|
2908
|
+
"coverage",
|
|
2909
|
+
"bin",
|
|
2910
|
+
"obj",
|
|
2911
|
+
".vs",
|
|
2912
|
+
"TestResults",
|
|
2913
|
+
".nuxt",
|
|
2914
|
+
".output",
|
|
2915
|
+
".svelte-kit"
|
|
2916
|
+
]);
|
|
2917
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2918
|
+
".map",
|
|
2919
|
+
".lock",
|
|
2920
|
+
".png",
|
|
2921
|
+
".jpg",
|
|
2922
|
+
".jpeg",
|
|
2923
|
+
".gif",
|
|
2924
|
+
".ico",
|
|
2925
|
+
".svg",
|
|
2926
|
+
".woff",
|
|
2927
|
+
".woff2",
|
|
2928
|
+
".ttf",
|
|
2929
|
+
".eot",
|
|
2930
|
+
".mp4",
|
|
2931
|
+
".webm"
|
|
2932
|
+
]);
|
|
2933
|
+
async function scanFileHotspots(rootDir) {
|
|
2934
|
+
const extensionCounts = {};
|
|
2935
|
+
const allFiles = [];
|
|
2936
|
+
let maxDepth = 0;
|
|
2937
|
+
async function walk(dir, depth) {
|
|
2938
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
2939
|
+
let entries;
|
|
2940
|
+
try {
|
|
2941
|
+
const dirents = await fs4.readdir(dir, { withFileTypes: true });
|
|
2942
|
+
entries = dirents.map((d) => ({
|
|
2943
|
+
name: d.name,
|
|
2944
|
+
isDirectory: d.isDirectory(),
|
|
2945
|
+
isFile: d.isFile()
|
|
2946
|
+
}));
|
|
2947
|
+
} catch {
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
for (const e of entries) {
|
|
2951
|
+
if (e.isDirectory) {
|
|
2952
|
+
if (SKIP_DIRS2.has(e.name)) continue;
|
|
2953
|
+
await walk(path10.join(dir, e.name), depth + 1);
|
|
2954
|
+
} else if (e.isFile) {
|
|
2955
|
+
const ext = path10.extname(e.name).toLowerCase();
|
|
2956
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
2957
|
+
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
2958
|
+
try {
|
|
2959
|
+
const stat3 = await fs4.stat(path10.join(dir, e.name));
|
|
2960
|
+
allFiles.push({
|
|
2961
|
+
path: path10.relative(rootDir, path10.join(dir, e.name)),
|
|
2962
|
+
bytes: stat3.size
|
|
2963
|
+
});
|
|
2964
|
+
} catch {
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
await walk(rootDir, 0);
|
|
2970
|
+
allFiles.sort((a, b) => b.bytes - a.bytes);
|
|
2971
|
+
const largestFiles = allFiles.slice(0, 20);
|
|
2972
|
+
return {
|
|
2973
|
+
fileCountByExtension: extensionCounts,
|
|
2974
|
+
largestFiles,
|
|
2975
|
+
totalFiles: allFiles.length,
|
|
2976
|
+
maxDirectoryDepth: maxDepth,
|
|
2977
|
+
mostUsedPackages: []
|
|
2978
|
+
// Filled in by caller with project data
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
// src/scanners/security-posture.ts
|
|
2983
|
+
import * as path11 from "path";
|
|
2984
|
+
var LOCKFILES = {
|
|
2985
|
+
"pnpm-lock.yaml": "pnpm",
|
|
2986
|
+
"package-lock.json": "npm",
|
|
2987
|
+
"yarn.lock": "yarn",
|
|
2988
|
+
"bun.lockb": "bun",
|
|
2989
|
+
"packages.lock.json": "nuget"
|
|
2990
|
+
};
|
|
2991
|
+
async function scanSecurityPosture(rootDir) {
|
|
2992
|
+
const result = {
|
|
2993
|
+
lockfilePresent: false,
|
|
2994
|
+
multipleLockfileTypes: false,
|
|
2995
|
+
gitignoreCoversEnv: false,
|
|
2996
|
+
gitignoreCoversNodeModules: false,
|
|
2997
|
+
envFilesTracked: false,
|
|
2998
|
+
lockfileTypes: []
|
|
2999
|
+
};
|
|
3000
|
+
const foundLockfiles = [];
|
|
3001
|
+
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
3002
|
+
if (await pathExists(path11.join(rootDir, file))) {
|
|
3003
|
+
foundLockfiles.push(type);
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
result.lockfilePresent = foundLockfiles.length > 0;
|
|
3007
|
+
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
3008
|
+
result.lockfileTypes = foundLockfiles.sort();
|
|
3009
|
+
const gitignorePath = path11.join(rootDir, ".gitignore");
|
|
3010
|
+
if (await pathExists(gitignorePath)) {
|
|
3011
|
+
try {
|
|
3012
|
+
const content = await readTextFile(gitignorePath);
|
|
3013
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
3014
|
+
result.gitignoreCoversEnv = lines.some(
|
|
3015
|
+
(line) => line === ".env" || line === ".env*" || line === ".env.*" || line === ".env.local" || line === "*.env"
|
|
3016
|
+
);
|
|
3017
|
+
result.gitignoreCoversNodeModules = lines.some(
|
|
3018
|
+
(line) => line === "node_modules" || line === "node_modules/" || line === "/node_modules"
|
|
3019
|
+
);
|
|
3020
|
+
} catch {
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
3024
|
+
if (await pathExists(path11.join(rootDir, envFile))) {
|
|
3025
|
+
if (!result.gitignoreCoversEnv) {
|
|
3026
|
+
result.envFilesTracked = true;
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
return result;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// src/scanners/service-dependencies.ts
|
|
3035
|
+
var SERVICE_CATEGORIES = {
|
|
3036
|
+
payment: {
|
|
3037
|
+
"stripe": "Stripe",
|
|
3038
|
+
"@stripe/stripe-js": "Stripe.js",
|
|
3039
|
+
"@stripe/react-stripe-js": "Stripe React",
|
|
3040
|
+
"braintree": "Braintree",
|
|
3041
|
+
"braintree-web": "Braintree Web",
|
|
3042
|
+
"@paypal/checkout-server-sdk": "PayPal",
|
|
3043
|
+
"@paypal/react-paypal-js": "PayPal React",
|
|
3044
|
+
"@paypal/paypal-js": "PayPal.js",
|
|
3045
|
+
"square": "Square",
|
|
3046
|
+
"razorpay": "Razorpay",
|
|
3047
|
+
"adyen-api-library": "Adyen",
|
|
3048
|
+
"@adyen/api-library": "Adyen",
|
|
3049
|
+
"@adyen/adyen-web": "Adyen Web",
|
|
3050
|
+
"mollie-api-node": "Mollie",
|
|
3051
|
+
"@lemonsqueezy/lemonsqueezy.js": "Lemon Squeezy",
|
|
3052
|
+
"paddle-sdk": "Paddle",
|
|
3053
|
+
"@paddle/paddle-node-sdk": "Paddle",
|
|
3054
|
+
"coinbase-commerce-node": "Coinbase Commerce",
|
|
3055
|
+
"gocardless-nodejs": "GoCardless",
|
|
3056
|
+
"klarna-checkout": "Klarna",
|
|
3057
|
+
"@recurly/recurly-js": "Recurly",
|
|
3058
|
+
"chargebee": "Chargebee"
|
|
3059
|
+
},
|
|
3060
|
+
auth: {
|
|
3061
|
+
"@auth0/auth0-spa-js": "Auth0 SPA",
|
|
3062
|
+
"@auth0/auth0-react": "Auth0 React",
|
|
3063
|
+
"@auth0/nextjs-auth0": "Auth0 Next.js",
|
|
3064
|
+
"auth0": "Auth0",
|
|
3065
|
+
"@clerk/clerk-sdk-node": "Clerk (Node)",
|
|
3066
|
+
"@clerk/nextjs": "Clerk (Next.js)",
|
|
3067
|
+
"@clerk/clerk-react": "Clerk (React)",
|
|
3068
|
+
"firebase-admin": "Firebase Admin",
|
|
3069
|
+
"firebase": "Firebase",
|
|
3070
|
+
"@firebase/auth": "Firebase Auth",
|
|
3071
|
+
"passport": "Passport.js",
|
|
3072
|
+
"passport-local": "Passport Local",
|
|
3073
|
+
"passport-google-oauth20": "Passport Google",
|
|
3074
|
+
"passport-github2": "Passport GitHub",
|
|
3075
|
+
"next-auth": "NextAuth",
|
|
3076
|
+
"@auth/core": "Auth.js",
|
|
3077
|
+
"@supabase/supabase-js": "Supabase",
|
|
3078
|
+
"@supabase/auth-helpers-nextjs": "Supabase Auth (Next.js)",
|
|
3079
|
+
"@supabase/ssr": "Supabase SSR",
|
|
3080
|
+
"jsonwebtoken": "JWT",
|
|
3081
|
+
"jose": "JOSE",
|
|
3082
|
+
"@okta/okta-auth-js": "Okta",
|
|
3083
|
+
"@okta/okta-react": "Okta React",
|
|
3084
|
+
"oidc-client-ts": "OIDC Client",
|
|
3085
|
+
"@casl/ability": "CASL",
|
|
3086
|
+
"casbin": "Casbin",
|
|
3087
|
+
"keycloak-js": "Keycloak",
|
|
3088
|
+
"@keycloak/keycloak-admin-client": "Keycloak Admin",
|
|
3089
|
+
"lucia": "Lucia Auth",
|
|
3090
|
+
"@lucia-auth/adapter-prisma": "Lucia (Prisma)",
|
|
3091
|
+
"arctic": "Arctic (OAuth)",
|
|
3092
|
+
"better-auth": "Better Auth",
|
|
3093
|
+
"grant": "Grant (OAuth)",
|
|
3094
|
+
"@workos-inc/node": "WorkOS",
|
|
3095
|
+
"stytch": "Stytch",
|
|
3096
|
+
"frontegg": "Frontegg",
|
|
3097
|
+
"@descope/node-sdk": "Descope",
|
|
3098
|
+
"kinde-auth-nextjs": "Kinde"
|
|
3099
|
+
},
|
|
3100
|
+
email: {
|
|
3101
|
+
"@sendgrid/mail": "SendGrid",
|
|
3102
|
+
"@sendgrid/client": "SendGrid Client",
|
|
3103
|
+
"nodemailer": "Nodemailer",
|
|
3104
|
+
"twilio": "Twilio",
|
|
3105
|
+
"@aws-sdk/client-ses": "AWS SES",
|
|
3106
|
+
"@aws-sdk/client-sesv2": "AWS SES v2",
|
|
3107
|
+
"postmark": "Postmark",
|
|
3108
|
+
"mailgun.js": "Mailgun",
|
|
3109
|
+
"resend": "Resend",
|
|
3110
|
+
"@react-email/components": "React Email",
|
|
3111
|
+
"react-email": "React Email",
|
|
3112
|
+
"mjml": "MJML",
|
|
3113
|
+
"email-templates": "Email Templates",
|
|
3114
|
+
"@mailchimp/mailchimp_marketing": "Mailchimp",
|
|
3115
|
+
"@mailchimp/mailchimp_transactional": "Mailchimp Transactional",
|
|
3116
|
+
"plunk-node": "Plunk",
|
|
3117
|
+
"brevo": "Brevo (Sendinblue)",
|
|
3118
|
+
"sib-api-v3-sdk": "Brevo v3",
|
|
3119
|
+
"@customer.io/node": "Customer.io",
|
|
3120
|
+
"sparkpost": "SparkPost",
|
|
3121
|
+
"mandrill-api": "Mandrill",
|
|
3122
|
+
"mail-listener5": "Mail Listener",
|
|
3123
|
+
"@azure/communication-email": "Azure Email"
|
|
3124
|
+
},
|
|
3125
|
+
cloud: {
|
|
3126
|
+
// AWS
|
|
3127
|
+
"aws-sdk": "AWS SDK v2",
|
|
3128
|
+
"@aws-sdk/client-s3": "AWS S3",
|
|
3129
|
+
"@aws-sdk/client-lambda": "AWS Lambda",
|
|
3130
|
+
"@aws-sdk/client-dynamodb": "AWS DynamoDB",
|
|
3131
|
+
"@aws-sdk/client-ec2": "AWS EC2",
|
|
3132
|
+
"@aws-sdk/client-ecs": "AWS ECS",
|
|
3133
|
+
"@aws-sdk/client-ecr": "AWS ECR",
|
|
3134
|
+
"@aws-sdk/client-cloudformation": "AWS CloudFormation",
|
|
3135
|
+
"@aws-sdk/client-cloudwatch": "AWS CloudWatch",
|
|
3136
|
+
"@aws-sdk/client-cloudfront": "AWS CloudFront",
|
|
3137
|
+
"@aws-sdk/client-iam": "AWS IAM",
|
|
3138
|
+
"@aws-sdk/client-secrets-manager": "AWS Secrets Manager",
|
|
3139
|
+
"@aws-sdk/client-ssm": "AWS SSM",
|
|
3140
|
+
"@aws-sdk/client-sts": "AWS STS",
|
|
3141
|
+
"@aws-sdk/client-cognito-identity-provider": "AWS Cognito",
|
|
3142
|
+
"@aws-sdk/client-route-53": "AWS Route 53",
|
|
3143
|
+
"@aws-sdk/client-step-functions": "AWS Step Functions",
|
|
3144
|
+
"@aws-sdk/client-eventbridge": "AWS EventBridge",
|
|
3145
|
+
"@aws-sdk/client-kinesis": "AWS Kinesis",
|
|
3146
|
+
"@aws-sdk/client-kms": "AWS KMS",
|
|
3147
|
+
"@aws-sdk/client-athena": "AWS Athena",
|
|
3148
|
+
"@aws-sdk/client-bedrock-runtime": "AWS Bedrock",
|
|
3149
|
+
"@aws-sdk/lib-dynamodb": "AWS DynamoDB Doc",
|
|
3150
|
+
// Azure
|
|
3151
|
+
"@azure/storage-blob": "Azure Blob",
|
|
3152
|
+
"@azure/identity": "Azure Identity",
|
|
3153
|
+
"@azure/cosmos": "Azure Cosmos DB",
|
|
3154
|
+
"@azure/keyvault-secrets": "Azure Key Vault",
|
|
3155
|
+
"@azure/service-bus": "Azure Service Bus",
|
|
3156
|
+
"@azure/event-hubs": "Azure Event Hubs",
|
|
3157
|
+
"@azure/functions": "Azure Functions",
|
|
3158
|
+
"@azure/ai-form-recognizer": "Azure Form Recognizer",
|
|
3159
|
+
"@azure/openai": "Azure OpenAI",
|
|
3160
|
+
"@azure/app-configuration": "Azure App Config",
|
|
3161
|
+
"@azure/monitor-opentelemetry": "Azure Monitor OTel",
|
|
3162
|
+
"@azure/container-registry": "Azure Container Registry",
|
|
3163
|
+
// GCP
|
|
3164
|
+
"@google-cloud/storage": "GCP Storage",
|
|
3165
|
+
"@google-cloud/functions-framework": "GCP Functions",
|
|
3166
|
+
"@google-cloud/bigquery": "GCP BigQuery",
|
|
3167
|
+
"@google-cloud/firestore": "GCP Firestore",
|
|
3168
|
+
"@google-cloud/pubsub": "GCP Pub/Sub",
|
|
3169
|
+
"@google-cloud/secret-manager": "GCP Secret Manager",
|
|
3170
|
+
"@google-cloud/tasks": "GCP Cloud Tasks",
|
|
3171
|
+
"@google-cloud/run": "GCP Cloud Run",
|
|
3172
|
+
"@google-cloud/logging": "GCP Logging",
|
|
3173
|
+
"@google-cloud/translate": "GCP Translate",
|
|
3174
|
+
"@google-cloud/vertexai": "GCP Vertex AI",
|
|
3175
|
+
// Multi-cloud / PaaS
|
|
3176
|
+
"@vercel/sdk": "Vercel",
|
|
3177
|
+
"@vercel/kv": "Vercel KV",
|
|
3178
|
+
"@vercel/blob": "Vercel Blob",
|
|
3179
|
+
"@vercel/postgres": "Vercel Postgres",
|
|
3180
|
+
"@vercel/edge-config": "Vercel Edge Config",
|
|
3181
|
+
"@netlify/functions": "Netlify Functions",
|
|
3182
|
+
"@cloudflare/workers-types": "Cloudflare Workers",
|
|
3183
|
+
"wrangler": "Wrangler",
|
|
3184
|
+
"@railway/cli": "Railway",
|
|
3185
|
+
"@fly/apps": "Fly.io",
|
|
3186
|
+
"heroku-client": "Heroku",
|
|
3187
|
+
"@pulumi/pulumi": "Pulumi",
|
|
3188
|
+
"@pulumi/aws": "Pulumi AWS",
|
|
3189
|
+
"cdktf": "CDK for Terraform",
|
|
3190
|
+
"aws-cdk-lib": "AWS CDK",
|
|
3191
|
+
"sst": "SST"
|
|
3192
|
+
},
|
|
3193
|
+
databases: {
|
|
3194
|
+
"pg": "PostgreSQL",
|
|
3195
|
+
"postgres": "Postgres.js",
|
|
3196
|
+
"mysql2": "MySQL",
|
|
3197
|
+
"mysql": "MySQL (legacy)",
|
|
3198
|
+
"mongodb": "MongoDB",
|
|
3199
|
+
"ioredis": "Redis (ioredis)",
|
|
3200
|
+
"redis": "Redis",
|
|
3201
|
+
"better-sqlite3": "SQLite",
|
|
3202
|
+
"sql.js": "sql.js",
|
|
3203
|
+
"@prisma/client": "Prisma",
|
|
3204
|
+
"drizzle-orm": "Drizzle",
|
|
3205
|
+
"typeorm": "TypeORM",
|
|
3206
|
+
"sequelize": "Sequelize",
|
|
3207
|
+
"knex": "Knex",
|
|
3208
|
+
"kysely": "Kysely",
|
|
3209
|
+
"objection": "Objection.js",
|
|
3210
|
+
"slonik": "Slonik",
|
|
3211
|
+
"massive": "Massive.js",
|
|
3212
|
+
"mongoose": "Mongoose",
|
|
3213
|
+
"@mikro-orm/core": "MikroORM",
|
|
3214
|
+
"mssql": "SQL Server",
|
|
3215
|
+
"oracledb": "Oracle",
|
|
3216
|
+
"cassandra-driver": "Cassandra",
|
|
3217
|
+
"neo4j-driver": "Neo4j",
|
|
3218
|
+
"arangojs": "ArangoDB",
|
|
3219
|
+
"@clickhouse/client": "ClickHouse",
|
|
3220
|
+
"influx": "InfluxDB",
|
|
3221
|
+
"@libsql/client": "libSQL (Turso)",
|
|
3222
|
+
"@neondatabase/serverless": "Neon",
|
|
3223
|
+
"@planetscale/database": "PlanetScale",
|
|
3224
|
+
"@electric-sql/pglite": "PGlite",
|
|
3225
|
+
"@upstash/redis": "Upstash Redis",
|
|
3226
|
+
"@upstash/kafka": "Upstash Kafka",
|
|
3227
|
+
"@upstash/qstash": "Upstash QStash",
|
|
3228
|
+
"@upstash/vector": "Upstash Vector",
|
|
3229
|
+
"dynamoose": "Dynamoose",
|
|
3230
|
+
"fauna": "Fauna",
|
|
3231
|
+
"faunadb": "FaunaDB",
|
|
3232
|
+
"@supabase/postgrest-js": "Supabase PostgREST",
|
|
3233
|
+
"couchbase": "Couchbase",
|
|
3234
|
+
"couchdb-nano": "CouchDB",
|
|
3235
|
+
"duckdb": "DuckDB",
|
|
3236
|
+
"@tidbcloud/serverless": "TiDB Serverless"
|
|
3237
|
+
},
|
|
3238
|
+
messaging: {
|
|
3239
|
+
"@aws-sdk/client-sqs": "AWS SQS",
|
|
3240
|
+
"@aws-sdk/client-sns": "AWS SNS",
|
|
3241
|
+
"amqplib": "RabbitMQ",
|
|
3242
|
+
"kafkajs": "Kafka",
|
|
3243
|
+
"bullmq": "BullMQ",
|
|
3244
|
+
"bull": "Bull",
|
|
3245
|
+
"@google-cloud/pubsub": "GCP Pub/Sub",
|
|
3246
|
+
"bee-queue": "Bee Queue",
|
|
3247
|
+
"pg-boss": "pg-boss",
|
|
3248
|
+
"agenda": "Agenda",
|
|
3249
|
+
"@azure/service-bus": "Azure Service Bus",
|
|
3250
|
+
"@azure/event-hubs": "Azure Event Hubs",
|
|
3251
|
+
"nats": "NATS",
|
|
3252
|
+
"mqtt": "MQTT",
|
|
3253
|
+
"zeromq": "ZeroMQ",
|
|
3254
|
+
"@temporalio/client": "Temporal Client",
|
|
3255
|
+
"@temporalio/worker": "Temporal Worker",
|
|
3256
|
+
"inngest": "Inngest",
|
|
3257
|
+
"@trigger.dev/sdk": "Trigger.dev",
|
|
3258
|
+
"@defer/client": "Defer",
|
|
3259
|
+
"graphile-worker": "Graphile Worker",
|
|
3260
|
+
"@quirrel/quirrel": "Quirrel",
|
|
3261
|
+
"rhea": "AMQP 1.0 (rhea)",
|
|
3262
|
+
"stompit": "STOMP",
|
|
3263
|
+
"@eventstore/db-client": "EventStoreDB",
|
|
3264
|
+
"@aws-sdk/client-eventbridge": "AWS EventBridge",
|
|
3265
|
+
"@aws-sdk/client-kinesis": "AWS Kinesis",
|
|
3266
|
+
"@aws-sdk/client-firehose": "AWS Firehose"
|
|
3267
|
+
},
|
|
3268
|
+
observability: {
|
|
3269
|
+
"@sentry/node": "Sentry (Node)",
|
|
3270
|
+
"@sentry/browser": "Sentry (Browser)",
|
|
3271
|
+
"@sentry/react": "Sentry (React)",
|
|
3272
|
+
"@sentry/nextjs": "Sentry (Next.js)",
|
|
3273
|
+
"@sentry/vue": "Sentry (Vue)",
|
|
3274
|
+
"@sentry/angular": "Sentry (Angular)",
|
|
3275
|
+
"@opentelemetry/api": "OpenTelemetry API",
|
|
3276
|
+
"@opentelemetry/sdk-node": "OpenTelemetry SDK",
|
|
3277
|
+
"@opentelemetry/auto-instrumentations-node": "OpenTelemetry Auto",
|
|
3278
|
+
"applicationinsights": "App Insights",
|
|
3279
|
+
"dd-trace": "Datadog",
|
|
3280
|
+
"@datadog/browser-rum": "Datadog RUM",
|
|
3281
|
+
"newrelic": "New Relic",
|
|
3282
|
+
"@newrelic/browser-agent": "New Relic Browser",
|
|
3283
|
+
"pino": "Pino",
|
|
3284
|
+
"winston": "Winston",
|
|
3285
|
+
"bunyan": "Bunyan",
|
|
3286
|
+
"consola": "Consola",
|
|
3287
|
+
"loglevel": "loglevel",
|
|
3288
|
+
"roarr": "Roarr",
|
|
3289
|
+
"prom-client": "Prometheus",
|
|
3290
|
+
"elastic-apm-node": "Elastic APM",
|
|
3291
|
+
"@logtail/node": "Logtail",
|
|
3292
|
+
"@axiomhq/pino": "Axiom (Pino)",
|
|
3293
|
+
"@axiomhq/winston": "Axiom (Winston)",
|
|
3294
|
+
"@baselime/node-opentelemetry": "Baselime",
|
|
3295
|
+
"highlight.run": "Highlight",
|
|
3296
|
+
"@google-cloud/trace-agent": "GCP Trace",
|
|
3297
|
+
"@google-cloud/logging": "GCP Logging",
|
|
3298
|
+
"@azure/monitor-opentelemetry": "Azure Monitor",
|
|
3299
|
+
"lightstep-tracer": "Lightstep",
|
|
3300
|
+
"honeycomb-beeline": "Honeycomb",
|
|
3301
|
+
"@honeycombio/opentelemetry-node": "Honeycomb OTel",
|
|
3302
|
+
"posthog-node": "PostHog",
|
|
3303
|
+
"@amplitude/node": "Amplitude",
|
|
3304
|
+
"mixpanel": "Mixpanel",
|
|
3305
|
+
"@segment/analytics-node": "Segment",
|
|
3306
|
+
"analytics-node": "Segment (legacy)",
|
|
3307
|
+
"@rudderstack/rudder-sdk-node": "RudderStack",
|
|
3308
|
+
"heap-api": "Heap",
|
|
3309
|
+
"@fullstory/browser": "FullStory",
|
|
3310
|
+
"@logrocket/react": "LogRocket",
|
|
3311
|
+
"logrocket": "LogRocket",
|
|
3312
|
+
"hotjar-js": "Hotjar",
|
|
3313
|
+
"@clarity-ai/clarity": "Microsoft Clarity"
|
|
3314
|
+
},
|
|
3315
|
+
crm: {
|
|
3316
|
+
"xero-node": "Xero",
|
|
3317
|
+
"hubspot-api-client": "HubSpot",
|
|
3318
|
+
"@hubspot/api-client": "HubSpot",
|
|
3319
|
+
"@slack/web-api": "Slack Web API",
|
|
3320
|
+
"@slack/bolt": "Slack Bolt",
|
|
3321
|
+
"@slack/events-api": "Slack Events",
|
|
3322
|
+
"discord.js": "Discord",
|
|
3323
|
+
"intercom-client": "Intercom",
|
|
3324
|
+
"salesforce-sdk": "Salesforce",
|
|
3325
|
+
"jsforce": "JSforce (Salesforce)",
|
|
3326
|
+
"@zendesk/sell-client": "Zendesk",
|
|
3327
|
+
"node-zendesk": "Zendesk",
|
|
3328
|
+
"freshdesk-api": "Freshdesk",
|
|
3329
|
+
"pipedrive": "Pipedrive",
|
|
3330
|
+
"airtable": "Airtable",
|
|
3331
|
+
"@notionhq/client": "Notion",
|
|
3332
|
+
"@linear/sdk": "Linear",
|
|
3333
|
+
"jira-client": "Jira",
|
|
3334
|
+
"jira.js": "Jira.js",
|
|
3335
|
+
"@octokit/rest": "GitHub (Octokit)",
|
|
3336
|
+
"@octokit/core": "GitHub (Octokit Core)",
|
|
3337
|
+
"gitlab": "GitLab",
|
|
3338
|
+
"@gitbeaker/rest": "GitLab (Gitbeaker)",
|
|
3339
|
+
"clickup.js": "ClickUp",
|
|
3340
|
+
"asana": "Asana",
|
|
3341
|
+
"monday-sdk-js": "Monday.com",
|
|
3342
|
+
"telegram-bot-api": "Telegram",
|
|
3343
|
+
"telegraf": "Telegraf (Telegram)",
|
|
3344
|
+
"@microsoft/microsoft-graph-client": "Microsoft Graph",
|
|
3345
|
+
"googleapis": "Google APIs",
|
|
3346
|
+
"@google-cloud/aiplatform": "Google AI Platform"
|
|
3347
|
+
},
|
|
3348
|
+
storage: {
|
|
3349
|
+
"@aws-sdk/client-s3": "AWS S3",
|
|
3350
|
+
"@aws-sdk/s3-request-presigner": "AWS S3 Presigner",
|
|
3351
|
+
"@azure/storage-blob": "Azure Blob",
|
|
3352
|
+
"@azure/storage-queue": "Azure Storage Queue",
|
|
3353
|
+
"@azure/storage-file-share": "Azure File Share",
|
|
3354
|
+
"@google-cloud/storage": "GCP Storage",
|
|
3355
|
+
"minio": "MinIO",
|
|
3356
|
+
"@supabase/storage-js": "Supabase Storage",
|
|
3357
|
+
"cloudinary": "Cloudinary",
|
|
3358
|
+
"uploadthing": "UploadThing",
|
|
3359
|
+
"@uploadthing/react": "UploadThing React",
|
|
3360
|
+
"multer": "Multer",
|
|
3361
|
+
"multer-s3": "Multer S3",
|
|
3362
|
+
"formidable": "Formidable",
|
|
3363
|
+
"busboy": "Busboy",
|
|
3364
|
+
"@vercel/blob": "Vercel Blob",
|
|
3365
|
+
"imagekit": "ImageKit",
|
|
3366
|
+
"sharp": "Sharp (image)",
|
|
3367
|
+
"jimp": "Jimp (image)",
|
|
3368
|
+
"fluent-ffmpeg": "FFmpeg",
|
|
3369
|
+
"@google-cloud/vision": "GCP Vision",
|
|
3370
|
+
"@aws-sdk/client-rekognition": "AWS Rekognition",
|
|
3371
|
+
"file-type": "file-type",
|
|
3372
|
+
"@backblaze-b2/sdk": "Backblaze B2",
|
|
3373
|
+
"wasabi-sdk": "Wasabi"
|
|
3374
|
+
},
|
|
3375
|
+
search: {
|
|
3376
|
+
"@elastic/elasticsearch": "Elasticsearch",
|
|
3377
|
+
"algoliasearch": "Algolia",
|
|
3378
|
+
"@algolia/client-search": "Algolia Client",
|
|
3379
|
+
"react-instantsearch": "Algolia InstantSearch",
|
|
3380
|
+
"meilisearch": "Meilisearch",
|
|
3381
|
+
"typesense": "Typesense",
|
|
3382
|
+
"@opensearch-project/opensearch": "OpenSearch",
|
|
3383
|
+
"solr-client": "Solr",
|
|
3384
|
+
"lunr": "Lunr",
|
|
3385
|
+
"flexsearch": "FlexSearch",
|
|
3386
|
+
"fuse.js": "Fuse.js",
|
|
3387
|
+
"minisearch": "MiniSearch",
|
|
3388
|
+
"orama": "Orama",
|
|
3389
|
+
"@orama/orama": "Orama",
|
|
3390
|
+
"@trieve/trieve-ts-sdk": "Trieve",
|
|
3391
|
+
"pagefind": "Pagefind",
|
|
3392
|
+
// Vector / AI search
|
|
3393
|
+
"@pinecone-database/pinecone": "Pinecone",
|
|
3394
|
+
"chromadb": "Chroma",
|
|
3395
|
+
"@qdrant/js-client-rest": "Qdrant",
|
|
3396
|
+
"weaviate-ts-client": "Weaviate",
|
|
3397
|
+
"@zilliz/milvus2-sdk-node": "Milvus",
|
|
3398
|
+
"openai": "OpenAI",
|
|
3399
|
+
"@anthropic-ai/sdk": "Anthropic",
|
|
3400
|
+
"@langchain/core": "LangChain",
|
|
3401
|
+
"langchain": "LangChain",
|
|
3402
|
+
"@google/generative-ai": "Google Gemini",
|
|
3403
|
+
"cohere-ai": "Cohere",
|
|
3404
|
+
"replicate": "Replicate",
|
|
3405
|
+
"@huggingface/inference": "Hugging Face",
|
|
3406
|
+
"ai": "Vercel AI SDK",
|
|
3407
|
+
"@ai-sdk/openai": "AI SDK (OpenAI)"
|
|
3408
|
+
}
|
|
3409
|
+
};
|
|
3410
|
+
function scanServiceDependencies(projects) {
|
|
3411
|
+
const packageVersions = /* @__PURE__ */ new Map();
|
|
3412
|
+
for (const project of projects) {
|
|
3413
|
+
for (const dep of project.dependencies) {
|
|
3414
|
+
if (!packageVersions.has(dep.package)) {
|
|
3415
|
+
packageVersions.set(dep.package, dep.resolvedVersion);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
const result = {
|
|
3420
|
+
payment: [],
|
|
3421
|
+
auth: [],
|
|
3422
|
+
email: [],
|
|
3423
|
+
cloud: [],
|
|
3424
|
+
databases: [],
|
|
3425
|
+
messaging: [],
|
|
3426
|
+
observability: [],
|
|
3427
|
+
crm: [],
|
|
3428
|
+
storage: [],
|
|
3429
|
+
search: []
|
|
3430
|
+
};
|
|
3431
|
+
for (const [category, packages] of Object.entries(SERVICE_CATEGORIES)) {
|
|
3432
|
+
const items = [];
|
|
3433
|
+
for (const [pkg2, displayName] of Object.entries(packages)) {
|
|
3434
|
+
if (packageVersions.has(pkg2)) {
|
|
3435
|
+
items.push({
|
|
3436
|
+
name: displayName,
|
|
3437
|
+
package: pkg2,
|
|
3438
|
+
version: packageVersions.get(pkg2) ?? null
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
3443
|
+
result[category] = items;
|
|
3444
|
+
}
|
|
3445
|
+
return result;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// src/commands/scan.ts
|
|
3449
|
+
async function runScan(rootDir, opts) {
|
|
3450
|
+
const config = await loadConfig(rootDir);
|
|
3451
|
+
const sem = new Semaphore(opts.concurrency);
|
|
3452
|
+
const npmCache = new NpmCache(rootDir, sem);
|
|
3453
|
+
const scanners = config.scanners;
|
|
3454
|
+
const progress = new ScanProgress(rootDir);
|
|
3455
|
+
const steps = [
|
|
3456
|
+
{ id: "config", label: "Loading configuration" },
|
|
3457
|
+
{ id: "vcs", label: "Detecting version control" },
|
|
3458
|
+
{ id: "node", label: "Scanning Node projects" },
|
|
3459
|
+
{ id: "dotnet", label: "Scanning .NET projects" },
|
|
3460
|
+
...scanners !== false ? [
|
|
3461
|
+
...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
3462
|
+
...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
3463
|
+
...scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
|
|
3464
|
+
...scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
|
|
3465
|
+
...scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
|
|
3466
|
+
...scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
|
|
3467
|
+
...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
|
|
3468
|
+
...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
|
|
3469
|
+
...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
|
|
3470
|
+
...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : []
|
|
3471
|
+
] : [],
|
|
3472
|
+
{ id: "drift", label: "Computing drift score" },
|
|
3473
|
+
{ id: "findings", label: "Generating findings" }
|
|
3474
|
+
];
|
|
3475
|
+
progress.setSteps(steps);
|
|
3476
|
+
progress.completeStep("config", "loaded");
|
|
3477
|
+
progress.startStep("vcs");
|
|
3478
|
+
const vcs = await detectVcs(rootDir);
|
|
3479
|
+
const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
|
|
3480
|
+
progress.completeStep("vcs", vcsDetail);
|
|
3481
|
+
progress.startStep("node");
|
|
3482
|
+
const nodeProjects = await scanNodeProjects(rootDir, npmCache);
|
|
3483
|
+
for (const p of nodeProjects) {
|
|
3484
|
+
progress.addDependencies(p.dependencies.length);
|
|
3485
|
+
progress.addFrameworks(p.frameworks.length);
|
|
3486
|
+
}
|
|
3487
|
+
progress.addProjects(nodeProjects.length);
|
|
3488
|
+
progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
|
|
3489
|
+
progress.startStep("dotnet");
|
|
3490
|
+
const dotnetProjects = await scanDotnetProjects(rootDir);
|
|
3491
|
+
for (const p of dotnetProjects) {
|
|
3492
|
+
progress.addDependencies(p.dependencies.length);
|
|
3493
|
+
progress.addFrameworks(p.frameworks.length);
|
|
3494
|
+
}
|
|
3495
|
+
progress.addProjects(dotnetProjects.length);
|
|
3496
|
+
progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
|
|
3497
|
+
const allProjects = [...nodeProjects, ...dotnetProjects];
|
|
3498
|
+
const extended = {};
|
|
3499
|
+
if (scanners !== false) {
|
|
3500
|
+
if (scanners?.platformMatrix?.enabled !== false) {
|
|
3501
|
+
progress.startStep("platform");
|
|
3502
|
+
extended.platformMatrix = await scanPlatformMatrix(rootDir);
|
|
3503
|
+
const nativeCount = extended.platformMatrix.nativeModules.length;
|
|
3504
|
+
const dockerCount = extended.platformMatrix.dockerBaseImages.length;
|
|
3505
|
+
const parts = [];
|
|
3506
|
+
if (nativeCount > 0) parts.push(`${nativeCount} native`);
|
|
3507
|
+
if (dockerCount > 0) parts.push(`${dockerCount} docker`);
|
|
3508
|
+
progress.completeStep("platform", parts.join(", ") || "clean", nativeCount + dockerCount);
|
|
3509
|
+
}
|
|
3510
|
+
if (scanners?.toolingInventory?.enabled !== false) {
|
|
3511
|
+
progress.startStep("tooling");
|
|
3512
|
+
extended.toolingInventory = scanToolingInventory(allProjects);
|
|
3513
|
+
const toolCount = Object.values(extended.toolingInventory).reduce((sum, arr) => sum + arr.length, 0);
|
|
3514
|
+
progress.completeStep("tooling", `${toolCount} tool${toolCount !== 1 ? "s" : ""} mapped`, toolCount);
|
|
3515
|
+
}
|
|
3516
|
+
if (scanners?.serviceDependencies?.enabled !== false) {
|
|
3517
|
+
progress.startStep("services");
|
|
3518
|
+
extended.serviceDependencies = scanServiceDependencies(allProjects);
|
|
3519
|
+
const svcCount = Object.values(extended.serviceDependencies).reduce((sum, arr) => sum + arr.length, 0);
|
|
3520
|
+
progress.completeStep("services", `${svcCount} service${svcCount !== 1 ? "s" : ""} detected`, svcCount);
|
|
3521
|
+
}
|
|
3522
|
+
if (scanners?.breakingChangeExposure?.enabled !== false) {
|
|
3523
|
+
progress.startStep("breaking");
|
|
3524
|
+
extended.breakingChangeExposure = scanBreakingChangeExposure(allProjects);
|
|
3525
|
+
const bc = extended.breakingChangeExposure;
|
|
3526
|
+
const bcTotal = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
|
|
3527
|
+
progress.completeStep(
|
|
3528
|
+
"breaking",
|
|
3529
|
+
bcTotal > 0 ? `${bc.deprecatedPackages.length} deprecated, ${bc.legacyPolyfills.length} polyfills` : "none found",
|
|
3530
|
+
bcTotal
|
|
3531
|
+
);
|
|
3532
|
+
}
|
|
3533
|
+
if (scanners?.securityPosture?.enabled !== false) {
|
|
3534
|
+
progress.startStep("security");
|
|
3535
|
+
extended.securityPosture = await scanSecurityPosture(rootDir);
|
|
3536
|
+
const sec = extended.securityPosture;
|
|
3537
|
+
const secDetail = sec.lockfilePresent ? `lockfile \u2714${sec.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
|
|
3538
|
+
progress.completeStep("security", secDetail);
|
|
3539
|
+
}
|
|
3540
|
+
if (scanners?.buildDeploy?.enabled !== false) {
|
|
3541
|
+
progress.startStep("build");
|
|
3542
|
+
extended.buildDeploy = await scanBuildDeploy(rootDir);
|
|
3543
|
+
const bd = extended.buildDeploy;
|
|
3544
|
+
const bdParts = [];
|
|
3545
|
+
if (bd.ci.length > 0) bdParts.push(bd.ci.join(", "));
|
|
3546
|
+
if (bd.docker.dockerfileCount > 0) bdParts.push(`${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""}`);
|
|
3547
|
+
progress.completeStep("build", bdParts.join(" \xB7 ") || "none detected");
|
|
3548
|
+
}
|
|
3549
|
+
if (scanners?.tsModernity?.enabled !== false) {
|
|
3550
|
+
progress.startStep("ts");
|
|
3551
|
+
extended.tsModernity = await scanTsModernity(rootDir);
|
|
3552
|
+
const ts = extended.tsModernity;
|
|
3553
|
+
const tsParts = [];
|
|
3554
|
+
if (ts.typescriptVersion) tsParts.push(`v${ts.typescriptVersion}`);
|
|
3555
|
+
if (ts.strict === true) tsParts.push("strict");
|
|
3556
|
+
if (ts.moduleType) tsParts.push(ts.moduleType.toUpperCase());
|
|
3557
|
+
progress.completeStep("ts", tsParts.join(" \xB7 ") || "no tsconfig");
|
|
3558
|
+
}
|
|
3559
|
+
if (scanners?.fileHotspots?.enabled !== false) {
|
|
3560
|
+
progress.startStep("hotspots");
|
|
3561
|
+
extended.fileHotspots = await scanFileHotspots(rootDir);
|
|
3562
|
+
progress.completeStep("hotspots", `${extended.fileHotspots.totalFiles} files`, extended.fileHotspots.totalFiles);
|
|
3563
|
+
}
|
|
3564
|
+
if (scanners?.dependencyGraph?.enabled !== false) {
|
|
3565
|
+
progress.startStep("depgraph");
|
|
3566
|
+
extended.dependencyGraph = await scanDependencyGraph(rootDir);
|
|
3567
|
+
const dg = extended.dependencyGraph;
|
|
3568
|
+
const dgDetail = dg.lockfileType ? `${dg.lockfileType} \xB7 ${dg.totalUnique} unique` : "no lockfile";
|
|
3569
|
+
progress.completeStep("depgraph", dgDetail, dg.totalUnique);
|
|
3570
|
+
}
|
|
3571
|
+
if (scanners?.dependencyRisk?.enabled !== false) {
|
|
3572
|
+
progress.startStep("deprisk");
|
|
3573
|
+
extended.dependencyRisk = scanDependencyRisk(allProjects);
|
|
3574
|
+
const dr = extended.dependencyRisk;
|
|
3575
|
+
const drParts = [];
|
|
3576
|
+
if (dr.deprecatedPackages.length > 0) drParts.push(`${dr.deprecatedPackages.length} deprecated`);
|
|
3577
|
+
if (dr.nativeModulePackages.length > 0) drParts.push(`${dr.nativeModulePackages.length} native`);
|
|
3578
|
+
progress.completeStep("deprisk", drParts.join(", ") || "low risk");
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
progress.startStep("drift");
|
|
3582
|
+
const drift = computeDriftScore(allProjects);
|
|
3583
|
+
progress.completeStep("drift", `${drift.score}/100 \u2014 ${drift.riskLevel} risk`);
|
|
3584
|
+
progress.startStep("findings");
|
|
3585
|
+
const findings = generateFindings(allProjects, config);
|
|
3586
|
+
const warnCount = findings.filter((f) => f.level === "warning").length;
|
|
3587
|
+
const errCount = findings.filter((f) => f.level === "error").length;
|
|
3588
|
+
const noteCount = findings.filter((f) => f.level === "note").length;
|
|
3589
|
+
progress.addFindings(warnCount, errCount, noteCount);
|
|
3590
|
+
const findingParts = [];
|
|
3591
|
+
if (errCount > 0) findingParts.push(`${errCount} error${errCount !== 1 ? "s" : ""}`);
|
|
3592
|
+
if (warnCount > 0) findingParts.push(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`);
|
|
3593
|
+
if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
|
|
3594
|
+
progress.completeStep("findings", findingParts.join(", ") || "none");
|
|
3595
|
+
progress.finish();
|
|
3596
|
+
if (allProjects.length === 0) {
|
|
3597
|
+
console.log(chalk3.yellow("No projects found."));
|
|
3598
|
+
}
|
|
3599
|
+
const artifact = {
|
|
3600
|
+
schemaVersion: "1.0",
|
|
3601
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3602
|
+
vibgrateVersion: VERSION,
|
|
3603
|
+
rootPath: path12.basename(rootDir),
|
|
3604
|
+
...vcs.type !== "unknown" ? { vcs } : {},
|
|
3605
|
+
projects: allProjects,
|
|
3606
|
+
drift,
|
|
3607
|
+
findings,
|
|
3608
|
+
...Object.keys(extended).length > 0 ? { extended } : {}
|
|
3609
|
+
};
|
|
3610
|
+
if (opts.baseline) {
|
|
3611
|
+
const baselinePath = path12.resolve(opts.baseline);
|
|
3612
|
+
if (await pathExists(baselinePath)) {
|
|
3613
|
+
try {
|
|
3614
|
+
const baseline = await readJsonFile(baselinePath);
|
|
3615
|
+
artifact.baseline = baselinePath;
|
|
3616
|
+
artifact.delta = artifact.drift.score - baseline.drift.score;
|
|
3617
|
+
} catch {
|
|
3618
|
+
console.error(chalk3.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
const vibgrateDir = path12.join(rootDir, ".vibgrate");
|
|
3623
|
+
await ensureDir(vibgrateDir);
|
|
3624
|
+
await writeJsonFile(path12.join(vibgrateDir, "scan_result.json"), artifact);
|
|
3625
|
+
if (opts.format === "json") {
|
|
3626
|
+
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
3627
|
+
if (opts.out) {
|
|
3628
|
+
await writeTextFile(path12.resolve(opts.out), jsonStr);
|
|
3629
|
+
console.log(chalk3.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
3630
|
+
} else {
|
|
3631
|
+
console.log(jsonStr);
|
|
3632
|
+
}
|
|
3633
|
+
} else if (opts.format === "sarif") {
|
|
3634
|
+
const sarif = formatSarif(artifact);
|
|
3635
|
+
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
3636
|
+
if (opts.out) {
|
|
3637
|
+
await writeTextFile(path12.resolve(opts.out), sarifStr);
|
|
3638
|
+
console.log(chalk3.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
3639
|
+
} else {
|
|
3640
|
+
console.log(sarifStr);
|
|
3641
|
+
}
|
|
3642
|
+
} else {
|
|
3643
|
+
const text = formatText(artifact);
|
|
3644
|
+
console.log(text);
|
|
3645
|
+
if (opts.out) {
|
|
3646
|
+
await writeTextFile(path12.resolve(opts.out), text);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
return artifact;
|
|
3650
|
+
}
|
|
3651
|
+
var scanCommand = new Command("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").action(async (targetPath, opts) => {
|
|
3652
|
+
const rootDir = path12.resolve(targetPath);
|
|
3653
|
+
if (!await pathExists(rootDir)) {
|
|
3654
|
+
console.error(chalk3.red(`Path does not exist: ${rootDir}`));
|
|
3655
|
+
process.exit(1);
|
|
3656
|
+
}
|
|
3657
|
+
const scanOpts = {
|
|
3658
|
+
out: opts.out,
|
|
3659
|
+
format: opts.format || "text",
|
|
3660
|
+
failOn: opts.failOn,
|
|
3661
|
+
baseline: opts.baseline,
|
|
3662
|
+
changedOnly: opts.changedOnly,
|
|
3663
|
+
concurrency: parseInt(opts.concurrency, 10) || 8
|
|
3664
|
+
};
|
|
3665
|
+
const artifact = await runScan(rootDir, scanOpts);
|
|
3666
|
+
if (opts.failOn) {
|
|
3667
|
+
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
3668
|
+
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
3669
|
+
if (opts.failOn === "error" && hasErrors) {
|
|
3670
|
+
console.error(chalk3.red(`
|
|
3671
|
+
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
3672
|
+
process.exit(2);
|
|
3673
|
+
}
|
|
3674
|
+
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
3675
|
+
console.error(chalk3.red(`
|
|
3676
|
+
Failing: findings detected at warn level or above.`));
|
|
3677
|
+
process.exit(2);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
});
|
|
3681
|
+
|
|
3682
|
+
export {
|
|
3683
|
+
readJsonFile,
|
|
3684
|
+
readTextFile,
|
|
3685
|
+
pathExists,
|
|
3686
|
+
ensureDir,
|
|
3687
|
+
writeJsonFile,
|
|
3688
|
+
writeTextFile,
|
|
3689
|
+
writeDefaultConfig,
|
|
3690
|
+
computeDriftScore,
|
|
3691
|
+
generateFindings,
|
|
3692
|
+
formatText,
|
|
3693
|
+
formatSarif,
|
|
3694
|
+
VERSION,
|
|
3695
|
+
runScan,
|
|
3696
|
+
scanCommand
|
|
3697
|
+
};
|