relionhq 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +318 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -217,14 +217,42 @@ function printReceipt(opts) {
|
|
|
217
217
|
console.log(`\u2514${"\u2500".repeat(width + 2)}\u2518`);
|
|
218
218
|
console.log("");
|
|
219
219
|
}
|
|
220
|
+
function stripAnsi(s) {
|
|
221
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
222
|
+
}
|
|
223
|
+
function wordWrap(text, maxWidth) {
|
|
224
|
+
const words = text.split(" ");
|
|
225
|
+
const lines = [];
|
|
226
|
+
let current = "";
|
|
227
|
+
for (const word of words) {
|
|
228
|
+
if (current.length === 0) {
|
|
229
|
+
current = word;
|
|
230
|
+
} else if (current.length + 1 + word.length <= maxWidth) {
|
|
231
|
+
current += " " + word;
|
|
232
|
+
} else {
|
|
233
|
+
lines.push(current);
|
|
234
|
+
current = word;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (current) lines.push(current);
|
|
238
|
+
return lines.length ? lines : [""];
|
|
239
|
+
}
|
|
240
|
+
function boxLine(content, width) {
|
|
241
|
+
const stripped = stripAnsi(content);
|
|
242
|
+
const gap = Math.max(0, width - stripped.length);
|
|
243
|
+
return `\u2502 ${content}${" ".repeat(gap)} \u2502`;
|
|
244
|
+
}
|
|
245
|
+
function blankLine(width) {
|
|
246
|
+
return `\u2502${" ".repeat(width + 2)}\u2502`;
|
|
247
|
+
}
|
|
220
248
|
function printPredeployReceipt(opts) {
|
|
221
249
|
if (QUIET) return;
|
|
222
|
-
const width =
|
|
250
|
+
const width = 62;
|
|
223
251
|
const line = "\u2500".repeat(width);
|
|
224
252
|
const pad = (label, value) => {
|
|
225
|
-
const stripped = value
|
|
226
|
-
const gap = width - label.length - stripped.length - 2;
|
|
227
|
-
return `\u2502 ${label}${" ".repeat(
|
|
253
|
+
const stripped = stripAnsi(value);
|
|
254
|
+
const gap = Math.max(1, width - label.length - stripped.length - 2);
|
|
255
|
+
return `\u2502 ${label}${" ".repeat(gap)}${value} \u2502`;
|
|
228
256
|
};
|
|
229
257
|
const verdictLabel = opts.verdict === "safe" ? color.green("\u2713 SAFE") : opts.verdict === "caution" ? color.yellow("\u26A0 CAUTION") : opts.verdict === "high_risk" ? color.red("\u2717 HIGH RISK") : opts.verdict === "blocked" ? color.red("\u2717 BLOCKED") : color.dim("\u2014 OFFLINE");
|
|
230
258
|
console.log("");
|
|
@@ -232,7 +260,7 @@ function printPredeployReceipt(opts) {
|
|
|
232
260
|
console.log(`\u2502 ${color.bold("Relion Pre-Deploy Check")}${" ".repeat(width - 22)} \u2502`);
|
|
233
261
|
console.log(`\u251C${line}\u2524`);
|
|
234
262
|
if (opts.branch || opts.commit) {
|
|
235
|
-
const meta = [opts.branch, opts.commit ? opts.commit.slice(0, 7) : ""].filter(Boolean).join("
|
|
263
|
+
const meta = [opts.branch, opts.commit ? opts.commit.slice(0, 7) : ""].filter(Boolean).join(" \xB7 ");
|
|
236
264
|
console.log(pad("Branch:", color.dim(meta)));
|
|
237
265
|
}
|
|
238
266
|
if (opts.baseBranch) console.log(pad("Comparing against:", color.dim(opts.baseBranch)));
|
|
@@ -245,23 +273,57 @@ function printPredeployReceipt(opts) {
|
|
|
245
273
|
console.log(pad("Verdict:", verdictLabel));
|
|
246
274
|
const nonSafe = opts.findings.filter((f) => f.riskLevel !== "safe" && f.riskLevel !== "info");
|
|
247
275
|
if (nonSafe.length > 0) {
|
|
248
|
-
console.log(`\
|
|
249
|
-
for (const finding of nonSafe
|
|
276
|
+
console.log(`\u251C${line}\u2524`);
|
|
277
|
+
for (const finding of nonSafe) {
|
|
278
|
+
console.log(blankLine(width));
|
|
250
279
|
const icon = finding.riskLevel === "blocked" ? color.red("\u2717") : finding.riskLevel === "high_risk" ? color.red("!") : color.yellow("\u26A0");
|
|
251
|
-
const
|
|
252
|
-
|
|
280
|
+
const badge = finding.riskLevel === "blocked" ? color.red("[BLOCKED]") : finding.riskLevel === "high_risk" ? color.red("[HIGH RISK]") : color.yellow("[CAUTION]");
|
|
281
|
+
const sourceTag = finding.source === "probe_failure" ? color.dim(" \xB7 live probe") : finding.source === "vendor_change" ? color.dim(" \xB7 spec change") : finding.source === "alert" ? color.dim(" \xB7 alert") : "";
|
|
282
|
+
const headerText = `${icon} ${badge} ${color.bold(finding.vendorName)}${sourceTag}`;
|
|
283
|
+
console.log(boxLine(headerText, width));
|
|
284
|
+
const title = finding.title ?? finding.description;
|
|
285
|
+
if (title) {
|
|
286
|
+
const titleLines = wordWrap(title, width - 4);
|
|
287
|
+
for (const tl of titleLines) {
|
|
288
|
+
console.log(boxLine(` ${color.dim(tl)}`, width));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (finding.versionFrom || finding.versionTo) {
|
|
292
|
+
const vRange = [finding.versionFrom, finding.versionTo].filter(Boolean).join(color.dim(" \u2192 "));
|
|
293
|
+
console.log(boxLine(` ${color.dim("v")}${vRange}`, width));
|
|
294
|
+
}
|
|
295
|
+
const body = finding.summary ?? finding.description;
|
|
296
|
+
if (body) {
|
|
297
|
+
const bodyLines = wordWrap(body, width - 4);
|
|
298
|
+
console.log(blankLine(width));
|
|
299
|
+
for (const bl of bodyLines) {
|
|
300
|
+
console.log(boxLine(` ${bl}`, width));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (finding.recommendation) {
|
|
304
|
+
const recLines = wordWrap(finding.recommendation, width - 7);
|
|
305
|
+
console.log(blankLine(width));
|
|
306
|
+
for (let i = 0; i < recLines.length; i++) {
|
|
307
|
+
const prefix = i === 0 ? ` ${color.cyan("\u2192")} ` : " ";
|
|
308
|
+
console.log(boxLine(`${prefix}${recLines[i]}`, width));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
253
311
|
}
|
|
254
|
-
|
|
255
|
-
|
|
312
|
+
console.log(blankLine(width));
|
|
313
|
+
if (nonSafe.length > 1) {
|
|
314
|
+
console.log(boxLine(color.dim(` ${nonSafe.length} finding${nonSafe.length === 1 ? "" : "s"} total`), width));
|
|
256
315
|
}
|
|
257
316
|
}
|
|
258
317
|
if (opts.dashboardUrl) {
|
|
259
318
|
console.log(`\u251C${line}\u2524`);
|
|
260
|
-
console.log(
|
|
261
|
-
console.log(`\u2502 ${color.dim("\u2192")} ${opts.dashboardUrl.slice(0, width - 2).padEnd(width - 2)} \u2502`);
|
|
319
|
+
console.log(boxLine(`${color.cyan("View:")} ${color.dim(opts.dashboardUrl)}`, width));
|
|
262
320
|
}
|
|
263
321
|
console.log(`\u2514${"\u2500".repeat(width + 2)}\u2518`);
|
|
264
322
|
console.log("");
|
|
323
|
+
if (opts.verdict === "safe" && !QUIET) {
|
|
324
|
+
console.log(`${color.green("\u2713")} No API risk detected. Safe to deploy.
|
|
325
|
+
`);
|
|
326
|
+
}
|
|
265
327
|
}
|
|
266
328
|
function printError(msg, hint) {
|
|
267
329
|
console.error(`
|
|
@@ -355,6 +417,81 @@ for (const v of VENDORS) {
|
|
|
355
417
|
for (const pfx of v.envPrefixes) PREFIX_TO_VENDOR.set(pfx, v);
|
|
356
418
|
for (const dom of v.domains) DOMAIN_TO_VENDOR.set(dom, v);
|
|
357
419
|
}
|
|
420
|
+
var PYTHON_PACKAGE_TO_VENDOR = /* @__PURE__ */ new Map([
|
|
421
|
+
["stripe", VENDORS.find((v) => v.key === "stripe")],
|
|
422
|
+
["openai", VENDORS.find((v) => v.key === "openai")],
|
|
423
|
+
["anthropic", VENDORS.find((v) => v.key === "anthropic")],
|
|
424
|
+
["twilio", VENDORS.find((v) => v.key === "twilio")],
|
|
425
|
+
["sendgrid", VENDORS.find((v) => v.key === "sendgrid")],
|
|
426
|
+
["sendgrid-python", VENDORS.find((v) => v.key === "sendgrid")],
|
|
427
|
+
["plaid-python", VENDORS.find((v) => v.key === "plaid")],
|
|
428
|
+
["slack-sdk", VENDORS.find((v) => v.key === "slack")],
|
|
429
|
+
["slack-bolt", VENDORS.find((v) => v.key === "slack")],
|
|
430
|
+
["PyGithub", VENDORS.find((v) => v.key === "github")],
|
|
431
|
+
["shopifyapi", VENDORS.find((v) => v.key === "shopify")],
|
|
432
|
+
["boto3", VENDORS.find((v) => v.key === "aws")],
|
|
433
|
+
["botocore", VENDORS.find((v) => v.key === "aws")],
|
|
434
|
+
["google-cloud-storage", VENDORS.find((v) => v.key === "google_cloud")],
|
|
435
|
+
["google-cloud-bigquery", VENDORS.find((v) => v.key === "google_cloud")],
|
|
436
|
+
["google-api-python-client", VENDORS.find((v) => v.key === "google_cloud")],
|
|
437
|
+
["datadog", VENDORS.find((v) => v.key === "datadog")],
|
|
438
|
+
["analytics-python", VENDORS.find((v) => v.key === "segment")],
|
|
439
|
+
["hubspot", VENDORS.find((v) => v.key === "hubspot")],
|
|
440
|
+
["simple-salesforce", VENDORS.find((v) => v.key === "salesforce")],
|
|
441
|
+
["pdpyras", VENDORS.find((v) => v.key === "pagerduty")],
|
|
442
|
+
["resend", VENDORS.find((v) => v.key === "resend")],
|
|
443
|
+
["supabase", VENDORS.find((v) => v.key === "supabase")],
|
|
444
|
+
["firebase-admin", VENDORS.find((v) => v.key === "firebase")],
|
|
445
|
+
["notion-client", VENDORS.find((v) => v.key === "notion")],
|
|
446
|
+
["airtable", VENDORS.find((v) => v.key === "airtable")]
|
|
447
|
+
]);
|
|
448
|
+
var GO_MODULE_TO_VENDOR = /* @__PURE__ */ new Map([
|
|
449
|
+
["github.com/stripe/stripe-go", VENDORS.find((v) => v.key === "stripe")],
|
|
450
|
+
["github.com/sashabaranov/go-openai", VENDORS.find((v) => v.key === "openai")],
|
|
451
|
+
["github.com/anthropics/anthropic-sdk-go", VENDORS.find((v) => v.key === "anthropic")],
|
|
452
|
+
["github.com/twilio/twilio-go", VENDORS.find((v) => v.key === "twilio")],
|
|
453
|
+
["github.com/sendgrid/sendgrid-go", VENDORS.find((v) => v.key === "sendgrid")],
|
|
454
|
+
["github.com/plaid/plaid-go", VENDORS.find((v) => v.key === "plaid")],
|
|
455
|
+
["github.com/slack-go/slack", VENDORS.find((v) => v.key === "slack")],
|
|
456
|
+
["github.com/google/go-github", VENDORS.find((v) => v.key === "github")],
|
|
457
|
+
["github.com/aws/aws-sdk-go", VENDORS.find((v) => v.key === "aws")],
|
|
458
|
+
["github.com/aws/aws-sdk-go-v2", VENDORS.find((v) => v.key === "aws")],
|
|
459
|
+
["google.golang.org/api", VENDORS.find((v) => v.key === "google_cloud")],
|
|
460
|
+
["cloud.google.com/go", VENDORS.find((v) => v.key === "google_cloud")],
|
|
461
|
+
["github.com/DataDog/datadog-go", VENDORS.find((v) => v.key === "datadog")],
|
|
462
|
+
["github.com/PagerDuty/go-pagerduty", VENDORS.find((v) => v.key === "pagerduty")],
|
|
463
|
+
["github.com/supabase-community/supabase-go", VENDORS.find((v) => v.key === "supabase")],
|
|
464
|
+
["firebase.google.com/go", VENDORS.find((v) => v.key === "firebase")],
|
|
465
|
+
["github.com/jomei/notionapi", VENDORS.find((v) => v.key === "notion")]
|
|
466
|
+
]);
|
|
467
|
+
var RUBY_GEM_TO_VENDOR = /* @__PURE__ */ new Map([
|
|
468
|
+
["stripe", VENDORS.find((v) => v.key === "stripe")],
|
|
469
|
+
["ruby-openai", VENDORS.find((v) => v.key === "openai")],
|
|
470
|
+
["anthropic-rb", VENDORS.find((v) => v.key === "anthropic")],
|
|
471
|
+
["twilio-ruby", VENDORS.find((v) => v.key === "twilio")],
|
|
472
|
+
["sendgrid-ruby", VENDORS.find((v) => v.key === "sendgrid")],
|
|
473
|
+
["plaid", VENDORS.find((v) => v.key === "plaid")],
|
|
474
|
+
["slack-ruby-client", VENDORS.find((v) => v.key === "slack")],
|
|
475
|
+
["octokit", VENDORS.find((v) => v.key === "github")],
|
|
476
|
+
["shopify_api", VENDORS.find((v) => v.key === "shopify")],
|
|
477
|
+
["aws-sdk", VENDORS.find((v) => v.key === "aws")],
|
|
478
|
+
["google-apis-core", VENDORS.find((v) => v.key === "google_cloud")],
|
|
479
|
+
["dogapi", VENDORS.find((v) => v.key === "datadog")],
|
|
480
|
+
["hubspot-api-client", VENDORS.find((v) => v.key === "hubspot")],
|
|
481
|
+
["restforce", VENDORS.find((v) => v.key === "salesforce")],
|
|
482
|
+
["supabase", VENDORS.find((v) => v.key === "supabase")]
|
|
483
|
+
]);
|
|
484
|
+
var RUST_CRATE_TO_VENDOR = /* @__PURE__ */ new Map([
|
|
485
|
+
["stripe", VENDORS.find((v) => v.key === "stripe")],
|
|
486
|
+
["async-openai", VENDORS.find((v) => v.key === "openai")],
|
|
487
|
+
["twilio", VENDORS.find((v) => v.key === "twilio")],
|
|
488
|
+
["aws-sdk-s3", VENDORS.find((v) => v.key === "aws")],
|
|
489
|
+
["aws-sdk-dynamodb", VENDORS.find((v) => v.key === "aws")],
|
|
490
|
+
["aws-config", VENDORS.find((v) => v.key === "aws")],
|
|
491
|
+
["google-cloud-storage", VENDORS.find((v) => v.key === "google_cloud")],
|
|
492
|
+
["octocrab", VENDORS.find((v) => v.key === "github")],
|
|
493
|
+
["slack-morphism", VENDORS.find((v) => v.key === "slack")]
|
|
494
|
+
]);
|
|
358
495
|
|
|
359
496
|
// src/engines/detect.ts
|
|
360
497
|
var MAX_FILE_BYTES = 256e3;
|
|
@@ -486,6 +623,73 @@ function detectVendorsInRoot(root) {
|
|
|
486
623
|
matchPackageJson(content, map, "package.json");
|
|
487
624
|
return [...map.values()];
|
|
488
625
|
}
|
|
626
|
+
function matchRequirementsTxt(content, map, filePath) {
|
|
627
|
+
for (const rawLine of content.split("\n")) {
|
|
628
|
+
const line = rawLine.trim();
|
|
629
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) continue;
|
|
630
|
+
const pkgName = line.split(/[=<>!~\[;]/)[0].trim().toLowerCase();
|
|
631
|
+
const vendor = PYTHON_PACKAGE_TO_VENDOR.get(pkgName) ?? PYTHON_PACKAGE_TO_VENDOR.get(line.split(/[=<>!~\[;]/)[0].trim());
|
|
632
|
+
if (vendor) upsert(map, vendor, "strong", `requirements.txt: ${pkgName}`, filePath);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function matchPyprojectToml(content, map, filePath) {
|
|
636
|
+
const depRe = /^([a-zA-Z0-9_\-]+)\s*=/gm;
|
|
637
|
+
let m;
|
|
638
|
+
while ((m = depRe.exec(content)) !== null) {
|
|
639
|
+
const pkgName = m[1].toLowerCase().replace(/_/g, "-");
|
|
640
|
+
const vendor = PYTHON_PACKAGE_TO_VENDOR.get(pkgName) ?? PYTHON_PACKAGE_TO_VENDOR.get(m[1].toLowerCase());
|
|
641
|
+
if (vendor) upsert(map, vendor, "strong", `pyproject.toml: ${pkgName}`, filePath);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function matchGoMod(content, map, filePath) {
|
|
645
|
+
const reqRe = /^\s+([a-zA-Z0-9.\-/]+)\s+v[\d.]+/gm;
|
|
646
|
+
let m;
|
|
647
|
+
while ((m = reqRe.exec(content)) !== null) {
|
|
648
|
+
const modPath = m[1];
|
|
649
|
+
for (const [key, vendor] of GO_MODULE_TO_VENDOR) {
|
|
650
|
+
if (modPath === key || modPath.startsWith(key + "/") || key.startsWith(modPath)) {
|
|
651
|
+
upsert(map, vendor, "strong", `go.mod: ${modPath}`, filePath);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function matchGemfile(content, map, filePath) {
|
|
658
|
+
const gemRe = /^\s*gem\s+['"]([a-zA-Z0-9_\-]+)['"]/gm;
|
|
659
|
+
let m;
|
|
660
|
+
while ((m = gemRe.exec(content)) !== null) {
|
|
661
|
+
const gemName = m[1];
|
|
662
|
+
const vendor = RUBY_GEM_TO_VENDOR.get(gemName);
|
|
663
|
+
if (vendor) upsert(map, vendor, "strong", `Gemfile: ${gemName}`, filePath);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function matchCargoToml(content, map, filePath) {
|
|
667
|
+
const depRe = /^\s*([a-zA-Z0-9_\-]+)\s*=/gm;
|
|
668
|
+
let m;
|
|
669
|
+
while ((m = depRe.exec(content)) !== null) {
|
|
670
|
+
const crateName = m[1].replace(/_/g, "-");
|
|
671
|
+
const vendor = RUST_CRATE_TO_VENDOR.get(crateName) ?? RUST_CRATE_TO_VENDOR.get(m[1]);
|
|
672
|
+
if (vendor) upsert(map, vendor, "strong", `Cargo.toml: ${crateName}`, filePath);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function scanProjectManifests(root) {
|
|
676
|
+
const map = /* @__PURE__ */ new Map();
|
|
677
|
+
const manifests = [
|
|
678
|
+
{ file: "package.json", handler: matchPackageJson },
|
|
679
|
+
{ file: "requirements.txt", handler: matchRequirementsTxt },
|
|
680
|
+
{ file: "requirements.in", handler: matchRequirementsTxt },
|
|
681
|
+
{ file: "pyproject.toml", handler: matchPyprojectToml },
|
|
682
|
+
{ file: "go.mod", handler: matchGoMod },
|
|
683
|
+
{ file: "Gemfile", handler: matchGemfile },
|
|
684
|
+
{ file: "Cargo.toml", handler: matchCargoToml }
|
|
685
|
+
];
|
|
686
|
+
for (const { file, handler } of manifests) {
|
|
687
|
+
const absPath = path2.join(root, file);
|
|
688
|
+
const content = readSafe(absPath);
|
|
689
|
+
if (content) handler(content, map, file);
|
|
690
|
+
}
|
|
691
|
+
return [...map.values()];
|
|
692
|
+
}
|
|
489
693
|
|
|
490
694
|
// src/commands/scan.ts
|
|
491
695
|
async function scanCommand(targetPath, flags) {
|
|
@@ -493,8 +697,9 @@ async function scanCommand(targetPath, flags) {
|
|
|
493
697
|
const startedAt = Date.now();
|
|
494
698
|
const config = resolveConfig({ token: flags.token, apiUrl: flags.url, repoUrl: flags.repoUrl, commit: flags.commit, branch: flags.branch }, root);
|
|
495
699
|
if (!config.token && !flags.dryRun) {
|
|
496
|
-
printError("No API token found.", "
|
|
497
|
-
process.
|
|
700
|
+
printError("No API token found.", "Run: relion login");
|
|
701
|
+
process.exitCode = 1;
|
|
702
|
+
return;
|
|
498
703
|
}
|
|
499
704
|
const meta = gitMeta(root);
|
|
500
705
|
const branch = flags.branch ?? config.branch ?? meta.branch ?? void 0;
|
|
@@ -524,7 +729,8 @@ Relion v2.0.0${repoUrl ? ` \xB7 ${repoUrl.replace("https://github.com/", "")}`
|
|
|
524
729
|
} else {
|
|
525
730
|
process.stdout.write(JSON.stringify({ vendors: detected, dryRun: true }, null, 2) + "\n");
|
|
526
731
|
}
|
|
527
|
-
process.
|
|
732
|
+
process.exitCode = detected.length === 0 ? 4 : 0;
|
|
733
|
+
return;
|
|
528
734
|
}
|
|
529
735
|
spinner.start("Uploading API metadata");
|
|
530
736
|
const payload = {
|
|
@@ -579,9 +785,9 @@ Relion v2.0.0${repoUrl ? ` \xB7 ${repoUrl.replace("https://github.com/", "")}`
|
|
|
579
785
|
const global = readGlobalConfig();
|
|
580
786
|
writeGlobalConfig({ ...global, lastScanId: receipt.scanId, lastScanAt: (/* @__PURE__ */ new Date()).toISOString(), lastDashboardUrl: receipt.dashboardUrl });
|
|
581
787
|
}
|
|
582
|
-
if (receipt.deployGate?.status === "blocked") process.
|
|
583
|
-
if (receipt.deployGate?.status === "pending") process.
|
|
584
|
-
if (detected.length === 0) process.
|
|
788
|
+
if (receipt.deployGate?.status === "blocked") process.exitCode = 2;
|
|
789
|
+
else if (receipt.deployGate?.status === "pending") process.exitCode = 3;
|
|
790
|
+
else if (detected.length === 0) process.exitCode = 4;
|
|
585
791
|
} catch (err) {
|
|
586
792
|
spinner.fail("Scan failed");
|
|
587
793
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -591,7 +797,7 @@ Relion v2.0.0${repoUrl ? ` \xB7 ${repoUrl.replace("https://github.com/", "")}`
|
|
|
591
797
|
printError(msg);
|
|
592
798
|
}
|
|
593
799
|
if (flags.verbose) console.error(err);
|
|
594
|
-
process.
|
|
800
|
+
process.exitCode = 1;
|
|
595
801
|
}
|
|
596
802
|
}
|
|
597
803
|
async function verifyToken(token, apiUrl) {
|
|
@@ -797,6 +1003,18 @@ ${color.bold("Last scan")} ${color.dim("(cached)")}`);
|
|
|
797
1003
|
var path4 = __toESM(require("path"));
|
|
798
1004
|
|
|
799
1005
|
// src/engines/api.ts
|
|
1006
|
+
function severityToRiskLevel(severity) {
|
|
1007
|
+
switch (severity?.toLowerCase()) {
|
|
1008
|
+
case "critical":
|
|
1009
|
+
return "blocked";
|
|
1010
|
+
case "high":
|
|
1011
|
+
return "high_risk";
|
|
1012
|
+
case "medium":
|
|
1013
|
+
return "caution";
|
|
1014
|
+
default:
|
|
1015
|
+
return "info";
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
800
1018
|
async function apiFetch(url, token, method = "GET", body) {
|
|
801
1019
|
const opts = {
|
|
802
1020
|
method,
|
|
@@ -810,16 +1028,72 @@ async function apiFetch(url, token, method = "GET", body) {
|
|
|
810
1028
|
}
|
|
811
1029
|
async function lookupRisk(vendorKeys, token, apiUrl) {
|
|
812
1030
|
if (vendorKeys.length === 0) return [];
|
|
813
|
-
const qs = vendorKeys.map(
|
|
1031
|
+
const qs = `vendors=${vendorKeys.map(encodeURIComponent).join(",")}`;
|
|
814
1032
|
const url = `${apiUrl.replace(/\/$/, "")}/api/predeploy/risk?${qs}`;
|
|
1033
|
+
let data;
|
|
815
1034
|
try {
|
|
816
1035
|
const res = await apiFetch(url, token);
|
|
817
1036
|
if (!res.ok) return [];
|
|
818
|
-
|
|
819
|
-
return data.findings ?? [];
|
|
1037
|
+
data = await res.json();
|
|
820
1038
|
} catch {
|
|
821
1039
|
return [];
|
|
822
1040
|
}
|
|
1041
|
+
const findings = [];
|
|
1042
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1043
|
+
for (const a of data.alerts ?? []) {
|
|
1044
|
+
const key = `alert:${a.id}`;
|
|
1045
|
+
if (seen.has(key)) continue;
|
|
1046
|
+
seen.add(key);
|
|
1047
|
+
findings.push({
|
|
1048
|
+
vendorKey: a.vendorKey ?? "",
|
|
1049
|
+
vendorName: a.vendorName ?? a.title,
|
|
1050
|
+
riskLevel: severityToRiskLevel(a.severity),
|
|
1051
|
+
findingType: "alert",
|
|
1052
|
+
source: "alert",
|
|
1053
|
+
title: a.title,
|
|
1054
|
+
description: a.vendorChange?.summary ?? a.summary,
|
|
1055
|
+
summary: a.summary,
|
|
1056
|
+
versionFrom: void 0,
|
|
1057
|
+
versionTo: void 0,
|
|
1058
|
+
detectedAt: a.createdAt
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
for (const vc of data.vendorChanges ?? []) {
|
|
1062
|
+
const key = `vc:${vc.id}`;
|
|
1063
|
+
if (seen.has(key)) continue;
|
|
1064
|
+
seen.add(key);
|
|
1065
|
+
findings.push({
|
|
1066
|
+
vendorKey: vc.vendorKey,
|
|
1067
|
+
vendorName: vc.vendorKey,
|
|
1068
|
+
riskLevel: severityToRiskLevel(vc.severity),
|
|
1069
|
+
findingType: "vendor_change",
|
|
1070
|
+
source: "vendor_change",
|
|
1071
|
+
title: vc.title,
|
|
1072
|
+
description: vc.summary,
|
|
1073
|
+
summary: vc.summary,
|
|
1074
|
+
versionFrom: vc.versionFrom ?? void 0,
|
|
1075
|
+
versionTo: vc.versionTo ?? void 0,
|
|
1076
|
+
detectedAt: vc.detectedAt
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
for (const p of data.probeFailures ?? []) {
|
|
1080
|
+
const key = `probe:${p.id}`;
|
|
1081
|
+
if (seen.has(key)) continue;
|
|
1082
|
+
seen.add(key);
|
|
1083
|
+
findings.push({
|
|
1084
|
+
vendorKey: p.vendorKey ?? "",
|
|
1085
|
+
vendorName: p.vendorName ?? p.name,
|
|
1086
|
+
riskLevel: p.consecutiveFailures >= 3 ? "high_risk" : "caution",
|
|
1087
|
+
findingType: "probe_failure",
|
|
1088
|
+
source: "probe_failure",
|
|
1089
|
+
title: `${p.name} endpoint is failing`,
|
|
1090
|
+
description: p.lastError ?? `${p.consecutiveFailures} consecutive failures on ${p.targetUrl}`,
|
|
1091
|
+
summary: `Live probe ${p.name} has failed ${p.consecutiveFailures} times in a row. Last error: ${p.lastError ?? "unknown"}`,
|
|
1092
|
+
recommendation: "Check the endpoint status before deploying changes that depend on it.",
|
|
1093
|
+
detectedAt: p.lastRunAt ?? void 0
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
return findings;
|
|
823
1097
|
}
|
|
824
1098
|
async function submitCheck(payload, token, apiUrl) {
|
|
825
1099
|
const url = `${apiUrl.replace(/\/$/, "")}/api/predeploy/check`;
|
|
@@ -845,8 +1119,9 @@ async function predeployCommand(targetPath, flags) {
|
|
|
845
1119
|
const config = resolveConfig({ token: flags.token, apiUrl: flags.url, repoUrl: flags.repoUrl, commit: flags.commit, branch: flags.branch }, root);
|
|
846
1120
|
const offline = flags.offline ?? !config.token;
|
|
847
1121
|
if (!config.token && !offline) {
|
|
848
|
-
printError("No API token found.", "
|
|
849
|
-
process.
|
|
1122
|
+
printError("No API token found.", "Run: relion login\nOr set RELION_TOKEN env var. Use --offline to skip cloud lookup.");
|
|
1123
|
+
process.exitCode = 1;
|
|
1124
|
+
return;
|
|
850
1125
|
}
|
|
851
1126
|
let scopeMode = "default";
|
|
852
1127
|
let diffBase;
|
|
@@ -871,12 +1146,25 @@ Relion pre-deploy check \u2014 ${scopeLabel}
|
|
|
871
1146
|
if (changedFiles.length === 0) {
|
|
872
1147
|
spinner.succeed("No changed files found");
|
|
873
1148
|
if (!flags.json) console.log("\nNothing to check \u2014 no changed files detected.\n");
|
|
874
|
-
|
|
1149
|
+
return;
|
|
875
1150
|
}
|
|
876
1151
|
spinner.succeed(`${changedFiles.length} changed file${changedFiles.length === 1 ? "" : "s"}`);
|
|
877
1152
|
spinner.start("Detecting API dependencies");
|
|
878
|
-
const
|
|
879
|
-
|
|
1153
|
+
const fromDiff = detectVendorsInFiles(root, changedFiles);
|
|
1154
|
+
const fromManifests = scanProjectManifests(root);
|
|
1155
|
+
const vendorMap = new Map(fromDiff.map((v) => [v.key, v]));
|
|
1156
|
+
for (const v of fromManifests) {
|
|
1157
|
+
if (!vendorMap.has(v.key)) {
|
|
1158
|
+
vendorMap.set(v.key, { ...v, signals: v.signals.map((s) => `[project] ${s}`) });
|
|
1159
|
+
} else {
|
|
1160
|
+
const existing = vendorMap.get(v.key);
|
|
1161
|
+
for (const s of v.signals) {
|
|
1162
|
+
if (!existing.signals.includes(s)) existing.signals.push(`[project] ${s}`);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const detected = [...vendorMap.values()];
|
|
1167
|
+
spinner.succeed(detected.length > 0 ? `${detected.length} API vendor${detected.length === 1 ? "" : "s"} detected` : "No vendor APIs detected");
|
|
880
1168
|
let findings = [];
|
|
881
1169
|
if (!offline && config.token && detected.length > 0) {
|
|
882
1170
|
spinner.start("Checking risk against monitored APIs");
|
|
@@ -950,7 +1238,7 @@ Relion pre-deploy check \u2014 ${scopeLabel}
|
|
|
950
1238
|
}
|
|
951
1239
|
}
|
|
952
1240
|
}
|
|
953
|
-
process.
|
|
1241
|
+
process.exitCode = exitCode;
|
|
954
1242
|
} catch (err) {
|
|
955
1243
|
spinner.fail("Pre-deploy check failed");
|
|
956
1244
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -962,7 +1250,7 @@ Relion pre-deploy check \u2014 ${scopeLabel}
|
|
|
962
1250
|
printError(msg);
|
|
963
1251
|
}
|
|
964
1252
|
if (flags.verbose) console.error(err);
|
|
965
|
-
process.
|
|
1253
|
+
process.exitCode = 1;
|
|
966
1254
|
}
|
|
967
1255
|
}
|
|
968
1256
|
|