@vibgrate/cli 0.1.4 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -9
- package/dist/{baseline-45AWVXG4.js → baseline-5SAUNH2V.js} +2 -2
- package/dist/{chunk-BTIIFIOD.js → chunk-GG5AUF7X.js} +1 -1
- package/dist/{chunk-AMOJCCF5.js → chunk-VXZT34Y5.js} +4 -1
- package/dist/{chunk-WO6EZ6AF.js → chunk-XZ4NRZMT.js} +559 -170
- package/dist/cli.js +32 -165
- package/dist/index.d.ts +15 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5
5
|
"node_modules",
|
|
6
6
|
".git",
|
|
7
|
+
".vibgrate",
|
|
7
8
|
".next",
|
|
8
9
|
"dist",
|
|
9
10
|
"build",
|
|
@@ -80,6 +81,7 @@ async function writeTextFile(filePath, content) {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
// src/scoring/drift-score.ts
|
|
84
|
+
import * as crypto from "crypto";
|
|
83
85
|
var DEFAULT_THRESHOLDS = {
|
|
84
86
|
failOnError: {
|
|
85
87
|
eolDays: 180,
|
|
@@ -274,12 +276,27 @@ function generateFindings(projects, config) {
|
|
|
274
276
|
}
|
|
275
277
|
return findings;
|
|
276
278
|
}
|
|
279
|
+
function computeProjectId(relativePath, projectName, workspaceId) {
|
|
280
|
+
const input = workspaceId ? `${relativePath}:${projectName}:${workspaceId}` : `${relativePath}:${projectName}`;
|
|
281
|
+
return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/version.ts
|
|
285
|
+
import { createRequire } from "module";
|
|
286
|
+
var require2 = createRequire(import.meta.url);
|
|
287
|
+
var pkg = require2("../package.json");
|
|
288
|
+
var VERSION = pkg.version;
|
|
277
289
|
|
|
278
290
|
// src/formatters/text.ts
|
|
279
291
|
import chalk from "chalk";
|
|
280
292
|
function formatText(artifact) {
|
|
281
293
|
const lines = [];
|
|
282
294
|
lines.push("");
|
|
295
|
+
lines.push(chalk.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk.greenBright("\u279C"));
|
|
296
|
+
lines.push(chalk.cyan(" \u256D\u2524") + chalk.greenBright("\u25C9 \u25C9") + chalk.cyan("\u251C\u256E") + " " + chalk.bold.white("V I B G R A T E"));
|
|
297
|
+
lines.push(chalk.cyan(" \u2570\u2524") + chalk.dim("\u2500\u2500\u2500") + chalk.cyan("\u251C\u256F") + " " + chalk.dim(`Drift Intelligence Engine v${VERSION}`));
|
|
298
|
+
lines.push(chalk.cyan(" \u2570\u2500\u2500\u2500\u256F"));
|
|
299
|
+
lines.push("");
|
|
283
300
|
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"));
|
|
284
301
|
lines.push(chalk.bold.cyan("\u2551 Vibgrate Drift Report \u2551"));
|
|
285
302
|
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"));
|
|
@@ -366,7 +383,15 @@ function formatText(artifact) {
|
|
|
366
383
|
lines.push("");
|
|
367
384
|
}
|
|
368
385
|
}
|
|
369
|
-
|
|
386
|
+
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
387
|
+
if (artifact.durationMs !== void 0) {
|
|
388
|
+
const secs = (artifact.durationMs / 1e3).toFixed(1);
|
|
389
|
+
scannedParts.push(`${secs}s`);
|
|
390
|
+
}
|
|
391
|
+
if (artifact.filesScanned !== void 0) {
|
|
392
|
+
scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
|
|
393
|
+
}
|
|
394
|
+
lines.push(chalk.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
370
395
|
lines.push("");
|
|
371
396
|
return lines.join("\n");
|
|
372
397
|
}
|
|
@@ -534,10 +559,17 @@ function generatePriorityActions(artifact) {
|
|
|
534
559
|
);
|
|
535
560
|
if (eolProjects.length > 0) {
|
|
536
561
|
const names = eolProjects.map((p) => p.name).join(", ");
|
|
537
|
-
|
|
562
|
+
let detail = `End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`;
|
|
563
|
+
const fileLines = [];
|
|
564
|
+
for (const p of eolProjects) {
|
|
565
|
+
fileLines.push(`
|
|
566
|
+
./${p.path}`);
|
|
567
|
+
fileLines.push(` ${p.runtime} \u2192 ${p.runtimeLatest} (${p.runtimeMajorsBehind} major${p.runtimeMajorsBehind > 1 ? "s" : ""} behind)`);
|
|
568
|
+
}
|
|
569
|
+
detail += fileLines.join("");
|
|
538
570
|
actions.push({
|
|
539
571
|
title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
|
|
540
|
-
explanation:
|
|
572
|
+
explanation: detail,
|
|
541
573
|
impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
|
|
542
574
|
severity: 100
|
|
543
575
|
});
|
|
@@ -546,16 +578,30 @@ function generatePriorityActions(artifact) {
|
|
|
546
578
|
for (const p of artifact.projects) {
|
|
547
579
|
for (const fw of p.frameworks) {
|
|
548
580
|
if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
|
|
549
|
-
severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name });
|
|
581
|
+
severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name, projectPath: p.path });
|
|
550
582
|
}
|
|
551
583
|
}
|
|
552
584
|
}
|
|
553
585
|
if (severeFrameworks.length > 0) {
|
|
554
586
|
const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
|
|
555
587
|
const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
|
|
588
|
+
let detail = `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`;
|
|
589
|
+
const fileLines = [];
|
|
590
|
+
let shown = 0;
|
|
591
|
+
for (const sf of severeFrameworks) {
|
|
592
|
+
if (shown >= 8) break;
|
|
593
|
+
fileLines.push(`
|
|
594
|
+
./${sf.projectPath}`);
|
|
595
|
+
fileLines.push(` ${sf.name}: ${sf.fw} (${sf.behind} major${sf.behind > 1 ? "s" : ""} behind)`);
|
|
596
|
+
shown++;
|
|
597
|
+
}
|
|
598
|
+
const remaining = severeFrameworks.length - shown;
|
|
599
|
+
detail += fileLines.join("");
|
|
600
|
+
if (remaining > 0) detail += `
|
|
601
|
+
... and ${remaining} more`;
|
|
556
602
|
actions.push({
|
|
557
603
|
title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
|
|
558
|
-
explanation:
|
|
604
|
+
explanation: detail,
|
|
559
605
|
impact: `+5\u201315 points (framework score)`,
|
|
560
606
|
severity: 90
|
|
561
607
|
});
|
|
@@ -566,9 +612,28 @@ function generatePriorityActions(artifact) {
|
|
|
566
612
|
if (total === 0) continue;
|
|
567
613
|
const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
|
|
568
614
|
if (twoPlusPct >= 40) {
|
|
615
|
+
let detail = `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`;
|
|
616
|
+
const worstDeps = p.dependencies.filter((d) => d.majorsBehind !== null && d.majorsBehind >= 2).sort((a, b2) => (b2.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
|
|
617
|
+
if (worstDeps.length > 0) {
|
|
618
|
+
const depLines = [];
|
|
619
|
+
let shown = 0;
|
|
620
|
+
depLines.push(`
|
|
621
|
+
./${p.path}`);
|
|
622
|
+
for (const dep of worstDeps) {
|
|
623
|
+
if (shown >= 8) break;
|
|
624
|
+
const current = dep.resolvedVersion ?? dep.currentSpec;
|
|
625
|
+
const latest = dep.latestStable ?? "?";
|
|
626
|
+
depLines.push(` ${dep.package}: ${current} \u2192 ${latest} (${dep.majorsBehind} major${dep.majorsBehind > 1 ? "s" : ""} behind)`);
|
|
627
|
+
shown++;
|
|
628
|
+
}
|
|
629
|
+
const remaining = worstDeps.length - shown;
|
|
630
|
+
detail += depLines.join("");
|
|
631
|
+
if (remaining > 0) detail += `
|
|
632
|
+
... and ${remaining} more`;
|
|
633
|
+
}
|
|
569
634
|
actions.push({
|
|
570
635
|
title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
|
|
571
|
-
explanation:
|
|
636
|
+
explanation: detail,
|
|
572
637
|
impact: `+5\u201310 points (dependency score)`,
|
|
573
638
|
severity: 80 + twoPlusPct / 10
|
|
574
639
|
});
|
|
@@ -578,7 +643,7 @@ function generatePriorityActions(artifact) {
|
|
|
578
643
|
for (const p of artifact.projects) {
|
|
579
644
|
for (const fw of p.frameworks) {
|
|
580
645
|
if (fw.majorsBehind === 2) {
|
|
581
|
-
twoMajorFrameworks.push({ name: fw.name, project: p.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
|
|
646
|
+
twoMajorFrameworks.push({ name: fw.name, project: p.name, projectPath: p.path, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
|
|
582
647
|
}
|
|
583
648
|
}
|
|
584
649
|
}
|
|
@@ -586,9 +651,23 @@ function generatePriorityActions(artifact) {
|
|
|
586
651
|
if (uniqueTwo.length > 0) {
|
|
587
652
|
const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
|
|
588
653
|
const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
|
|
654
|
+
let detail = `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`;
|
|
655
|
+
const fileLines = [];
|
|
656
|
+
let shown = 0;
|
|
657
|
+
for (const tf of twoMajorFrameworks) {
|
|
658
|
+
if (shown >= 8) break;
|
|
659
|
+
fileLines.push(`
|
|
660
|
+
./${tf.projectPath}`);
|
|
661
|
+
fileLines.push(` ${tf.name}: ${tf.fw}`);
|
|
662
|
+
shown++;
|
|
663
|
+
}
|
|
664
|
+
const remaining = twoMajorFrameworks.length - shown;
|
|
665
|
+
detail += fileLines.join("");
|
|
666
|
+
if (remaining > 0) detail += `
|
|
667
|
+
... and ${remaining} more`;
|
|
589
668
|
actions.push({
|
|
590
669
|
title: `Plan major framework upgrades: ${list}${moreCount}`,
|
|
591
|
-
explanation:
|
|
670
|
+
explanation: detail,
|
|
592
671
|
impact: `+5\u201310 points (framework score)`,
|
|
593
672
|
severity: 60
|
|
594
673
|
});
|
|
@@ -599,9 +678,31 @@ function generatePriorityActions(artifact) {
|
|
|
599
678
|
if (total > 0) {
|
|
600
679
|
const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
|
|
601
680
|
const moreCount = total > 5 ? ` +${total - 5} more` : "";
|
|
681
|
+
let detail = `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`;
|
|
682
|
+
const allPkgNames = /* @__PURE__ */ new Set([...bc.deprecatedPackages, ...bc.legacyPolyfills]);
|
|
683
|
+
const fileLines = [];
|
|
684
|
+
let shown = 0;
|
|
685
|
+
for (const p of artifact.projects) {
|
|
686
|
+
const matches = p.dependencies.filter((d) => allPkgNames.has(d.package));
|
|
687
|
+
if (matches.length === 0) continue;
|
|
688
|
+
if (shown >= 10) break;
|
|
689
|
+
fileLines.push(`
|
|
690
|
+
./${p.path}`);
|
|
691
|
+
for (const dep of matches) {
|
|
692
|
+
if (shown >= 10) break;
|
|
693
|
+
const ver = dep.resolvedVersion ?? dep.currentSpec;
|
|
694
|
+
const label = bc.deprecatedPackages.includes(dep.package) ? "deprecated" : "polyfill";
|
|
695
|
+
fileLines.push(` ${dep.package}: ${ver} (${label})`);
|
|
696
|
+
shown++;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const remaining = total - shown;
|
|
700
|
+
detail += fileLines.join("");
|
|
701
|
+
if (remaining > 0) detail += `
|
|
702
|
+
... and ${remaining} more`;
|
|
602
703
|
actions.push({
|
|
603
704
|
title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
|
|
604
|
-
explanation:
|
|
705
|
+
explanation: detail,
|
|
605
706
|
severity: 55
|
|
606
707
|
});
|
|
607
708
|
}
|
|
@@ -648,9 +749,20 @@ function generatePriorityActions(artifact) {
|
|
|
648
749
|
const issues = [];
|
|
649
750
|
if (sec.envFilesTracked) issues.push(".env files are tracked in git");
|
|
650
751
|
if (!sec.lockfilePresent) issues.push("no lockfile found");
|
|
752
|
+
let detail;
|
|
753
|
+
if (sec.envFilesTracked) {
|
|
754
|
+
detail = "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately.";
|
|
755
|
+
detail += "\n ./.gitignore";
|
|
756
|
+
detail += "\n Add: .env, .env.*, .env.local";
|
|
757
|
+
} else {
|
|
758
|
+
detail = "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.";
|
|
759
|
+
detail += "\n ./";
|
|
760
|
+
detail += `
|
|
761
|
+
Missing: ${sec.lockfileTypes.length > 0 ? sec.lockfileTypes.join(", ") + " (multiple types detected)" : "package-lock.json, pnpm-lock.yaml, or yarn.lock"}`;
|
|
762
|
+
}
|
|
651
763
|
actions.push({
|
|
652
764
|
title: `Fix security posture: ${issues.join(", ")}`,
|
|
653
|
-
explanation:
|
|
765
|
+
explanation: detail,
|
|
654
766
|
severity: 95
|
|
655
767
|
});
|
|
656
768
|
}
|
|
@@ -660,9 +772,22 @@ function generatePriorityActions(artifact) {
|
|
|
660
772
|
const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
|
|
661
773
|
if (highImpactDupes.length >= 3) {
|
|
662
774
|
const names = highImpactDupes.slice(0, 4).map((d) => `${d.name} (${d.versions.length}v)`).join(", ");
|
|
775
|
+
let detail = `${highImpactDupes.length} packages have 3+ versions installed. Run \`npm dedupe\` to reduce bundle size and install time.`;
|
|
776
|
+
const dupeLines = [];
|
|
777
|
+
let shown = 0;
|
|
778
|
+
for (const d of highImpactDupes) {
|
|
779
|
+
if (shown >= 8) break;
|
|
780
|
+
dupeLines.push(`
|
|
781
|
+
${d.name}: ${d.versions.join(", ")} (${d.consumers} consumer${d.consumers !== 1 ? "s" : ""})`);
|
|
782
|
+
shown++;
|
|
783
|
+
}
|
|
784
|
+
const remaining = highImpactDupes.length - shown;
|
|
785
|
+
detail += dupeLines.join("");
|
|
786
|
+
if (remaining > 0) detail += `
|
|
787
|
+
... and ${remaining} more`;
|
|
663
788
|
actions.push({
|
|
664
789
|
title: `Deduplicate heavily-versioned packages`,
|
|
665
|
-
explanation:
|
|
790
|
+
explanation: detail,
|
|
666
791
|
severity: 35
|
|
667
792
|
});
|
|
668
793
|
}
|
|
@@ -753,19 +878,146 @@ function toSarifResult(finding) {
|
|
|
753
878
|
};
|
|
754
879
|
}
|
|
755
880
|
|
|
756
|
-
// src/
|
|
757
|
-
import
|
|
758
|
-
|
|
759
|
-
var pkg = require2("../package.json");
|
|
760
|
-
var VERSION = pkg.version;
|
|
761
|
-
|
|
762
|
-
// src/commands/scan.ts
|
|
763
|
-
import * as path12 from "path";
|
|
881
|
+
// src/commands/dsn.ts
|
|
882
|
+
import * as crypto2 from "crypto";
|
|
883
|
+
import * as path2 from "path";
|
|
764
884
|
import { Command } from "commander";
|
|
885
|
+
import chalk2 from "chalk";
|
|
886
|
+
var REGION_HOSTS = {
|
|
887
|
+
us: "us.ingest.vibgrate.com",
|
|
888
|
+
eu: "eu.ingest.vibgrate.com"
|
|
889
|
+
};
|
|
890
|
+
function resolveIngestHost(region, ingest) {
|
|
891
|
+
if (ingest) {
|
|
892
|
+
try {
|
|
893
|
+
return new URL(ingest).host;
|
|
894
|
+
} catch {
|
|
895
|
+
throw new Error(`Invalid ingest URL: ${ingest}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
const r = (region ?? "us").toLowerCase();
|
|
899
|
+
const host = REGION_HOSTS[r];
|
|
900
|
+
if (!host) {
|
|
901
|
+
throw new Error(`Unknown region "${r}". Supported: ${Object.keys(REGION_HOSTS).join(", ")}`);
|
|
902
|
+
}
|
|
903
|
+
return host;
|
|
904
|
+
}
|
|
905
|
+
var dsnCommand = new Command("dsn").description("Manage DSN tokens");
|
|
906
|
+
dsnCommand.command("create").description("Create a new DSN token").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", "Data residency region (us, eu)", "us").requiredOption("--workspace <id>", "Workspace ID").option("--write <path>", "Write DSN to file").action(async (opts) => {
|
|
907
|
+
const keyId = crypto2.randomBytes(8).toString("hex");
|
|
908
|
+
const secret = crypto2.randomBytes(32).toString("hex");
|
|
909
|
+
let ingestHost;
|
|
910
|
+
try {
|
|
911
|
+
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
912
|
+
} catch (e) {
|
|
913
|
+
console.error(chalk2.red(e instanceof Error ? e.message : String(e)));
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
const dsn = `vibgrate+https://${keyId}:${secret}@${ingestHost}/${opts.workspace}`;
|
|
917
|
+
console.log(chalk2.green("\u2714") + " DSN created");
|
|
918
|
+
console.log("");
|
|
919
|
+
console.log(chalk2.bold("DSN:"));
|
|
920
|
+
console.log(` ${dsn}`);
|
|
921
|
+
console.log("");
|
|
922
|
+
console.log(chalk2.bold("Key ID:"));
|
|
923
|
+
console.log(` ${keyId}`);
|
|
924
|
+
console.log("");
|
|
925
|
+
console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
|
|
926
|
+
console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
|
|
927
|
+
if (opts.write) {
|
|
928
|
+
const writePath = path2.resolve(opts.write);
|
|
929
|
+
await writeTextFile(writePath, dsn + "\n");
|
|
930
|
+
console.log("");
|
|
931
|
+
console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
|
|
932
|
+
console.log(chalk2.yellow("\u26A0") + " Add this file to .gitignore!");
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// src/commands/push.ts
|
|
937
|
+
import * as crypto3 from "crypto";
|
|
938
|
+
import * as path3 from "path";
|
|
939
|
+
import { Command as Command2 } from "commander";
|
|
765
940
|
import chalk3 from "chalk";
|
|
941
|
+
function parseDsn(dsn) {
|
|
942
|
+
const match = dsn.match(/^vibgrate\+https:\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/);
|
|
943
|
+
if (!match) return null;
|
|
944
|
+
return {
|
|
945
|
+
keyId: match[1],
|
|
946
|
+
secret: match[2],
|
|
947
|
+
host: match[3],
|
|
948
|
+
workspaceId: match[4]
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function computeHmac(body, secret) {
|
|
952
|
+
return crypto3.createHmac("sha256", secret).update(body).digest("base64");
|
|
953
|
+
}
|
|
954
|
+
var pushCommand = new Command2("push").description("Push scan results to Vibgrate API").option("--dsn <dsn>", "DSN token (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region (us, eu)").option("--file <file>", "Scan artifact file", ".vibgrate/scan_result.json").option("--strict", "Fail on upload errors").action(async (opts) => {
|
|
955
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
956
|
+
if (!dsn) {
|
|
957
|
+
console.error(chalk3.red("No DSN provided."));
|
|
958
|
+
console.error(chalk3.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
|
|
959
|
+
if (opts.strict) process.exit(1);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const parsed = parseDsn(dsn);
|
|
963
|
+
if (!parsed) {
|
|
964
|
+
console.error(chalk3.red("Invalid DSN format."));
|
|
965
|
+
console.error(chalk3.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>"));
|
|
966
|
+
if (opts.strict) process.exit(1);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const filePath = path3.resolve(opts.file);
|
|
970
|
+
if (!await pathExists(filePath)) {
|
|
971
|
+
console.error(chalk3.red(`Scan artifact not found: ${filePath}`));
|
|
972
|
+
console.error(chalk3.dim('Run "vibgrate scan" first.'));
|
|
973
|
+
if (opts.strict) process.exit(1);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const body = await readTextFile(filePath);
|
|
977
|
+
const timestamp = String(Date.now());
|
|
978
|
+
const hmac = computeHmac(body, parsed.secret);
|
|
979
|
+
let host = parsed.host;
|
|
980
|
+
if (opts.region) {
|
|
981
|
+
try {
|
|
982
|
+
host = resolveIngestHost(opts.region);
|
|
983
|
+
} catch (e) {
|
|
984
|
+
console.error(chalk3.red(e instanceof Error ? e.message : String(e)));
|
|
985
|
+
if (opts.strict) process.exit(1);
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const url = `https://${host}/v1/ingest/scan`;
|
|
990
|
+
console.log(chalk3.dim(`Uploading to ${host}...`));
|
|
991
|
+
try {
|
|
992
|
+
const response = await fetch(url, {
|
|
993
|
+
method: "POST",
|
|
994
|
+
headers: {
|
|
995
|
+
"Content-Type": "application/json",
|
|
996
|
+
"X-Vibgrate-Timestamp": timestamp,
|
|
997
|
+
"Authorization": `VibgrateDSN ${parsed.keyId}:${hmac}`
|
|
998
|
+
},
|
|
999
|
+
body
|
|
1000
|
+
});
|
|
1001
|
+
if (!response.ok) {
|
|
1002
|
+
const text = await response.text();
|
|
1003
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1004
|
+
}
|
|
1005
|
+
const result = await response.json();
|
|
1006
|
+
console.log(chalk3.green("\u2714") + ` Uploaded successfully (${result.ingestId ?? "ok"})`);
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1009
|
+
console.error(chalk3.red(`Upload failed: ${msg}`));
|
|
1010
|
+
if (opts.strict) process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// src/commands/scan.ts
|
|
1015
|
+
import * as path14 from "path";
|
|
1016
|
+
import { Command as Command3 } from "commander";
|
|
1017
|
+
import chalk5 from "chalk";
|
|
766
1018
|
|
|
767
1019
|
// src/scanners/node-scanner.ts
|
|
768
|
-
import * as
|
|
1020
|
+
import * as path4 from "path";
|
|
769
1021
|
import * as semver2 from "semver";
|
|
770
1022
|
|
|
771
1023
|
// src/scanners/npm-cache.ts
|
|
@@ -780,7 +1032,7 @@ function maxStable(versions) {
|
|
|
780
1032
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
781
1033
|
}
|
|
782
1034
|
async function npmViewJson(args, cwd) {
|
|
783
|
-
return new Promise((
|
|
1035
|
+
return new Promise((resolve6, reject) => {
|
|
784
1036
|
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
785
1037
|
cwd,
|
|
786
1038
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -797,13 +1049,13 @@ async function npmViewJson(args, cwd) {
|
|
|
797
1049
|
}
|
|
798
1050
|
const trimmed = out.trim();
|
|
799
1051
|
if (!trimmed) {
|
|
800
|
-
|
|
1052
|
+
resolve6(null);
|
|
801
1053
|
return;
|
|
802
1054
|
}
|
|
803
1055
|
try {
|
|
804
|
-
|
|
1056
|
+
resolve6(JSON.parse(trimmed));
|
|
805
1057
|
} catch {
|
|
806
|
-
|
|
1058
|
+
resolve6(trimmed.replace(/^"|"$/g, ""));
|
|
807
1059
|
}
|
|
808
1060
|
});
|
|
809
1061
|
});
|
|
@@ -955,8 +1207,8 @@ async function scanNodeProjects(rootDir, npmCache) {
|
|
|
955
1207
|
}
|
|
956
1208
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
957
1209
|
const pj = await readJsonFile(packageJsonPath);
|
|
958
|
-
const absProjectPath =
|
|
959
|
-
const projectPath =
|
|
1210
|
+
const absProjectPath = path4.dirname(packageJsonPath);
|
|
1211
|
+
const projectPath = path4.relative(rootDir, absProjectPath) || ".";
|
|
960
1212
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
961
1213
|
let runtimeLatest;
|
|
962
1214
|
let runtimeMajorsBehind;
|
|
@@ -1038,7 +1290,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
|
1038
1290
|
return {
|
|
1039
1291
|
type: "node",
|
|
1040
1292
|
path: projectPath,
|
|
1041
|
-
name: pj.name ??
|
|
1293
|
+
name: pj.name ?? path4.basename(absProjectPath),
|
|
1042
1294
|
runtime: nodeEngine,
|
|
1043
1295
|
runtimeLatest,
|
|
1044
1296
|
runtimeMajorsBehind,
|
|
@@ -1049,7 +1301,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
|
1049
1301
|
}
|
|
1050
1302
|
|
|
1051
1303
|
// src/scanners/dotnet-scanner.ts
|
|
1052
|
-
import * as
|
|
1304
|
+
import * as path5 from "path";
|
|
1053
1305
|
import { XMLParser } from "fast-xml-parser";
|
|
1054
1306
|
var parser = new XMLParser({
|
|
1055
1307
|
ignoreAttributes: false,
|
|
@@ -1250,7 +1502,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1250
1502
|
const parsed = parser.parse(xml);
|
|
1251
1503
|
const project = parsed?.Project;
|
|
1252
1504
|
if (!project) {
|
|
1253
|
-
return { targetFrameworks: [], packageReferences: [], projectName:
|
|
1505
|
+
return { targetFrameworks: [], packageReferences: [], projectName: path5.basename(filePath, ".csproj") };
|
|
1254
1506
|
}
|
|
1255
1507
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1256
1508
|
const targetFrameworks = [];
|
|
@@ -1278,7 +1530,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1278
1530
|
return {
|
|
1279
1531
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1280
1532
|
packageReferences,
|
|
1281
|
-
projectName:
|
|
1533
|
+
projectName: path5.basename(filePath, ".csproj")
|
|
1282
1534
|
};
|
|
1283
1535
|
}
|
|
1284
1536
|
async function scanDotnetProjects(rootDir) {
|
|
@@ -1288,12 +1540,12 @@ async function scanDotnetProjects(rootDir) {
|
|
|
1288
1540
|
for (const slnPath of slnFiles) {
|
|
1289
1541
|
try {
|
|
1290
1542
|
const slnContent = await readTextFile(slnPath);
|
|
1291
|
-
const slnDir =
|
|
1543
|
+
const slnDir = path5.dirname(slnPath);
|
|
1292
1544
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1293
1545
|
let match;
|
|
1294
1546
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1295
1547
|
if (match[1]) {
|
|
1296
|
-
const csprojPath =
|
|
1548
|
+
const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1297
1549
|
slnCsprojPaths.add(csprojPath);
|
|
1298
1550
|
}
|
|
1299
1551
|
}
|
|
@@ -1349,7 +1601,7 @@ async function scanOneCsproj(csprojPath, rootDir) {
|
|
|
1349
1601
|
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
|
|
1350
1602
|
return {
|
|
1351
1603
|
type: "dotnet",
|
|
1352
|
-
path:
|
|
1604
|
+
path: path5.relative(rootDir, path5.dirname(csprojPath)) || ".",
|
|
1353
1605
|
name: data.projectName,
|
|
1354
1606
|
targetFramework,
|
|
1355
1607
|
runtime: primaryTfm,
|
|
@@ -1381,7 +1633,7 @@ var Semaphore = class {
|
|
|
1381
1633
|
this.available--;
|
|
1382
1634
|
return Promise.resolve();
|
|
1383
1635
|
}
|
|
1384
|
-
return new Promise((
|
|
1636
|
+
return new Promise((resolve6) => this.queue.push(resolve6));
|
|
1385
1637
|
}
|
|
1386
1638
|
release() {
|
|
1387
1639
|
const next = this.queue.shift();
|
|
@@ -1391,7 +1643,7 @@ var Semaphore = class {
|
|
|
1391
1643
|
};
|
|
1392
1644
|
|
|
1393
1645
|
// src/config.ts
|
|
1394
|
-
import * as
|
|
1646
|
+
import * as path6 from "path";
|
|
1395
1647
|
import * as fs2 from "fs/promises";
|
|
1396
1648
|
var CONFIG_FILES = [
|
|
1397
1649
|
"vibgrate.config.ts",
|
|
@@ -1414,7 +1666,7 @@ var DEFAULT_CONFIG = {
|
|
|
1414
1666
|
};
|
|
1415
1667
|
async function loadConfig(rootDir) {
|
|
1416
1668
|
for (const file of CONFIG_FILES) {
|
|
1417
|
-
const configPath =
|
|
1669
|
+
const configPath = path6.join(rootDir, file);
|
|
1418
1670
|
if (await pathExists(configPath)) {
|
|
1419
1671
|
if (file.endsWith(".json")) {
|
|
1420
1672
|
const txt = await readTextFile(configPath);
|
|
@@ -1430,7 +1682,7 @@ async function loadConfig(rootDir) {
|
|
|
1430
1682
|
return DEFAULT_CONFIG;
|
|
1431
1683
|
}
|
|
1432
1684
|
async function writeDefaultConfig(rootDir) {
|
|
1433
|
-
const configPath =
|
|
1685
|
+
const configPath = path6.join(rootDir, "vibgrate.config.ts");
|
|
1434
1686
|
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
1435
1687
|
|
|
1436
1688
|
const config: VibgrateConfig = {
|
|
@@ -1455,7 +1707,7 @@ export default config;
|
|
|
1455
1707
|
}
|
|
1456
1708
|
|
|
1457
1709
|
// src/utils/vcs.ts
|
|
1458
|
-
import * as
|
|
1710
|
+
import * as path7 from "path";
|
|
1459
1711
|
import * as fs3 from "fs/promises";
|
|
1460
1712
|
async function detectVcs(rootDir) {
|
|
1461
1713
|
try {
|
|
@@ -1469,7 +1721,7 @@ async function detectGit(rootDir) {
|
|
|
1469
1721
|
if (!gitDir) {
|
|
1470
1722
|
return { type: "unknown" };
|
|
1471
1723
|
}
|
|
1472
|
-
const headPath =
|
|
1724
|
+
const headPath = path7.join(gitDir, "HEAD");
|
|
1473
1725
|
let headContent;
|
|
1474
1726
|
try {
|
|
1475
1727
|
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
@@ -1493,10 +1745,10 @@ async function detectGit(rootDir) {
|
|
|
1493
1745
|
};
|
|
1494
1746
|
}
|
|
1495
1747
|
async function findGitDir(startDir) {
|
|
1496
|
-
let dir =
|
|
1497
|
-
const root =
|
|
1748
|
+
let dir = path7.resolve(startDir);
|
|
1749
|
+
const root = path7.parse(dir).root;
|
|
1498
1750
|
while (dir !== root) {
|
|
1499
|
-
const gitPath =
|
|
1751
|
+
const gitPath = path7.join(dir, ".git");
|
|
1500
1752
|
try {
|
|
1501
1753
|
const stat3 = await fs3.stat(gitPath);
|
|
1502
1754
|
if (stat3.isDirectory()) {
|
|
@@ -1505,18 +1757,18 @@ async function findGitDir(startDir) {
|
|
|
1505
1757
|
if (stat3.isFile()) {
|
|
1506
1758
|
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
1507
1759
|
if (content.startsWith("gitdir: ")) {
|
|
1508
|
-
const resolved =
|
|
1760
|
+
const resolved = path7.resolve(dir, content.slice(8));
|
|
1509
1761
|
return resolved;
|
|
1510
1762
|
}
|
|
1511
1763
|
}
|
|
1512
1764
|
} catch {
|
|
1513
1765
|
}
|
|
1514
|
-
dir =
|
|
1766
|
+
dir = path7.dirname(dir);
|
|
1515
1767
|
}
|
|
1516
1768
|
return null;
|
|
1517
1769
|
}
|
|
1518
1770
|
async function resolveRef(gitDir, refPath) {
|
|
1519
|
-
const loosePath =
|
|
1771
|
+
const loosePath = path7.join(gitDir, refPath);
|
|
1520
1772
|
try {
|
|
1521
1773
|
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
1522
1774
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
@@ -1524,7 +1776,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
1524
1776
|
}
|
|
1525
1777
|
} catch {
|
|
1526
1778
|
}
|
|
1527
|
-
const packedPath =
|
|
1779
|
+
const packedPath = path7.join(gitDir, "packed-refs");
|
|
1528
1780
|
try {
|
|
1529
1781
|
const packed = await fs3.readFile(packedPath, "utf8");
|
|
1530
1782
|
for (const line of packed.split("\n")) {
|
|
@@ -1540,16 +1792,16 @@ async function resolveRef(gitDir, refPath) {
|
|
|
1540
1792
|
}
|
|
1541
1793
|
|
|
1542
1794
|
// src/ui/progress.ts
|
|
1543
|
-
import
|
|
1795
|
+
import chalk4 from "chalk";
|
|
1544
1796
|
var ROBOT = [
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1797
|
+
chalk4.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk4.greenBright("\u279C"),
|
|
1798
|
+
chalk4.cyan(" \u256D\u2524") + chalk4.greenBright("\u25C9 \u25C9") + chalk4.cyan("\u251C\u256E"),
|
|
1799
|
+
chalk4.cyan(" \u2570\u2524") + chalk4.dim("\u2500\u2500\u2500") + chalk4.cyan("\u251C\u256F"),
|
|
1800
|
+
chalk4.cyan(" \u2570\u2500\u2500\u2500\u256F")
|
|
1549
1801
|
];
|
|
1550
1802
|
var BRAND = [
|
|
1551
|
-
|
|
1552
|
-
|
|
1803
|
+
chalk4.bold.white(" V I B G R A T E"),
|
|
1804
|
+
chalk4.dim(` Drift Intelligence Engine`) + chalk4.dim(` v${VERSION}`)
|
|
1553
1805
|
];
|
|
1554
1806
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1555
1807
|
var ScanProgress = class {
|
|
@@ -1643,7 +1895,7 @@ var ScanProgress = class {
|
|
|
1643
1895
|
const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
1644
1896
|
const doneCount = this.steps.filter((s) => s.status === "done").length;
|
|
1645
1897
|
process.stderr.write(
|
|
1646
|
-
|
|
1898
|
+
chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
|
|
1647
1899
|
|
|
1648
1900
|
`)
|
|
1649
1901
|
);
|
|
@@ -1675,16 +1927,16 @@ var ScanProgress = class {
|
|
|
1675
1927
|
lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
|
|
1676
1928
|
lines.push(` ${ROBOT[1]} ${BRAND[1]}`);
|
|
1677
1929
|
lines.push(` ${ROBOT[2]}`);
|
|
1678
|
-
lines.push(` ${ROBOT[3]} ${
|
|
1930
|
+
lines.push(` ${ROBOT[3]} ${chalk4.dim(this.rootDir)}`);
|
|
1679
1931
|
lines.push("");
|
|
1680
1932
|
const totalSteps = this.steps.length;
|
|
1681
1933
|
const doneSteps = this.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
|
|
1682
1934
|
const pct = totalSteps > 0 ? Math.round(doneSteps / totalSteps * 100) : 0;
|
|
1683
1935
|
const barWidth = 30;
|
|
1684
1936
|
const filled = Math.round(doneSteps / Math.max(totalSteps, 1) * barWidth);
|
|
1685
|
-
const bar =
|
|
1937
|
+
const bar = chalk4.greenBright("\u2501".repeat(filled)) + chalk4.dim("\u254C".repeat(barWidth - filled));
|
|
1686
1938
|
const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
1687
|
-
lines.push(` ${bar} ${
|
|
1939
|
+
lines.push(` ${bar} ${chalk4.bold.white(`${pct}%`)} ${chalk4.dim(`${elapsed}s`)}`);
|
|
1688
1940
|
lines.push("");
|
|
1689
1941
|
for (const step of this.steps) {
|
|
1690
1942
|
lines.push(this.renderStep(step));
|
|
@@ -1703,27 +1955,27 @@ var ScanProgress = class {
|
|
|
1703
1955
|
let detail = "";
|
|
1704
1956
|
switch (step.status) {
|
|
1705
1957
|
case "done":
|
|
1706
|
-
icon =
|
|
1707
|
-
label =
|
|
1958
|
+
icon = chalk4.green("\u2714");
|
|
1959
|
+
label = chalk4.white(step.label);
|
|
1708
1960
|
break;
|
|
1709
1961
|
case "active":
|
|
1710
|
-
icon =
|
|
1711
|
-
label =
|
|
1962
|
+
icon = chalk4.cyan(spinner);
|
|
1963
|
+
label = chalk4.bold.white(step.label);
|
|
1712
1964
|
break;
|
|
1713
1965
|
case "skipped":
|
|
1714
|
-
icon =
|
|
1715
|
-
label =
|
|
1966
|
+
icon = chalk4.dim("\u25CC");
|
|
1967
|
+
label = chalk4.dim.strikethrough(step.label);
|
|
1716
1968
|
break;
|
|
1717
1969
|
default:
|
|
1718
|
-
icon =
|
|
1719
|
-
label =
|
|
1970
|
+
icon = chalk4.dim("\u25CB");
|
|
1971
|
+
label = chalk4.dim(step.label);
|
|
1720
1972
|
break;
|
|
1721
1973
|
}
|
|
1722
1974
|
if (step.detail) {
|
|
1723
|
-
detail =
|
|
1975
|
+
detail = chalk4.dim(` \xB7 ${step.detail}`);
|
|
1724
1976
|
}
|
|
1725
1977
|
if (step.count !== void 0 && step.count > 0) {
|
|
1726
|
-
detail +=
|
|
1978
|
+
detail += chalk4.cyan(` (${step.count})`);
|
|
1727
1979
|
}
|
|
1728
1980
|
return ` ${icon} ${label}${detail}`;
|
|
1729
1981
|
}
|
|
@@ -1735,18 +1987,18 @@ var ScanProgress = class {
|
|
|
1735
1987
|
const e = this.stats.findings.errors;
|
|
1736
1988
|
const n = this.stats.findings.notes;
|
|
1737
1989
|
const parts = [
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1990
|
+
chalk4.bold.white(` ${p}`) + chalk4.dim(` project${p !== 1 ? "s" : ""}`),
|
|
1991
|
+
chalk4.white(`${d}`) + chalk4.dim(` dep${d !== 1 ? "s" : ""}`),
|
|
1992
|
+
chalk4.white(`${f}`) + chalk4.dim(` framework${f !== 1 ? "s" : ""}`)
|
|
1741
1993
|
];
|
|
1742
1994
|
const findingParts = [];
|
|
1743
|
-
if (e > 0) findingParts.push(
|
|
1744
|
-
if (w > 0) findingParts.push(
|
|
1745
|
-
if (n > 0) findingParts.push(
|
|
1995
|
+
if (e > 0) findingParts.push(chalk4.red(`${e} \u2716`));
|
|
1996
|
+
if (w > 0) findingParts.push(chalk4.yellow(`${w} \u26A0`));
|
|
1997
|
+
if (n > 0) findingParts.push(chalk4.blue(`${n} \u2139`));
|
|
1746
1998
|
if (findingParts.length > 0) {
|
|
1747
|
-
parts.push(findingParts.join(
|
|
1999
|
+
parts.push(findingParts.join(chalk4.dim(" \xB7 ")));
|
|
1748
2000
|
}
|
|
1749
|
-
return ` ${
|
|
2001
|
+
return ` ${chalk4.dim("\u2503")} ${parts.join(chalk4.dim(" \u2502 "))}`;
|
|
1750
2002
|
}
|
|
1751
2003
|
/** Simple CI-friendly output (no ANSI rewriting) */
|
|
1752
2004
|
lastCIStep = null;
|
|
@@ -1765,7 +2017,7 @@ var ScanProgress = class {
|
|
|
1765
2017
|
};
|
|
1766
2018
|
|
|
1767
2019
|
// src/scanners/platform-matrix.ts
|
|
1768
|
-
import * as
|
|
2020
|
+
import * as path8 from "path";
|
|
1769
2021
|
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
1770
2022
|
// Image / media processing
|
|
1771
2023
|
"sharp",
|
|
@@ -2042,7 +2294,7 @@ async function scanPlatformMatrix(rootDir) {
|
|
|
2042
2294
|
}
|
|
2043
2295
|
result.dockerBaseImages = [...baseImages].sort();
|
|
2044
2296
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
2045
|
-
if (await pathExists(
|
|
2297
|
+
if (await pathExists(path8.join(rootDir, file))) {
|
|
2046
2298
|
result.nodeVersionFiles.push(file);
|
|
2047
2299
|
}
|
|
2048
2300
|
}
|
|
@@ -2118,7 +2370,7 @@ function scanDependencyRisk(projects) {
|
|
|
2118
2370
|
}
|
|
2119
2371
|
|
|
2120
2372
|
// src/scanners/dependency-graph.ts
|
|
2121
|
-
import * as
|
|
2373
|
+
import * as path9 from "path";
|
|
2122
2374
|
function parsePnpmLock(content) {
|
|
2123
2375
|
const entries = [];
|
|
2124
2376
|
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
@@ -2177,9 +2429,9 @@ async function scanDependencyGraph(rootDir) {
|
|
|
2177
2429
|
phantomDependencies: []
|
|
2178
2430
|
};
|
|
2179
2431
|
let entries = [];
|
|
2180
|
-
const pnpmLock =
|
|
2181
|
-
const npmLock =
|
|
2182
|
-
const yarnLock =
|
|
2432
|
+
const pnpmLock = path9.join(rootDir, "pnpm-lock.yaml");
|
|
2433
|
+
const npmLock = path9.join(rootDir, "package-lock.json");
|
|
2434
|
+
const yarnLock = path9.join(rootDir, "yarn.lock");
|
|
2183
2435
|
if (await pathExists(pnpmLock)) {
|
|
2184
2436
|
result.lockfileType = "pnpm";
|
|
2185
2437
|
const content = await readTextFile(pnpmLock);
|
|
@@ -2224,7 +2476,7 @@ async function scanDependencyGraph(rootDir) {
|
|
|
2224
2476
|
for (const pjPath of pkgFiles) {
|
|
2225
2477
|
try {
|
|
2226
2478
|
const pj = await readJsonFile(pjPath);
|
|
2227
|
-
const relPath =
|
|
2479
|
+
const relPath = path9.relative(rootDir, pjPath);
|
|
2228
2480
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
2229
2481
|
const deps = pj[section];
|
|
2230
2482
|
if (!deps) continue;
|
|
@@ -2570,7 +2822,7 @@ function scanToolingInventory(projects) {
|
|
|
2570
2822
|
}
|
|
2571
2823
|
|
|
2572
2824
|
// src/scanners/build-deploy.ts
|
|
2573
|
-
import * as
|
|
2825
|
+
import * as path10 from "path";
|
|
2574
2826
|
var CI_FILES = {
|
|
2575
2827
|
".github/workflows": "github-actions",
|
|
2576
2828
|
".gitlab-ci.yml": "gitlab-ci",
|
|
@@ -2620,12 +2872,12 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2620
2872
|
};
|
|
2621
2873
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
2622
2874
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
2623
|
-
const fullPath =
|
|
2875
|
+
const fullPath = path10.join(rootDir, file);
|
|
2624
2876
|
if (await pathExists(fullPath)) {
|
|
2625
2877
|
ciSystems.add(system);
|
|
2626
2878
|
}
|
|
2627
2879
|
}
|
|
2628
|
-
const ghWorkflowDir =
|
|
2880
|
+
const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
|
|
2629
2881
|
if (await pathExists(ghWorkflowDir)) {
|
|
2630
2882
|
try {
|
|
2631
2883
|
const files = await findFiles(
|
|
@@ -2673,11 +2925,11 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2673
2925
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
2674
2926
|
);
|
|
2675
2927
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
2676
|
-
if (await pathExists(
|
|
2928
|
+
if (await pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
2677
2929
|
result.iac = [...iacSystems].sort();
|
|
2678
2930
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
2679
2931
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
2680
|
-
if (await pathExists(
|
|
2932
|
+
if (await pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
|
|
2681
2933
|
}
|
|
2682
2934
|
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
2683
2935
|
for (const pjPath of pkgFiles) {
|
|
@@ -2702,19 +2954,19 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2702
2954
|
};
|
|
2703
2955
|
const managers = /* @__PURE__ */ new Set();
|
|
2704
2956
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
2705
|
-
if (await pathExists(
|
|
2957
|
+
if (await pathExists(path10.join(rootDir, file))) managers.add(manager);
|
|
2706
2958
|
}
|
|
2707
2959
|
result.packageManagers = [...managers].sort();
|
|
2708
2960
|
const monoTools = /* @__PURE__ */ new Set();
|
|
2709
2961
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
2710
|
-
if (await pathExists(
|
|
2962
|
+
if (await pathExists(path10.join(rootDir, file))) monoTools.add(tool);
|
|
2711
2963
|
}
|
|
2712
2964
|
result.monorepoTools = [...monoTools].sort();
|
|
2713
2965
|
return result;
|
|
2714
2966
|
}
|
|
2715
2967
|
|
|
2716
2968
|
// src/scanners/ts-modernity.ts
|
|
2717
|
-
import * as
|
|
2969
|
+
import * as path11 from "path";
|
|
2718
2970
|
async function scanTsModernity(rootDir) {
|
|
2719
2971
|
const result = {
|
|
2720
2972
|
typescriptVersion: null,
|
|
@@ -2752,7 +3004,7 @@ async function scanTsModernity(rootDir) {
|
|
|
2752
3004
|
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
2753
3005
|
else if (hasEsm) result.moduleType = "esm";
|
|
2754
3006
|
else if (hasCjs) result.moduleType = "cjs";
|
|
2755
|
-
let tsConfigPath =
|
|
3007
|
+
let tsConfigPath = path11.join(rootDir, "tsconfig.json");
|
|
2756
3008
|
if (!await pathExists(tsConfigPath)) {
|
|
2757
3009
|
const tsConfigs = await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
2758
3010
|
if (tsConfigs.length > 0) {
|
|
@@ -3098,7 +3350,7 @@ function scanBreakingChangeExposure(projects) {
|
|
|
3098
3350
|
|
|
3099
3351
|
// src/scanners/file-hotspots.ts
|
|
3100
3352
|
import * as fs4 from "fs/promises";
|
|
3101
|
-
import * as
|
|
3353
|
+
import * as path12 from "path";
|
|
3102
3354
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3103
3355
|
"node_modules",
|
|
3104
3356
|
".git",
|
|
@@ -3153,15 +3405,15 @@ async function scanFileHotspots(rootDir) {
|
|
|
3153
3405
|
for (const e of entries) {
|
|
3154
3406
|
if (e.isDirectory) {
|
|
3155
3407
|
if (SKIP_DIRS2.has(e.name)) continue;
|
|
3156
|
-
await walk(
|
|
3408
|
+
await walk(path12.join(dir, e.name), depth + 1);
|
|
3157
3409
|
} else if (e.isFile) {
|
|
3158
|
-
const ext =
|
|
3410
|
+
const ext = path12.extname(e.name).toLowerCase();
|
|
3159
3411
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3160
3412
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3161
3413
|
try {
|
|
3162
|
-
const stat3 = await fs4.stat(
|
|
3414
|
+
const stat3 = await fs4.stat(path12.join(dir, e.name));
|
|
3163
3415
|
allFiles.push({
|
|
3164
|
-
path:
|
|
3416
|
+
path: path12.relative(rootDir, path12.join(dir, e.name)),
|
|
3165
3417
|
bytes: stat3.size
|
|
3166
3418
|
});
|
|
3167
3419
|
} catch {
|
|
@@ -3183,7 +3435,7 @@ async function scanFileHotspots(rootDir) {
|
|
|
3183
3435
|
}
|
|
3184
3436
|
|
|
3185
3437
|
// src/scanners/security-posture.ts
|
|
3186
|
-
import * as
|
|
3438
|
+
import * as path13 from "path";
|
|
3187
3439
|
var LOCKFILES = {
|
|
3188
3440
|
"pnpm-lock.yaml": "pnpm",
|
|
3189
3441
|
"package-lock.json": "npm",
|
|
@@ -3202,14 +3454,14 @@ async function scanSecurityPosture(rootDir) {
|
|
|
3202
3454
|
};
|
|
3203
3455
|
const foundLockfiles = [];
|
|
3204
3456
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
3205
|
-
if (await pathExists(
|
|
3457
|
+
if (await pathExists(path13.join(rootDir, file))) {
|
|
3206
3458
|
foundLockfiles.push(type);
|
|
3207
3459
|
}
|
|
3208
3460
|
}
|
|
3209
3461
|
result.lockfilePresent = foundLockfiles.length > 0;
|
|
3210
3462
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
3211
3463
|
result.lockfileTypes = foundLockfiles.sort();
|
|
3212
|
-
const gitignorePath =
|
|
3464
|
+
const gitignorePath = path13.join(rootDir, ".gitignore");
|
|
3213
3465
|
if (await pathExists(gitignorePath)) {
|
|
3214
3466
|
try {
|
|
3215
3467
|
const content = await readTextFile(gitignorePath);
|
|
@@ -3224,7 +3476,7 @@ async function scanSecurityPosture(rootDir) {
|
|
|
3224
3476
|
}
|
|
3225
3477
|
}
|
|
3226
3478
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
3227
|
-
if (await pathExists(
|
|
3479
|
+
if (await pathExists(path13.join(rootDir, envFile))) {
|
|
3228
3480
|
if (!result.gitignoreCoversEnv) {
|
|
3229
3481
|
result.envFilesTracked = true;
|
|
3230
3482
|
break;
|
|
@@ -3650,10 +3902,12 @@ function scanServiceDependencies(projects) {
|
|
|
3650
3902
|
|
|
3651
3903
|
// src/commands/scan.ts
|
|
3652
3904
|
async function runScan(rootDir, opts) {
|
|
3905
|
+
const scanStart = Date.now();
|
|
3653
3906
|
const config = await loadConfig(rootDir);
|
|
3654
3907
|
const sem = new Semaphore(opts.concurrency);
|
|
3655
3908
|
const npmCache = new NpmCache(rootDir, sem);
|
|
3656
3909
|
const scanners = config.scanners;
|
|
3910
|
+
let filesScanned = 0;
|
|
3657
3911
|
const progress = new ScanProgress(rootDir);
|
|
3658
3912
|
const steps = [
|
|
3659
3913
|
{ id: "config", label: "Loading configuration" },
|
|
@@ -3687,6 +3941,7 @@ async function runScan(rootDir, opts) {
|
|
|
3687
3941
|
progress.addDependencies(p.dependencies.length);
|
|
3688
3942
|
progress.addFrameworks(p.frameworks.length);
|
|
3689
3943
|
}
|
|
3944
|
+
filesScanned += nodeProjects.length;
|
|
3690
3945
|
progress.addProjects(nodeProjects.length);
|
|
3691
3946
|
progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
|
|
3692
3947
|
progress.startStep("dotnet");
|
|
@@ -3695,91 +3950,137 @@ async function runScan(rootDir, opts) {
|
|
|
3695
3950
|
progress.addDependencies(p.dependencies.length);
|
|
3696
3951
|
progress.addFrameworks(p.frameworks.length);
|
|
3697
3952
|
}
|
|
3953
|
+
filesScanned += dotnetProjects.length;
|
|
3698
3954
|
progress.addProjects(dotnetProjects.length);
|
|
3699
3955
|
progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
|
|
3700
3956
|
const allProjects = [...nodeProjects, ...dotnetProjects];
|
|
3957
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
3958
|
+
const parsedDsn = dsn ? parseDsn(dsn) : null;
|
|
3959
|
+
const workspaceId = parsedDsn?.workspaceId;
|
|
3960
|
+
for (const project of allProjects) {
|
|
3961
|
+
project.drift = computeDriftScore([project]);
|
|
3962
|
+
project.projectId = computeProjectId(project.path, project.name, workspaceId);
|
|
3963
|
+
}
|
|
3701
3964
|
const extended = {};
|
|
3702
3965
|
if (scanners !== false) {
|
|
3966
|
+
const scannerTasks = [];
|
|
3703
3967
|
if (scanners?.platformMatrix?.enabled !== false) {
|
|
3704
3968
|
progress.startStep("platform");
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3969
|
+
scannerTasks.push(
|
|
3970
|
+
scanPlatformMatrix(rootDir).then((result) => {
|
|
3971
|
+
extended.platformMatrix = result;
|
|
3972
|
+
const nativeCount = result.nativeModules.length;
|
|
3973
|
+
const dockerCount = result.dockerBaseImages.length;
|
|
3974
|
+
const parts = [];
|
|
3975
|
+
if (nativeCount > 0) parts.push(`${nativeCount} native`);
|
|
3976
|
+
if (dockerCount > 0) parts.push(`${dockerCount} docker`);
|
|
3977
|
+
progress.completeStep("platform", parts.join(", ") || "clean", nativeCount + dockerCount);
|
|
3978
|
+
})
|
|
3979
|
+
);
|
|
3712
3980
|
}
|
|
3713
3981
|
if (scanners?.toolingInventory?.enabled !== false) {
|
|
3714
3982
|
progress.startStep("tooling");
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3983
|
+
scannerTasks.push(
|
|
3984
|
+
Promise.resolve().then(() => {
|
|
3985
|
+
extended.toolingInventory = scanToolingInventory(allProjects);
|
|
3986
|
+
const toolCount = Object.values(extended.toolingInventory).reduce((sum, arr) => sum + arr.length, 0);
|
|
3987
|
+
progress.completeStep("tooling", `${toolCount} tool${toolCount !== 1 ? "s" : ""} mapped`, toolCount);
|
|
3988
|
+
})
|
|
3989
|
+
);
|
|
3718
3990
|
}
|
|
3719
3991
|
if (scanners?.serviceDependencies?.enabled !== false) {
|
|
3720
3992
|
progress.startStep("services");
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3993
|
+
scannerTasks.push(
|
|
3994
|
+
Promise.resolve().then(() => {
|
|
3995
|
+
extended.serviceDependencies = scanServiceDependencies(allProjects);
|
|
3996
|
+
const svcCount = Object.values(extended.serviceDependencies).reduce((sum, arr) => sum + arr.length, 0);
|
|
3997
|
+
progress.completeStep("services", `${svcCount} service${svcCount !== 1 ? "s" : ""} detected`, svcCount);
|
|
3998
|
+
})
|
|
3999
|
+
);
|
|
3724
4000
|
}
|
|
3725
4001
|
if (scanners?.breakingChangeExposure?.enabled !== false) {
|
|
3726
4002
|
progress.startStep("breaking");
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
4003
|
+
scannerTasks.push(
|
|
4004
|
+
Promise.resolve().then(() => {
|
|
4005
|
+
extended.breakingChangeExposure = scanBreakingChangeExposure(allProjects);
|
|
4006
|
+
const bc = extended.breakingChangeExposure;
|
|
4007
|
+
const bcTotal = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
|
|
4008
|
+
progress.completeStep(
|
|
4009
|
+
"breaking",
|
|
4010
|
+
bcTotal > 0 ? `${bc.deprecatedPackages.length} deprecated, ${bc.legacyPolyfills.length} polyfills` : "none found",
|
|
4011
|
+
bcTotal
|
|
4012
|
+
);
|
|
4013
|
+
})
|
|
3734
4014
|
);
|
|
3735
4015
|
}
|
|
3736
4016
|
if (scanners?.securityPosture?.enabled !== false) {
|
|
3737
4017
|
progress.startStep("security");
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
4018
|
+
scannerTasks.push(
|
|
4019
|
+
scanSecurityPosture(rootDir).then((result) => {
|
|
4020
|
+
extended.securityPosture = result;
|
|
4021
|
+
const secDetail = result.lockfilePresent ? `lockfile \u2714${result.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
|
|
4022
|
+
progress.completeStep("security", secDetail);
|
|
4023
|
+
})
|
|
4024
|
+
);
|
|
3742
4025
|
}
|
|
3743
4026
|
if (scanners?.buildDeploy?.enabled !== false) {
|
|
3744
4027
|
progress.startStep("build");
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
4028
|
+
scannerTasks.push(
|
|
4029
|
+
scanBuildDeploy(rootDir).then((result) => {
|
|
4030
|
+
extended.buildDeploy = result;
|
|
4031
|
+
const bdParts = [];
|
|
4032
|
+
if (result.ci.length > 0) bdParts.push(result.ci.join(", "));
|
|
4033
|
+
if (result.docker.dockerfileCount > 0) bdParts.push(`${result.docker.dockerfileCount} Dockerfile${result.docker.dockerfileCount !== 1 ? "s" : ""}`);
|
|
4034
|
+
progress.completeStep("build", bdParts.join(" \xB7 ") || "none detected");
|
|
4035
|
+
})
|
|
4036
|
+
);
|
|
3751
4037
|
}
|
|
3752
4038
|
if (scanners?.tsModernity?.enabled !== false) {
|
|
3753
4039
|
progress.startStep("ts");
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
4040
|
+
scannerTasks.push(
|
|
4041
|
+
scanTsModernity(rootDir).then((result) => {
|
|
4042
|
+
extended.tsModernity = result;
|
|
4043
|
+
const tsParts = [];
|
|
4044
|
+
if (result.typescriptVersion) tsParts.push(`v${result.typescriptVersion}`);
|
|
4045
|
+
if (result.strict === true) tsParts.push("strict");
|
|
4046
|
+
if (result.moduleType) tsParts.push(result.moduleType.toUpperCase());
|
|
4047
|
+
progress.completeStep("ts", tsParts.join(" \xB7 ") || "no tsconfig");
|
|
4048
|
+
})
|
|
4049
|
+
);
|
|
3761
4050
|
}
|
|
3762
4051
|
if (scanners?.fileHotspots?.enabled !== false) {
|
|
3763
4052
|
progress.startStep("hotspots");
|
|
3764
|
-
|
|
3765
|
-
|
|
4053
|
+
scannerTasks.push(
|
|
4054
|
+
scanFileHotspots(rootDir).then((result) => {
|
|
4055
|
+
extended.fileHotspots = result;
|
|
4056
|
+
progress.completeStep("hotspots", `${result.totalFiles} files`, result.totalFiles);
|
|
4057
|
+
})
|
|
4058
|
+
);
|
|
3766
4059
|
}
|
|
3767
4060
|
if (scanners?.dependencyGraph?.enabled !== false) {
|
|
3768
4061
|
progress.startStep("depgraph");
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
4062
|
+
scannerTasks.push(
|
|
4063
|
+
scanDependencyGraph(rootDir).then((result) => {
|
|
4064
|
+
extended.dependencyGraph = result;
|
|
4065
|
+
const dgDetail = result.lockfileType ? `${result.lockfileType} \xB7 ${result.totalUnique} unique` : "no lockfile";
|
|
4066
|
+
progress.completeStep("depgraph", dgDetail, result.totalUnique);
|
|
4067
|
+
})
|
|
4068
|
+
);
|
|
3773
4069
|
}
|
|
3774
4070
|
if (scanners?.dependencyRisk?.enabled !== false) {
|
|
3775
4071
|
progress.startStep("deprisk");
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
4072
|
+
scannerTasks.push(
|
|
4073
|
+
Promise.resolve().then(() => {
|
|
4074
|
+
extended.dependencyRisk = scanDependencyRisk(allProjects);
|
|
4075
|
+
const dr = extended.dependencyRisk;
|
|
4076
|
+
const drParts = [];
|
|
4077
|
+
if (dr.deprecatedPackages.length > 0) drParts.push(`${dr.deprecatedPackages.length} deprecated`);
|
|
4078
|
+
if (dr.nativeModulePackages.length > 0) drParts.push(`${dr.nativeModulePackages.length} native`);
|
|
4079
|
+
progress.completeStep("deprisk", drParts.join(", ") || "low risk");
|
|
4080
|
+
})
|
|
4081
|
+
);
|
|
3782
4082
|
}
|
|
4083
|
+
await Promise.all(scannerTasks);
|
|
3783
4084
|
}
|
|
3784
4085
|
progress.startStep("drift");
|
|
3785
4086
|
const drift = computeDriftScore(allProjects);
|
|
@@ -3797,39 +4098,69 @@ async function runScan(rootDir, opts) {
|
|
|
3797
4098
|
progress.completeStep("findings", findingParts.join(", ") || "none");
|
|
3798
4099
|
progress.finish();
|
|
3799
4100
|
if (allProjects.length === 0) {
|
|
3800
|
-
console.log(
|
|
4101
|
+
console.log(chalk5.yellow("No projects found."));
|
|
4102
|
+
}
|
|
4103
|
+
if (extended.fileHotspots) filesScanned += extended.fileHotspots.totalFiles;
|
|
4104
|
+
if (extended.securityPosture) filesScanned += 1;
|
|
4105
|
+
if (extended.tsModernity?.typescriptVersion) filesScanned += 1;
|
|
4106
|
+
if (extended.dependencyGraph?.lockfileType) filesScanned += 1;
|
|
4107
|
+
if (extended.buildDeploy) {
|
|
4108
|
+
filesScanned += extended.buildDeploy.docker.dockerfileCount;
|
|
4109
|
+
filesScanned += extended.buildDeploy.ci.length;
|
|
3801
4110
|
}
|
|
4111
|
+
const durationMs = Date.now() - scanStart;
|
|
3802
4112
|
const artifact = {
|
|
3803
4113
|
schemaVersion: "1.0",
|
|
3804
4114
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3805
4115
|
vibgrateVersion: VERSION,
|
|
3806
|
-
rootPath:
|
|
4116
|
+
rootPath: path14.basename(rootDir),
|
|
3807
4117
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
3808
4118
|
projects: allProjects,
|
|
3809
4119
|
drift,
|
|
3810
4120
|
findings,
|
|
3811
|
-
...Object.keys(extended).length > 0 ? { extended } : {}
|
|
4121
|
+
...Object.keys(extended).length > 0 ? { extended } : {},
|
|
4122
|
+
durationMs,
|
|
4123
|
+
filesScanned
|
|
3812
4124
|
};
|
|
3813
4125
|
if (opts.baseline) {
|
|
3814
|
-
const baselinePath =
|
|
4126
|
+
const baselinePath = path14.resolve(opts.baseline);
|
|
3815
4127
|
if (await pathExists(baselinePath)) {
|
|
3816
4128
|
try {
|
|
3817
4129
|
const baseline = await readJsonFile(baselinePath);
|
|
3818
4130
|
artifact.baseline = baselinePath;
|
|
3819
4131
|
artifact.delta = artifact.drift.score - baseline.drift.score;
|
|
3820
4132
|
} catch {
|
|
3821
|
-
console.error(
|
|
4133
|
+
console.error(chalk5.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
|
|
3822
4134
|
}
|
|
3823
4135
|
}
|
|
3824
4136
|
}
|
|
3825
|
-
const vibgrateDir =
|
|
4137
|
+
const vibgrateDir = path14.join(rootDir, ".vibgrate");
|
|
3826
4138
|
await ensureDir(vibgrateDir);
|
|
3827
|
-
await writeJsonFile(
|
|
4139
|
+
await writeJsonFile(path14.join(vibgrateDir, "scan_result.json"), artifact);
|
|
4140
|
+
for (const project of allProjects) {
|
|
4141
|
+
if (project.drift && project.path) {
|
|
4142
|
+
const projectDir = path14.resolve(rootDir, project.path);
|
|
4143
|
+
const projectVibgrateDir = path14.join(projectDir, ".vibgrate");
|
|
4144
|
+
await ensureDir(projectVibgrateDir);
|
|
4145
|
+
await writeJsonFile(path14.join(projectVibgrateDir, "project_score.json"), {
|
|
4146
|
+
projectId: project.projectId,
|
|
4147
|
+
name: project.name,
|
|
4148
|
+
type: project.type,
|
|
4149
|
+
path: project.path,
|
|
4150
|
+
score: project.drift.score,
|
|
4151
|
+
riskLevel: project.drift.riskLevel,
|
|
4152
|
+
components: project.drift.components,
|
|
4153
|
+
measured: project.drift.measured,
|
|
4154
|
+
scannedAt: artifact.timestamp,
|
|
4155
|
+
vibgrateVersion: VERSION
|
|
4156
|
+
});
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
3828
4159
|
if (opts.format === "json") {
|
|
3829
4160
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
3830
4161
|
if (opts.out) {
|
|
3831
|
-
await writeTextFile(
|
|
3832
|
-
console.log(
|
|
4162
|
+
await writeTextFile(path14.resolve(opts.out), jsonStr);
|
|
4163
|
+
console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
3833
4164
|
} else {
|
|
3834
4165
|
console.log(jsonStr);
|
|
3835
4166
|
}
|
|
@@ -3837,8 +4168,8 @@ async function runScan(rootDir, opts) {
|
|
|
3837
4168
|
const sarif = formatSarif(artifact);
|
|
3838
4169
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
3839
4170
|
if (opts.out) {
|
|
3840
|
-
await writeTextFile(
|
|
3841
|
-
console.log(
|
|
4171
|
+
await writeTextFile(path14.resolve(opts.out), sarifStr);
|
|
4172
|
+
console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
3842
4173
|
} else {
|
|
3843
4174
|
console.log(sarifStr);
|
|
3844
4175
|
}
|
|
@@ -3846,15 +4177,66 @@ async function runScan(rootDir, opts) {
|
|
|
3846
4177
|
const text = formatText(artifact);
|
|
3847
4178
|
console.log(text);
|
|
3848
4179
|
if (opts.out) {
|
|
3849
|
-
await writeTextFile(
|
|
4180
|
+
await writeTextFile(path14.resolve(opts.out), text);
|
|
3850
4181
|
}
|
|
3851
4182
|
}
|
|
3852
4183
|
return artifact;
|
|
3853
4184
|
}
|
|
3854
|
-
|
|
3855
|
-
const
|
|
4185
|
+
async function autoPush(artifact, rootDir, opts) {
|
|
4186
|
+
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
4187
|
+
if (!dsn) {
|
|
4188
|
+
console.error(chalk5.red("No DSN provided for push."));
|
|
4189
|
+
console.error(chalk5.dim("Set VIBGRATE_DSN environment variable or use --dsn flag."));
|
|
4190
|
+
if (opts.strict) process.exit(1);
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
const parsed = parseDsn(dsn);
|
|
4194
|
+
if (!parsed) {
|
|
4195
|
+
console.error(chalk5.red("Invalid DSN format."));
|
|
4196
|
+
if (opts.strict) process.exit(1);
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
const body = JSON.stringify(artifact);
|
|
4200
|
+
const timestamp = String(Date.now());
|
|
4201
|
+
const hmac = computeHmac(body, parsed.secret);
|
|
4202
|
+
let host = parsed.host;
|
|
4203
|
+
if (opts.region) {
|
|
4204
|
+
try {
|
|
4205
|
+
host = resolveIngestHost(opts.region);
|
|
4206
|
+
} catch (e) {
|
|
4207
|
+
console.error(chalk5.red(e instanceof Error ? e.message : String(e)));
|
|
4208
|
+
if (opts.strict) process.exit(1);
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
const url = `https://${host}/v1/ingest/scan`;
|
|
4213
|
+
console.log(chalk5.dim(`Uploading to ${host}...`));
|
|
4214
|
+
try {
|
|
4215
|
+
const response = await fetch(url, {
|
|
4216
|
+
method: "POST",
|
|
4217
|
+
headers: {
|
|
4218
|
+
"Content-Type": "application/json",
|
|
4219
|
+
"X-Vibgrate-Timestamp": timestamp,
|
|
4220
|
+
"Authorization": `VibgrateDSN ${parsed.keyId}:${hmac}`
|
|
4221
|
+
},
|
|
4222
|
+
body
|
|
4223
|
+
});
|
|
4224
|
+
if (!response.ok) {
|
|
4225
|
+
const text = await response.text();
|
|
4226
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
4227
|
+
}
|
|
4228
|
+
const result = await response.json();
|
|
4229
|
+
console.log(chalk5.green("\u2714") + ` Uploaded successfully (${result.ingestId ?? "ok"})`);
|
|
4230
|
+
} catch (e) {
|
|
4231
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4232
|
+
console.error(chalk5.red(`Upload failed: ${msg}`));
|
|
4233
|
+
if (opts.strict) process.exit(1);
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
var scanCommand = new Command3("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").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").action(async (targetPath, opts) => {
|
|
4237
|
+
const rootDir = path14.resolve(targetPath);
|
|
3856
4238
|
if (!await pathExists(rootDir)) {
|
|
3857
|
-
console.error(
|
|
4239
|
+
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
3858
4240
|
process.exit(1);
|
|
3859
4241
|
}
|
|
3860
4242
|
const scanOpts = {
|
|
@@ -3863,38 +4245,45 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
3863
4245
|
failOn: opts.failOn,
|
|
3864
4246
|
baseline: opts.baseline,
|
|
3865
4247
|
changedOnly: opts.changedOnly,
|
|
3866
|
-
concurrency: parseInt(opts.concurrency, 10) || 8
|
|
4248
|
+
concurrency: parseInt(opts.concurrency, 10) || 8,
|
|
4249
|
+
push: opts.push,
|
|
4250
|
+
dsn: opts.dsn,
|
|
4251
|
+
region: opts.region,
|
|
4252
|
+
strict: opts.strict
|
|
3867
4253
|
};
|
|
3868
4254
|
const artifact = await runScan(rootDir, scanOpts);
|
|
3869
4255
|
if (opts.failOn) {
|
|
3870
4256
|
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
3871
4257
|
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
3872
4258
|
if (opts.failOn === "error" && hasErrors) {
|
|
3873
|
-
console.error(
|
|
4259
|
+
console.error(chalk5.red(`
|
|
3874
4260
|
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
3875
4261
|
process.exit(2);
|
|
3876
4262
|
}
|
|
3877
4263
|
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
3878
|
-
console.error(
|
|
4264
|
+
console.error(chalk5.red(`
|
|
3879
4265
|
Failing: findings detected at warn level or above.`));
|
|
3880
4266
|
process.exit(2);
|
|
3881
4267
|
}
|
|
3882
4268
|
}
|
|
4269
|
+
if (opts.push) {
|
|
4270
|
+
await autoPush(artifact, rootDir, scanOpts);
|
|
4271
|
+
}
|
|
3883
4272
|
});
|
|
3884
4273
|
|
|
3885
4274
|
export {
|
|
3886
4275
|
readJsonFile,
|
|
3887
|
-
readTextFile,
|
|
3888
4276
|
pathExists,
|
|
3889
4277
|
ensureDir,
|
|
3890
4278
|
writeJsonFile,
|
|
3891
|
-
writeTextFile,
|
|
3892
4279
|
writeDefaultConfig,
|
|
3893
4280
|
computeDriftScore,
|
|
3894
4281
|
generateFindings,
|
|
4282
|
+
VERSION,
|
|
3895
4283
|
formatText,
|
|
3896
4284
|
formatSarif,
|
|
3897
|
-
|
|
4285
|
+
dsnCommand,
|
|
4286
|
+
pushCommand,
|
|
3898
4287
|
runScan,
|
|
3899
4288
|
scanCommand
|
|
3900
4289
|
};
|