@runsec/mcp 1.0.75 → 1.0.77

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 +161 -65
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -413,8 +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 STRICT_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
- var ENV_INTERP_RE = STRICT_ENV_INTERP_RE;
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;
418
417
  var LOCKFILE_BASENAMES = /^(?:poetry\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|cargo\.lock|composer\.lock|gemfile\.lock)$/i;
419
418
  function hasEnvironmentInterpolation(text) {
420
419
  return ENV_INTERP_RE.test(text);
@@ -432,37 +431,54 @@ function isLockfileOrModulesPath(relPath) {
432
431
  function isTrufflehogVerified(verifiedFlag, description) {
433
432
  return verifiedFlag || /\(verified\)/i.test(description);
434
433
  }
435
- function isStaticLayoutDumpPath(relPath) {
436
- const normalized = relPath.replace(/\\/g, "/").toLowerCase();
437
- const base = normalized.split("/").pop() ?? normalized;
438
- if (base.endsWith(".storyboard") || base.endsWith(".xib") || base.endsWith(".docx")) return true;
439
- if (base.endsWith(".lock") || /\.lock$/i.test(base)) return true;
440
- return false;
441
- }
442
- function matchTextHasStrictEnvInterpolation(matchText, snippet) {
443
- const m = (matchText ?? "").trim();
444
- const s = (snippet ?? "").trim();
445
- if (m && STRICT_ENV_INTERP_RE.test(m)) return true;
446
- if (s && STRICT_ENV_INTERP_RE.test(s)) return true;
447
- return false;
434
+ var LOCKFILE_LAYOUT_RE = /(?:poetry\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|\.xib|\.storyboard|\.docx)/i;
435
+ function isLockfileLayoutArtifactPath(relPath) {
436
+ return LOCKFILE_LAYOUT_RE.test(relPath.replace(/\\/g, "/"));
448
437
  }
449
438
 
450
439
  // src/engine/cognitiveEngine.ts
451
- var ABSOLUTE_ENV_INTERP_CAP = 0.05;
452
- var ABSOLUTE_STATIC_DUMP_CAP = 0.01;
453
- function resolveAbsoluteSuppression(finding, relPath) {
454
- if (matchTextHasStrictEnvInterpolation(finding.match_text ?? "", finding.snippet ?? "")) {
455
- return { cap: ABSOLUTE_ENV_INTERP_CAP, reason: "environment_variable_interpolation" };
456
- }
457
- const verified = isTrufflehogVerified(
458
- /\(verified\)/i.test(finding.description) || /\(verified\)/i.test(finding.match_text ?? ""),
459
- finding.description
460
- );
461
- if (!verified && isStaticLayoutDumpPath(relPath)) {
462
- return { cap: ABSOLUTE_STATIC_DUMP_CAP, reason: "static_layout_or_lockfile_unverified" };
440
+ var NUCLEAR_HARD_DROP_CONFIDENCE = 0.01;
441
+ var CUSTOMREGEX_UNVERIFIED_CAP = 0.1;
442
+ function findingIsVerified(finding) {
443
+ if (finding.verified === true) return true;
444
+ return isTrufflehogVerified(false, finding.description) || /\(verified\)/i.test(finding.match_text ?? "");
445
+ }
446
+ function applyNuclearHardDrop(finding, relPath) {
447
+ const matchText = finding.match_text ?? "";
448
+ const snippet = finding.snippet ?? "";
449
+ if (ENV_INTERP_RE.test(matchText) || ENV_INTERP_RE.test(snippet)) {
450
+ return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "env_variable_interpolation" };
451
+ }
452
+ if (!findingIsVerified(finding) && LOCKFILE_LAYOUT_RE.test(relPath)) {
453
+ return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "lockfile_or_layout_artifact" };
463
454
  }
464
455
  return null;
465
456
  }
457
+ function isCustomRegexFinding(finding) {
458
+ const detector = String(finding.detector_name ?? "").trim().toLowerCase();
459
+ if (detector === "customregex") return true;
460
+ const title = String(finding.title ?? finding.description ?? "");
461
+ if (/customregex/i.test(title)) return true;
462
+ return /(?:^|\.)customregex$/i.test(finding.rule_id) || finding.rule_id.includes("trufflehog.customregex");
463
+ }
464
+ function looksLikeHighEntropyRawToken(finding) {
465
+ const blob = `${finding.match_text ?? ""} ${finding.snippet ?? ""}`.trim();
466
+ if (blob.length < 24 || ENV_INTERP_RE.test(blob)) return false;
467
+ if (/^(?:ghp_|gho_|github_pat_|glpat-|sk-[a-zA-Z0-9]{10,}|AKIA[0-9A-Z]{16}|xox[baprs]-|eyJ[A-Za-z0-9_-]{10,}\.)/i.test(
468
+ blob
469
+ )) {
470
+ return true;
471
+ }
472
+ const compact = blob.replace(/\s+/g, "");
473
+ if (compact.length >= 40 && /^[A-Za-z0-9+/=_-]+$/.test(compact)) return true;
474
+ return false;
475
+ }
476
+ function customRegexNeedsBaseClamp(finding) {
477
+ if (!isCustomRegexFinding(finding)) return false;
478
+ if (findingIsVerified(finding)) return false;
479
+ if (looksLikeHighEntropyRawToken(finding)) return false;
480
+ return true;
481
+ }
466
482
  var PRIMARY_LOG_THRESHOLD = 0.8;
467
483
  var CONFIDENCE_THRESHOLD = PRIMARY_LOG_THRESHOLD;
468
484
  var ELITE_LOW_CONFIDENCE_CAP = 0.28;
@@ -853,13 +869,13 @@ function attackPathConcrete(finding, confidence, eliteHard, allReasons, snippet)
853
869
  return confidence >= PRIMARY_LOG_THRESHOLD;
854
870
  }
855
871
  function baseConfidenceForFinding(finding, phase1, relPath, category, repoRoot) {
856
- const absolute = resolveAbsoluteSuppression(finding, relPath);
857
- if (absolute) {
858
- return [absolute.cap, [absolute.reason]];
872
+ const nuclear = applyNuclearHardDrop(finding, relPath);
873
+ if (nuclear) {
874
+ return [nuclear.cap, [nuclear.reason]];
859
875
  }
860
876
  const reasons = [];
861
877
  let score = category === "secrets" ? 0.9 : category === "dependencies" ? 0.78 : 0.82;
862
- const title = finding.description;
878
+ const title = finding.title ?? finding.description;
863
879
  const sev = (finding.severity || "").toUpperCase();
864
880
  if (category === "code") {
865
881
  if (sev === "CRITICAL" || sev === "ERROR") score = 0.92;
@@ -867,23 +883,27 @@ function baseConfidenceForFinding(finding, phase1, relPath, category, repoRoot)
867
883
  else score = 0.55;
868
884
  } else if (category === "secrets") {
869
885
  if (sev === "CRITICAL") score = 0.95;
870
- else if (finding.match_text.includes("(verified)")) score = 0.93;
886
+ else if (findingIsVerified(finding) || finding.match_text.includes("(verified)")) score = 0.93;
871
887
  if (/pii\s+email/i.test(title) || finding.rule_id.includes("pii-email")) {
872
- const piiVerified = /\(verified\)/i.test(finding.description);
888
+ const piiVerified = findingIsVerified(finding);
873
889
  score = Math.min(score, piiVerified ? 0.45 : 0.32);
874
890
  reasons.push("pii_email_deprioritized");
875
891
  }
876
892
  if (isUnverifiedTrufflehogSecret(finding)) {
877
893
  const secretBlob = `${finding.match_text} ${finding.snippet ?? ""}`;
878
894
  if (hasEnvironmentInterpolation(secretBlob)) {
879
- score = Math.min(score, ABSOLUTE_ENV_INTERP_CAP);
880
- reasons.push("environment_variable_interpolation");
895
+ score = Math.min(score, NUCLEAR_HARD_DROP_CONFIDENCE);
896
+ reasons.push("env_variable_interpolation");
881
897
  }
882
- if (isLockfileOrModulesPath(relPath)) {
883
- score = Math.min(score, 0.1);
884
- reasons.push("lockfile_or_modules_path");
898
+ if (isLockfileOrModulesPath(relPath) || isLockfileLayoutArtifactPath(relPath)) {
899
+ score = Math.min(score, NUCLEAR_HARD_DROP_CONFIDENCE);
900
+ reasons.push("lockfile_or_layout_artifact");
885
901
  }
886
902
  }
903
+ if (customRegexNeedsBaseClamp(finding)) {
904
+ score = Math.min(score, CUSTOMREGEX_UNVERIFIED_CAP);
905
+ reasons.push("customregex_unverified_clamped");
906
+ }
887
907
  }
888
908
  const libs = phase1.protection_libs_detected ?? [];
889
909
  if (libs.length && protectionMatchesMetric(title, libs)) {
@@ -957,7 +977,19 @@ function downgradeSeverity(severity, cap) {
957
977
  function enrichAuditFinding(repoRoot, finding, opts) {
958
978
  const applyFp = opts?.applyFalsePositiveFilter ?? !isCalibrationTestbedPath(finding.file_path);
959
979
  const relPath = finding.file_path.replace(/\\/g, "/");
960
- const absoluteSuppress = resolveAbsoluteSuppression(finding, relPath);
980
+ const nuclearDrop = applyNuclearHardDrop(finding, relPath);
981
+ 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
+ };
992
+ }
961
993
  const phase1 = phase1ContextResearch(repoRoot, relPath);
962
994
  let [conf, reasons] = baseConfidenceForFinding(finding, phase1, relPath, finding.category, repoRoot);
963
995
  const [boost, boostReason] = comparativeAnalysisBoost(repoRoot, relPath, applyFp);
@@ -982,18 +1014,18 @@ function enrichAuditFinding(repoRoot, finding, opts) {
982
1014
  }
983
1015
  }
984
1016
  }
985
- if (absoluteSuppress) {
986
- conf = Math.min(conf, absoluteSuppress.cap);
987
- if (!reasons.includes(absoluteSuppress.reason)) {
988
- reasons.push(absoluteSuppress.reason);
989
- }
1017
+ const postNuclear = applyNuclearHardDrop(finding, relPath);
1018
+ if (postNuclear) {
1019
+ conf = Math.min(conf, postNuclear.cap);
1020
+ if (!reasons.includes(postNuclear.reason)) reasons.push(postNuclear.reason);
990
1021
  } else {
991
1022
  conf = Math.max(0.05, Math.min(1, conf));
992
1023
  }
993
1024
  const snippetL = snippetLower(finding);
994
1025
  const attackConcrete = attackPathConcrete(finding, conf, eliteHard, reasons, snippetL);
995
1026
  const critique = selfCritique(finding, conf, phase1);
996
- const primaryOk = !absoluteSuppress && conf >= PRIMARY_LOG_THRESHOLD && !hardExcludeFromPrimaryLog(relPath, finding.description, conf, eliteHard);
1027
+ const hardDropped = Boolean(postNuclear);
1028
+ const primaryOk = !hardDropped && conf >= PRIMARY_LOG_THRESHOLD && !hardExcludeFromPrimaryLog(relPath, finding.description, conf, eliteHard);
997
1029
  let severity = finding.severity;
998
1030
  const originalSeverity = finding.original_severity ?? finding.severity;
999
1031
  if (reasons.includes("phase1_protection_lib_present") && conf <= 0.25) {
@@ -1001,7 +1033,7 @@ function enrichAuditFinding(repoRoot, finding, opts) {
1001
1033
  } else if (!primaryOk && conf < PRIMARY_LOG_THRESHOLD) {
1002
1034
  severity = downgradeSeverity(severity, critique.suggested_severity_cap);
1003
1035
  }
1004
- const forceSuppressed = Boolean(absoluteSuppress);
1036
+ const forceSuppressed = hardDropped;
1005
1037
  return {
1006
1038
  ...finding,
1007
1039
  severity,
@@ -1011,7 +1043,7 @@ function enrichAuditFinding(repoRoot, finding, opts) {
1011
1043
  primary_log_eligible: primaryOk,
1012
1044
  ...forceSuppressed ? {
1013
1045
  suppressed: true,
1014
- suppression_reason: absoluteSuppress.reason
1046
+ suppression_reason: postNuclear.reason
1015
1047
  } : {},
1016
1048
  attack_path_concrete: attackConcrete,
1017
1049
  cognitive: {
@@ -1755,6 +1787,9 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
1755
1787
  asvsTrace: "V6.4.1",
1756
1788
  severity,
1757
1789
  description,
1790
+ title: description,
1791
+ detector_name: detector,
1792
+ verified,
1758
1793
  file_path: rel,
1759
1794
  line,
1760
1795
  match_text: display.slice(0, 200),
@@ -5740,6 +5775,45 @@ function isRemediationTool(name) {
5740
5775
  return REMEDIATION_TOOL_NAMES.includes(name);
5741
5776
  }
5742
5777
 
5778
+ // src/hubUploadUrl.ts
5779
+ var HUB_UPLOAD_REPORT_PATH = "/api/mcp/upload-report";
5780
+ var LEGACY_TELEMETRY_INGEST_PATH = "/api/v1/telemetry/ingest";
5781
+ function stripTrailingSlash(url) {
5782
+ return url.replace(/\/$/, "");
5783
+ }
5784
+ function migrateLegacyIngestUrl(url) {
5785
+ if (!url.includes(LEGACY_TELEMETRY_INGEST_PATH)) return url;
5786
+ console.warn(
5787
+ `[runsec] Legacy telemetry ingest URL detected (${LEGACY_TELEMETRY_INGEST_PATH}); using ${HUB_UPLOAD_REPORT_PATH} instead`
5788
+ );
5789
+ return url.replace(LEGACY_TELEMETRY_INGEST_PATH, HUB_UPLOAD_REPORT_PATH);
5790
+ }
5791
+ function resolveHubUploadUrl() {
5792
+ const explicit = [
5793
+ process.env.RUNSEC_HUB_INGEST_URL,
5794
+ process.env.RUNSEC_HUB_UPLOAD_URL,
5795
+ process.env.RUNSEC_TELEMETRY_URL
5796
+ ].map((v) => v?.trim()).find(Boolean);
5797
+ if (explicit) {
5798
+ const migrated = migrateLegacyIngestUrl(explicit);
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);
5806
+ }
5807
+ const base = stripTrailingSlash(
5808
+ process.env.RUNSEC_HUB_URL?.trim() || process.env.RUNSEC_API_URL?.trim() || "https://runsec.io"
5809
+ );
5810
+ const migratedBase = migrateLegacyIngestUrl(base);
5811
+ if (migratedBase.includes(HUB_UPLOAD_REPORT_PATH)) {
5812
+ return migratedBase;
5813
+ }
5814
+ return `${migratedBase}${HUB_UPLOAD_REPORT_PATH}`;
5815
+ }
5816
+
5743
5817
  // src/complianceScores.ts
5744
5818
  var SEVERITY_PENALTY = {
5745
5819
  CRITICAL: 15,
@@ -5782,6 +5856,26 @@ function buildHubComplianceBlock(result) {
5782
5856
  }
5783
5857
 
5784
5858
  // src/telemetryClient.ts
5859
+ var HUB_UPLOAD_TIMEOUT_MS = 12e4;
5860
+ function hubAuthHeaders(apiKey) {
5861
+ return {
5862
+ Authorization: `Bearer ${apiKey}`,
5863
+ // Cloudflare/tunnel may strip Authorization on POST; Hub accepts this duplicate.
5864
+ "X-RunSec-Api-Key": apiKey,
5865
+ "Content-Type": "application/json",
5866
+ Accept: "application/json"
5867
+ };
5868
+ }
5869
+ function formatHubSyncErrorMessage(error, targetUrl, response) {
5870
+ const status = response?.status != null ? String(response.status) : error?.response?.status != null ? String(error.response.status) : "n/a";
5871
+ const err = error instanceof Error ? error : new Error(String(error));
5872
+ const cause = err.cause instanceof Error ? err.cause.message : err.cause != null ? String(err.cause) : "";
5873
+ let message = `Sync failed: ${err.message}. Target URL: ${targetUrl}. Status: ${status}`;
5874
+ if (cause && !message.includes(cause)) {
5875
+ message += `. Cause: ${cause}`;
5876
+ }
5877
+ return message;
5878
+ }
5785
5879
  function countSeverityMetrics(findings) {
5786
5880
  const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
5787
5881
  for (const f of findings) {
@@ -5837,30 +5931,29 @@ async function uploadScanResultsToHub(payload, apiKey) {
5837
5931
  console.warn("[runsec] Hub telemetry skipped: API key missing");
5838
5932
  return { success: false, message: "Sync failed: API key missing" };
5839
5933
  }
5840
- const baseUrl = (process.env.RUNSEC_API_URL || "https://runsec.io").replace(/\/$/, "");
5841
- const url = `${baseUrl}/api/mcp/upload-report`;
5934
+ const url = resolveHubUploadUrl();
5935
+ console.error(`[runsec] Hub telemetry upload \u2192 ${url}`);
5842
5936
  try {
5843
5937
  const response = await fetch(url, {
5844
5938
  method: "POST",
5845
- headers: {
5846
- Authorization: `Bearer ${trimmedKey}`,
5847
- // Cloudflare/tunnel may strip Authorization on POST; Hub accepts this duplicate.
5848
- "X-RunSec-Api-Key": trimmedKey,
5849
- "Content-Type": "application/json",
5850
- Accept: "application/json"
5851
- },
5852
- body: JSON.stringify(payload)
5939
+ headers: hubAuthHeaders(trimmedKey),
5940
+ body: JSON.stringify(payload),
5941
+ signal: AbortSignal.timeout(HUB_UPLOAD_TIMEOUT_MS)
5853
5942
  });
5854
5943
  if (!response.ok) {
5855
5944
  const detail = await response.text().catch(() => "");
5856
5945
  const detailSnippet = detail.slice(0, 500).trim();
5857
5946
  const statusLabel = response.statusText || String(response.status);
5858
- console.warn(
5859
- `[runsec] Hub telemetry upload failed (${response.status}): ${detailSnippet || statusLabel}`
5947
+ const httpMessage = detailSnippet || statusLabel;
5948
+ const message = formatHubSyncErrorMessage(
5949
+ new Error(httpMessage),
5950
+ url,
5951
+ response
5860
5952
  );
5953
+ console.warn(`[runsec] Hub telemetry upload failed: ${message}`);
5861
5954
  return {
5862
5955
  success: false,
5863
- message: `Sync failed: ${detailSnippet || statusLabel}`
5956
+ message
5864
5957
  };
5865
5958
  }
5866
5959
  const body = await response.json().catch(() => ({}));
@@ -5876,9 +5969,9 @@ async function uploadScanResultsToHub(payload, apiKey) {
5876
5969
  projectId
5877
5970
  };
5878
5971
  } catch (error) {
5879
- const message = error instanceof Error ? error.message : String(error);
5972
+ const message = formatHubSyncErrorMessage(error, url);
5880
5973
  console.warn(`[runsec] Hub telemetry upload error (scan saved locally): ${message}`);
5881
- return { success: false, message: `Sync failed: ${message}` };
5974
+ return { success: false, message };
5882
5975
  }
5883
5976
  }
5884
5977
 
@@ -5920,10 +6013,13 @@ function appendCloudSyncToDirective(directive, syncResult) {
5920
6013
  ${lines}`;
5921
6014
  }
5922
6015
  async function verifyApiKey(apiKey) {
5923
- const baseUrl = (process.env.RUNSEC_API_URL || "https://runsec.io").replace(/\/$/, "");
6016
+ const verifyUrl = resolveHubUploadUrl().replace(/\/upload-report\/?$/, "/verify-key");
5924
6017
  try {
5925
- const response = await fetch(`${baseUrl}/api/mcp/verify-key`, {
5926
- headers: { Authorization: `Bearer ${apiKey}` }
6018
+ const response = await fetch(verifyUrl, {
6019
+ headers: {
6020
+ Authorization: `Bearer ${apiKey}`,
6021
+ "X-RunSec-Api-Key": apiKey
6022
+ }
5927
6023
  });
5928
6024
  if (response.status === 401 || response.status === 403) {
5929
6025
  console.error("\u274C FATAL: Invalid RunSec API Key.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runsec/mcp",
3
- "version": "1.0.75",
3
+ "version": "1.0.77",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
6
  "dist",