relionhq 2.0.3 → 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 +303 -18
- 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) {
|
|
@@ -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");
|