@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.
- package/dist/index.js +161 -65
- 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
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
|
452
|
-
var
|
|
453
|
-
function
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
857
|
-
if (
|
|
858
|
-
return [
|
|
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 =
|
|
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,
|
|
880
|
-
reasons.push("
|
|
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,
|
|
884
|
-
reasons.push("
|
|
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
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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
|
|
5841
|
-
|
|
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
|
-
|
|
5847
|
-
|
|
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
|
-
|
|
5859
|
-
|
|
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
|
|
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
|
|
5972
|
+
const message = formatHubSyncErrorMessage(error, url);
|
|
5880
5973
|
console.warn(`[runsec] Hub telemetry upload error (scan saved locally): ${message}`);
|
|
5881
|
-
return { success: false, 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
|
|
6016
|
+
const verifyUrl = resolveHubUploadUrl().replace(/\/upload-report\/?$/, "/verify-key");
|
|
5924
6017
|
try {
|
|
5925
|
-
const response = await fetch(
|
|
5926
|
-
headers: {
|
|
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.");
|