@runsec/mcp 1.0.78 → 1.0.80
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 +149 -15
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -435,6 +435,26 @@ 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
|
+
var DEV_GENERIC_CREDENTIAL_RE = /(?:postgres:postgres|root:root|root:password|admin:admin|user:password|test:test|guest:guest|changeme:changeme)/i;
|
|
439
|
+
var DEV_DB_HOST_RE = /@(?:dev_db|dev-db|localhost|127\.0\.0\.1|0\.0\.0\.0|host\.docker\.internal|\.local\b|docker\.internal)(?::|\/|$)/i;
|
|
440
|
+
var DEV_DATABASE_URI_RE = /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/(?:[^:@\s]+:[^@\s]+@)?(?:dev_db|dev-db|localhost|127\.0\.0\.1|host\.docker\.internal)/i;
|
|
441
|
+
function blobHasDevDatabaseSecret(text) {
|
|
442
|
+
const blob = (text ?? "").trim();
|
|
443
|
+
if (!blob) return false;
|
|
444
|
+
if (DEV_GENERIC_CREDENTIAL_RE.test(blob)) return true;
|
|
445
|
+
if (DEV_DB_HOST_RE.test(blob)) return true;
|
|
446
|
+
if (DEV_DATABASE_URI_RE.test(blob)) return true;
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
function findingHasDevDatabaseSecret(finding) {
|
|
450
|
+
const parts = [
|
|
451
|
+
finding.match_text ?? "",
|
|
452
|
+
finding.snippet ?? "",
|
|
453
|
+
finding.description ?? "",
|
|
454
|
+
finding.title ?? ""
|
|
455
|
+
];
|
|
456
|
+
return parts.some((p) => blobHasDevDatabaseSecret(p));
|
|
457
|
+
}
|
|
438
458
|
function findingBlobHasEnvInterpolation(finding) {
|
|
439
459
|
const parts = [
|
|
440
460
|
finding.match_text ?? "",
|
|
@@ -458,6 +478,9 @@ function applyNuclearHardDrop(finding, relPath) {
|
|
|
458
478
|
if (ENV_INTERP_RE.test(matchText) || ENV_INTERP_RE.test(snippet) || findingBlobHasEnvInterpolation(finding)) {
|
|
459
479
|
return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "env_variable_interpolation" };
|
|
460
480
|
}
|
|
481
|
+
if (findingHasDevDatabaseSecret(finding)) {
|
|
482
|
+
return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "dev_database_placeholder" };
|
|
483
|
+
}
|
|
461
484
|
if (!findingIsVerified(finding) && (LOCKFILE_LAYOUT_RE.test(relPath) || isLockfileOrModulesPath(relPath) || isLockfileLayoutArtifactPath(relPath))) {
|
|
462
485
|
return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "lockfile_or_layout_artifact" };
|
|
463
486
|
}
|
|
@@ -496,7 +519,8 @@ function isCustomRegexFinding(finding) {
|
|
|
496
519
|
}
|
|
497
520
|
function looksLikeHighEntropyRawToken(finding) {
|
|
498
521
|
const blob = `${finding.match_text ?? ""} ${finding.snippet ?? ""}`.trim();
|
|
499
|
-
if (blob.length < 24 || ENV_INTERP_RE.test(blob)) return false;
|
|
522
|
+
if (blob.length < 24 || ENV_INTERP_RE.test(blob) || findingHasDevDatabaseSecret(finding)) return false;
|
|
523
|
+
if (/^(?:postgres(?:ql)?|mysql|mongodb|redis):\/\//i.test(blob)) return false;
|
|
500
524
|
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(
|
|
501
525
|
blob
|
|
502
526
|
)) {
|
|
@@ -1821,6 +1845,7 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
|
|
|
1821
1845
|
if (isLockfileOrModulesPath(rel)) continue;
|
|
1822
1846
|
const blob = `${display} ${rawSecret} ${description}`;
|
|
1823
1847
|
if (hasEnvironmentInterpolation(blob)) continue;
|
|
1848
|
+
if (blobHasDevDatabaseSecret(blob)) continue;
|
|
1824
1849
|
if (detector.toLowerCase() === "customregex") continue;
|
|
1825
1850
|
}
|
|
1826
1851
|
const severity = severityForSecret(detector, verified);
|
|
@@ -3093,7 +3118,7 @@ async function runUnifiedScanPipeline(opts) {
|
|
|
3093
3118
|
`[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}`
|
|
3094
3119
|
);
|
|
3095
3120
|
console.error(
|
|
3096
|
-
`[runsec] X-RunSec-Verdict: ${cognitiveResult.verdict.http_headers["X-RunSec-Verdict"]}
|
|
3121
|
+
`[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}`
|
|
3097
3122
|
);
|
|
3098
3123
|
return {
|
|
3099
3124
|
standard,
|
|
@@ -5823,8 +5848,10 @@ function isRemediationTool(name) {
|
|
|
5823
5848
|
}
|
|
5824
5849
|
|
|
5825
5850
|
// src/hubUploadUrl.ts
|
|
5851
|
+
var import_node_fs17 = __toESM(require("fs"));
|
|
5826
5852
|
var HUB_UPLOAD_REPORT_PATH = "/api/mcp/upload-report";
|
|
5827
5853
|
var DEFAULT_PRODUCTION_HUB_ORIGIN = "https://runsec.io";
|
|
5854
|
+
var DEFAULT_DOCKER_INTERNAL_HUB_ORIGIN = "http://frontend:3000";
|
|
5828
5855
|
var LEGACY_TELEMETRY_INGEST_PATH = "/api/v1/telemetry/ingest";
|
|
5829
5856
|
function stripTrailingSlash(url) {
|
|
5830
5857
|
return url.replace(/\/$/, "");
|
|
@@ -5856,11 +5883,24 @@ function colocatedHubOrigin() {
|
|
|
5856
5883
|
if (!port || !/^\d+$/.test(port)) return null;
|
|
5857
5884
|
return `http://127.0.0.1:${port}`;
|
|
5858
5885
|
}
|
|
5886
|
+
function isDockerRuntime() {
|
|
5887
|
+
try {
|
|
5888
|
+
return import_node_fs17.default.existsSync("/.dockerenv");
|
|
5889
|
+
} catch {
|
|
5890
|
+
return false;
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5893
|
+
function resolveDockerInternalHubOrigin() {
|
|
5894
|
+
return stripTrailingSlash(
|
|
5895
|
+
process.env.RUNSEC_HUB_INTERNAL_URL?.trim() || process.env.FRONTEND_INTERNAL_URL?.trim() || DEFAULT_DOCKER_INTERNAL_HUB_ORIGIN
|
|
5896
|
+
);
|
|
5897
|
+
}
|
|
5859
5898
|
function resolveHubBaseUrl() {
|
|
5860
5899
|
const candidates = [
|
|
5861
5900
|
process.env.RUNSEC_HUB_URL,
|
|
5862
5901
|
process.env.RUNSEC_API_URL,
|
|
5863
5902
|
process.env.RUNSEC_HUB_ORIGIN,
|
|
5903
|
+
process.env.RUNSEC_HUB_INTERNAL_URL,
|
|
5864
5904
|
process.env.NEXT_PUBLIC_APP_URL,
|
|
5865
5905
|
process.env.PUBLIC_APP_URL,
|
|
5866
5906
|
process.env.NEXTAUTH_URL,
|
|
@@ -5871,6 +5911,13 @@ function resolveHubBaseUrl() {
|
|
|
5871
5911
|
if (candidates.length > 0) {
|
|
5872
5912
|
return stripTrailingSlash(migrateLegacyIngestUrl(candidates[0]));
|
|
5873
5913
|
}
|
|
5914
|
+
if (isDockerRuntime()) {
|
|
5915
|
+
const internal = resolveDockerInternalHubOrigin();
|
|
5916
|
+
console.error(
|
|
5917
|
+
`[runsec] Docker runtime \u2014 Hub upload via internal origin ${internal} (set RUNSEC_HUB_URL to override)`
|
|
5918
|
+
);
|
|
5919
|
+
return internal;
|
|
5920
|
+
}
|
|
5874
5921
|
if (isProductionRuntime()) {
|
|
5875
5922
|
const colocated = colocatedHubOrigin();
|
|
5876
5923
|
if (colocated) {
|
|
@@ -5910,6 +5957,24 @@ function localhostAlternateUploadUrl(url) {
|
|
|
5910
5957
|
return null;
|
|
5911
5958
|
}
|
|
5912
5959
|
}
|
|
5960
|
+
function dockerInternalAlternateUploadUrl(url) {
|
|
5961
|
+
if (!isDockerRuntime()) return null;
|
|
5962
|
+
try {
|
|
5963
|
+
const parsed = new URL(url);
|
|
5964
|
+
const internal = new URL(resolveDockerInternalHubOrigin());
|
|
5965
|
+
if (parsed.origin === internal.origin) return null;
|
|
5966
|
+
internal.pathname = HUB_UPLOAD_REPORT_PATH;
|
|
5967
|
+
internal.search = "";
|
|
5968
|
+
internal.hash = "";
|
|
5969
|
+
return internal.toString();
|
|
5970
|
+
} catch {
|
|
5971
|
+
return appendUploadPath(resolveDockerInternalHubOrigin());
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
5974
|
+
|
|
5975
|
+
// src/telemetryClient.ts
|
|
5976
|
+
var import_node_http = __toESM(require("http"));
|
|
5977
|
+
var import_node_https = __toESM(require("https"));
|
|
5913
5978
|
|
|
5914
5979
|
// src/complianceScores.ts
|
|
5915
5980
|
var SEVERITY_PENALTY = {
|
|
@@ -6019,14 +6084,70 @@ function logHubSyncFailure(message, error, targetUrl) {
|
|
|
6019
6084
|
}
|
|
6020
6085
|
console.error(`[runsec] Hub telemetry upload error (scan saved locally): ${message}`);
|
|
6021
6086
|
}
|
|
6022
|
-
|
|
6023
|
-
return
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6087
|
+
function postHubUploadNodeHttp(url, apiKey, payload) {
|
|
6088
|
+
return new Promise((resolve, reject) => {
|
|
6089
|
+
const body = JSON.stringify(payload);
|
|
6090
|
+
let parsed;
|
|
6091
|
+
try {
|
|
6092
|
+
parsed = new URL(url);
|
|
6093
|
+
} catch (err) {
|
|
6094
|
+
reject(err);
|
|
6095
|
+
return;
|
|
6096
|
+
}
|
|
6097
|
+
const headers = {
|
|
6098
|
+
...hubAuthHeaders(apiKey),
|
|
6099
|
+
"Content-Length": String(Buffer.byteLength(body))
|
|
6100
|
+
};
|
|
6101
|
+
const lib = parsed.protocol === "https:" ? import_node_https.default : import_node_http.default;
|
|
6102
|
+
const port = parsed.port !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
|
6103
|
+
const req = lib.request(
|
|
6104
|
+
{
|
|
6105
|
+
hostname: parsed.hostname,
|
|
6106
|
+
port,
|
|
6107
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
6108
|
+
method: "POST",
|
|
6109
|
+
headers,
|
|
6110
|
+
timeout: HUB_UPLOAD_TIMEOUT_MS
|
|
6111
|
+
},
|
|
6112
|
+
(res) => {
|
|
6113
|
+
const chunks = [];
|
|
6114
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
6115
|
+
res.on("end", () => {
|
|
6116
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
6117
|
+
const status = res.statusCode ?? 0;
|
|
6118
|
+
resolve({
|
|
6119
|
+
ok: status >= 200 && status < 300,
|
|
6120
|
+
status,
|
|
6121
|
+
statusText: res.statusMessage ?? "",
|
|
6122
|
+
text: async () => text,
|
|
6123
|
+
json: async () => JSON.parse(text || "{}")
|
|
6124
|
+
});
|
|
6125
|
+
});
|
|
6126
|
+
}
|
|
6127
|
+
);
|
|
6128
|
+
req.on("error", reject);
|
|
6129
|
+
req.on("timeout", () => {
|
|
6130
|
+
req.destroy();
|
|
6131
|
+
reject(Object.assign(new Error("Hub upload request timeout"), { code: "ETIMEDOUT" }));
|
|
6132
|
+
});
|
|
6133
|
+
req.write(body);
|
|
6134
|
+
req.end();
|
|
6028
6135
|
});
|
|
6029
6136
|
}
|
|
6137
|
+
async function postHubUpload(url, apiKey, payload) {
|
|
6138
|
+
try {
|
|
6139
|
+
return await fetch(url, {
|
|
6140
|
+
method: "POST",
|
|
6141
|
+
headers: hubAuthHeaders(apiKey),
|
|
6142
|
+
body: JSON.stringify(payload),
|
|
6143
|
+
signal: AbortSignal.timeout(HUB_UPLOAD_TIMEOUT_MS)
|
|
6144
|
+
});
|
|
6145
|
+
} catch (fetchError) {
|
|
6146
|
+
console.error("[runsec] fetch() failed \u2014 retrying Hub upload via node:http(s)");
|
|
6147
|
+
dumpHubNetworkError(fetchError, url);
|
|
6148
|
+
return postHubUploadNodeHttp(url, apiKey, payload);
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6030
6151
|
function countSeverityMetrics(findings) {
|
|
6031
6152
|
const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
|
|
6032
6153
|
for (const f of findings) {
|
|
@@ -6085,14 +6206,27 @@ async function uploadScanResultsToHub(payload, apiKey) {
|
|
|
6085
6206
|
const url = resolveHubUploadUrl();
|
|
6086
6207
|
console.error(`[runsec] Hub telemetry upload \u2192 ${url}`);
|
|
6087
6208
|
const attemptUpload = async (targetUrl) => {
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6209
|
+
const errors = [];
|
|
6210
|
+
const urls = [targetUrl];
|
|
6211
|
+
const localhostAlt = localhostAlternateUploadUrl(targetUrl);
|
|
6212
|
+
if (localhostAlt && localhostAlt !== targetUrl) urls.push(localhostAlt);
|
|
6213
|
+
const dockerAlt = dockerInternalAlternateUploadUrl(targetUrl);
|
|
6214
|
+
if (dockerAlt && !urls.includes(dockerAlt)) urls.push(dockerAlt);
|
|
6215
|
+
let lastError = new Error("No upload attempts made");
|
|
6216
|
+
for (let i = 0; i < urls.length; i++) {
|
|
6217
|
+
const tryUrl = urls[i];
|
|
6218
|
+
if (i > 0) {
|
|
6219
|
+
console.error(`[runsec] Hub telemetry retry \u2192 ${tryUrl}`);
|
|
6220
|
+
}
|
|
6221
|
+
try {
|
|
6222
|
+
return await postHubUpload(tryUrl, trimmedKey, payload);
|
|
6223
|
+
} catch (err) {
|
|
6224
|
+
lastError = err;
|
|
6225
|
+
errors.push(err);
|
|
6226
|
+
dumpHubNetworkError(err, tryUrl);
|
|
6227
|
+
}
|
|
6095
6228
|
}
|
|
6229
|
+
throw lastError;
|
|
6096
6230
|
};
|
|
6097
6231
|
try {
|
|
6098
6232
|
let response;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runsec/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
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",
|