@runsec/mcp 1.0.77 → 1.0.79
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 +261 -47
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -413,7 +413,7 @@ var import_node_fs3 = __toESM(require("fs"));
|
|
|
413
413
|
var import_node_path3 = __toESM(require("path"));
|
|
414
414
|
|
|
415
415
|
// src/engine/secretHeuristics.ts
|
|
416
|
-
var ENV_INTERP_RE = /(?:\$\{[A-Z0-9_]+\}|\$[A-Z][A-Z0-9_]{2,}|%\([A-Za-z0-9_.]+\)s|process\.env\.|os\.getenv\(|getenv\()/i;
|
|
416
|
+
var ENV_INTERP_RE = /(?:\$\{[A-Z0-9_]+\}|\$\{[^}]+\}|\$[A-Z][A-Z0-9_]{2,}|%\([A-Za-z0-9_.]+\)s|process\.env\.|os\.getenv\(|getenv\(|environ\[)/i;
|
|
417
417
|
var LOCKFILE_BASENAMES = /^(?:poetry\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|cargo\.lock|composer\.lock|gemfile\.lock)$/i;
|
|
418
418
|
function hasEnvironmentInterpolation(text) {
|
|
419
419
|
return ENV_INTERP_RE.test(text);
|
|
@@ -435,6 +435,15 @@ var LOCKFILE_LAYOUT_RE = /(?:poetry\.lock|package-lock\.json|pnpm-lock\.yaml|yar
|
|
|
435
435
|
function isLockfileLayoutArtifactPath(relPath) {
|
|
436
436
|
return LOCKFILE_LAYOUT_RE.test(relPath.replace(/\\/g, "/"));
|
|
437
437
|
}
|
|
438
|
+
function findingBlobHasEnvInterpolation(finding) {
|
|
439
|
+
const parts = [
|
|
440
|
+
finding.match_text ?? "",
|
|
441
|
+
finding.snippet ?? "",
|
|
442
|
+
finding.description ?? "",
|
|
443
|
+
finding.title ?? ""
|
|
444
|
+
];
|
|
445
|
+
return parts.some((p) => p.trim() && hasEnvironmentInterpolation(p));
|
|
446
|
+
}
|
|
438
447
|
|
|
439
448
|
// src/engine/cognitiveEngine.ts
|
|
440
449
|
var NUCLEAR_HARD_DROP_CONFIDENCE = 0.01;
|
|
@@ -446,14 +455,38 @@ function findingIsVerified(finding) {
|
|
|
446
455
|
function applyNuclearHardDrop(finding, relPath) {
|
|
447
456
|
const matchText = finding.match_text ?? "";
|
|
448
457
|
const snippet = finding.snippet ?? "";
|
|
449
|
-
if (ENV_INTERP_RE.test(matchText) || ENV_INTERP_RE.test(snippet)) {
|
|
458
|
+
if (ENV_INTERP_RE.test(matchText) || ENV_INTERP_RE.test(snippet) || findingBlobHasEnvInterpolation(finding)) {
|
|
450
459
|
return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "env_variable_interpolation" };
|
|
451
460
|
}
|
|
452
|
-
if (!findingIsVerified(finding) && LOCKFILE_LAYOUT_RE.test(relPath)) {
|
|
461
|
+
if (!findingIsVerified(finding) && (LOCKFILE_LAYOUT_RE.test(relPath) || isLockfileOrModulesPath(relPath) || isLockfileLayoutArtifactPath(relPath))) {
|
|
453
462
|
return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "lockfile_or_layout_artifact" };
|
|
454
463
|
}
|
|
464
|
+
if (isCustomRegexFinding(finding) && !findingIsVerified(finding)) {
|
|
465
|
+
return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "customregex_unverified" };
|
|
466
|
+
}
|
|
455
467
|
return null;
|
|
456
468
|
}
|
|
469
|
+
function materializeNuclearSuppressedFinding(finding, drop) {
|
|
470
|
+
return {
|
|
471
|
+
...finding,
|
|
472
|
+
severity: downgradeSeverity(finding.severity, "LOW"),
|
|
473
|
+
confidence_score: drop.cap,
|
|
474
|
+
confidence_reasons: [drop.reason],
|
|
475
|
+
primary_log_eligible: false,
|
|
476
|
+
suppressed: true,
|
|
477
|
+
suppression_reason: drop.reason,
|
|
478
|
+
attack_path_concrete: false
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
function relPathForFinding(finding) {
|
|
482
|
+
return finding.file_path.replace(/\\/g, "/");
|
|
483
|
+
}
|
|
484
|
+
function isNuclearSuppressedFinding(finding) {
|
|
485
|
+
if (finding.suppressed) return true;
|
|
486
|
+
const conf = finding.confidence_score;
|
|
487
|
+
if (conf != null && conf <= NUCLEAR_HARD_DROP_CONFIDENCE) return true;
|
|
488
|
+
return applyNuclearHardDrop(finding, relPathForFinding(finding)) != null;
|
|
489
|
+
}
|
|
457
490
|
function isCustomRegexFinding(finding) {
|
|
458
491
|
const detector = String(finding.detector_name ?? "").trim().toLowerCase();
|
|
459
492
|
if (detector === "customregex") return true;
|
|
@@ -979,16 +1012,7 @@ function enrichAuditFinding(repoRoot, finding, opts) {
|
|
|
979
1012
|
const relPath = finding.file_path.replace(/\\/g, "/");
|
|
980
1013
|
const nuclearDrop = applyNuclearHardDrop(finding, relPath);
|
|
981
1014
|
if (nuclearDrop) {
|
|
982
|
-
return
|
|
983
|
-
...finding,
|
|
984
|
-
severity: downgradeSeverity(finding.severity, "LOW"),
|
|
985
|
-
confidence_score: nuclearDrop.cap,
|
|
986
|
-
confidence_reasons: [nuclearDrop.reason],
|
|
987
|
-
primary_log_eligible: false,
|
|
988
|
-
suppressed: true,
|
|
989
|
-
suppression_reason: nuclearDrop.reason,
|
|
990
|
-
attack_path_concrete: false
|
|
991
|
-
};
|
|
1015
|
+
return materializeNuclearSuppressedFinding(finding, nuclearDrop);
|
|
992
1016
|
}
|
|
993
1017
|
const phase1 = phase1ContextResearch(repoRoot, relPath);
|
|
994
1018
|
let [conf, reasons] = baseConfidenceForFinding(finding, phase1, relPath, finding.category, repoRoot);
|
|
@@ -1126,7 +1150,7 @@ function deduplicateSameLineFindings(findings) {
|
|
|
1126
1150
|
}
|
|
1127
1151
|
function buildVerdict(primaryFindings) {
|
|
1128
1152
|
const blocking = primaryFindings.filter(
|
|
1129
|
-
(f) => isBlockingSeverity(f.severity) && (f.confidence_score ?? 0) >= PRIMARY_LOG_THRESHOLD
|
|
1153
|
+
(f) => !f.suppressed && !isNuclearSuppressedFinding(f) && isBlockingSeverity(f.severity) && (f.confidence_score ?? 0) >= PRIMARY_LOG_THRESHOLD
|
|
1130
1154
|
);
|
|
1131
1155
|
const isSafe = blocking.length === 0;
|
|
1132
1156
|
const status = isSafe ? "PASS" : "FAIL";
|
|
@@ -1140,17 +1164,38 @@ function buildVerdict(primaryFindings) {
|
|
|
1140
1164
|
};
|
|
1141
1165
|
}
|
|
1142
1166
|
function applyCognitivePipeline(workspaceRoot, findings) {
|
|
1143
|
-
const
|
|
1167
|
+
const nuclearSuppressed = [];
|
|
1168
|
+
const active = [];
|
|
1169
|
+
for (const finding of findings) {
|
|
1170
|
+
const relPath = relPathForFinding(finding);
|
|
1171
|
+
const drop = applyNuclearHardDrop(finding, relPath);
|
|
1172
|
+
if (drop) {
|
|
1173
|
+
nuclearSuppressed.push(materializeNuclearSuppressedFinding(finding, drop));
|
|
1174
|
+
} else {
|
|
1175
|
+
active.push(finding);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
const enriched = active.map((f) => enrichAuditFinding(workspaceRoot, f));
|
|
1144
1179
|
const { kept, duplicates } = deduplicateSameLineFindings(enriched);
|
|
1145
|
-
const primary = kept.filter(
|
|
1146
|
-
|
|
1180
|
+
const primary = kept.filter(
|
|
1181
|
+
(f) => f.primary_log_eligible && !f.suppressed && !isNuclearSuppressedFinding(f)
|
|
1182
|
+
);
|
|
1183
|
+
const suppressed = nuclearSuppressed.concat(
|
|
1184
|
+
kept.filter(
|
|
1185
|
+
(f) => f.suppressed || isNuclearSuppressedFinding(f) || !f.primary_log_eligible
|
|
1186
|
+
)
|
|
1187
|
+
).concat(duplicates);
|
|
1188
|
+
const allScored = [...primary, ...suppressed];
|
|
1189
|
+
console.error(
|
|
1190
|
+
`[runsec] cognitive: raw=${findings.length} nuclear=${nuclearSuppressed.length} primary=${primary.length} suppressed=${suppressed.length}`
|
|
1191
|
+
);
|
|
1147
1192
|
return {
|
|
1148
1193
|
primary,
|
|
1149
1194
|
suppressed,
|
|
1150
1195
|
summary: {
|
|
1151
1196
|
version: "v1.0",
|
|
1152
1197
|
primary_log_threshold: PRIMARY_LOG_THRESHOLD,
|
|
1153
|
-
findings_total:
|
|
1198
|
+
findings_total: allScored.length,
|
|
1154
1199
|
findings_primary: primary.length,
|
|
1155
1200
|
findings_suppressed: suppressed.length,
|
|
1156
1201
|
false_positive_filtering: true
|
|
@@ -1774,8 +1819,9 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
|
|
|
1774
1819
|
const description = `TruffleHog: exposed ${detector}${verified ? " (verified)" : ""}`;
|
|
1775
1820
|
if (!isTrufflehogVerified(verified, description)) {
|
|
1776
1821
|
if (isLockfileOrModulesPath(rel)) continue;
|
|
1777
|
-
const blob = `${display} ${rawSecret}`;
|
|
1822
|
+
const blob = `${display} ${rawSecret} ${description}`;
|
|
1778
1823
|
if (hasEnvironmentInterpolation(blob)) continue;
|
|
1824
|
+
if (detector.toLowerCase() === "customregex") continue;
|
|
1779
1825
|
}
|
|
1780
1826
|
const severity = severityForSecret(detector, verified);
|
|
1781
1827
|
findings.push({
|
|
@@ -3047,7 +3093,7 @@ async function runUnifiedScanPipeline(opts) {
|
|
|
3047
3093
|
`[runsec] unified scan: engines=${concurrent_duration_ms}ms merge+cognitive=${merge_duration_ms}ms | raw=${allRaw.length} primary=${findings.length} suppressed_cognitive=${findings_suppressed.length}`
|
|
3048
3094
|
);
|
|
3049
3095
|
console.error(
|
|
3050
|
-
`[runsec] X-RunSec-Verdict: ${cognitiveResult.verdict.http_headers["X-RunSec-Verdict"]}
|
|
3096
|
+
`[runsec] @runsec/mcp cognitive complete | X-RunSec-Verdict: ${cognitiveResult.verdict.http_headers["X-RunSec-Verdict"]} | primary=${findings.length} suppressed=${findings_suppressed.length} blocking=${cognitiveResult.verdict.blocking_findings_count}`
|
|
3051
3097
|
);
|
|
3052
3098
|
return {
|
|
3053
3099
|
standard,
|
|
@@ -3607,6 +3653,7 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
|
|
|
3607
3653
|
const out = [];
|
|
3608
3654
|
out.push(`# RunSec Unified Security Report`);
|
|
3609
3655
|
out.push("");
|
|
3656
|
+
out.push(`**Generated at:** ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
3610
3657
|
out.push(`**Standard:** ${safeText(standard)}`);
|
|
3611
3658
|
out.push(`**X-RunSec-Verdict:** \`${safeText(verdictLabel)}\`${metrics.verdict?.is_safe === false && metrics.verdict.fail_reason ? ` \u2014 ${safeText(metrics.verdict.fail_reason)}` : ""}`);
|
|
3612
3659
|
out.push(
|
|
@@ -5776,7 +5823,10 @@ function isRemediationTool(name) {
|
|
|
5776
5823
|
}
|
|
5777
5824
|
|
|
5778
5825
|
// src/hubUploadUrl.ts
|
|
5826
|
+
var import_node_fs17 = __toESM(require("fs"));
|
|
5779
5827
|
var HUB_UPLOAD_REPORT_PATH = "/api/mcp/upload-report";
|
|
5828
|
+
var DEFAULT_PRODUCTION_HUB_ORIGIN = "https://runsec.io";
|
|
5829
|
+
var DEFAULT_DOCKER_INTERNAL_HUB_ORIGIN = "http://frontend:3000";
|
|
5780
5830
|
var LEGACY_TELEMETRY_INGEST_PATH = "/api/v1/telemetry/ingest";
|
|
5781
5831
|
function stripTrailingSlash(url) {
|
|
5782
5832
|
return url.replace(/\/$/, "");
|
|
@@ -5788,6 +5838,79 @@ function migrateLegacyIngestUrl(url) {
|
|
|
5788
5838
|
);
|
|
5789
5839
|
return url.replace(LEGACY_TELEMETRY_INGEST_PATH, HUB_UPLOAD_REPORT_PATH);
|
|
5790
5840
|
}
|
|
5841
|
+
function isProductionRuntime() {
|
|
5842
|
+
const nodeEnv = String(process.env.NODE_ENV ?? "").toLowerCase();
|
|
5843
|
+
const environment = String(process.env.ENVIRONMENT ?? "").toLowerCase();
|
|
5844
|
+
return nodeEnv === "production" || environment === "production";
|
|
5845
|
+
}
|
|
5846
|
+
function firstAllowedOrigin(raw) {
|
|
5847
|
+
if (!raw?.trim()) return null;
|
|
5848
|
+
const first = raw.split(",")[0]?.trim();
|
|
5849
|
+
return first || null;
|
|
5850
|
+
}
|
|
5851
|
+
function vercelDeploymentUrl() {
|
|
5852
|
+
const host = process.env.VERCEL_URL?.trim();
|
|
5853
|
+
if (!host) return null;
|
|
5854
|
+
return host.startsWith("http") ? stripTrailingSlash(host) : `https://${host}`;
|
|
5855
|
+
}
|
|
5856
|
+
function colocatedHubOrigin() {
|
|
5857
|
+
const port = process.env.PORT?.trim() || process.env.WEB_PORT?.trim();
|
|
5858
|
+
if (!port || !/^\d+$/.test(port)) return null;
|
|
5859
|
+
return `http://127.0.0.1:${port}`;
|
|
5860
|
+
}
|
|
5861
|
+
function isDockerRuntime() {
|
|
5862
|
+
try {
|
|
5863
|
+
return import_node_fs17.default.existsSync("/.dockerenv");
|
|
5864
|
+
} catch {
|
|
5865
|
+
return false;
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
function resolveDockerInternalHubOrigin() {
|
|
5869
|
+
return stripTrailingSlash(
|
|
5870
|
+
process.env.RUNSEC_HUB_INTERNAL_URL?.trim() || process.env.FRONTEND_INTERNAL_URL?.trim() || DEFAULT_DOCKER_INTERNAL_HUB_ORIGIN
|
|
5871
|
+
);
|
|
5872
|
+
}
|
|
5873
|
+
function resolveHubBaseUrl() {
|
|
5874
|
+
const candidates = [
|
|
5875
|
+
process.env.RUNSEC_HUB_URL,
|
|
5876
|
+
process.env.RUNSEC_API_URL,
|
|
5877
|
+
process.env.RUNSEC_HUB_ORIGIN,
|
|
5878
|
+
process.env.RUNSEC_HUB_INTERNAL_URL,
|
|
5879
|
+
process.env.NEXT_PUBLIC_APP_URL,
|
|
5880
|
+
process.env.PUBLIC_APP_URL,
|
|
5881
|
+
process.env.NEXTAUTH_URL,
|
|
5882
|
+
process.env.AUTH_URL,
|
|
5883
|
+
firstAllowedOrigin(process.env.ALLOWED_ORIGINS),
|
|
5884
|
+
vercelDeploymentUrl()
|
|
5885
|
+
].map((v) => v?.trim()).filter(Boolean);
|
|
5886
|
+
if (candidates.length > 0) {
|
|
5887
|
+
return stripTrailingSlash(migrateLegacyIngestUrl(candidates[0]));
|
|
5888
|
+
}
|
|
5889
|
+
if (isDockerRuntime()) {
|
|
5890
|
+
const internal = resolveDockerInternalHubOrigin();
|
|
5891
|
+
console.error(
|
|
5892
|
+
`[runsec] Docker runtime \u2014 Hub upload via internal origin ${internal} (set RUNSEC_HUB_URL to override)`
|
|
5893
|
+
);
|
|
5894
|
+
return internal;
|
|
5895
|
+
}
|
|
5896
|
+
if (isProductionRuntime()) {
|
|
5897
|
+
const colocated = colocatedHubOrigin();
|
|
5898
|
+
if (colocated) {
|
|
5899
|
+
console.error(
|
|
5900
|
+
`[runsec] RUNSEC_HUB_URL unset \u2014 using colocated Hub origin ${colocated} (override with RUNSEC_HUB_URL if needed)`
|
|
5901
|
+
);
|
|
5902
|
+
return colocated;
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
return DEFAULT_PRODUCTION_HUB_ORIGIN;
|
|
5906
|
+
}
|
|
5907
|
+
function appendUploadPath(base) {
|
|
5908
|
+
const migrated = migrateLegacyIngestUrl(base);
|
|
5909
|
+
if (migrated.includes(HUB_UPLOAD_REPORT_PATH)) {
|
|
5910
|
+
return stripTrailingSlash(migrated);
|
|
5911
|
+
}
|
|
5912
|
+
return `${stripTrailingSlash(migrated)}${HUB_UPLOAD_REPORT_PATH}`;
|
|
5913
|
+
}
|
|
5791
5914
|
function resolveHubUploadUrl() {
|
|
5792
5915
|
const explicit = [
|
|
5793
5916
|
process.env.RUNSEC_HUB_INGEST_URL,
|
|
@@ -5795,23 +5918,33 @@ function resolveHubUploadUrl() {
|
|
|
5795
5918
|
process.env.RUNSEC_TELEMETRY_URL
|
|
5796
5919
|
].map((v) => v?.trim()).find(Boolean);
|
|
5797
5920
|
if (explicit) {
|
|
5798
|
-
|
|
5799
|
-
if (migrated.includes(HUB_UPLOAD_REPORT_PATH)) {
|
|
5800
|
-
return stripTrailingSlash(migrated);
|
|
5801
|
-
}
|
|
5802
|
-
if (/^https?:\/\//i.test(migrated)) {
|
|
5803
|
-
return `${stripTrailingSlash(migrated)}${HUB_UPLOAD_REPORT_PATH}`;
|
|
5804
|
-
}
|
|
5805
|
-
return stripTrailingSlash(migrated);
|
|
5921
|
+
return appendUploadPath(explicit);
|
|
5806
5922
|
}
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
return
|
|
5923
|
+
return appendUploadPath(resolveHubBaseUrl());
|
|
5924
|
+
}
|
|
5925
|
+
function localhostAlternateUploadUrl(url) {
|
|
5926
|
+
try {
|
|
5927
|
+
const parsed = new URL(url);
|
|
5928
|
+
if (parsed.hostname !== "localhost") return null;
|
|
5929
|
+
parsed.hostname = "127.0.0.1";
|
|
5930
|
+
return parsed.toString();
|
|
5931
|
+
} catch {
|
|
5932
|
+
return null;
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
function dockerInternalAlternateUploadUrl(url) {
|
|
5936
|
+
if (!isDockerRuntime()) return null;
|
|
5937
|
+
try {
|
|
5938
|
+
const parsed = new URL(url);
|
|
5939
|
+
const internal = new URL(resolveDockerInternalHubOrigin());
|
|
5940
|
+
if (parsed.origin === internal.origin) return null;
|
|
5941
|
+
internal.pathname = HUB_UPLOAD_REPORT_PATH;
|
|
5942
|
+
internal.search = "";
|
|
5943
|
+
internal.hash = "";
|
|
5944
|
+
return internal.toString();
|
|
5945
|
+
} catch {
|
|
5946
|
+
return appendUploadPath(resolveDockerInternalHubOrigin());
|
|
5813
5947
|
}
|
|
5814
|
-
return `${migratedBase}${HUB_UPLOAD_REPORT_PATH}`;
|
|
5815
5948
|
}
|
|
5816
5949
|
|
|
5817
5950
|
// src/complianceScores.ts
|
|
@@ -5866,16 +5999,70 @@ function hubAuthHeaders(apiKey) {
|
|
|
5866
5999
|
Accept: "application/json"
|
|
5867
6000
|
};
|
|
5868
6001
|
}
|
|
5869
|
-
function
|
|
6002
|
+
function networkErrorCode(error) {
|
|
6003
|
+
const err = error instanceof Error ? error : null;
|
|
6004
|
+
if (err && "code" in err && typeof err.code === "string") {
|
|
6005
|
+
return String(err.code);
|
|
6006
|
+
}
|
|
6007
|
+
const cause = err?.cause;
|
|
6008
|
+
if (cause && typeof cause === "object" && "code" in cause) {
|
|
6009
|
+
return String(cause.code);
|
|
6010
|
+
}
|
|
6011
|
+
return "";
|
|
6012
|
+
}
|
|
6013
|
+
function dumpHubNetworkError(error, targetUrl) {
|
|
6014
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
6015
|
+
const code = networkErrorCode(error) || "n/a";
|
|
6016
|
+
console.error("[runsec-network-error]", code, err.message, "Target URL:", targetUrl);
|
|
6017
|
+
if (err.cause != null) {
|
|
6018
|
+
console.error("[runsec-network-error] cause:", err.cause);
|
|
6019
|
+
}
|
|
6020
|
+
if (err.stack) {
|
|
6021
|
+
console.error("[runsec-network-error] stack:", err.stack);
|
|
6022
|
+
}
|
|
6023
|
+
try {
|
|
6024
|
+
const extras = {};
|
|
6025
|
+
for (const key of Object.getOwnPropertyNames(err)) {
|
|
6026
|
+
if (key === "message" || key === "stack") continue;
|
|
6027
|
+
extras[key] = err[key];
|
|
6028
|
+
}
|
|
6029
|
+
if (Object.keys(extras).length > 0) {
|
|
6030
|
+
console.error("[runsec-network-error] details:", extras);
|
|
6031
|
+
}
|
|
6032
|
+
} catch {
|
|
6033
|
+
console.error("[runsec-network-error] raw:", error);
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
function formatHubSyncErrorMessage(error, targetUrl, response, responseBody) {
|
|
5870
6037
|
const status = response?.status != null ? String(response.status) : error?.response?.status != null ? String(error.response.status) : "n/a";
|
|
5871
6038
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
5872
6039
|
const cause = err.cause instanceof Error ? err.cause.message : err.cause != null ? String(err.cause) : "";
|
|
6040
|
+
const code = networkErrorCode(error);
|
|
5873
6041
|
let message = `Sync failed: ${err.message}. Target URL: ${targetUrl}. Status: ${status}`;
|
|
5874
|
-
if (
|
|
5875
|
-
|
|
6042
|
+
if (code) message += `. Code: ${code}`;
|
|
6043
|
+
if (cause && !message.includes(cause)) message += `. Cause: ${cause}`;
|
|
6044
|
+
if (responseBody?.trim()) {
|
|
6045
|
+
message += `. Body: ${responseBody.trim().slice(0, 300)}`;
|
|
6046
|
+
}
|
|
6047
|
+
if (status === "n/a" && /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|CERT|SSL|TLS/i.test(err.message + code)) {
|
|
6048
|
+
message += ". Hint: set RUNSEC_HUB_URL to your production Hub origin (same value as NEXT_PUBLIC_APP_URL on the Hub server).";
|
|
5876
6049
|
}
|
|
5877
6050
|
return message;
|
|
5878
6051
|
}
|
|
6052
|
+
function logHubSyncFailure(message, error, targetUrl) {
|
|
6053
|
+
if (error != null && targetUrl) {
|
|
6054
|
+
dumpHubNetworkError(error, targetUrl);
|
|
6055
|
+
}
|
|
6056
|
+
console.error(`[runsec] Hub telemetry upload error (scan saved locally): ${message}`);
|
|
6057
|
+
}
|
|
6058
|
+
async function postHubUpload(url, apiKey, payload) {
|
|
6059
|
+
return await fetch(url, {
|
|
6060
|
+
method: "POST",
|
|
6061
|
+
headers: hubAuthHeaders(apiKey),
|
|
6062
|
+
body: JSON.stringify(payload),
|
|
6063
|
+
signal: AbortSignal.timeout(HUB_UPLOAD_TIMEOUT_MS)
|
|
6064
|
+
});
|
|
6065
|
+
}
|
|
5879
6066
|
function countSeverityMetrics(findings) {
|
|
5880
6067
|
const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
|
|
5881
6068
|
for (const f of findings) {
|
|
@@ -5933,13 +6120,38 @@ async function uploadScanResultsToHub(payload, apiKey) {
|
|
|
5933
6120
|
}
|
|
5934
6121
|
const url = resolveHubUploadUrl();
|
|
5935
6122
|
console.error(`[runsec] Hub telemetry upload \u2192 ${url}`);
|
|
6123
|
+
const attemptUpload = async (targetUrl) => {
|
|
6124
|
+
const errors = [];
|
|
6125
|
+
const urls = [targetUrl];
|
|
6126
|
+
const localhostAlt = localhostAlternateUploadUrl(targetUrl);
|
|
6127
|
+
if (localhostAlt && localhostAlt !== targetUrl) urls.push(localhostAlt);
|
|
6128
|
+
const dockerAlt = dockerInternalAlternateUploadUrl(targetUrl);
|
|
6129
|
+
if (dockerAlt && !urls.includes(dockerAlt)) urls.push(dockerAlt);
|
|
6130
|
+
let lastError = new Error("No upload attempts made");
|
|
6131
|
+
for (let i = 0; i < urls.length; i++) {
|
|
6132
|
+
const tryUrl = urls[i];
|
|
6133
|
+
if (i > 0) {
|
|
6134
|
+
console.error(`[runsec] Hub telemetry retry \u2192 ${tryUrl}`);
|
|
6135
|
+
}
|
|
6136
|
+
try {
|
|
6137
|
+
return await postHubUpload(tryUrl, trimmedKey, payload);
|
|
6138
|
+
} catch (err) {
|
|
6139
|
+
lastError = err;
|
|
6140
|
+
errors.push(err);
|
|
6141
|
+
dumpHubNetworkError(err, tryUrl);
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
throw lastError;
|
|
6145
|
+
};
|
|
5936
6146
|
try {
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
6147
|
+
let response;
|
|
6148
|
+
try {
|
|
6149
|
+
response = await attemptUpload(url);
|
|
6150
|
+
} catch (networkError) {
|
|
6151
|
+
const message = formatHubSyncErrorMessage(networkError, url);
|
|
6152
|
+
logHubSyncFailure(message, networkError, url);
|
|
6153
|
+
return { success: false, message };
|
|
6154
|
+
}
|
|
5943
6155
|
if (!response.ok) {
|
|
5944
6156
|
const detail = await response.text().catch(() => "");
|
|
5945
6157
|
const detailSnippet = detail.slice(0, 500).trim();
|
|
@@ -5948,9 +6160,10 @@ async function uploadScanResultsToHub(payload, apiKey) {
|
|
|
5948
6160
|
const message = formatHubSyncErrorMessage(
|
|
5949
6161
|
new Error(httpMessage),
|
|
5950
6162
|
url,
|
|
5951
|
-
response
|
|
6163
|
+
response,
|
|
6164
|
+
detailSnippet
|
|
5952
6165
|
);
|
|
5953
|
-
|
|
6166
|
+
logHubSyncFailure(message, new Error(httpMessage), url);
|
|
5954
6167
|
return {
|
|
5955
6168
|
success: false,
|
|
5956
6169
|
message
|
|
@@ -5970,7 +6183,7 @@ async function uploadScanResultsToHub(payload, apiKey) {
|
|
|
5970
6183
|
};
|
|
5971
6184
|
} catch (error) {
|
|
5972
6185
|
const message = formatHubSyncErrorMessage(error, url);
|
|
5973
|
-
|
|
6186
|
+
logHubSyncFailure(message, error, url);
|
|
5974
6187
|
return { success: false, message };
|
|
5975
6188
|
}
|
|
5976
6189
|
}
|
|
@@ -6303,6 +6516,7 @@ CRITICAL INSTRUCTIONS FOR LLM:
|
|
|
6303
6516
|
async function main() {
|
|
6304
6517
|
const key = getApiKey();
|
|
6305
6518
|
process.env.RUNSEC_API_KEY = key;
|
|
6519
|
+
console.error(`[runsec] MCP @runsec/mcp starting \u2014 Hub upload target: ${resolveHubUploadUrl()}`);
|
|
6306
6520
|
await verifyApiKey(key);
|
|
6307
6521
|
const summary = validateRules();
|
|
6308
6522
|
console.error("Rules registry validated:", summary);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runsec/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.79",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"test": "vitest run",
|
|
31
31
|
"test:gold": "tsx scripts/qaValidationReport.ts",
|
|
32
32
|
"test:gold:parse": "tsx scripts/qaValidationReport.ts --parse-only",
|
|
33
|
-
"simulate:output": "tsx scripts/simulate_output.ts"
|
|
33
|
+
"simulate:output": "tsx scripts/simulate_output.ts",
|
|
34
|
+
"debug:pipeline": "tsx scripts/debug-pipeline.ts",
|
|
35
|
+
"test:e2e": "npm run build && tsx scripts/debug-pipeline.ts"
|
|
34
36
|
},
|
|
35
37
|
"keywords": [
|
|
36
38
|
"mcp",
|