@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
package/dist/chunk-DLRBJYO6.js
DELETED
|
@@ -1,1077 +0,0 @@
|
|
|
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 fs4 = frameworkScore(projects);
|
|
156
|
-
const ds = dependencyScore(projects);
|
|
157
|
-
const es = eolScore(projects);
|
|
158
|
-
const score = Math.round(rs * 0.25 + fs4 * 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: fs4,
|
|
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
|
-
lines.push(chalk.dim(` Scanned at ${artifact.timestamp}`));
|
|
311
|
-
lines.push("");
|
|
312
|
-
return lines.join("\n");
|
|
313
|
-
}
|
|
314
|
-
function riskBadge(level) {
|
|
315
|
-
switch (level) {
|
|
316
|
-
case "low":
|
|
317
|
-
return chalk.bgGreen.black(" LOW ");
|
|
318
|
-
case "moderate":
|
|
319
|
-
return chalk.bgYellow.black(" MODERATE ");
|
|
320
|
-
case "high":
|
|
321
|
-
return chalk.bgRed.white(" HIGH ");
|
|
322
|
-
default:
|
|
323
|
-
return level;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function scoreBar(score) {
|
|
327
|
-
const width = 20;
|
|
328
|
-
const filled = Math.round(score / 100 * width);
|
|
329
|
-
const empty = width - filled;
|
|
330
|
-
const color = score >= 70 ? chalk.green : score >= 40 ? chalk.yellow : chalk.red;
|
|
331
|
-
return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + ` ${score}`;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// src/formatters/sarif.ts
|
|
335
|
-
function formatSarif(artifact) {
|
|
336
|
-
const rules = buildRules(artifact.findings);
|
|
337
|
-
const results = artifact.findings.map((f) => toSarifResult(f));
|
|
338
|
-
return {
|
|
339
|
-
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
|
|
340
|
-
version: "2.1.0",
|
|
341
|
-
runs: [
|
|
342
|
-
{
|
|
343
|
-
tool: {
|
|
344
|
-
driver: {
|
|
345
|
-
name: "vibgrate",
|
|
346
|
-
version: artifact.vibgrateVersion,
|
|
347
|
-
informationUri: "https://vibgrate.com",
|
|
348
|
-
rules
|
|
349
|
-
}
|
|
350
|
-
},
|
|
351
|
-
results,
|
|
352
|
-
invocations: [
|
|
353
|
-
{
|
|
354
|
-
executionSuccessful: true,
|
|
355
|
-
startTimeUtc: artifact.timestamp
|
|
356
|
-
}
|
|
357
|
-
]
|
|
358
|
-
}
|
|
359
|
-
]
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
function buildRules(findings) {
|
|
363
|
-
const ruleIds = [...new Set(findings.map((f) => f.ruleId))];
|
|
364
|
-
return ruleIds.map((id) => {
|
|
365
|
-
const descriptions = {
|
|
366
|
-
"vibgrate/runtime-eol": {
|
|
367
|
-
id: "vibgrate/runtime-eol",
|
|
368
|
-
shortDescription: { text: "Runtime at or past end-of-life" },
|
|
369
|
-
helpUri: "https://vibgrate.com/rules/runtime-eol"
|
|
370
|
-
},
|
|
371
|
-
"vibgrate/runtime-lag": {
|
|
372
|
-
id: "vibgrate/runtime-lag",
|
|
373
|
-
shortDescription: { text: "Runtime major version lag" },
|
|
374
|
-
helpUri: "https://vibgrate.com/rules/runtime-lag"
|
|
375
|
-
},
|
|
376
|
-
"vibgrate/framework-major-lag": {
|
|
377
|
-
id: "vibgrate/framework-major-lag",
|
|
378
|
-
shortDescription: { text: "Framework major version behind latest" },
|
|
379
|
-
helpUri: "https://vibgrate.com/rules/framework-major-lag"
|
|
380
|
-
},
|
|
381
|
-
"vibgrate/dependency-rot": {
|
|
382
|
-
id: "vibgrate/dependency-rot",
|
|
383
|
-
shortDescription: { text: "High percentage of outdated dependencies" },
|
|
384
|
-
helpUri: "https://vibgrate.com/rules/dependency-rot"
|
|
385
|
-
},
|
|
386
|
-
"vibgrate/dependency-major-lag": {
|
|
387
|
-
id: "vibgrate/dependency-major-lag",
|
|
388
|
-
shortDescription: { text: "Individual dependency severely behind" },
|
|
389
|
-
helpUri: "https://vibgrate.com/rules/dependency-major-lag"
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
return descriptions[id] ?? {
|
|
393
|
-
id,
|
|
394
|
-
shortDescription: { text: id },
|
|
395
|
-
helpUri: "https://vibgrate.com"
|
|
396
|
-
};
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
function toSarifResult(finding) {
|
|
400
|
-
return {
|
|
401
|
-
ruleId: finding.ruleId,
|
|
402
|
-
level: finding.level === "error" ? "error" : finding.level === "warning" ? "warning" : "note",
|
|
403
|
-
message: { text: finding.message },
|
|
404
|
-
locations: [
|
|
405
|
-
{
|
|
406
|
-
physicalLocation: {
|
|
407
|
-
artifactLocation: {
|
|
408
|
-
uri: finding.location
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
]
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// src/version.ts
|
|
417
|
-
import { createRequire } from "module";
|
|
418
|
-
var require2 = createRequire(import.meta.url);
|
|
419
|
-
var pkg = require2("../package.json");
|
|
420
|
-
var VERSION = pkg.version;
|
|
421
|
-
|
|
422
|
-
// src/commands/scan.ts
|
|
423
|
-
import * as path6 from "path";
|
|
424
|
-
import { Command } from "commander";
|
|
425
|
-
import chalk2 from "chalk";
|
|
426
|
-
|
|
427
|
-
// src/scanners/node-scanner.ts
|
|
428
|
-
import * as path2 from "path";
|
|
429
|
-
import * as semver2 from "semver";
|
|
430
|
-
|
|
431
|
-
// src/scanners/npm-cache.ts
|
|
432
|
-
import { spawn } from "child_process";
|
|
433
|
-
import * as semver from "semver";
|
|
434
|
-
function stableOnly(versions) {
|
|
435
|
-
return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
|
|
436
|
-
}
|
|
437
|
-
function maxStable(versions) {
|
|
438
|
-
const stable = stableOnly(versions);
|
|
439
|
-
if (stable.length === 0) return null;
|
|
440
|
-
return stable.sort(semver.rcompare)[0] ?? null;
|
|
441
|
-
}
|
|
442
|
-
async function npmViewJson(args, cwd) {
|
|
443
|
-
return new Promise((resolve4, reject) => {
|
|
444
|
-
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
445
|
-
cwd,
|
|
446
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
447
|
-
});
|
|
448
|
-
let out = "";
|
|
449
|
-
let err = "";
|
|
450
|
-
child.stdout.on("data", (d) => out += String(d));
|
|
451
|
-
child.stderr.on("data", (d) => err += String(d));
|
|
452
|
-
child.on("error", reject);
|
|
453
|
-
child.on("close", (code) => {
|
|
454
|
-
if (code !== 0) {
|
|
455
|
-
reject(new Error(`npm view ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
const trimmed = out.trim();
|
|
459
|
-
if (!trimmed) {
|
|
460
|
-
resolve4(null);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
try {
|
|
464
|
-
resolve4(JSON.parse(trimmed));
|
|
465
|
-
} catch {
|
|
466
|
-
resolve4(trimmed.replace(/^"|"$/g, ""));
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
var NpmCache = class {
|
|
472
|
-
constructor(cwd, sem) {
|
|
473
|
-
this.cwd = cwd;
|
|
474
|
-
this.sem = sem;
|
|
475
|
-
}
|
|
476
|
-
meta = /* @__PURE__ */ new Map();
|
|
477
|
-
get(pkg2) {
|
|
478
|
-
const existing = this.meta.get(pkg2);
|
|
479
|
-
if (existing) return existing;
|
|
480
|
-
const p = this.sem.run(async () => {
|
|
481
|
-
let latest = null;
|
|
482
|
-
try {
|
|
483
|
-
const dist = await npmViewJson([pkg2, "dist-tags"], this.cwd);
|
|
484
|
-
if (dist && typeof dist === "object" && typeof dist.latest === "string") {
|
|
485
|
-
latest = dist.latest;
|
|
486
|
-
}
|
|
487
|
-
} catch {
|
|
488
|
-
}
|
|
489
|
-
let versions = [];
|
|
490
|
-
try {
|
|
491
|
-
const v = await npmViewJson([pkg2, "versions"], this.cwd);
|
|
492
|
-
if (Array.isArray(v)) versions = v.map(String);
|
|
493
|
-
else if (typeof v === "string") versions = [v];
|
|
494
|
-
} catch {
|
|
495
|
-
}
|
|
496
|
-
const stable = stableOnly(versions);
|
|
497
|
-
const latestStableOverall = maxStable(stable);
|
|
498
|
-
if (!latest && latestStableOverall) latest = latestStableOverall;
|
|
499
|
-
return { latest, stableVersions: stable, latestStableOverall };
|
|
500
|
-
});
|
|
501
|
-
this.meta.set(pkg2, p);
|
|
502
|
-
return p;
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
function isSemverSpec(spec) {
|
|
506
|
-
const s = spec.trim();
|
|
507
|
-
if (!s) return false;
|
|
508
|
-
if (s.startsWith("workspace:")) return false;
|
|
509
|
-
if (s.startsWith("file:")) return false;
|
|
510
|
-
if (s.startsWith("link:")) return false;
|
|
511
|
-
if (s.startsWith("git+")) return false;
|
|
512
|
-
if (s.includes("://")) return false;
|
|
513
|
-
if (s.startsWith("github:")) return false;
|
|
514
|
-
if (s === "*" || s.toLowerCase() === "latest") return true;
|
|
515
|
-
return semver.validRange(s) !== null;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// src/scanners/node-scanner.ts
|
|
519
|
-
var KNOWN_FRAMEWORKS = {
|
|
520
|
-
"next": "Next.js",
|
|
521
|
-
"@nestjs/core": "NestJS",
|
|
522
|
-
"react": "React",
|
|
523
|
-
"vue": "Vue",
|
|
524
|
-
"express": "Express",
|
|
525
|
-
"@angular/core": "Angular",
|
|
526
|
-
"svelte": "Svelte",
|
|
527
|
-
"nuxt": "Nuxt",
|
|
528
|
-
"fastify": "Fastify",
|
|
529
|
-
"hono": "Hono",
|
|
530
|
-
"typescript": "TypeScript"
|
|
531
|
-
};
|
|
532
|
-
async function scanNodeProjects(rootDir, npmCache) {
|
|
533
|
-
const packageJsonFiles = await findPackageJsonFiles(rootDir);
|
|
534
|
-
const results = [];
|
|
535
|
-
for (const pjPath of packageJsonFiles) {
|
|
536
|
-
try {
|
|
537
|
-
const scan = await scanOnePackageJson(pjPath, rootDir, npmCache);
|
|
538
|
-
results.push(scan);
|
|
539
|
-
} catch (e) {
|
|
540
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
541
|
-
console.error(`Error scanning ${pjPath}: ${msg}`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return results;
|
|
545
|
-
}
|
|
546
|
-
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
547
|
-
const pj = await readJsonFile(packageJsonPath);
|
|
548
|
-
const absProjectPath = path2.dirname(packageJsonPath);
|
|
549
|
-
const projectPath = path2.relative(rootDir, absProjectPath) || ".";
|
|
550
|
-
const nodeEngine = pj.engines?.node ?? void 0;
|
|
551
|
-
let runtimeLatest;
|
|
552
|
-
let runtimeMajorsBehind;
|
|
553
|
-
if (nodeEngine) {
|
|
554
|
-
const latestLtsMajor = 22;
|
|
555
|
-
const parsed = semver2.minVersion(nodeEngine);
|
|
556
|
-
if (parsed) {
|
|
557
|
-
const currentMajor = semver2.major(parsed);
|
|
558
|
-
runtimeLatest = `${latestLtsMajor}.0.0`;
|
|
559
|
-
runtimeMajorsBehind = Math.max(0, latestLtsMajor - currentMajor);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
const sections = [
|
|
563
|
-
{ name: "dependencies", deps: pj.dependencies },
|
|
564
|
-
{ name: "devDependencies", deps: pj.devDependencies },
|
|
565
|
-
{ name: "peerDependencies", deps: pj.peerDependencies },
|
|
566
|
-
{ name: "optionalDependencies", deps: pj.optionalDependencies }
|
|
567
|
-
];
|
|
568
|
-
const dependencies = [];
|
|
569
|
-
const frameworks = [];
|
|
570
|
-
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: 0 };
|
|
571
|
-
const depEntries = [];
|
|
572
|
-
for (const s of sections) {
|
|
573
|
-
if (!s.deps) continue;
|
|
574
|
-
for (const [pkg2, spec] of Object.entries(s.deps)) {
|
|
575
|
-
if (!isSemverSpec(spec)) continue;
|
|
576
|
-
depEntries.push({ pkg: pkg2, section: s.name, spec });
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const metaPromises = depEntries.map(async (entry) => {
|
|
580
|
-
const meta = await npmCache.get(entry.pkg);
|
|
581
|
-
return { ...entry, meta };
|
|
582
|
-
});
|
|
583
|
-
const resolved = await Promise.all(metaPromises);
|
|
584
|
-
for (const { pkg: pkg2, section, spec, meta } of resolved) {
|
|
585
|
-
const resolvedVersion = meta.stableVersions.length > 0 ? semver2.maxSatisfying(meta.stableVersions, spec) ?? null : null;
|
|
586
|
-
const latestStable = meta.latestStableOverall;
|
|
587
|
-
let majorsBehind = null;
|
|
588
|
-
let drift = "unknown";
|
|
589
|
-
if (resolvedVersion && latestStable) {
|
|
590
|
-
const currentMajor = semver2.major(resolvedVersion);
|
|
591
|
-
const latestMajor = semver2.major(latestStable);
|
|
592
|
-
majorsBehind = latestMajor - currentMajor;
|
|
593
|
-
if (majorsBehind === 0) {
|
|
594
|
-
drift = semver2.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
|
|
595
|
-
} else {
|
|
596
|
-
drift = "major-behind";
|
|
597
|
-
}
|
|
598
|
-
if (majorsBehind === 0) buckets.current++;
|
|
599
|
-
else if (majorsBehind === 1) buckets.oneBehind++;
|
|
600
|
-
else buckets.twoPlusBehind++;
|
|
601
|
-
} else {
|
|
602
|
-
buckets.unknown++;
|
|
603
|
-
}
|
|
604
|
-
dependencies.push({
|
|
605
|
-
package: pkg2,
|
|
606
|
-
section,
|
|
607
|
-
currentSpec: spec,
|
|
608
|
-
resolvedVersion,
|
|
609
|
-
latestStable,
|
|
610
|
-
majorsBehind,
|
|
611
|
-
drift
|
|
612
|
-
});
|
|
613
|
-
if (pkg2 in KNOWN_FRAMEWORKS) {
|
|
614
|
-
frameworks.push({
|
|
615
|
-
name: KNOWN_FRAMEWORKS[pkg2],
|
|
616
|
-
currentVersion: resolvedVersion,
|
|
617
|
-
latestVersion: latestStable,
|
|
618
|
-
majorsBehind
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
dependencies.sort((a, b) => {
|
|
623
|
-
const order = { "major-behind": 0, "minor-behind": 1, "current": 2, "unknown": 3 };
|
|
624
|
-
const diff = (order[a.drift] ?? 9) - (order[b.drift] ?? 9);
|
|
625
|
-
if (diff !== 0) return diff;
|
|
626
|
-
return a.package.localeCompare(b.package);
|
|
627
|
-
});
|
|
628
|
-
return {
|
|
629
|
-
type: "node",
|
|
630
|
-
path: projectPath,
|
|
631
|
-
name: pj.name ?? path2.basename(absProjectPath),
|
|
632
|
-
runtime: nodeEngine,
|
|
633
|
-
runtimeLatest,
|
|
634
|
-
runtimeMajorsBehind,
|
|
635
|
-
frameworks,
|
|
636
|
-
dependencies,
|
|
637
|
-
dependencyAgeBuckets: buckets
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// src/scanners/dotnet-scanner.ts
|
|
642
|
-
import * as path3 from "path";
|
|
643
|
-
import { XMLParser } from "fast-xml-parser";
|
|
644
|
-
var parser = new XMLParser({
|
|
645
|
-
ignoreAttributes: false,
|
|
646
|
-
attributeNamePrefix: "@_"
|
|
647
|
-
});
|
|
648
|
-
var KNOWN_DOTNET_FRAMEWORKS = {
|
|
649
|
-
"Microsoft.AspNetCore.App": "ASP.NET Core",
|
|
650
|
-
"Microsoft.EntityFrameworkCore": "EF Core",
|
|
651
|
-
"Microsoft.Extensions.Hosting": ".NET Hosting",
|
|
652
|
-
"Swashbuckle.AspNetCore": "Swashbuckle",
|
|
653
|
-
"MediatR": "MediatR",
|
|
654
|
-
"AutoMapper": "AutoMapper",
|
|
655
|
-
"FluentValidation": "FluentValidation",
|
|
656
|
-
"Serilog": "Serilog",
|
|
657
|
-
"xunit": "xUnit",
|
|
658
|
-
"NUnit": "NUnit",
|
|
659
|
-
"Moq": "Moq"
|
|
660
|
-
};
|
|
661
|
-
var LATEST_DOTNET_MAJOR = 9;
|
|
662
|
-
function parseTfmMajor(tfm) {
|
|
663
|
-
const match = tfm.match(/^net(\d+)\.\d+$/);
|
|
664
|
-
if (match?.[1]) return parseInt(match[1], 10);
|
|
665
|
-
const coreMatch = tfm.match(/^netcoreapp(\d+)\.\d+$/);
|
|
666
|
-
if (coreMatch?.[1]) return parseInt(coreMatch[1], 10);
|
|
667
|
-
if (tfm.startsWith("netstandard")) return null;
|
|
668
|
-
const fxMatch = tfm.match(/^net(\d)(\d+)?$/);
|
|
669
|
-
if (fxMatch) return null;
|
|
670
|
-
return null;
|
|
671
|
-
}
|
|
672
|
-
function parseCsproj(xml, filePath) {
|
|
673
|
-
const parsed = parser.parse(xml);
|
|
674
|
-
const project = parsed?.Project;
|
|
675
|
-
if (!project) {
|
|
676
|
-
return { targetFrameworks: [], packageReferences: [], projectName: path3.basename(filePath, ".csproj") };
|
|
677
|
-
}
|
|
678
|
-
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
679
|
-
const targetFrameworks = [];
|
|
680
|
-
for (const pg of propertyGroups) {
|
|
681
|
-
if (pg.TargetFramework) {
|
|
682
|
-
targetFrameworks.push(String(pg.TargetFramework));
|
|
683
|
-
}
|
|
684
|
-
if (pg.TargetFrameworks) {
|
|
685
|
-
const tfms = String(pg.TargetFrameworks).split(";").map((s) => s.trim()).filter(Boolean);
|
|
686
|
-
targetFrameworks.push(...tfms);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
const itemGroups = Array.isArray(project.ItemGroup) ? project.ItemGroup : project.ItemGroup ? [project.ItemGroup] : [];
|
|
690
|
-
const packageReferences = [];
|
|
691
|
-
for (const ig of itemGroups) {
|
|
692
|
-
const refs = Array.isArray(ig.PackageReference) ? ig.PackageReference : ig.PackageReference ? [ig.PackageReference] : [];
|
|
693
|
-
for (const ref of refs) {
|
|
694
|
-
const name = ref["@_Include"] ?? ref["@_include"] ?? "";
|
|
695
|
-
const version = ref["@_Version"] ?? ref["@_version"] ?? ref.Version ?? "";
|
|
696
|
-
if (name && version) {
|
|
697
|
-
packageReferences.push({ name: String(name), version: String(version) });
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
return {
|
|
702
|
-
targetFrameworks: [...new Set(targetFrameworks)],
|
|
703
|
-
packageReferences,
|
|
704
|
-
projectName: path3.basename(filePath, ".csproj")
|
|
705
|
-
};
|
|
706
|
-
}
|
|
707
|
-
async function scanDotnetProjects(rootDir) {
|
|
708
|
-
const csprojFiles = await findCsprojFiles(rootDir);
|
|
709
|
-
const slnFiles = await findSolutionFiles(rootDir);
|
|
710
|
-
const slnCsprojPaths = /* @__PURE__ */ new Set();
|
|
711
|
-
for (const slnPath of slnFiles) {
|
|
712
|
-
try {
|
|
713
|
-
const slnContent = await readTextFile(slnPath);
|
|
714
|
-
const slnDir = path3.dirname(slnPath);
|
|
715
|
-
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
716
|
-
let match;
|
|
717
|
-
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
718
|
-
if (match[1]) {
|
|
719
|
-
const csprojPath = path3.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
720
|
-
slnCsprojPaths.add(csprojPath);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
} catch {
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
const allCsprojFiles = /* @__PURE__ */ new Set([...csprojFiles, ...slnCsprojPaths]);
|
|
727
|
-
const results = [];
|
|
728
|
-
for (const csprojPath of allCsprojFiles) {
|
|
729
|
-
try {
|
|
730
|
-
const scan = await scanOneCsproj(csprojPath, rootDir);
|
|
731
|
-
results.push(scan);
|
|
732
|
-
} catch (e) {
|
|
733
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
734
|
-
console.error(`Error scanning ${csprojPath}: ${msg}`);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return results;
|
|
738
|
-
}
|
|
739
|
-
async function scanOneCsproj(csprojPath, rootDir) {
|
|
740
|
-
const xml = await readTextFile(csprojPath);
|
|
741
|
-
const data = parseCsproj(xml, csprojPath);
|
|
742
|
-
const primaryTfm = data.targetFrameworks[0];
|
|
743
|
-
let runtimeMajorsBehind;
|
|
744
|
-
let targetFramework = primaryTfm;
|
|
745
|
-
if (primaryTfm) {
|
|
746
|
-
const major2 = parseTfmMajor(primaryTfm);
|
|
747
|
-
if (major2 !== null) {
|
|
748
|
-
runtimeMajorsBehind = Math.max(0, LATEST_DOTNET_MAJOR - major2);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
const dependencies = data.packageReferences.map((ref) => ({
|
|
752
|
-
package: ref.name,
|
|
753
|
-
section: "dependencies",
|
|
754
|
-
currentSpec: ref.version,
|
|
755
|
-
resolvedVersion: ref.version,
|
|
756
|
-
latestStable: null,
|
|
757
|
-
// NuGet lookup not implemented in v1
|
|
758
|
-
majorsBehind: null,
|
|
759
|
-
drift: "unknown"
|
|
760
|
-
}));
|
|
761
|
-
const frameworks = [];
|
|
762
|
-
for (const ref of data.packageReferences) {
|
|
763
|
-
if (ref.name in KNOWN_DOTNET_FRAMEWORKS) {
|
|
764
|
-
frameworks.push({
|
|
765
|
-
name: KNOWN_DOTNET_FRAMEWORKS[ref.name],
|
|
766
|
-
currentVersion: ref.version,
|
|
767
|
-
latestVersion: null,
|
|
768
|
-
majorsBehind: null
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
|
|
773
|
-
return {
|
|
774
|
-
type: "dotnet",
|
|
775
|
-
path: path3.relative(rootDir, path3.dirname(csprojPath)) || ".",
|
|
776
|
-
name: data.projectName,
|
|
777
|
-
targetFramework,
|
|
778
|
-
runtime: primaryTfm,
|
|
779
|
-
runtimeLatest: `net${LATEST_DOTNET_MAJOR}.0`,
|
|
780
|
-
runtimeMajorsBehind,
|
|
781
|
-
frameworks,
|
|
782
|
-
dependencies,
|
|
783
|
-
dependencyAgeBuckets: buckets
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// src/utils/semaphore.ts
|
|
788
|
-
var Semaphore = class {
|
|
789
|
-
available;
|
|
790
|
-
queue = [];
|
|
791
|
-
constructor(max) {
|
|
792
|
-
this.available = max;
|
|
793
|
-
}
|
|
794
|
-
async run(fn) {
|
|
795
|
-
await this.acquire();
|
|
796
|
-
try {
|
|
797
|
-
return await fn();
|
|
798
|
-
} finally {
|
|
799
|
-
this.release();
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
acquire() {
|
|
803
|
-
if (this.available > 0) {
|
|
804
|
-
this.available--;
|
|
805
|
-
return Promise.resolve();
|
|
806
|
-
}
|
|
807
|
-
return new Promise((resolve4) => this.queue.push(resolve4));
|
|
808
|
-
}
|
|
809
|
-
release() {
|
|
810
|
-
const next = this.queue.shift();
|
|
811
|
-
if (next) next();
|
|
812
|
-
else this.available++;
|
|
813
|
-
}
|
|
814
|
-
};
|
|
815
|
-
|
|
816
|
-
// src/config.ts
|
|
817
|
-
import * as path4 from "path";
|
|
818
|
-
import * as fs2 from "fs/promises";
|
|
819
|
-
var CONFIG_FILES = [
|
|
820
|
-
"vibgrate.config.ts",
|
|
821
|
-
"vibgrate.config.js",
|
|
822
|
-
"vibgrate.config.json"
|
|
823
|
-
];
|
|
824
|
-
var DEFAULT_CONFIG = {
|
|
825
|
-
exclude: [],
|
|
826
|
-
thresholds: {
|
|
827
|
-
failOnError: {
|
|
828
|
-
eolDays: 180,
|
|
829
|
-
frameworkMajorLag: 3,
|
|
830
|
-
dependencyTwoPlusPercent: 50
|
|
831
|
-
},
|
|
832
|
-
warn: {
|
|
833
|
-
frameworkMajorLag: 2,
|
|
834
|
-
dependencyTwoPlusPercent: 30
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
};
|
|
838
|
-
async function loadConfig(rootDir) {
|
|
839
|
-
for (const file of CONFIG_FILES) {
|
|
840
|
-
const configPath = path4.join(rootDir, file);
|
|
841
|
-
if (await pathExists(configPath)) {
|
|
842
|
-
if (file.endsWith(".json")) {
|
|
843
|
-
const txt = await readTextFile(configPath);
|
|
844
|
-
return { ...DEFAULT_CONFIG, ...JSON.parse(txt) };
|
|
845
|
-
}
|
|
846
|
-
try {
|
|
847
|
-
const mod = await import(configPath);
|
|
848
|
-
return { ...DEFAULT_CONFIG, ...mod.default ?? mod };
|
|
849
|
-
} catch {
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return DEFAULT_CONFIG;
|
|
854
|
-
}
|
|
855
|
-
async function writeDefaultConfig(rootDir) {
|
|
856
|
-
const configPath = path4.join(rootDir, "vibgrate.config.ts");
|
|
857
|
-
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
858
|
-
|
|
859
|
-
const config: VibgrateConfig = {
|
|
860
|
-
// exclude: ['legacy/**'],
|
|
861
|
-
thresholds: {
|
|
862
|
-
failOnError: {
|
|
863
|
-
eolDays: 180,
|
|
864
|
-
frameworkMajorLag: 3,
|
|
865
|
-
dependencyTwoPlusPercent: 50,
|
|
866
|
-
},
|
|
867
|
-
warn: {
|
|
868
|
-
frameworkMajorLag: 2,
|
|
869
|
-
dependencyTwoPlusPercent: 30,
|
|
870
|
-
},
|
|
871
|
-
},
|
|
872
|
-
};
|
|
873
|
-
|
|
874
|
-
export default config;
|
|
875
|
-
`;
|
|
876
|
-
await fs2.writeFile(configPath, content, "utf8");
|
|
877
|
-
return configPath;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// src/utils/vcs.ts
|
|
881
|
-
import * as path5 from "path";
|
|
882
|
-
import * as fs3 from "fs/promises";
|
|
883
|
-
async function detectVcs(rootDir) {
|
|
884
|
-
try {
|
|
885
|
-
return await detectGit(rootDir);
|
|
886
|
-
} catch {
|
|
887
|
-
return { type: "unknown" };
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
async function detectGit(rootDir) {
|
|
891
|
-
const gitDir = await findGitDir(rootDir);
|
|
892
|
-
if (!gitDir) {
|
|
893
|
-
return { type: "unknown" };
|
|
894
|
-
}
|
|
895
|
-
const headPath = path5.join(gitDir, "HEAD");
|
|
896
|
-
let headContent;
|
|
897
|
-
try {
|
|
898
|
-
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
899
|
-
} catch {
|
|
900
|
-
return { type: "unknown" };
|
|
901
|
-
}
|
|
902
|
-
let sha;
|
|
903
|
-
let branch;
|
|
904
|
-
if (headContent.startsWith("ref: ")) {
|
|
905
|
-
const refPath = headContent.slice(5);
|
|
906
|
-
branch = refPath.startsWith("refs/heads/") ? refPath.slice(11) : refPath;
|
|
907
|
-
sha = await resolveRef(gitDir, refPath);
|
|
908
|
-
} else if (/^[0-9a-f]{40}$/i.test(headContent)) {
|
|
909
|
-
sha = headContent;
|
|
910
|
-
}
|
|
911
|
-
return {
|
|
912
|
-
type: "git",
|
|
913
|
-
sha: sha ?? void 0,
|
|
914
|
-
shortSha: sha ? sha.slice(0, 7) : void 0,
|
|
915
|
-
branch: branch ?? void 0
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
async function findGitDir(startDir) {
|
|
919
|
-
let dir = path5.resolve(startDir);
|
|
920
|
-
const root = path5.parse(dir).root;
|
|
921
|
-
while (dir !== root) {
|
|
922
|
-
const gitPath = path5.join(dir, ".git");
|
|
923
|
-
try {
|
|
924
|
-
const stat2 = await fs3.stat(gitPath);
|
|
925
|
-
if (stat2.isDirectory()) {
|
|
926
|
-
return gitPath;
|
|
927
|
-
}
|
|
928
|
-
if (stat2.isFile()) {
|
|
929
|
-
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
930
|
-
if (content.startsWith("gitdir: ")) {
|
|
931
|
-
const resolved = path5.resolve(dir, content.slice(8));
|
|
932
|
-
return resolved;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
} catch {
|
|
936
|
-
}
|
|
937
|
-
dir = path5.dirname(dir);
|
|
938
|
-
}
|
|
939
|
-
return null;
|
|
940
|
-
}
|
|
941
|
-
async function resolveRef(gitDir, refPath) {
|
|
942
|
-
const loosePath = path5.join(gitDir, refPath);
|
|
943
|
-
try {
|
|
944
|
-
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
945
|
-
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
946
|
-
return sha;
|
|
947
|
-
}
|
|
948
|
-
} catch {
|
|
949
|
-
}
|
|
950
|
-
const packedPath = path5.join(gitDir, "packed-refs");
|
|
951
|
-
try {
|
|
952
|
-
const packed = await fs3.readFile(packedPath, "utf8");
|
|
953
|
-
for (const line of packed.split("\n")) {
|
|
954
|
-
if (line.startsWith("#") || line.startsWith("^")) continue;
|
|
955
|
-
const parts = line.trim().split(" ");
|
|
956
|
-
if (parts.length >= 2 && parts[1] === refPath) {
|
|
957
|
-
return parts[0];
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
} catch {
|
|
961
|
-
}
|
|
962
|
-
return void 0;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// src/commands/scan.ts
|
|
966
|
-
async function runScan(rootDir, opts) {
|
|
967
|
-
const config = await loadConfig(rootDir);
|
|
968
|
-
const sem = new Semaphore(opts.concurrency);
|
|
969
|
-
const npmCache = new NpmCache(rootDir, sem);
|
|
970
|
-
console.log(chalk2.dim(`Scanning ${rootDir}...`));
|
|
971
|
-
const vcs = await detectVcs(rootDir);
|
|
972
|
-
const nodeProjects = await scanNodeProjects(rootDir, npmCache);
|
|
973
|
-
const dotnetProjects = await scanDotnetProjects(rootDir);
|
|
974
|
-
const allProjects = [...nodeProjects, ...dotnetProjects];
|
|
975
|
-
if (allProjects.length === 0) {
|
|
976
|
-
console.log(chalk2.yellow("No projects found."));
|
|
977
|
-
}
|
|
978
|
-
const drift = computeDriftScore(allProjects);
|
|
979
|
-
const findings = generateFindings(allProjects, config);
|
|
980
|
-
const artifact = {
|
|
981
|
-
schemaVersion: "1.0",
|
|
982
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
983
|
-
vibgrateVersion: VERSION,
|
|
984
|
-
rootPath: path6.basename(rootDir),
|
|
985
|
-
...vcs.type !== "unknown" ? { vcs } : {},
|
|
986
|
-
projects: allProjects,
|
|
987
|
-
drift,
|
|
988
|
-
findings
|
|
989
|
-
};
|
|
990
|
-
if (opts.baseline) {
|
|
991
|
-
const baselinePath = path6.resolve(opts.baseline);
|
|
992
|
-
if (await pathExists(baselinePath)) {
|
|
993
|
-
try {
|
|
994
|
-
const baseline = await readJsonFile(baselinePath);
|
|
995
|
-
artifact.baseline = baselinePath;
|
|
996
|
-
artifact.delta = artifact.drift.score - baseline.drift.score;
|
|
997
|
-
} catch {
|
|
998
|
-
console.error(chalk2.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
const vibgrateDir = path6.join(rootDir, ".vibgrate");
|
|
1003
|
-
await ensureDir(vibgrateDir);
|
|
1004
|
-
await writeJsonFile(path6.join(vibgrateDir, "scan_result.json"), artifact);
|
|
1005
|
-
if (opts.format === "json") {
|
|
1006
|
-
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
1007
|
-
if (opts.out) {
|
|
1008
|
-
await writeTextFile(path6.resolve(opts.out), jsonStr);
|
|
1009
|
-
console.log(chalk2.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
1010
|
-
} else {
|
|
1011
|
-
console.log(jsonStr);
|
|
1012
|
-
}
|
|
1013
|
-
} else if (opts.format === "sarif") {
|
|
1014
|
-
const sarif = formatSarif(artifact);
|
|
1015
|
-
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
1016
|
-
if (opts.out) {
|
|
1017
|
-
await writeTextFile(path6.resolve(opts.out), sarifStr);
|
|
1018
|
-
console.log(chalk2.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
1019
|
-
} else {
|
|
1020
|
-
console.log(sarifStr);
|
|
1021
|
-
}
|
|
1022
|
-
} else {
|
|
1023
|
-
const text = formatText(artifact);
|
|
1024
|
-
console.log(text);
|
|
1025
|
-
if (opts.out) {
|
|
1026
|
-
await writeTextFile(path6.resolve(opts.out), text);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
return artifact;
|
|
1030
|
-
}
|
|
1031
|
-
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) => {
|
|
1032
|
-
const rootDir = path6.resolve(targetPath);
|
|
1033
|
-
if (!await pathExists(rootDir)) {
|
|
1034
|
-
console.error(chalk2.red(`Path does not exist: ${rootDir}`));
|
|
1035
|
-
process.exit(1);
|
|
1036
|
-
}
|
|
1037
|
-
const scanOpts = {
|
|
1038
|
-
out: opts.out,
|
|
1039
|
-
format: opts.format || "text",
|
|
1040
|
-
failOn: opts.failOn,
|
|
1041
|
-
baseline: opts.baseline,
|
|
1042
|
-
changedOnly: opts.changedOnly,
|
|
1043
|
-
concurrency: parseInt(opts.concurrency, 10) || 8
|
|
1044
|
-
};
|
|
1045
|
-
const artifact = await runScan(rootDir, scanOpts);
|
|
1046
|
-
if (opts.failOn) {
|
|
1047
|
-
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
1048
|
-
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
1049
|
-
if (opts.failOn === "error" && hasErrors) {
|
|
1050
|
-
console.error(chalk2.red(`
|
|
1051
|
-
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
1052
|
-
process.exit(2);
|
|
1053
|
-
}
|
|
1054
|
-
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
1055
|
-
console.error(chalk2.red(`
|
|
1056
|
-
Failing: findings detected at warn level or above.`));
|
|
1057
|
-
process.exit(2);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
export {
|
|
1063
|
-
readJsonFile,
|
|
1064
|
-
readTextFile,
|
|
1065
|
-
pathExists,
|
|
1066
|
-
ensureDir,
|
|
1067
|
-
writeJsonFile,
|
|
1068
|
-
writeTextFile,
|
|
1069
|
-
writeDefaultConfig,
|
|
1070
|
-
computeDriftScore,
|
|
1071
|
-
generateFindings,
|
|
1072
|
-
formatText,
|
|
1073
|
-
formatSarif,
|
|
1074
|
-
VERSION,
|
|
1075
|
-
runScan,
|
|
1076
|
-
scanCommand
|
|
1077
|
-
};
|