@vibgrate/cli 1.0.1 → 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 +46 -7
- package/dist/{baseline-35XRSRAD.js → baseline-5SAUNH2V.js} +2 -2
- package/dist/{chunk-NTRKEIKP.js → chunk-GG5AUF7X.js} +1 -1
- package/dist/{chunk-VMNBKARQ.js → chunk-XZ4NRZMT.js} +435 -112
- package/dist/cli.js +31 -164
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -1
- 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,6 +276,10 @@ 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
|
+
}
|
|
277
283
|
|
|
278
284
|
// src/version.ts
|
|
279
285
|
import { createRequire } from "module";
|
|
@@ -553,10 +559,17 @@ function generatePriorityActions(artifact) {
|
|
|
553
559
|
);
|
|
554
560
|
if (eolProjects.length > 0) {
|
|
555
561
|
const names = eolProjects.map((p) => p.name).join(", ");
|
|
556
|
-
|
|
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("");
|
|
557
570
|
actions.push({
|
|
558
571
|
title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
|
|
559
|
-
explanation:
|
|
572
|
+
explanation: detail,
|
|
560
573
|
impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
|
|
561
574
|
severity: 100
|
|
562
575
|
});
|
|
@@ -565,16 +578,30 @@ function generatePriorityActions(artifact) {
|
|
|
565
578
|
for (const p of artifact.projects) {
|
|
566
579
|
for (const fw of p.frameworks) {
|
|
567
580
|
if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
|
|
568
|
-
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 });
|
|
569
582
|
}
|
|
570
583
|
}
|
|
571
584
|
}
|
|
572
585
|
if (severeFrameworks.length > 0) {
|
|
573
586
|
const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
|
|
574
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`;
|
|
575
602
|
actions.push({
|
|
576
603
|
title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
|
|
577
|
-
explanation:
|
|
604
|
+
explanation: detail,
|
|
578
605
|
impact: `+5\u201315 points (framework score)`,
|
|
579
606
|
severity: 90
|
|
580
607
|
});
|
|
@@ -585,9 +612,28 @@ function generatePriorityActions(artifact) {
|
|
|
585
612
|
if (total === 0) continue;
|
|
586
613
|
const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
|
|
587
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
|
+
}
|
|
588
634
|
actions.push({
|
|
589
635
|
title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
|
|
590
|
-
explanation:
|
|
636
|
+
explanation: detail,
|
|
591
637
|
impact: `+5\u201310 points (dependency score)`,
|
|
592
638
|
severity: 80 + twoPlusPct / 10
|
|
593
639
|
});
|
|
@@ -597,7 +643,7 @@ function generatePriorityActions(artifact) {
|
|
|
597
643
|
for (const p of artifact.projects) {
|
|
598
644
|
for (const fw of p.frameworks) {
|
|
599
645
|
if (fw.majorsBehind === 2) {
|
|
600
|
-
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}` });
|
|
601
647
|
}
|
|
602
648
|
}
|
|
603
649
|
}
|
|
@@ -605,9 +651,23 @@ function generatePriorityActions(artifact) {
|
|
|
605
651
|
if (uniqueTwo.length > 0) {
|
|
606
652
|
const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
|
|
607
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`;
|
|
608
668
|
actions.push({
|
|
609
669
|
title: `Plan major framework upgrades: ${list}${moreCount}`,
|
|
610
|
-
explanation:
|
|
670
|
+
explanation: detail,
|
|
611
671
|
impact: `+5\u201310 points (framework score)`,
|
|
612
672
|
severity: 60
|
|
613
673
|
});
|
|
@@ -618,9 +678,31 @@ function generatePriorityActions(artifact) {
|
|
|
618
678
|
if (total > 0) {
|
|
619
679
|
const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
|
|
620
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`;
|
|
621
703
|
actions.push({
|
|
622
704
|
title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
|
|
623
|
-
explanation:
|
|
705
|
+
explanation: detail,
|
|
624
706
|
severity: 55
|
|
625
707
|
});
|
|
626
708
|
}
|
|
@@ -667,9 +749,20 @@ function generatePriorityActions(artifact) {
|
|
|
667
749
|
const issues = [];
|
|
668
750
|
if (sec.envFilesTracked) issues.push(".env files are tracked in git");
|
|
669
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
|
+
}
|
|
670
763
|
actions.push({
|
|
671
764
|
title: `Fix security posture: ${issues.join(", ")}`,
|
|
672
|
-
explanation:
|
|
765
|
+
explanation: detail,
|
|
673
766
|
severity: 95
|
|
674
767
|
});
|
|
675
768
|
}
|
|
@@ -679,9 +772,22 @@ function generatePriorityActions(artifact) {
|
|
|
679
772
|
const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
|
|
680
773
|
if (highImpactDupes.length >= 3) {
|
|
681
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`;
|
|
682
788
|
actions.push({
|
|
683
789
|
title: `Deduplicate heavily-versioned packages`,
|
|
684
|
-
explanation:
|
|
790
|
+
explanation: detail,
|
|
685
791
|
severity: 35
|
|
686
792
|
});
|
|
687
793
|
}
|
|
@@ -772,13 +878,146 @@ function toSarifResult(finding) {
|
|
|
772
878
|
};
|
|
773
879
|
}
|
|
774
880
|
|
|
775
|
-
// src/commands/
|
|
776
|
-
import * as
|
|
881
|
+
// src/commands/dsn.ts
|
|
882
|
+
import * as crypto2 from "crypto";
|
|
883
|
+
import * as path2 from "path";
|
|
777
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";
|
|
778
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";
|
|
779
1018
|
|
|
780
1019
|
// src/scanners/node-scanner.ts
|
|
781
|
-
import * as
|
|
1020
|
+
import * as path4 from "path";
|
|
782
1021
|
import * as semver2 from "semver";
|
|
783
1022
|
|
|
784
1023
|
// src/scanners/npm-cache.ts
|
|
@@ -793,7 +1032,7 @@ function maxStable(versions) {
|
|
|
793
1032
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
794
1033
|
}
|
|
795
1034
|
async function npmViewJson(args, cwd) {
|
|
796
|
-
return new Promise((
|
|
1035
|
+
return new Promise((resolve6, reject) => {
|
|
797
1036
|
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
798
1037
|
cwd,
|
|
799
1038
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -810,13 +1049,13 @@ async function npmViewJson(args, cwd) {
|
|
|
810
1049
|
}
|
|
811
1050
|
const trimmed = out.trim();
|
|
812
1051
|
if (!trimmed) {
|
|
813
|
-
|
|
1052
|
+
resolve6(null);
|
|
814
1053
|
return;
|
|
815
1054
|
}
|
|
816
1055
|
try {
|
|
817
|
-
|
|
1056
|
+
resolve6(JSON.parse(trimmed));
|
|
818
1057
|
} catch {
|
|
819
|
-
|
|
1058
|
+
resolve6(trimmed.replace(/^"|"$/g, ""));
|
|
820
1059
|
}
|
|
821
1060
|
});
|
|
822
1061
|
});
|
|
@@ -968,8 +1207,8 @@ async function scanNodeProjects(rootDir, npmCache) {
|
|
|
968
1207
|
}
|
|
969
1208
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
970
1209
|
const pj = await readJsonFile(packageJsonPath);
|
|
971
|
-
const absProjectPath =
|
|
972
|
-
const projectPath =
|
|
1210
|
+
const absProjectPath = path4.dirname(packageJsonPath);
|
|
1211
|
+
const projectPath = path4.relative(rootDir, absProjectPath) || ".";
|
|
973
1212
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
974
1213
|
let runtimeLatest;
|
|
975
1214
|
let runtimeMajorsBehind;
|
|
@@ -1051,7 +1290,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
|
1051
1290
|
return {
|
|
1052
1291
|
type: "node",
|
|
1053
1292
|
path: projectPath,
|
|
1054
|
-
name: pj.name ??
|
|
1293
|
+
name: pj.name ?? path4.basename(absProjectPath),
|
|
1055
1294
|
runtime: nodeEngine,
|
|
1056
1295
|
runtimeLatest,
|
|
1057
1296
|
runtimeMajorsBehind,
|
|
@@ -1062,7 +1301,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
|
|
|
1062
1301
|
}
|
|
1063
1302
|
|
|
1064
1303
|
// src/scanners/dotnet-scanner.ts
|
|
1065
|
-
import * as
|
|
1304
|
+
import * as path5 from "path";
|
|
1066
1305
|
import { XMLParser } from "fast-xml-parser";
|
|
1067
1306
|
var parser = new XMLParser({
|
|
1068
1307
|
ignoreAttributes: false,
|
|
@@ -1263,7 +1502,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1263
1502
|
const parsed = parser.parse(xml);
|
|
1264
1503
|
const project = parsed?.Project;
|
|
1265
1504
|
if (!project) {
|
|
1266
|
-
return { targetFrameworks: [], packageReferences: [], projectName:
|
|
1505
|
+
return { targetFrameworks: [], packageReferences: [], projectName: path5.basename(filePath, ".csproj") };
|
|
1267
1506
|
}
|
|
1268
1507
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1269
1508
|
const targetFrameworks = [];
|
|
@@ -1291,7 +1530,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1291
1530
|
return {
|
|
1292
1531
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1293
1532
|
packageReferences,
|
|
1294
|
-
projectName:
|
|
1533
|
+
projectName: path5.basename(filePath, ".csproj")
|
|
1295
1534
|
};
|
|
1296
1535
|
}
|
|
1297
1536
|
async function scanDotnetProjects(rootDir) {
|
|
@@ -1301,12 +1540,12 @@ async function scanDotnetProjects(rootDir) {
|
|
|
1301
1540
|
for (const slnPath of slnFiles) {
|
|
1302
1541
|
try {
|
|
1303
1542
|
const slnContent = await readTextFile(slnPath);
|
|
1304
|
-
const slnDir =
|
|
1543
|
+
const slnDir = path5.dirname(slnPath);
|
|
1305
1544
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1306
1545
|
let match;
|
|
1307
1546
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1308
1547
|
if (match[1]) {
|
|
1309
|
-
const csprojPath =
|
|
1548
|
+
const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1310
1549
|
slnCsprojPaths.add(csprojPath);
|
|
1311
1550
|
}
|
|
1312
1551
|
}
|
|
@@ -1362,7 +1601,7 @@ async function scanOneCsproj(csprojPath, rootDir) {
|
|
|
1362
1601
|
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
|
|
1363
1602
|
return {
|
|
1364
1603
|
type: "dotnet",
|
|
1365
|
-
path:
|
|
1604
|
+
path: path5.relative(rootDir, path5.dirname(csprojPath)) || ".",
|
|
1366
1605
|
name: data.projectName,
|
|
1367
1606
|
targetFramework,
|
|
1368
1607
|
runtime: primaryTfm,
|
|
@@ -1394,7 +1633,7 @@ var Semaphore = class {
|
|
|
1394
1633
|
this.available--;
|
|
1395
1634
|
return Promise.resolve();
|
|
1396
1635
|
}
|
|
1397
|
-
return new Promise((
|
|
1636
|
+
return new Promise((resolve6) => this.queue.push(resolve6));
|
|
1398
1637
|
}
|
|
1399
1638
|
release() {
|
|
1400
1639
|
const next = this.queue.shift();
|
|
@@ -1404,7 +1643,7 @@ var Semaphore = class {
|
|
|
1404
1643
|
};
|
|
1405
1644
|
|
|
1406
1645
|
// src/config.ts
|
|
1407
|
-
import * as
|
|
1646
|
+
import * as path6 from "path";
|
|
1408
1647
|
import * as fs2 from "fs/promises";
|
|
1409
1648
|
var CONFIG_FILES = [
|
|
1410
1649
|
"vibgrate.config.ts",
|
|
@@ -1427,7 +1666,7 @@ var DEFAULT_CONFIG = {
|
|
|
1427
1666
|
};
|
|
1428
1667
|
async function loadConfig(rootDir) {
|
|
1429
1668
|
for (const file of CONFIG_FILES) {
|
|
1430
|
-
const configPath =
|
|
1669
|
+
const configPath = path6.join(rootDir, file);
|
|
1431
1670
|
if (await pathExists(configPath)) {
|
|
1432
1671
|
if (file.endsWith(".json")) {
|
|
1433
1672
|
const txt = await readTextFile(configPath);
|
|
@@ -1443,7 +1682,7 @@ async function loadConfig(rootDir) {
|
|
|
1443
1682
|
return DEFAULT_CONFIG;
|
|
1444
1683
|
}
|
|
1445
1684
|
async function writeDefaultConfig(rootDir) {
|
|
1446
|
-
const configPath =
|
|
1685
|
+
const configPath = path6.join(rootDir, "vibgrate.config.ts");
|
|
1447
1686
|
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
1448
1687
|
|
|
1449
1688
|
const config: VibgrateConfig = {
|
|
@@ -1468,7 +1707,7 @@ export default config;
|
|
|
1468
1707
|
}
|
|
1469
1708
|
|
|
1470
1709
|
// src/utils/vcs.ts
|
|
1471
|
-
import * as
|
|
1710
|
+
import * as path7 from "path";
|
|
1472
1711
|
import * as fs3 from "fs/promises";
|
|
1473
1712
|
async function detectVcs(rootDir) {
|
|
1474
1713
|
try {
|
|
@@ -1482,7 +1721,7 @@ async function detectGit(rootDir) {
|
|
|
1482
1721
|
if (!gitDir) {
|
|
1483
1722
|
return { type: "unknown" };
|
|
1484
1723
|
}
|
|
1485
|
-
const headPath =
|
|
1724
|
+
const headPath = path7.join(gitDir, "HEAD");
|
|
1486
1725
|
let headContent;
|
|
1487
1726
|
try {
|
|
1488
1727
|
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
@@ -1506,10 +1745,10 @@ async function detectGit(rootDir) {
|
|
|
1506
1745
|
};
|
|
1507
1746
|
}
|
|
1508
1747
|
async function findGitDir(startDir) {
|
|
1509
|
-
let dir =
|
|
1510
|
-
const root =
|
|
1748
|
+
let dir = path7.resolve(startDir);
|
|
1749
|
+
const root = path7.parse(dir).root;
|
|
1511
1750
|
while (dir !== root) {
|
|
1512
|
-
const gitPath =
|
|
1751
|
+
const gitPath = path7.join(dir, ".git");
|
|
1513
1752
|
try {
|
|
1514
1753
|
const stat3 = await fs3.stat(gitPath);
|
|
1515
1754
|
if (stat3.isDirectory()) {
|
|
@@ -1518,18 +1757,18 @@ async function findGitDir(startDir) {
|
|
|
1518
1757
|
if (stat3.isFile()) {
|
|
1519
1758
|
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
1520
1759
|
if (content.startsWith("gitdir: ")) {
|
|
1521
|
-
const resolved =
|
|
1760
|
+
const resolved = path7.resolve(dir, content.slice(8));
|
|
1522
1761
|
return resolved;
|
|
1523
1762
|
}
|
|
1524
1763
|
}
|
|
1525
1764
|
} catch {
|
|
1526
1765
|
}
|
|
1527
|
-
dir =
|
|
1766
|
+
dir = path7.dirname(dir);
|
|
1528
1767
|
}
|
|
1529
1768
|
return null;
|
|
1530
1769
|
}
|
|
1531
1770
|
async function resolveRef(gitDir, refPath) {
|
|
1532
|
-
const loosePath =
|
|
1771
|
+
const loosePath = path7.join(gitDir, refPath);
|
|
1533
1772
|
try {
|
|
1534
1773
|
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
1535
1774
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
@@ -1537,7 +1776,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
1537
1776
|
}
|
|
1538
1777
|
} catch {
|
|
1539
1778
|
}
|
|
1540
|
-
const packedPath =
|
|
1779
|
+
const packedPath = path7.join(gitDir, "packed-refs");
|
|
1541
1780
|
try {
|
|
1542
1781
|
const packed = await fs3.readFile(packedPath, "utf8");
|
|
1543
1782
|
for (const line of packed.split("\n")) {
|
|
@@ -1553,16 +1792,16 @@ async function resolveRef(gitDir, refPath) {
|
|
|
1553
1792
|
}
|
|
1554
1793
|
|
|
1555
1794
|
// src/ui/progress.ts
|
|
1556
|
-
import
|
|
1795
|
+
import chalk4 from "chalk";
|
|
1557
1796
|
var ROBOT = [
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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")
|
|
1562
1801
|
];
|
|
1563
1802
|
var BRAND = [
|
|
1564
|
-
|
|
1565
|
-
|
|
1803
|
+
chalk4.bold.white(" V I B G R A T E"),
|
|
1804
|
+
chalk4.dim(` Drift Intelligence Engine`) + chalk4.dim(` v${VERSION}`)
|
|
1566
1805
|
];
|
|
1567
1806
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1568
1807
|
var ScanProgress = class {
|
|
@@ -1656,7 +1895,7 @@ var ScanProgress = class {
|
|
|
1656
1895
|
const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
1657
1896
|
const doneCount = this.steps.filter((s) => s.status === "done").length;
|
|
1658
1897
|
process.stderr.write(
|
|
1659
|
-
|
|
1898
|
+
chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
|
|
1660
1899
|
|
|
1661
1900
|
`)
|
|
1662
1901
|
);
|
|
@@ -1688,16 +1927,16 @@ var ScanProgress = class {
|
|
|
1688
1927
|
lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
|
|
1689
1928
|
lines.push(` ${ROBOT[1]} ${BRAND[1]}`);
|
|
1690
1929
|
lines.push(` ${ROBOT[2]}`);
|
|
1691
|
-
lines.push(` ${ROBOT[3]} ${
|
|
1930
|
+
lines.push(` ${ROBOT[3]} ${chalk4.dim(this.rootDir)}`);
|
|
1692
1931
|
lines.push("");
|
|
1693
1932
|
const totalSteps = this.steps.length;
|
|
1694
1933
|
const doneSteps = this.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
|
|
1695
1934
|
const pct = totalSteps > 0 ? Math.round(doneSteps / totalSteps * 100) : 0;
|
|
1696
1935
|
const barWidth = 30;
|
|
1697
1936
|
const filled = Math.round(doneSteps / Math.max(totalSteps, 1) * barWidth);
|
|
1698
|
-
const bar =
|
|
1937
|
+
const bar = chalk4.greenBright("\u2501".repeat(filled)) + chalk4.dim("\u254C".repeat(barWidth - filled));
|
|
1699
1938
|
const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
|
|
1700
|
-
lines.push(` ${bar} ${
|
|
1939
|
+
lines.push(` ${bar} ${chalk4.bold.white(`${pct}%`)} ${chalk4.dim(`${elapsed}s`)}`);
|
|
1701
1940
|
lines.push("");
|
|
1702
1941
|
for (const step of this.steps) {
|
|
1703
1942
|
lines.push(this.renderStep(step));
|
|
@@ -1716,27 +1955,27 @@ var ScanProgress = class {
|
|
|
1716
1955
|
let detail = "";
|
|
1717
1956
|
switch (step.status) {
|
|
1718
1957
|
case "done":
|
|
1719
|
-
icon =
|
|
1720
|
-
label =
|
|
1958
|
+
icon = chalk4.green("\u2714");
|
|
1959
|
+
label = chalk4.white(step.label);
|
|
1721
1960
|
break;
|
|
1722
1961
|
case "active":
|
|
1723
|
-
icon =
|
|
1724
|
-
label =
|
|
1962
|
+
icon = chalk4.cyan(spinner);
|
|
1963
|
+
label = chalk4.bold.white(step.label);
|
|
1725
1964
|
break;
|
|
1726
1965
|
case "skipped":
|
|
1727
|
-
icon =
|
|
1728
|
-
label =
|
|
1966
|
+
icon = chalk4.dim("\u25CC");
|
|
1967
|
+
label = chalk4.dim.strikethrough(step.label);
|
|
1729
1968
|
break;
|
|
1730
1969
|
default:
|
|
1731
|
-
icon =
|
|
1732
|
-
label =
|
|
1970
|
+
icon = chalk4.dim("\u25CB");
|
|
1971
|
+
label = chalk4.dim(step.label);
|
|
1733
1972
|
break;
|
|
1734
1973
|
}
|
|
1735
1974
|
if (step.detail) {
|
|
1736
|
-
detail =
|
|
1975
|
+
detail = chalk4.dim(` \xB7 ${step.detail}`);
|
|
1737
1976
|
}
|
|
1738
1977
|
if (step.count !== void 0 && step.count > 0) {
|
|
1739
|
-
detail +=
|
|
1978
|
+
detail += chalk4.cyan(` (${step.count})`);
|
|
1740
1979
|
}
|
|
1741
1980
|
return ` ${icon} ${label}${detail}`;
|
|
1742
1981
|
}
|
|
@@ -1748,18 +1987,18 @@ var ScanProgress = class {
|
|
|
1748
1987
|
const e = this.stats.findings.errors;
|
|
1749
1988
|
const n = this.stats.findings.notes;
|
|
1750
1989
|
const parts = [
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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" : ""}`)
|
|
1754
1993
|
];
|
|
1755
1994
|
const findingParts = [];
|
|
1756
|
-
if (e > 0) findingParts.push(
|
|
1757
|
-
if (w > 0) findingParts.push(
|
|
1758
|
-
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`));
|
|
1759
1998
|
if (findingParts.length > 0) {
|
|
1760
|
-
parts.push(findingParts.join(
|
|
1999
|
+
parts.push(findingParts.join(chalk4.dim(" \xB7 ")));
|
|
1761
2000
|
}
|
|
1762
|
-
return ` ${
|
|
2001
|
+
return ` ${chalk4.dim("\u2503")} ${parts.join(chalk4.dim(" \u2502 "))}`;
|
|
1763
2002
|
}
|
|
1764
2003
|
/** Simple CI-friendly output (no ANSI rewriting) */
|
|
1765
2004
|
lastCIStep = null;
|
|
@@ -1778,7 +2017,7 @@ var ScanProgress = class {
|
|
|
1778
2017
|
};
|
|
1779
2018
|
|
|
1780
2019
|
// src/scanners/platform-matrix.ts
|
|
1781
|
-
import * as
|
|
2020
|
+
import * as path8 from "path";
|
|
1782
2021
|
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
1783
2022
|
// Image / media processing
|
|
1784
2023
|
"sharp",
|
|
@@ -2055,7 +2294,7 @@ async function scanPlatformMatrix(rootDir) {
|
|
|
2055
2294
|
}
|
|
2056
2295
|
result.dockerBaseImages = [...baseImages].sort();
|
|
2057
2296
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
2058
|
-
if (await pathExists(
|
|
2297
|
+
if (await pathExists(path8.join(rootDir, file))) {
|
|
2059
2298
|
result.nodeVersionFiles.push(file);
|
|
2060
2299
|
}
|
|
2061
2300
|
}
|
|
@@ -2131,7 +2370,7 @@ function scanDependencyRisk(projects) {
|
|
|
2131
2370
|
}
|
|
2132
2371
|
|
|
2133
2372
|
// src/scanners/dependency-graph.ts
|
|
2134
|
-
import * as
|
|
2373
|
+
import * as path9 from "path";
|
|
2135
2374
|
function parsePnpmLock(content) {
|
|
2136
2375
|
const entries = [];
|
|
2137
2376
|
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
@@ -2190,9 +2429,9 @@ async function scanDependencyGraph(rootDir) {
|
|
|
2190
2429
|
phantomDependencies: []
|
|
2191
2430
|
};
|
|
2192
2431
|
let entries = [];
|
|
2193
|
-
const pnpmLock =
|
|
2194
|
-
const npmLock =
|
|
2195
|
-
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");
|
|
2196
2435
|
if (await pathExists(pnpmLock)) {
|
|
2197
2436
|
result.lockfileType = "pnpm";
|
|
2198
2437
|
const content = await readTextFile(pnpmLock);
|
|
@@ -2237,7 +2476,7 @@ async function scanDependencyGraph(rootDir) {
|
|
|
2237
2476
|
for (const pjPath of pkgFiles) {
|
|
2238
2477
|
try {
|
|
2239
2478
|
const pj = await readJsonFile(pjPath);
|
|
2240
|
-
const relPath =
|
|
2479
|
+
const relPath = path9.relative(rootDir, pjPath);
|
|
2241
2480
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
2242
2481
|
const deps = pj[section];
|
|
2243
2482
|
if (!deps) continue;
|
|
@@ -2583,7 +2822,7 @@ function scanToolingInventory(projects) {
|
|
|
2583
2822
|
}
|
|
2584
2823
|
|
|
2585
2824
|
// src/scanners/build-deploy.ts
|
|
2586
|
-
import * as
|
|
2825
|
+
import * as path10 from "path";
|
|
2587
2826
|
var CI_FILES = {
|
|
2588
2827
|
".github/workflows": "github-actions",
|
|
2589
2828
|
".gitlab-ci.yml": "gitlab-ci",
|
|
@@ -2633,12 +2872,12 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2633
2872
|
};
|
|
2634
2873
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
2635
2874
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
2636
|
-
const fullPath =
|
|
2875
|
+
const fullPath = path10.join(rootDir, file);
|
|
2637
2876
|
if (await pathExists(fullPath)) {
|
|
2638
2877
|
ciSystems.add(system);
|
|
2639
2878
|
}
|
|
2640
2879
|
}
|
|
2641
|
-
const ghWorkflowDir =
|
|
2880
|
+
const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
|
|
2642
2881
|
if (await pathExists(ghWorkflowDir)) {
|
|
2643
2882
|
try {
|
|
2644
2883
|
const files = await findFiles(
|
|
@@ -2686,11 +2925,11 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2686
2925
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
2687
2926
|
);
|
|
2688
2927
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
2689
|
-
if (await pathExists(
|
|
2928
|
+
if (await pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
2690
2929
|
result.iac = [...iacSystems].sort();
|
|
2691
2930
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
2692
2931
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
2693
|
-
if (await pathExists(
|
|
2932
|
+
if (await pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
|
|
2694
2933
|
}
|
|
2695
2934
|
const pkgFiles = await findPackageJsonFiles(rootDir);
|
|
2696
2935
|
for (const pjPath of pkgFiles) {
|
|
@@ -2715,19 +2954,19 @@ async function scanBuildDeploy(rootDir) {
|
|
|
2715
2954
|
};
|
|
2716
2955
|
const managers = /* @__PURE__ */ new Set();
|
|
2717
2956
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
2718
|
-
if (await pathExists(
|
|
2957
|
+
if (await pathExists(path10.join(rootDir, file))) managers.add(manager);
|
|
2719
2958
|
}
|
|
2720
2959
|
result.packageManagers = [...managers].sort();
|
|
2721
2960
|
const monoTools = /* @__PURE__ */ new Set();
|
|
2722
2961
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
2723
|
-
if (await pathExists(
|
|
2962
|
+
if (await pathExists(path10.join(rootDir, file))) monoTools.add(tool);
|
|
2724
2963
|
}
|
|
2725
2964
|
result.monorepoTools = [...monoTools].sort();
|
|
2726
2965
|
return result;
|
|
2727
2966
|
}
|
|
2728
2967
|
|
|
2729
2968
|
// src/scanners/ts-modernity.ts
|
|
2730
|
-
import * as
|
|
2969
|
+
import * as path11 from "path";
|
|
2731
2970
|
async function scanTsModernity(rootDir) {
|
|
2732
2971
|
const result = {
|
|
2733
2972
|
typescriptVersion: null,
|
|
@@ -2765,7 +3004,7 @@ async function scanTsModernity(rootDir) {
|
|
|
2765
3004
|
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
2766
3005
|
else if (hasEsm) result.moduleType = "esm";
|
|
2767
3006
|
else if (hasCjs) result.moduleType = "cjs";
|
|
2768
|
-
let tsConfigPath =
|
|
3007
|
+
let tsConfigPath = path11.join(rootDir, "tsconfig.json");
|
|
2769
3008
|
if (!await pathExists(tsConfigPath)) {
|
|
2770
3009
|
const tsConfigs = await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
2771
3010
|
if (tsConfigs.length > 0) {
|
|
@@ -3111,7 +3350,7 @@ function scanBreakingChangeExposure(projects) {
|
|
|
3111
3350
|
|
|
3112
3351
|
// src/scanners/file-hotspots.ts
|
|
3113
3352
|
import * as fs4 from "fs/promises";
|
|
3114
|
-
import * as
|
|
3353
|
+
import * as path12 from "path";
|
|
3115
3354
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3116
3355
|
"node_modules",
|
|
3117
3356
|
".git",
|
|
@@ -3166,15 +3405,15 @@ async function scanFileHotspots(rootDir) {
|
|
|
3166
3405
|
for (const e of entries) {
|
|
3167
3406
|
if (e.isDirectory) {
|
|
3168
3407
|
if (SKIP_DIRS2.has(e.name)) continue;
|
|
3169
|
-
await walk(
|
|
3408
|
+
await walk(path12.join(dir, e.name), depth + 1);
|
|
3170
3409
|
} else if (e.isFile) {
|
|
3171
|
-
const ext =
|
|
3410
|
+
const ext = path12.extname(e.name).toLowerCase();
|
|
3172
3411
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3173
3412
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3174
3413
|
try {
|
|
3175
|
-
const stat3 = await fs4.stat(
|
|
3414
|
+
const stat3 = await fs4.stat(path12.join(dir, e.name));
|
|
3176
3415
|
allFiles.push({
|
|
3177
|
-
path:
|
|
3416
|
+
path: path12.relative(rootDir, path12.join(dir, e.name)),
|
|
3178
3417
|
bytes: stat3.size
|
|
3179
3418
|
});
|
|
3180
3419
|
} catch {
|
|
@@ -3196,7 +3435,7 @@ async function scanFileHotspots(rootDir) {
|
|
|
3196
3435
|
}
|
|
3197
3436
|
|
|
3198
3437
|
// src/scanners/security-posture.ts
|
|
3199
|
-
import * as
|
|
3438
|
+
import * as path13 from "path";
|
|
3200
3439
|
var LOCKFILES = {
|
|
3201
3440
|
"pnpm-lock.yaml": "pnpm",
|
|
3202
3441
|
"package-lock.json": "npm",
|
|
@@ -3215,14 +3454,14 @@ async function scanSecurityPosture(rootDir) {
|
|
|
3215
3454
|
};
|
|
3216
3455
|
const foundLockfiles = [];
|
|
3217
3456
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
3218
|
-
if (await pathExists(
|
|
3457
|
+
if (await pathExists(path13.join(rootDir, file))) {
|
|
3219
3458
|
foundLockfiles.push(type);
|
|
3220
3459
|
}
|
|
3221
3460
|
}
|
|
3222
3461
|
result.lockfilePresent = foundLockfiles.length > 0;
|
|
3223
3462
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
3224
3463
|
result.lockfileTypes = foundLockfiles.sort();
|
|
3225
|
-
const gitignorePath =
|
|
3464
|
+
const gitignorePath = path13.join(rootDir, ".gitignore");
|
|
3226
3465
|
if (await pathExists(gitignorePath)) {
|
|
3227
3466
|
try {
|
|
3228
3467
|
const content = await readTextFile(gitignorePath);
|
|
@@ -3237,7 +3476,7 @@ async function scanSecurityPosture(rootDir) {
|
|
|
3237
3476
|
}
|
|
3238
3477
|
}
|
|
3239
3478
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
3240
|
-
if (await pathExists(
|
|
3479
|
+
if (await pathExists(path13.join(rootDir, envFile))) {
|
|
3241
3480
|
if (!result.gitignoreCoversEnv) {
|
|
3242
3481
|
result.envFilesTracked = true;
|
|
3243
3482
|
break;
|
|
@@ -3715,6 +3954,13 @@ async function runScan(rootDir, opts) {
|
|
|
3715
3954
|
progress.addProjects(dotnetProjects.length);
|
|
3716
3955
|
progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
|
|
3717
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
|
+
}
|
|
3718
3964
|
const extended = {};
|
|
3719
3965
|
if (scanners !== false) {
|
|
3720
3966
|
const scannerTasks = [];
|
|
@@ -3852,7 +4098,7 @@ async function runScan(rootDir, opts) {
|
|
|
3852
4098
|
progress.completeStep("findings", findingParts.join(", ") || "none");
|
|
3853
4099
|
progress.finish();
|
|
3854
4100
|
if (allProjects.length === 0) {
|
|
3855
|
-
console.log(
|
|
4101
|
+
console.log(chalk5.yellow("No projects found."));
|
|
3856
4102
|
}
|
|
3857
4103
|
if (extended.fileHotspots) filesScanned += extended.fileHotspots.totalFiles;
|
|
3858
4104
|
if (extended.securityPosture) filesScanned += 1;
|
|
@@ -3867,7 +4113,7 @@ async function runScan(rootDir, opts) {
|
|
|
3867
4113
|
schemaVersion: "1.0",
|
|
3868
4114
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3869
4115
|
vibgrateVersion: VERSION,
|
|
3870
|
-
rootPath:
|
|
4116
|
+
rootPath: path14.basename(rootDir),
|
|
3871
4117
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
3872
4118
|
projects: allProjects,
|
|
3873
4119
|
drift,
|
|
@@ -3877,25 +4123,44 @@ async function runScan(rootDir, opts) {
|
|
|
3877
4123
|
filesScanned
|
|
3878
4124
|
};
|
|
3879
4125
|
if (opts.baseline) {
|
|
3880
|
-
const baselinePath =
|
|
4126
|
+
const baselinePath = path14.resolve(opts.baseline);
|
|
3881
4127
|
if (await pathExists(baselinePath)) {
|
|
3882
4128
|
try {
|
|
3883
4129
|
const baseline = await readJsonFile(baselinePath);
|
|
3884
4130
|
artifact.baseline = baselinePath;
|
|
3885
4131
|
artifact.delta = artifact.drift.score - baseline.drift.score;
|
|
3886
4132
|
} catch {
|
|
3887
|
-
console.error(
|
|
4133
|
+
console.error(chalk5.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
|
|
3888
4134
|
}
|
|
3889
4135
|
}
|
|
3890
4136
|
}
|
|
3891
|
-
const vibgrateDir =
|
|
4137
|
+
const vibgrateDir = path14.join(rootDir, ".vibgrate");
|
|
3892
4138
|
await ensureDir(vibgrateDir);
|
|
3893
|
-
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
|
+
}
|
|
3894
4159
|
if (opts.format === "json") {
|
|
3895
4160
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
3896
4161
|
if (opts.out) {
|
|
3897
|
-
await writeTextFile(
|
|
3898
|
-
console.log(
|
|
4162
|
+
await writeTextFile(path14.resolve(opts.out), jsonStr);
|
|
4163
|
+
console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
3899
4164
|
} else {
|
|
3900
4165
|
console.log(jsonStr);
|
|
3901
4166
|
}
|
|
@@ -3903,8 +4168,8 @@ async function runScan(rootDir, opts) {
|
|
|
3903
4168
|
const sarif = formatSarif(artifact);
|
|
3904
4169
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
3905
4170
|
if (opts.out) {
|
|
3906
|
-
await writeTextFile(
|
|
3907
|
-
console.log(
|
|
4171
|
+
await writeTextFile(path14.resolve(opts.out), sarifStr);
|
|
4172
|
+
console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
3908
4173
|
} else {
|
|
3909
4174
|
console.log(sarifStr);
|
|
3910
4175
|
}
|
|
@@ -3912,15 +4177,66 @@ async function runScan(rootDir, opts) {
|
|
|
3912
4177
|
const text = formatText(artifact);
|
|
3913
4178
|
console.log(text);
|
|
3914
4179
|
if (opts.out) {
|
|
3915
|
-
await writeTextFile(
|
|
4180
|
+
await writeTextFile(path14.resolve(opts.out), text);
|
|
3916
4181
|
}
|
|
3917
4182
|
}
|
|
3918
4183
|
return artifact;
|
|
3919
4184
|
}
|
|
3920
|
-
|
|
3921
|
-
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);
|
|
3922
4238
|
if (!await pathExists(rootDir)) {
|
|
3923
|
-
console.error(
|
|
4239
|
+
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
3924
4240
|
process.exit(1);
|
|
3925
4241
|
}
|
|
3926
4242
|
const scanOpts = {
|
|
@@ -3929,38 +4245,45 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
3929
4245
|
failOn: opts.failOn,
|
|
3930
4246
|
baseline: opts.baseline,
|
|
3931
4247
|
changedOnly: opts.changedOnly,
|
|
3932
|
-
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
|
|
3933
4253
|
};
|
|
3934
4254
|
const artifact = await runScan(rootDir, scanOpts);
|
|
3935
4255
|
if (opts.failOn) {
|
|
3936
4256
|
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
3937
4257
|
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
3938
4258
|
if (opts.failOn === "error" && hasErrors) {
|
|
3939
|
-
console.error(
|
|
4259
|
+
console.error(chalk5.red(`
|
|
3940
4260
|
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
3941
4261
|
process.exit(2);
|
|
3942
4262
|
}
|
|
3943
4263
|
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
3944
|
-
console.error(
|
|
4264
|
+
console.error(chalk5.red(`
|
|
3945
4265
|
Failing: findings detected at warn level or above.`));
|
|
3946
4266
|
process.exit(2);
|
|
3947
4267
|
}
|
|
3948
4268
|
}
|
|
4269
|
+
if (opts.push) {
|
|
4270
|
+
await autoPush(artifact, rootDir, scanOpts);
|
|
4271
|
+
}
|
|
3949
4272
|
});
|
|
3950
4273
|
|
|
3951
4274
|
export {
|
|
3952
4275
|
readJsonFile,
|
|
3953
|
-
readTextFile,
|
|
3954
4276
|
pathExists,
|
|
3955
4277
|
ensureDir,
|
|
3956
4278
|
writeJsonFile,
|
|
3957
|
-
writeTextFile,
|
|
3958
4279
|
writeDefaultConfig,
|
|
3959
4280
|
computeDriftScore,
|
|
3960
4281
|
generateFindings,
|
|
3961
4282
|
VERSION,
|
|
3962
4283
|
formatText,
|
|
3963
4284
|
formatSarif,
|
|
4285
|
+
dsnCommand,
|
|
4286
|
+
pushCommand,
|
|
3964
4287
|
runScan,
|
|
3965
4288
|
scanCommand
|
|
3966
4289
|
};
|