relionhq 2.0.3 → 2.1.1
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 +311 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -217,51 +217,113 @@ 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
|
+
var INNER = 64;
|
|
241
|
+
var TEXT = INNER - 4;
|
|
242
|
+
function boxLine(content) {
|
|
243
|
+
const stripped = stripAnsi(content);
|
|
244
|
+
const gap = Math.max(0, TEXT - stripped.length);
|
|
245
|
+
return `\u2502 ${content}${" ".repeat(gap)} \u2502`;
|
|
246
|
+
}
|
|
247
|
+
function blankLine() {
|
|
248
|
+
return `\u2502${" ".repeat(INNER)}\u2502`;
|
|
249
|
+
}
|
|
250
|
+
function divider() {
|
|
251
|
+
return `\u251C${"\u2500".repeat(INNER)}\u2524`;
|
|
252
|
+
}
|
|
220
253
|
function printPredeployReceipt(opts) {
|
|
221
254
|
if (QUIET) return;
|
|
222
|
-
const width = 60;
|
|
223
|
-
const line = "\u2500".repeat(width);
|
|
224
255
|
const pad = (label, value) => {
|
|
225
|
-
const stripped = value
|
|
226
|
-
const gap =
|
|
227
|
-
return `\u2502 ${label}${" ".repeat(
|
|
256
|
+
const stripped = stripAnsi(value);
|
|
257
|
+
const gap = Math.max(1, TEXT - label.length - stripped.length);
|
|
258
|
+
return `\u2502 ${label}${" ".repeat(gap)}${value} \u2502`;
|
|
228
259
|
};
|
|
229
260
|
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
261
|
console.log("");
|
|
231
|
-
console.log(`\u250C${"\u2500".repeat(
|
|
232
|
-
console.log(`\u2502 ${color.bold("Relion Pre-Deploy Check")}${" ".repeat(
|
|
233
|
-
console.log(
|
|
262
|
+
console.log(`\u250C${"\u2500".repeat(INNER)}\u2510`);
|
|
263
|
+
console.log(`\u2502 ${color.bold("Relion Pre-Deploy Check")}${" ".repeat(TEXT - 22)} \u2502`);
|
|
264
|
+
console.log(divider());
|
|
234
265
|
if (opts.branch || opts.commit) {
|
|
235
|
-
const meta = [opts.branch, opts.commit ? opts.commit.slice(0, 7) : ""].filter(Boolean).join("
|
|
266
|
+
const meta = [opts.branch, opts.commit ? opts.commit.slice(0, 7) : ""].filter(Boolean).join(" \xB7 ");
|
|
236
267
|
console.log(pad("Branch:", color.dim(meta)));
|
|
237
268
|
}
|
|
238
269
|
if (opts.baseBranch) console.log(pad("Comparing against:", color.dim(opts.baseBranch)));
|
|
239
270
|
console.log(pad("Duration:", color.dim(`${(opts.durationMs / 1e3).toFixed(1)}s`)));
|
|
240
271
|
if (opts.offline) console.log(pad("Mode:", color.yellow("offline")));
|
|
241
|
-
console.log(
|
|
272
|
+
console.log(divider());
|
|
242
273
|
console.log(pad("Files in diff:", String(opts.filesChangedCount)));
|
|
243
274
|
console.log(pad("APIs involved:", String(opts.apisInvolvedCount)));
|
|
244
|
-
console.log(
|
|
275
|
+
console.log(divider());
|
|
245
276
|
console.log(pad("Verdict:", verdictLabel));
|
|
246
277
|
const nonSafe = opts.findings.filter((f) => f.riskLevel !== "safe" && f.riskLevel !== "info");
|
|
247
278
|
if (nonSafe.length > 0) {
|
|
248
|
-
console.log(
|
|
249
|
-
for (const finding of nonSafe
|
|
279
|
+
console.log(divider());
|
|
280
|
+
for (const finding of nonSafe) {
|
|
281
|
+
console.log(blankLine());
|
|
250
282
|
const icon = finding.riskLevel === "blocked" ? color.red("\u2717") : finding.riskLevel === "high_risk" ? color.red("!") : color.yellow("\u26A0");
|
|
251
|
-
const
|
|
252
|
-
|
|
283
|
+
const badge = finding.riskLevel === "blocked" ? color.red("[BLOCKED]") : finding.riskLevel === "high_risk" ? color.red("[HIGH RISK]") : color.yellow("[CAUTION]");
|
|
284
|
+
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") : "";
|
|
285
|
+
console.log(boxLine(`${icon} ${badge} ${color.bold(finding.vendorName)}${sourceTag}`));
|
|
286
|
+
const title = finding.title ?? finding.description;
|
|
287
|
+
if (title) {
|
|
288
|
+
for (const tl of wordWrap(title, TEXT - 4)) {
|
|
289
|
+
console.log(boxLine(` ${color.dim(tl)}`));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (finding.versionFrom || finding.versionTo) {
|
|
293
|
+
const vRange = [finding.versionFrom, finding.versionTo].filter(Boolean).join(" \u2192 ");
|
|
294
|
+
console.log(boxLine(` v${vRange}`));
|
|
295
|
+
}
|
|
296
|
+
const body = finding.summary ?? finding.description;
|
|
297
|
+
if (body) {
|
|
298
|
+
console.log(blankLine());
|
|
299
|
+
for (const bl of wordWrap(body, TEXT - 4)) {
|
|
300
|
+
console.log(boxLine(` ${bl}`));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (finding.recommendation) {
|
|
304
|
+
console.log(blankLine());
|
|
305
|
+
const recLines = wordWrap(finding.recommendation, TEXT - 7);
|
|
306
|
+
for (let i = 0; i < recLines.length; i++) {
|
|
307
|
+
const prefix = i === 0 ? ` ${color.cyan("\u2192")} ` : " ";
|
|
308
|
+
console.log(boxLine(`${prefix}${recLines[i]}`));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
253
311
|
}
|
|
254
|
-
|
|
255
|
-
|
|
312
|
+
console.log(blankLine());
|
|
313
|
+
if (nonSafe.length > 1) {
|
|
314
|
+
console.log(boxLine(color.dim(` ${nonSafe.length} findings total`)));
|
|
256
315
|
}
|
|
257
316
|
}
|
|
258
317
|
if (opts.dashboardUrl) {
|
|
259
|
-
console.log(
|
|
260
|
-
console.log(
|
|
261
|
-
console.log(`\u2502 ${color.dim("\u2192")} ${opts.dashboardUrl.slice(0, width - 2).padEnd(width - 2)} \u2502`);
|
|
318
|
+
console.log(divider());
|
|
319
|
+
console.log(boxLine(`${color.cyan("View:")} ${color.dim(opts.dashboardUrl)}`));
|
|
262
320
|
}
|
|
263
|
-
console.log(`\u2514${"\u2500".repeat(
|
|
321
|
+
console.log(`\u2514${"\u2500".repeat(INNER)}\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) {
|
|
@@ -799,6 +1003,18 @@ ${color.bold("Last scan")} ${color.dim("(cached)")}`);
|
|
|
799
1003
|
var path4 = __toESM(require("path"));
|
|
800
1004
|
|
|
801
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
|
+
}
|
|
802
1018
|
async function apiFetch(url, token, method = "GET", body) {
|
|
803
1019
|
const opts = {
|
|
804
1020
|
method,
|
|
@@ -812,16 +1028,72 @@ async function apiFetch(url, token, method = "GET", body) {
|
|
|
812
1028
|
}
|
|
813
1029
|
async function lookupRisk(vendorKeys, token, apiUrl) {
|
|
814
1030
|
if (vendorKeys.length === 0) return [];
|
|
815
|
-
const qs = vendorKeys.map(
|
|
1031
|
+
const qs = `vendors=${vendorKeys.map(encodeURIComponent).join(",")}`;
|
|
816
1032
|
const url = `${apiUrl.replace(/\/$/, "")}/api/predeploy/risk?${qs}`;
|
|
1033
|
+
let data;
|
|
817
1034
|
try {
|
|
818
1035
|
const res = await apiFetch(url, token);
|
|
819
1036
|
if (!res.ok) return [];
|
|
820
|
-
|
|
821
|
-
return data.findings ?? [];
|
|
1037
|
+
data = await res.json();
|
|
822
1038
|
} catch {
|
|
823
1039
|
return [];
|
|
824
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;
|
|
825
1097
|
}
|
|
826
1098
|
async function submitCheck(payload, token, apiUrl) {
|
|
827
1099
|
const url = `${apiUrl.replace(/\/$/, "")}/api/predeploy/check`;
|
|
@@ -878,8 +1150,21 @@ Relion pre-deploy check \u2014 ${scopeLabel}
|
|
|
878
1150
|
}
|
|
879
1151
|
spinner.succeed(`${changedFiles.length} changed file${changedFiles.length === 1 ? "" : "s"}`);
|
|
880
1152
|
spinner.start("Detecting API dependencies");
|
|
881
|
-
const
|
|
882
|
-
|
|
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");
|
|
883
1168
|
let findings = [];
|
|
884
1169
|
if (!offline && config.token && detected.length > 0) {
|
|
885
1170
|
spinner.start("Checking risk against monitored APIs");
|