@runsec/mcp 1.0.76 → 1.0.78

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.
Files changed (2) hide show
  1. package/dist/index.js +267 -41
  2. package/package.json +1 -1
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 enriched = findings.map((f) => enrichAuditFinding(workspaceRoot, f));
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((f) => f.primary_log_eligible && !f.suppressed);
1146
- const suppressed = kept.filter((f) => !f.primary_log_eligible || f.suppressed).concat(duplicates);
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: enriched.length,
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({
@@ -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(
@@ -5775,6 +5822,95 @@ function isRemediationTool(name) {
5775
5822
  return REMEDIATION_TOOL_NAMES.includes(name);
5776
5823
  }
5777
5824
 
5825
+ // src/hubUploadUrl.ts
5826
+ var HUB_UPLOAD_REPORT_PATH = "/api/mcp/upload-report";
5827
+ var DEFAULT_PRODUCTION_HUB_ORIGIN = "https://runsec.io";
5828
+ var LEGACY_TELEMETRY_INGEST_PATH = "/api/v1/telemetry/ingest";
5829
+ function stripTrailingSlash(url) {
5830
+ return url.replace(/\/$/, "");
5831
+ }
5832
+ function migrateLegacyIngestUrl(url) {
5833
+ if (!url.includes(LEGACY_TELEMETRY_INGEST_PATH)) return url;
5834
+ console.warn(
5835
+ `[runsec] Legacy telemetry ingest URL detected (${LEGACY_TELEMETRY_INGEST_PATH}); using ${HUB_UPLOAD_REPORT_PATH} instead`
5836
+ );
5837
+ return url.replace(LEGACY_TELEMETRY_INGEST_PATH, HUB_UPLOAD_REPORT_PATH);
5838
+ }
5839
+ function isProductionRuntime() {
5840
+ const nodeEnv = String(process.env.NODE_ENV ?? "").toLowerCase();
5841
+ const environment = String(process.env.ENVIRONMENT ?? "").toLowerCase();
5842
+ return nodeEnv === "production" || environment === "production";
5843
+ }
5844
+ function firstAllowedOrigin(raw) {
5845
+ if (!raw?.trim()) return null;
5846
+ const first = raw.split(",")[0]?.trim();
5847
+ return first || null;
5848
+ }
5849
+ function vercelDeploymentUrl() {
5850
+ const host = process.env.VERCEL_URL?.trim();
5851
+ if (!host) return null;
5852
+ return host.startsWith("http") ? stripTrailingSlash(host) : `https://${host}`;
5853
+ }
5854
+ function colocatedHubOrigin() {
5855
+ const port = process.env.PORT?.trim() || process.env.WEB_PORT?.trim();
5856
+ if (!port || !/^\d+$/.test(port)) return null;
5857
+ return `http://127.0.0.1:${port}`;
5858
+ }
5859
+ function resolveHubBaseUrl() {
5860
+ const candidates = [
5861
+ process.env.RUNSEC_HUB_URL,
5862
+ process.env.RUNSEC_API_URL,
5863
+ process.env.RUNSEC_HUB_ORIGIN,
5864
+ process.env.NEXT_PUBLIC_APP_URL,
5865
+ process.env.PUBLIC_APP_URL,
5866
+ process.env.NEXTAUTH_URL,
5867
+ process.env.AUTH_URL,
5868
+ firstAllowedOrigin(process.env.ALLOWED_ORIGINS),
5869
+ vercelDeploymentUrl()
5870
+ ].map((v) => v?.trim()).filter(Boolean);
5871
+ if (candidates.length > 0) {
5872
+ return stripTrailingSlash(migrateLegacyIngestUrl(candidates[0]));
5873
+ }
5874
+ if (isProductionRuntime()) {
5875
+ const colocated = colocatedHubOrigin();
5876
+ if (colocated) {
5877
+ console.error(
5878
+ `[runsec] RUNSEC_HUB_URL unset \u2014 using colocated Hub origin ${colocated} (override with RUNSEC_HUB_URL if needed)`
5879
+ );
5880
+ return colocated;
5881
+ }
5882
+ }
5883
+ return DEFAULT_PRODUCTION_HUB_ORIGIN;
5884
+ }
5885
+ function appendUploadPath(base) {
5886
+ const migrated = migrateLegacyIngestUrl(base);
5887
+ if (migrated.includes(HUB_UPLOAD_REPORT_PATH)) {
5888
+ return stripTrailingSlash(migrated);
5889
+ }
5890
+ return `${stripTrailingSlash(migrated)}${HUB_UPLOAD_REPORT_PATH}`;
5891
+ }
5892
+ function resolveHubUploadUrl() {
5893
+ const explicit = [
5894
+ process.env.RUNSEC_HUB_INGEST_URL,
5895
+ process.env.RUNSEC_HUB_UPLOAD_URL,
5896
+ process.env.RUNSEC_TELEMETRY_URL
5897
+ ].map((v) => v?.trim()).find(Boolean);
5898
+ if (explicit) {
5899
+ return appendUploadPath(explicit);
5900
+ }
5901
+ return appendUploadPath(resolveHubBaseUrl());
5902
+ }
5903
+ function localhostAlternateUploadUrl(url) {
5904
+ try {
5905
+ const parsed = new URL(url);
5906
+ if (parsed.hostname !== "localhost") return null;
5907
+ parsed.hostname = "127.0.0.1";
5908
+ return parsed.toString();
5909
+ } catch {
5910
+ return null;
5911
+ }
5912
+ }
5913
+
5778
5914
  // src/complianceScores.ts
5779
5915
  var SEVERITY_PENALTY = {
5780
5916
  CRITICAL: 15,
@@ -5817,6 +5953,80 @@ function buildHubComplianceBlock(result) {
5817
5953
  }
5818
5954
 
5819
5955
  // src/telemetryClient.ts
5956
+ var HUB_UPLOAD_TIMEOUT_MS = 12e4;
5957
+ function hubAuthHeaders(apiKey) {
5958
+ return {
5959
+ Authorization: `Bearer ${apiKey}`,
5960
+ // Cloudflare/tunnel may strip Authorization on POST; Hub accepts this duplicate.
5961
+ "X-RunSec-Api-Key": apiKey,
5962
+ "Content-Type": "application/json",
5963
+ Accept: "application/json"
5964
+ };
5965
+ }
5966
+ function networkErrorCode(error) {
5967
+ const err = error instanceof Error ? error : null;
5968
+ if (err && "code" in err && typeof err.code === "string") {
5969
+ return String(err.code);
5970
+ }
5971
+ const cause = err?.cause;
5972
+ if (cause && typeof cause === "object" && "code" in cause) {
5973
+ return String(cause.code);
5974
+ }
5975
+ return "";
5976
+ }
5977
+ function dumpHubNetworkError(error, targetUrl) {
5978
+ const err = error instanceof Error ? error : new Error(String(error));
5979
+ const code = networkErrorCode(error) || "n/a";
5980
+ console.error("[runsec-network-error]", code, err.message, "Target URL:", targetUrl);
5981
+ if (err.cause != null) {
5982
+ console.error("[runsec-network-error] cause:", err.cause);
5983
+ }
5984
+ if (err.stack) {
5985
+ console.error("[runsec-network-error] stack:", err.stack);
5986
+ }
5987
+ try {
5988
+ const extras = {};
5989
+ for (const key of Object.getOwnPropertyNames(err)) {
5990
+ if (key === "message" || key === "stack") continue;
5991
+ extras[key] = err[key];
5992
+ }
5993
+ if (Object.keys(extras).length > 0) {
5994
+ console.error("[runsec-network-error] details:", extras);
5995
+ }
5996
+ } catch {
5997
+ console.error("[runsec-network-error] raw:", error);
5998
+ }
5999
+ }
6000
+ function formatHubSyncErrorMessage(error, targetUrl, response, responseBody) {
6001
+ const status = response?.status != null ? String(response.status) : error?.response?.status != null ? String(error.response.status) : "n/a";
6002
+ const err = error instanceof Error ? error : new Error(String(error));
6003
+ const cause = err.cause instanceof Error ? err.cause.message : err.cause != null ? String(err.cause) : "";
6004
+ const code = networkErrorCode(error);
6005
+ let message = `Sync failed: ${err.message}. Target URL: ${targetUrl}. Status: ${status}`;
6006
+ if (code) message += `. Code: ${code}`;
6007
+ if (cause && !message.includes(cause)) message += `. Cause: ${cause}`;
6008
+ if (responseBody?.trim()) {
6009
+ message += `. Body: ${responseBody.trim().slice(0, 300)}`;
6010
+ }
6011
+ if (status === "n/a" && /fetch failed|ECONNREFUSED|ENOTFOUND|ETIMEDOUT|CERT|SSL|TLS/i.test(err.message + code)) {
6012
+ message += ". Hint: set RUNSEC_HUB_URL to your production Hub origin (same value as NEXT_PUBLIC_APP_URL on the Hub server).";
6013
+ }
6014
+ return message;
6015
+ }
6016
+ function logHubSyncFailure(message, error, targetUrl) {
6017
+ if (error != null && targetUrl) {
6018
+ dumpHubNetworkError(error, targetUrl);
6019
+ }
6020
+ console.error(`[runsec] Hub telemetry upload error (scan saved locally): ${message}`);
6021
+ }
6022
+ async function postHubUpload(url, apiKey, payload) {
6023
+ return await fetch(url, {
6024
+ method: "POST",
6025
+ headers: hubAuthHeaders(apiKey),
6026
+ body: JSON.stringify(payload),
6027
+ signal: AbortSignal.timeout(HUB_UPLOAD_TIMEOUT_MS)
6028
+ });
6029
+ }
5820
6030
  function countSeverityMetrics(findings) {
5821
6031
  const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
5822
6032
  for (const f of findings) {
@@ -5872,30 +6082,42 @@ async function uploadScanResultsToHub(payload, apiKey) {
5872
6082
  console.warn("[runsec] Hub telemetry skipped: API key missing");
5873
6083
  return { success: false, message: "Sync failed: API key missing" };
5874
6084
  }
5875
- const baseUrl = (process.env.RUNSEC_API_URL || "https://runsec.io").replace(/\/$/, "");
5876
- const url = `${baseUrl}/api/mcp/upload-report`;
6085
+ const url = resolveHubUploadUrl();
6086
+ console.error(`[runsec] Hub telemetry upload \u2192 ${url}`);
6087
+ const attemptUpload = async (targetUrl) => {
6088
+ try {
6089
+ return await postHubUpload(targetUrl, trimmedKey, payload);
6090
+ } catch (firstError) {
6091
+ const alt = localhostAlternateUploadUrl(targetUrl);
6092
+ if (!alt || alt === targetUrl) throw firstError;
6093
+ console.error(`[runsec] Hub telemetry retry \u2192 ${alt}`);
6094
+ return await postHubUpload(alt, trimmedKey, payload);
6095
+ }
6096
+ };
5877
6097
  try {
5878
- const response = await fetch(url, {
5879
- method: "POST",
5880
- headers: {
5881
- Authorization: `Bearer ${trimmedKey}`,
5882
- // Cloudflare/tunnel may strip Authorization on POST; Hub accepts this duplicate.
5883
- "X-RunSec-Api-Key": trimmedKey,
5884
- "Content-Type": "application/json",
5885
- Accept: "application/json"
5886
- },
5887
- body: JSON.stringify(payload)
5888
- });
6098
+ let response;
6099
+ try {
6100
+ response = await attemptUpload(url);
6101
+ } catch (networkError) {
6102
+ const message = formatHubSyncErrorMessage(networkError, url);
6103
+ logHubSyncFailure(message, networkError, url);
6104
+ return { success: false, message };
6105
+ }
5889
6106
  if (!response.ok) {
5890
6107
  const detail = await response.text().catch(() => "");
5891
6108
  const detailSnippet = detail.slice(0, 500).trim();
5892
6109
  const statusLabel = response.statusText || String(response.status);
5893
- console.warn(
5894
- `[runsec] Hub telemetry upload failed (${response.status}): ${detailSnippet || statusLabel}`
6110
+ const httpMessage = detailSnippet || statusLabel;
6111
+ const message = formatHubSyncErrorMessage(
6112
+ new Error(httpMessage),
6113
+ url,
6114
+ response,
6115
+ detailSnippet
5895
6116
  );
6117
+ logHubSyncFailure(message, new Error(httpMessage), url);
5896
6118
  return {
5897
6119
  success: false,
5898
- message: `Sync failed: ${detailSnippet || statusLabel}`
6120
+ message
5899
6121
  };
5900
6122
  }
5901
6123
  const body = await response.json().catch(() => ({}));
@@ -5911,9 +6133,9 @@ async function uploadScanResultsToHub(payload, apiKey) {
5911
6133
  projectId
5912
6134
  };
5913
6135
  } catch (error) {
5914
- const message = error instanceof Error ? error.message : String(error);
5915
- console.warn(`[runsec] Hub telemetry upload error (scan saved locally): ${message}`);
5916
- return { success: false, message: `Sync failed: ${message}` };
6136
+ const message = formatHubSyncErrorMessage(error, url);
6137
+ logHubSyncFailure(message, error, url);
6138
+ return { success: false, message };
5917
6139
  }
5918
6140
  }
5919
6141
 
@@ -5955,10 +6177,13 @@ function appendCloudSyncToDirective(directive, syncResult) {
5955
6177
  ${lines}`;
5956
6178
  }
5957
6179
  async function verifyApiKey(apiKey) {
5958
- const baseUrl = (process.env.RUNSEC_API_URL || "https://runsec.io").replace(/\/$/, "");
6180
+ const verifyUrl = resolveHubUploadUrl().replace(/\/upload-report\/?$/, "/verify-key");
5959
6181
  try {
5960
- const response = await fetch(`${baseUrl}/api/mcp/verify-key`, {
5961
- headers: { Authorization: `Bearer ${apiKey}` }
6182
+ const response = await fetch(verifyUrl, {
6183
+ headers: {
6184
+ Authorization: `Bearer ${apiKey}`,
6185
+ "X-RunSec-Api-Key": apiKey
6186
+ }
5962
6187
  });
5963
6188
  if (response.status === 401 || response.status === 403) {
5964
6189
  console.error("\u274C FATAL: Invalid RunSec API Key.");
@@ -6242,6 +6467,7 @@ CRITICAL INSTRUCTIONS FOR LLM:
6242
6467
  async function main() {
6243
6468
  const key = getApiKey();
6244
6469
  process.env.RUNSEC_API_KEY = key;
6470
+ console.error(`[runsec] MCP @runsec/mcp starting \u2014 Hub upload target: ${resolveHubUploadUrl()}`);
6245
6471
  await verifyApiKey(key);
6246
6472
  const summary = validateRules();
6247
6473
  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.76",
3
+ "version": "1.0.78",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist",