@runsec/mcp 1.0.79 → 1.0.82

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 +274 -126
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -435,6 +435,45 @@ 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 isStaticLayoutDumpPath(relPath) {
439
+ return isLockfileLayoutArtifactPath(relPath);
440
+ }
441
+ function isHexChecksumBlob(text) {
442
+ const compact = (text ?? "").replace(/\s+/g, "").trim();
443
+ if (compact.length < 40 || compact.length > 128) return false;
444
+ return /^[0-9a-f]+$/i.test(compact);
445
+ }
446
+ var UNVERIFIED_NOISE_DETECTORS = /* @__PURE__ */ new Set([
447
+ "customregex",
448
+ "sentrytoken",
449
+ "hashicorpvault"
450
+ ]);
451
+ function isUnverifiedTrufflehogNoiseDetector(detectorName) {
452
+ const d = detectorName.trim().toLowerCase().replace(/\s+/g, "");
453
+ if (UNVERIFIED_NOISE_DETECTORS.has(d)) return true;
454
+ if (d === "sentry" || d === "sentrytoken") return true;
455
+ return false;
456
+ }
457
+ var DEV_GENERIC_CREDENTIAL_RE = /(?:postgres:postgres|root:root|root:password|admin:admin|user:password|test:test|guest:guest|changeme:changeme)/i;
458
+ 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;
459
+ 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;
460
+ function blobHasDevDatabaseSecret(text) {
461
+ const blob = (text ?? "").trim();
462
+ if (!blob) return false;
463
+ if (DEV_GENERIC_CREDENTIAL_RE.test(blob)) return true;
464
+ if (DEV_DB_HOST_RE.test(blob)) return true;
465
+ if (DEV_DATABASE_URI_RE.test(blob)) return true;
466
+ return false;
467
+ }
468
+ function findingHasDevDatabaseSecret(finding) {
469
+ const parts = [
470
+ finding.match_text ?? "",
471
+ finding.snippet ?? "",
472
+ finding.description ?? "",
473
+ finding.title ?? ""
474
+ ];
475
+ return parts.some((p) => blobHasDevDatabaseSecret(p));
476
+ }
438
477
  function findingBlobHasEnvInterpolation(finding) {
439
478
  const parts = [
440
479
  finding.match_text ?? "",
@@ -458,9 +497,20 @@ function applyNuclearHardDrop(finding, relPath) {
458
497
  if (ENV_INTERP_RE.test(matchText) || ENV_INTERP_RE.test(snippet) || findingBlobHasEnvInterpolation(finding)) {
459
498
  return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "env_variable_interpolation" };
460
499
  }
461
- if (!findingIsVerified(finding) && (LOCKFILE_LAYOUT_RE.test(relPath) || isLockfileOrModulesPath(relPath) || isLockfileLayoutArtifactPath(relPath))) {
500
+ if (findingHasDevDatabaseSecret(finding)) {
501
+ return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "dev_database_placeholder" };
502
+ }
503
+ const secretBlob = `${matchText} ${snippet}`.trim();
504
+ if (!findingIsVerified(finding) && isHexChecksumBlob(secretBlob)) {
505
+ return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "package_checksum_hex" };
506
+ }
507
+ if (!findingIsVerified(finding) && (LOCKFILE_LAYOUT_RE.test(relPath) || isLockfileOrModulesPath(relPath) || isLockfileLayoutArtifactPath(relPath) || isStaticLayoutDumpPath(relPath))) {
462
508
  return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "lockfile_or_layout_artifact" };
463
509
  }
510
+ const detector = String(finding.detector_name ?? "").trim();
511
+ if (!findingIsVerified(finding) && isUnverifiedTrufflehogNoiseDetector(detector)) {
512
+ return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "unverified_trufflehog_noise_detector" };
513
+ }
464
514
  if (isCustomRegexFinding(finding) && !findingIsVerified(finding)) {
465
515
  return { cap: NUCLEAR_HARD_DROP_CONFIDENCE, reason: "customregex_unverified" };
466
516
  }
@@ -496,14 +546,18 @@ function isCustomRegexFinding(finding) {
496
546
  }
497
547
  function looksLikeHighEntropyRawToken(finding) {
498
548
  const blob = `${finding.match_text ?? ""} ${finding.snippet ?? ""}`.trim();
499
- if (blob.length < 24 || ENV_INTERP_RE.test(blob)) return false;
549
+ if (blob.length < 24 || ENV_INTERP_RE.test(blob) || findingHasDevDatabaseSecret(finding)) return false;
550
+ if (/^(?:postgres(?:ql)?|mysql|mongodb|redis):\/\//i.test(blob)) return false;
500
551
  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
552
  blob
502
553
  )) {
503
554
  return true;
504
555
  }
505
556
  const compact = blob.replace(/\s+/g, "");
506
- if (compact.length >= 40 && /^[A-Za-z0-9+/=_-]+$/.test(compact)) return true;
557
+ if (isHexChecksumBlob(compact)) return false;
558
+ if (compact.length >= 40 && /^[A-Za-z0-9+/=_-]+$/.test(compact) && /[A-Z]/.test(compact) && /[a-z]/.test(compact)) {
559
+ return true;
560
+ }
507
561
  return false;
508
562
  }
509
563
  function customRegexNeedsBaseClamp(finding) {
@@ -1818,10 +1872,12 @@ function mapTrufflehogFindings(rows, workspaceRoot) {
1818
1872
  const display = redacted || rawSecret || "[secret redacted]";
1819
1873
  const description = `TruffleHog: exposed ${detector}${verified ? " (verified)" : ""}`;
1820
1874
  if (!isTrufflehogVerified(verified, description)) {
1821
- if (isLockfileOrModulesPath(rel)) continue;
1822
1875
  const blob = `${display} ${rawSecret} ${description}`;
1876
+ if (isLockfileOrModulesPath(rel) || isStaticLayoutDumpPath(rel)) continue;
1823
1877
  if (hasEnvironmentInterpolation(blob)) continue;
1824
- if (detector.toLowerCase() === "customregex") continue;
1878
+ if (blobHasDevDatabaseSecret(blob)) continue;
1879
+ if (isHexChecksumBlob(display) || isHexChecksumBlob(rawSecret)) continue;
1880
+ if (isUnverifiedTrufflehogNoiseDetector(detector)) continue;
1825
1881
  }
1826
1882
  const severity = severityForSecret(detector, verified);
1827
1883
  findings.push({
@@ -3262,8 +3318,29 @@ async function executeAudit(toolName, args) {
3262
3318
  }
3263
3319
 
3264
3320
  // src/engine/reportFormatter.ts
3321
+ var import_node_fs10 = __toESM(require("fs"));
3322
+ var import_node_path16 = __toESM(require("path"));
3323
+
3324
+ // src/version.ts
3265
3325
  var import_node_fs9 = __toESM(require("fs"));
3266
3326
  var import_node_path15 = __toESM(require("path"));
3327
+ function loadVersion() {
3328
+ const candidates = [
3329
+ import_node_path15.default.join(__dirname, "..", "package.json"),
3330
+ import_node_path15.default.join(__dirname, "package.json")
3331
+ ];
3332
+ for (const candidate of candidates) {
3333
+ try {
3334
+ const raw = JSON.parse(import_node_fs9.default.readFileSync(candidate, "utf8"));
3335
+ if (raw.version?.trim()) return raw.version.trim();
3336
+ } catch {
3337
+ }
3338
+ }
3339
+ return "unknown";
3340
+ }
3341
+ var RUNSEC_MCP_VERSION = loadVersion();
3342
+
3343
+ // src/engine/reportFormatter.ts
3267
3344
  var REPORT_SECTION_TITLES = {
3268
3345
  code: "Code Vulnerabilities",
3269
3346
  secrets: "Exposed Secrets",
@@ -3356,7 +3433,7 @@ function shortRuleLabel(ruleId) {
3356
3433
  return parts[parts.length - 1] || ruleId;
3357
3434
  }
3358
3435
  function snippetLanguage(filePath) {
3359
- const ext = import_node_path15.default.extname(filePath).replace(/^\./, "").toLowerCase();
3436
+ const ext = import_node_path16.default.extname(filePath).replace(/^\./, "").toLowerCase();
3360
3437
  const map = {
3361
3438
  yml: "yaml",
3362
3439
  yaml: "yaml",
@@ -3458,9 +3535,9 @@ var TECH_STACK_BY_EXT = {
3458
3535
  };
3459
3536
  function techStackFromFilePath(filePath) {
3460
3537
  const normalized = String(filePath ?? "").replace(/\\/g, "/");
3461
- const base = import_node_path15.default.basename(normalized).toLowerCase();
3538
+ const base = import_node_path16.default.basename(normalized).toLowerCase();
3462
3539
  if (base === "dockerfile" || base.startsWith("dockerfile.")) return "Docker";
3463
- const ext = import_node_path15.default.extname(normalized).toLowerCase();
3540
+ const ext = import_node_path16.default.extname(normalized).toLowerCase();
3464
3541
  return TECH_STACK_BY_EXT[ext] ?? "Other";
3465
3542
  }
3466
3543
  function countFindingsByTechStack(findings) {
@@ -3654,6 +3731,7 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
3654
3731
  out.push(`# RunSec Unified Security Report`);
3655
3732
  out.push("");
3656
3733
  out.push(`**Generated at:** ${(/* @__PURE__ */ new Date()).toISOString()}`);
3734
+ out.push(`**MCP package:** \`@runsec/mcp@${safeText(RUNSEC_MCP_VERSION)}\``);
3657
3735
  out.push(`**Standard:** ${safeText(standard)}`);
3658
3736
  out.push(`**X-RunSec-Verdict:** \`${safeText(verdictLabel)}\`${metrics.verdict?.is_safe === false && metrics.verdict.fail_reason ? ` \u2014 ${safeText(metrics.verdict.fail_reason)}` : ""}`);
3659
3737
  out.push(
@@ -3723,15 +3801,15 @@ function buildServerSideReportMarkdown(standard, findings, metrics) {
3723
3801
  return finalizeReportMarkdown(out.join("\n"));
3724
3802
  }
3725
3803
  function resolveReportPath(workspacePath) {
3726
- const base = workspacePath?.trim() ? import_node_path15.default.resolve(workspacePath) : process.cwd();
3727
- return import_node_path15.default.join(base, "runsec-report.md");
3804
+ const base = workspacePath?.trim() ? import_node_path16.default.resolve(workspacePath) : process.cwd();
3805
+ return import_node_path16.default.join(base, "runsec-report.md");
3728
3806
  }
3729
3807
  function generateMarkdownReport(standard, findings, metrics, workspacePath) {
3730
3808
  const m = metrics || {};
3731
3809
  const rows = Array.isArray(findings) ? findings : [];
3732
3810
  const reportContent = finalizeReportMarkdown(buildServerSideReportMarkdown(standard, rows, m));
3733
3811
  const reportPath = resolveReportPath(workspacePath);
3734
- import_node_fs9.default.writeFileSync(reportPath, reportContent, "utf-8");
3812
+ import_node_fs10.default.writeFileSync(reportPath, reportContent, "utf-8");
3735
3813
  console.error(`[runsec] wrote unified report to: ${reportPath}`);
3736
3814
  console.error(`[runsec] X-RunSec-Verdict: ${m.verdict?.http_headers?.["X-RunSec-Verdict"] ?? m.verdict?.status ?? "PASS"}`);
3737
3815
  return `
@@ -3746,17 +3824,17 @@ Simply confirm that the scan is complete and the report is saved at the path abo
3746
3824
  }
3747
3825
 
3748
3826
  // src/engine/reviewFormatter.ts
3749
- var import_node_fs14 = __toESM(require("fs"));
3750
- var import_node_path21 = __toESM(require("path"));
3827
+ var import_node_fs15 = __toESM(require("fs"));
3828
+ var import_node_path22 = __toESM(require("path"));
3751
3829
 
3752
3830
  // src/engine/threatModelEngine.ts
3753
3831
  var import_node_crypto4 = require("crypto");
3754
- var import_node_fs13 = __toESM(require("fs"));
3755
- var import_node_path20 = __toESM(require("path"));
3832
+ var import_node_fs14 = __toESM(require("fs"));
3833
+ var import_node_path21 = __toESM(require("path"));
3756
3834
 
3757
3835
  // src/skills/skillsApi.ts
3758
- var import_node_fs12 = __toESM(require("fs"));
3759
- var import_node_path19 = __toESM(require("path"));
3836
+ var import_node_fs13 = __toESM(require("fs"));
3837
+ var import_node_path20 = __toESM(require("path"));
3760
3838
 
3761
3839
  // src/skills/patternParser.ts
3762
3840
  var METRIC_ID_RE = /^[A-Z0-9]{2,4}-[0-9A-Za-z.\-]+$/;
@@ -3829,43 +3907,43 @@ function parsePatternRows(patternsText) {
3829
3907
  }
3830
3908
 
3831
3909
  // src/skills/paths.ts
3832
- var import_node_path16 = __toESM(require("path"));
3910
+ var import_node_path17 = __toESM(require("path"));
3833
3911
  var RUNSEC_RELEASE_VERSION = "v1.0";
3834
3912
  var RAG_CACHE_SCHEMA_VERSION = 3;
3835
3913
  var ANTI_HALLUCINATION_PROMPT = "You MUST re-run a RunSec audit tool (runsec_audit_general or a scoped runsec_audit_*) after every remediation. Security claims without a fresh scanner PASS are invalid.";
3836
3914
  function getSkillsDirectory() {
3837
- return import_node_path16.default.join(getDataDirectory(), "skills");
3915
+ return import_node_path17.default.join(getDataDirectory(), "skills");
3838
3916
  }
3839
3917
  function getRagCachePath() {
3840
- return import_node_path16.default.join(getDataDirectory(), ".rag-cache.json");
3918
+ return import_node_path17.default.join(getDataDirectory(), ".rag-cache.json");
3841
3919
  }
3842
3920
 
3843
3921
  // src/skills/ragIndex.ts
3844
3922
  var import_node_crypto3 = require("crypto");
3845
- var import_node_fs11 = __toESM(require("fs"));
3846
- var import_node_path18 = __toESM(require("path"));
3923
+ var import_node_fs12 = __toESM(require("fs"));
3924
+ var import_node_path19 = __toESM(require("path"));
3847
3925
 
3848
3926
  // src/skills/skillLoader.ts
3849
- var import_node_fs10 = __toESM(require("fs"));
3850
- var import_node_path17 = __toESM(require("path"));
3927
+ var import_node_fs11 = __toESM(require("fs"));
3928
+ var import_node_path18 = __toESM(require("path"));
3851
3929
  function loadSkillManifests() {
3852
3930
  const skillsDir = getSkillsDirectory();
3853
3931
  const manifests = {};
3854
- if (!import_node_fs10.default.existsSync(skillsDir)) {
3932
+ if (!import_node_fs11.default.existsSync(skillsDir)) {
3855
3933
  return manifests;
3856
3934
  }
3857
- for (const entry of import_node_fs10.default.readdirSync(skillsDir, { withFileTypes: true })) {
3935
+ for (const entry of import_node_fs11.default.readdirSync(skillsDir, { withFileTypes: true })) {
3858
3936
  if (!entry.isDirectory()) continue;
3859
- const skillJson = import_node_path17.default.join(skillsDir, entry.name, "skill.json");
3860
- if (!import_node_fs10.default.existsSync(skillJson)) continue;
3861
- const data = JSON.parse(import_node_fs10.default.readFileSync(skillJson, "utf-8"));
3937
+ const skillJson = import_node_path18.default.join(skillsDir, entry.name, "skill.json");
3938
+ if (!import_node_fs11.default.existsSync(skillJson)) continue;
3939
+ const data = JSON.parse(import_node_fs11.default.readFileSync(skillJson, "utf-8"));
3862
3940
  const sid = String(data.skill_id ?? entry.name);
3863
3941
  manifests[sid] = { ...data, skill_id: sid, __dir_name: entry.name };
3864
3942
  }
3865
3943
  return manifests;
3866
3944
  }
3867
3945
  function skillDirectory(manifest) {
3868
- return import_node_path17.default.join(getSkillsDirectory(), manifest.__dir_name);
3946
+ return import_node_path18.default.join(getSkillsDirectory(), manifest.__dir_name);
3869
3947
  }
3870
3948
 
3871
3949
  // src/skills/ragIndex.ts
@@ -3911,28 +3989,28 @@ function cosine(a, b) {
3911
3989
  }
3912
3990
  function hashFile(filePath) {
3913
3991
  const h = (0, import_node_crypto3.createHash)("sha256");
3914
- h.update(import_node_fs11.default.readFileSync(filePath));
3992
+ h.update(import_node_fs12.default.readFileSync(filePath));
3915
3993
  return h.digest("hex");
3916
3994
  }
3917
3995
  function listSkillSourceFiles() {
3918
3996
  const skillsDir = getSkillsDirectory();
3919
3997
  const files = [];
3920
3998
  const walk = (dir) => {
3921
- for (const entry of import_node_fs11.default.readdirSync(dir, { withFileTypes: true })) {
3922
- const full = import_node_path18.default.join(dir, entry.name);
3999
+ for (const entry of import_node_fs12.default.readdirSync(dir, { withFileTypes: true })) {
4000
+ const full = import_node_path19.default.join(dir, entry.name);
3923
4001
  if (entry.isDirectory()) walk(full);
3924
4002
  else if (entry.isFile()) files.push(full);
3925
4003
  }
3926
4004
  };
3927
- if (import_node_fs11.default.existsSync(skillsDir)) walk(skillsDir);
4005
+ if (import_node_fs12.default.existsSync(skillsDir)) walk(skillsDir);
3928
4006
  return files.sort();
3929
4007
  }
3930
4008
  function computeFilesChecksum() {
3931
4009
  const files = listSkillSourceFiles();
3932
4010
  const entries = [];
3933
4011
  for (const full of files) {
3934
- const st = import_node_fs11.default.statSync(full);
3935
- const rel = import_node_path18.default.relative(getSkillsDirectory(), full).replace(/\\/g, "/");
4012
+ const st = import_node_fs12.default.statSync(full);
4013
+ const rel = import_node_path19.default.relative(getSkillsDirectory(), full).replace(/\\/g, "/");
3936
4014
  const mtimeNs = typeof st.mtimeNs === "bigint" ? Number(st.mtimeNs) : Math.round(st.mtimeMs * 1e6);
3937
4015
  entries.push({
3938
4016
  path: rel,
@@ -3963,11 +4041,11 @@ function buildRagIndex() {
3963
4041
  const manifests = loadSkillManifests();
3964
4042
  for (const [skill_id, manifest] of Object.entries(manifests)) {
3965
4043
  const dir = skillDirectory(manifest);
3966
- const indexPath = import_node_path18.default.join(dir, "index.md");
3967
- const patternsPath = import_node_path18.default.join(dir, "patterns.md");
3968
- if (!import_node_fs11.default.existsSync(indexPath) || !import_node_fs11.default.existsSync(patternsPath)) continue;
3969
- const indexText = import_node_fs11.default.readFileSync(indexPath, "utf-8");
3970
- const patternsText = import_node_fs11.default.readFileSync(patternsPath, "utf-8");
4044
+ const indexPath = import_node_path19.default.join(dir, "index.md");
4045
+ const patternsPath = import_node_path19.default.join(dir, "patterns.md");
4046
+ if (!import_node_fs12.default.existsSync(indexPath) || !import_node_fs12.default.existsSync(patternsPath)) continue;
4047
+ const indexText = import_node_fs12.default.readFileSync(indexPath, "utf-8");
4048
+ const patternsText = import_node_fs12.default.readFileSync(patternsPath, "utf-8");
3971
4049
  const example_path = String(manifest.few_shot_examples ?? "");
3972
4050
  for (const paragraph of indexText.split(/\n\n+/).map((p) => p.trim()).filter(Boolean)) {
3973
4051
  chunks.push({
@@ -4008,9 +4086,9 @@ function buildRagIndex() {
4008
4086
  }
4009
4087
  function loadRagCache(current) {
4010
4088
  const cachePath = getRagCachePath();
4011
- if (!import_node_fs11.default.existsSync(cachePath)) return null;
4089
+ if (!import_node_fs12.default.existsSync(cachePath)) return null;
4012
4090
  try {
4013
- const payload = JSON.parse(import_node_fs11.default.readFileSync(cachePath, "utf-8"));
4091
+ const payload = JSON.parse(import_node_fs12.default.readFileSync(cachePath, "utf-8"));
4014
4092
  if (payload.schema_version !== RAG_CACHE_SCHEMA_VERSION) return null;
4015
4093
  if (JSON.stringify(payload.files_checksum) !== JSON.stringify(current)) return null;
4016
4094
  return deserializeChunks(payload.chunks ?? []);
@@ -4025,7 +4103,7 @@ function saveRagCache(chunks, filesChecksum) {
4025
4103
  files_checksum: filesChecksum,
4026
4104
  chunks: serializeChunks(chunks)
4027
4105
  };
4028
- import_node_fs11.default.writeFileSync(getRagCachePath(), JSON.stringify(payload), "utf-8");
4106
+ import_node_fs12.default.writeFileSync(getRagCachePath(), JSON.stringify(payload), "utf-8");
4029
4107
  } catch {
4030
4108
  console.error("[runsec] RAG cache write failed; continuing in-memory only");
4031
4109
  }
@@ -4189,7 +4267,7 @@ function selectSkillsForContext(opts) {
4189
4267
  const query = [opts.question ?? "", opts.file_path ?? "", opts.file_content ?? ""].filter(Boolean).join("\n");
4190
4268
  const semScores = query ? semanticSkillScores(query) : {};
4191
4269
  const keywordBoosts = query ? skillBoosts(query) : {};
4192
- const suffix = import_node_path18.default.extname(opts.file_path ?? "").toLowerCase();
4270
+ const suffix = import_node_path19.default.extname(opts.file_path ?? "").toLowerCase();
4193
4271
  const hay = query.toLowerCase();
4194
4272
  const ranked = [];
4195
4273
  for (const [sid, manifest] of Object.entries(manifests)) {
@@ -4226,20 +4304,20 @@ function findPatternChunkByMetric(metricId) {
4226
4304
 
4227
4305
  // src/skills/skillsApi.ts
4228
4306
  function loadComplianceSnapshot() {
4229
- const p = import_node_path19.default.join(getDataDirectory(), "rule-compliance-map.json");
4230
- if (!import_node_fs12.default.existsSync(p)) return {};
4307
+ const p = import_node_path20.default.join(getDataDirectory(), "rule-compliance-map.json");
4308
+ if (!import_node_fs13.default.existsSync(p)) return {};
4231
4309
  try {
4232
- return JSON.parse(import_node_fs12.default.readFileSync(p, "utf-8"));
4310
+ return JSON.parse(import_node_fs13.default.readFileSync(p, "utf-8"));
4233
4311
  } catch {
4234
4312
  return {};
4235
4313
  }
4236
4314
  }
4237
4315
  function extractTestbedExample(metricId, examplePath, skillsRoot) {
4238
4316
  if (!examplePath) return "";
4239
- const p = import_node_path19.default.isAbsolute(examplePath) ? examplePath : import_node_path19.default.join(import_node_path19.default.dirname(skillsRoot), "..", "..", examplePath);
4240
- const resolved = import_node_fs12.default.existsSync(p) ? p : import_node_path19.default.join(getDataDirectory(), "..", "..", examplePath);
4241
- if (!import_node_fs12.default.existsSync(resolved)) return "";
4242
- const lines = import_node_fs12.default.readFileSync(resolved, "utf-8").split(/\r?\n/);
4317
+ const p = import_node_path20.default.isAbsolute(examplePath) ? examplePath : import_node_path20.default.join(import_node_path20.default.dirname(skillsRoot), "..", "..", examplePath);
4318
+ const resolved = import_node_fs13.default.existsSync(p) ? p : import_node_path20.default.join(getDataDirectory(), "..", "..", examplePath);
4319
+ if (!import_node_fs13.default.existsSync(resolved)) return "";
4320
+ const lines = import_node_fs13.default.readFileSync(resolved, "utf-8").split(/\r?\n/);
4243
4321
  const needle = `Vulnerable: ${metricId}`;
4244
4322
  for (let idx = 0; idx < lines.length; idx += 1) {
4245
4323
  if (lines[idx].includes(needle)) {
@@ -4291,16 +4369,16 @@ function getSkillContext(args) {
4291
4369
  }
4292
4370
  const manifest = manifests[skill_id];
4293
4371
  const dir = skillDirectory(manifest);
4294
- const indexPath = import_node_path19.default.join(dir, "index.md");
4295
- const patternsPath = import_node_path19.default.join(dir, "patterns.md");
4296
- if (!import_node_fs12.default.existsSync(indexPath) || !import_node_fs12.default.existsSync(patternsPath)) {
4372
+ const indexPath = import_node_path20.default.join(dir, "index.md");
4373
+ const patternsPath = import_node_path20.default.join(dir, "patterns.md");
4374
+ if (!import_node_fs13.default.existsSync(indexPath) || !import_node_fs13.default.existsSync(patternsPath)) {
4297
4375
  return {
4298
4376
  error: `incomplete skill data for ${skill_id}`,
4299
- index_exists: import_node_fs12.default.existsSync(indexPath),
4300
- patterns_exists: import_node_fs12.default.existsSync(patternsPath)
4377
+ index_exists: import_node_fs13.default.existsSync(indexPath),
4378
+ patterns_exists: import_node_fs13.default.existsSync(patternsPath)
4301
4379
  };
4302
4380
  }
4303
- const parsedRows = parsePatternRows(import_node_fs12.default.readFileSync(patternsPath, "utf-8"));
4381
+ const parsedRows = parsePatternRows(import_node_fs13.default.readFileSync(patternsPath, "utf-8"));
4304
4382
  const grouped = {};
4305
4383
  for (const row of parsedRows) {
4306
4384
  const stack = row.stack.trim() || "Generic";
@@ -4313,13 +4391,13 @@ function getSkillContext(args) {
4313
4391
  source: row.source
4314
4392
  });
4315
4393
  }
4316
- const skillsRoot = import_node_path19.default.dirname(dir);
4394
+ const skillsRoot = import_node_path20.default.dirname(dir);
4317
4395
  const response = {
4318
4396
  skill_id,
4319
- index_path: import_node_path19.default.relative(getDataDirectory(), indexPath).replace(/\\/g, "/"),
4320
- patterns_path: import_node_path19.default.relative(getDataDirectory(), patternsPath).replace(/\\/g, "/"),
4321
- index_md: import_node_fs12.default.readFileSync(indexPath, "utf-8"),
4322
- patterns_md: import_node_fs12.default.readFileSync(patternsPath, "utf-8"),
4397
+ index_path: import_node_path20.default.relative(getDataDirectory(), indexPath).replace(/\\/g, "/"),
4398
+ patterns_path: import_node_path20.default.relative(getDataDirectory(), patternsPath).replace(/\\/g, "/"),
4399
+ index_md: import_node_fs13.default.readFileSync(indexPath, "utf-8"),
4400
+ patterns_md: import_node_fs13.default.readFileSync(patternsPath, "utf-8"),
4323
4401
  patterns_by_stack: Object.fromEntries(Object.keys(grouped).sort().map((k) => [k, grouped[k]])),
4324
4402
  agent_system_insert: ANTI_HALLUCINATION_PROMPT,
4325
4403
  compliance_snapshot: loadComplianceSnapshot()
@@ -4359,7 +4437,7 @@ function askGuidance(question) {
4359
4437
  if (!q) return { error: "question is required" };
4360
4438
  const best = semanticSearch(q, 50, "pattern", true);
4361
4439
  const manifests = loadSkillManifests();
4362
- const skillsRoot = import_node_path19.default.join(getDataDirectory(), "skills");
4440
+ const skillsRoot = import_node_path20.default.join(getDataDirectory(), "skills");
4363
4441
  const required = requiredMetricIds(q);
4364
4442
  const out = [];
4365
4443
  const seen = /* @__PURE__ */ new Set();
@@ -4422,7 +4500,7 @@ var STRIDE_LABELS = {
4422
4500
  };
4423
4501
  function readTextSafe(filePath, limit = 4e5) {
4424
4502
  try {
4425
- const data = import_node_fs13.default.readFileSync(filePath, "utf-8");
4503
+ const data = import_node_fs14.default.readFileSync(filePath, "utf-8");
4426
4504
  return data.length > limit ? data.slice(0, limit) : data;
4427
4505
  } catch {
4428
4506
  return "";
@@ -4462,13 +4540,13 @@ function walkFiles2(root, opts) {
4462
4540
  let dir;
4463
4541
  try {
4464
4542
  dir = stack.pop();
4465
- const entries = import_node_fs13.default.readdirSync(dir, { withFileTypes: true });
4543
+ const entries = import_node_fs14.default.readdirSync(dir, { withFileTypes: true });
4466
4544
  for (const ent of entries) {
4467
4545
  if (out.length >= max) break;
4468
- const full = import_node_path20.default.join(dir, ent.name);
4546
+ const full = import_node_path21.default.join(dir, ent.name);
4469
4547
  if (ent.isDirectory()) {
4470
4548
  if (!skip.has(ent.name)) stack.push(full);
4471
- } else if (ent.isFile() && exts.has(import_node_path20.default.extname(ent.name).toLowerCase())) {
4549
+ } else if (ent.isFile() && exts.has(import_node_path21.default.extname(ent.name).toLowerCase())) {
4472
4550
  out.push(full);
4473
4551
  }
4474
4552
  }
@@ -4509,7 +4587,7 @@ function syftPackages(payload) {
4509
4587
  return out.sort((a, b) => a.name.localeCompare(b.name));
4510
4588
  }
4511
4589
  function collectRepoThreatSignals(scanRoot, syftPayload) {
4512
- const root = import_node_path20.default.resolve(scanRoot);
4590
+ const root = import_node_path21.default.resolve(scanRoot);
4513
4591
  const scanRel = ".";
4514
4592
  const signals = {
4515
4593
  scan_root: scanRel,
@@ -4531,7 +4609,7 @@ function collectRepoThreatSignals(scanRoot, syftPayload) {
4531
4609
  }
4532
4610
  };
4533
4611
  try {
4534
- for (const ent of import_node_fs13.default.readdirSync(root, { withFileTypes: true })) {
4612
+ for (const ent of import_node_fs14.default.readdirSync(root, { withFileTypes: true })) {
4535
4613
  if (ent.isDirectory() && !ent.name.startsWith(".")) {
4536
4614
  signals.top_level_dirs.push(ent.name);
4537
4615
  if (signals.top_level_dirs.length >= 40) break;
@@ -4550,16 +4628,16 @@ function collectRepoThreatSignals(scanRoot, syftPayload) {
4550
4628
  const dir = stack.pop();
4551
4629
  let entries;
4552
4630
  try {
4553
- entries = import_node_fs13.default.readdirSync(dir, { withFileTypes: true });
4631
+ entries = import_node_fs14.default.readdirSync(dir, { withFileTypes: true });
4554
4632
  } catch {
4555
4633
  continue;
4556
4634
  }
4557
4635
  for (const ent of entries) {
4558
- const full = import_node_path20.default.join(dir, ent.name);
4636
+ const full = import_node_path21.default.join(dir, ent.name);
4559
4637
  if (ent.isDirectory()) {
4560
4638
  if (!skip.has(ent.name) && !ent.name.startsWith(".")) stack.push(full);
4561
4639
  } else if (ent.isFile() && patternNames.includes(ent.name)) {
4562
- const rel = import_node_path20.default.relative(root, full).replace(/\\/g, "/");
4640
+ const rel = import_node_path21.default.relative(root, full).replace(/\\/g, "/");
4563
4641
  if (!rel.startsWith("..")) handler(full, rel);
4564
4642
  }
4565
4643
  }
@@ -4571,30 +4649,30 @@ function collectRepoThreatSignals(scanRoot, syftPayload) {
4571
4649
  });
4572
4650
  globWalk(["requirements.txt", "pyproject.toml", "go.mod"], (full, rel) => {
4573
4651
  addKey(rel);
4574
- if (import_node_path20.default.basename(full) === "requirements.txt") {
4652
+ if (import_node_path21.default.basename(full) === "requirements.txt") {
4575
4653
  for (const d of parseRequirementsDeps(full)) deps.add(d);
4576
- } else if (import_node_path20.default.basename(full) === "pyproject.toml") {
4654
+ } else if (import_node_path21.default.basename(full) === "pyproject.toml") {
4577
4655
  const txt = readTextSafe(full, 8e4).toLowerCase();
4578
4656
  for (const m of txt.matchAll(/['"]([a-zA-Z0-9_.\-]+)['"]/g)) deps.add(m[1].toLowerCase());
4579
4657
  }
4580
4658
  });
4581
4659
  for (const name of ["Dockerfile", "docker-compose.yml", "docker-compose.yaml"]) {
4582
- const fp = import_node_path20.default.join(root, name);
4583
- if (import_node_fs13.default.existsSync(fp)) {
4660
+ const fp = import_node_path21.default.join(root, name);
4661
+ if (import_node_fs14.default.existsSync(fp)) {
4584
4662
  addKey(name);
4585
4663
  if (name === "Dockerfile") signals.flags.has_dockerfile = true;
4586
4664
  else signals.flags.has_compose = true;
4587
4665
  }
4588
4666
  }
4589
4667
  for (const full of walkFiles2(root, { maxFiles: 80, extensions: /* @__PURE__ */ new Set([".yaml", ".yml"]) })) {
4590
- const base = import_node_path20.default.basename(full).toLowerCase();
4668
+ const base = import_node_path21.default.basename(full).toLowerCase();
4591
4669
  if (base.includes("deploy") || base.includes("helm") || base.includes("k8s") || base.includes("values")) {
4592
4670
  signals.flags.has_k8s_yaml = true;
4593
- addKey(import_node_path20.default.relative(root, full).replace(/\\/g, "/"));
4671
+ addKey(import_node_path21.default.relative(root, full).replace(/\\/g, "/"));
4594
4672
  break;
4595
4673
  }
4596
4674
  }
4597
- if (import_node_fs13.default.existsSync(import_node_path20.default.join(root, ".github", "workflows"))) {
4675
+ if (import_node_fs14.default.existsSync(import_node_path21.default.join(root, ".github", "workflows"))) {
4598
4676
  signals.flags.has_github_workflows = true;
4599
4677
  }
4600
4678
  for (const pkg of signals.syft_packages) {
@@ -4626,12 +4704,12 @@ ${context}
4626
4704
  ${repoFp}`).digest("hex");
4627
4705
  }
4628
4706
  function loadThreatModelCache(workspaceRoot) {
4629
- const p = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4630
- if (!import_node_fs13.default.existsSync(p)) {
4707
+ const p = import_node_path21.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4708
+ if (!import_node_fs14.default.existsSync(p)) {
4631
4709
  return { schema_version: THREAT_MODEL_CACHE_SCHEMA_VERSION, entries: {} };
4632
4710
  }
4633
4711
  try {
4634
- const data = JSON.parse(import_node_fs13.default.readFileSync(p, "utf-8"));
4712
+ const data = JSON.parse(import_node_fs14.default.readFileSync(p, "utf-8"));
4635
4713
  if (data.schema_version !== THREAT_MODEL_CACHE_SCHEMA_VERSION) {
4636
4714
  return { schema_version: THREAT_MODEL_CACHE_SCHEMA_VERSION, entries: {} };
4637
4715
  }
@@ -4642,7 +4720,7 @@ function loadThreatModelCache(workspaceRoot) {
4642
4720
  }
4643
4721
  }
4644
4722
  function saveThreatModelCache(workspaceRoot, cacheKey, markdown, baseline) {
4645
- const p = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4723
+ const p = import_node_path21.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE);
4646
4724
  const payload = loadThreatModelCache(workspaceRoot);
4647
4725
  payload.entries[cacheKey] = {
4648
4726
  markdown,
@@ -4651,7 +4729,7 @@ function saveThreatModelCache(workspaceRoot, cacheKey, markdown, baseline) {
4651
4729
  repo_fingerprint: String(baseline.repo_fingerprint ?? "")
4652
4730
  };
4653
4731
  payload.baseline = baseline;
4654
- import_node_fs13.default.writeFileSync(p, JSON.stringify(payload, null, 2), "utf-8");
4732
+ import_node_fs14.default.writeFileSync(p, JSON.stringify(payload, null, 2), "utf-8");
4655
4733
  return p;
4656
4734
  }
4657
4735
  function strideClassifyClause(clause) {
@@ -4855,7 +4933,7 @@ function loadRepoCrosscheckHaystack(scanRoot, maxChars = 35e4) {
4855
4933
  let total = 0;
4856
4934
  const files = walkFiles2(scanRoot, { maxFiles: 220 });
4857
4935
  for (const full of files) {
4858
- const rel = import_node_path20.default.relative(scanRoot, full).replace(/\\/g, "/");
4936
+ const rel = import_node_path21.default.relative(scanRoot, full).replace(/\\/g, "/");
4859
4937
  const snippet = readTextSafe(full, 14e3);
4860
4938
  const block = `
4861
4939
  --- ${rel} ---
@@ -5065,8 +5143,8 @@ function generateThreatModelMarkdown(profile, baseline, signals, ragKeywords, ca
5065
5143
  `;
5066
5144
  }
5067
5145
  async function runThreatModelEngine(opts) {
5068
- const workspaceRoot = import_node_path20.default.resolve(opts.workspace_path);
5069
- const projectName = (opts.project_name ?? import_node_path20.default.basename(workspaceRoot)).trim() || "project";
5146
+ const workspaceRoot = import_node_path21.default.resolve(opts.workspace_path);
5147
+ const projectName = (opts.project_name ?? import_node_path21.default.basename(workspaceRoot)).trim() || "project";
5070
5148
  const userContext = (opts.context ?? "").trim();
5071
5149
  const profile = detectSecurityProfile(projectName, userContext);
5072
5150
  const baseline = ARCHITECTURE_BASELINES[profile];
@@ -5081,13 +5159,13 @@ Project: ${projectName}`;
5081
5159
  const cache = loadThreatModelCache(workspaceRoot);
5082
5160
  const cached = cache.entries[cacheKey];
5083
5161
  if (cached?.markdown) {
5084
- const reportPath2 = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
5085
- import_node_fs13.default.writeFileSync(reportPath2, cached.markdown, "utf-8");
5162
+ const reportPath2 = import_node_path21.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
5163
+ import_node_fs14.default.writeFileSync(reportPath2, cached.markdown, "utf-8");
5086
5164
  return {
5087
5165
  profile,
5088
5166
  markdown: cached.markdown,
5089
5167
  report_path: reportPath2,
5090
- cache_path: import_node_path20.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE),
5168
+ cache_path: import_node_path21.default.join(workspaceRoot, THREAT_MODEL_CACHE_FILE),
5091
5169
  cache_hit: true,
5092
5170
  cache_key: cacheKey,
5093
5171
  repo_fingerprint: repoFp,
@@ -5119,8 +5197,8 @@ Project: ${projectName}`;
5119
5197
  rag_keywords: [...ragKeywords].sort().slice(0, 40)
5120
5198
  };
5121
5199
  const cachePath = saveThreatModelCache(workspaceRoot, cacheKey, markdown, baselinePayload);
5122
- const reportPath = import_node_path20.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
5123
- import_node_fs13.default.writeFileSync(reportPath, markdown, "utf-8");
5200
+ const reportPath = import_node_path21.default.join(workspaceRoot, THREAT_MODEL_REPORT_FILE);
5201
+ import_node_fs14.default.writeFileSync(reportPath, markdown, "utf-8");
5124
5202
  return {
5125
5203
  profile,
5126
5204
  markdown,
@@ -5335,18 +5413,18 @@ function buildSecurityReviewMarkdown(opts) {
5335
5413
  return out.join("\n");
5336
5414
  }
5337
5415
  function resolveSecurityReviewPath(workspacePath) {
5338
- const base = workspacePath?.trim() ? import_node_path21.default.resolve(workspacePath) : process.cwd();
5339
- return import_node_path21.default.join(base, SECURITY_REVIEW_REPORT_FILE);
5416
+ const base = workspacePath?.trim() ? import_node_path22.default.resolve(workspacePath) : process.cwd();
5417
+ return import_node_path22.default.join(base, SECURITY_REVIEW_REPORT_FILE);
5340
5418
  }
5341
5419
  function writeSecurityReviewReport(markdown, workspacePath) {
5342
5420
  const reportPath = resolveSecurityReviewPath(workspacePath);
5343
- import_node_fs14.default.writeFileSync(reportPath, markdown, "utf-8");
5421
+ import_node_fs15.default.writeFileSync(reportPath, markdown, "utf-8");
5344
5422
  console.error(`[runsec] wrote security review to: ${reportPath}`);
5345
5423
  return reportPath;
5346
5424
  }
5347
5425
  async function executeSecurityReview(opts) {
5348
- const workspaceRoot = import_node_path21.default.resolve(opts.workspace_path);
5349
- const projectName = (opts.project_name ?? import_node_path21.default.basename(workspaceRoot)).trim() || import_node_path21.default.basename(workspaceRoot);
5426
+ const workspaceRoot = import_node_path22.default.resolve(opts.workspace_path);
5427
+ const projectName = (opts.project_name ?? import_node_path22.default.basename(workspaceRoot)).trim() || import_node_path22.default.basename(workspaceRoot);
5350
5428
  console.error("[runsec] security review: starting unified scan + STRIDE threat model");
5351
5429
  const [audit, threatModel] = await Promise.all([
5352
5430
  executeAudit("runsec_audit_general", {
@@ -5379,20 +5457,20 @@ async function executeSecurityReview(opts) {
5379
5457
  }
5380
5458
 
5381
5459
  // src/engine/remediation.ts
5382
- var import_node_fs16 = __toESM(require("fs"));
5383
- var import_node_path23 = __toESM(require("path"));
5460
+ var import_node_fs17 = __toESM(require("fs"));
5461
+ var import_node_path24 = __toESM(require("path"));
5384
5462
 
5385
5463
  // src/skills/metricLookup.ts
5386
- var import_node_fs15 = __toESM(require("fs"));
5387
- var import_node_path22 = __toESM(require("path"));
5464
+ var import_node_fs16 = __toESM(require("fs"));
5465
+ var import_node_path23 = __toESM(require("path"));
5388
5466
  function findMetricRow(metricId) {
5389
5467
  const mid = metricId.trim().toUpperCase();
5390
5468
  if (!mid) return null;
5391
5469
  const manifests = loadSkillManifests();
5392
5470
  for (const [, manifest] of Object.entries(manifests)) {
5393
- const patternsPath = import_node_path22.default.join(skillDirectory(manifest), "patterns.md");
5394
- if (!import_node_fs15.default.existsSync(patternsPath)) continue;
5395
- const rows = parsePatternRows(import_node_fs15.default.readFileSync(patternsPath, "utf-8"));
5471
+ const patternsPath = import_node_path23.default.join(skillDirectory(manifest), "patterns.md");
5472
+ if (!import_node_fs16.default.existsSync(patternsPath)) continue;
5473
+ const rows = parsePatternRows(import_node_fs16.default.readFileSync(patternsPath, "utf-8"));
5396
5474
  const row = rows.find((r) => r.metric_id.toUpperCase() === mid);
5397
5475
  if (row) return row;
5398
5476
  }
@@ -5405,12 +5483,12 @@ function normalizeRelPath2(p) {
5405
5483
  return p.replace(/\\/g, "/").replace(/^\.\/+/, "");
5406
5484
  }
5407
5485
  function resolveTargetFile(workspaceRoot, filePath) {
5408
- const root = import_node_path23.default.resolve(workspaceRoot);
5486
+ const root = import_node_path24.default.resolve(workspaceRoot);
5409
5487
  const rel = normalizeRelPath2(filePath.trim());
5410
5488
  if (!rel) return { error: "file_path is required" };
5411
- const abs = import_node_path23.default.resolve(root, rel);
5412
- const relFromRoot = import_node_path23.default.relative(root, abs).replace(/\\/g, "/");
5413
- if (relFromRoot.startsWith("..") || import_node_path23.default.isAbsolute(relFromRoot)) {
5489
+ const abs = import_node_path24.default.resolve(root, rel);
5490
+ const relFromRoot = import_node_path24.default.relative(root, abs).replace(/\\/g, "/");
5491
+ if (relFromRoot.startsWith("..") || import_node_path24.default.isAbsolute(relFromRoot)) {
5414
5492
  return { error: `file_path must be inside workspace: ${root}` };
5415
5493
  }
5416
5494
  for (const seg of BLOCKED_PATH_SEGMENTS) {
@@ -5418,8 +5496,8 @@ function resolveTargetFile(workspaceRoot, filePath) {
5418
5496
  return { error: `refusing to modify path under blocked segment: ${seg}` };
5419
5497
  }
5420
5498
  }
5421
- if (!import_node_fs16.default.existsSync(abs)) return { error: `path does not exist: ${relFromRoot}` };
5422
- if (!import_node_fs16.default.statSync(abs).isFile()) return { error: `path is not a file: ${relFromRoot}` };
5499
+ if (!import_node_fs17.default.existsSync(abs)) return { error: `path does not exist: ${relFromRoot}` };
5500
+ if (!import_node_fs17.default.statSync(abs).isFile()) return { error: `path is not a file: ${relFromRoot}` };
5423
5501
  return { abs, rel: relFromRoot };
5424
5502
  }
5425
5503
  function normalizeNewlines(text) {
@@ -5486,12 +5564,12 @@ function locateTargetInFile(content, target) {
5486
5564
  }
5487
5565
  function writeBackup(absPath) {
5488
5566
  const backupPath = `${absPath}.bak`;
5489
- import_node_fs16.default.copyFileSync(absPath, backupPath);
5567
+ import_node_fs17.default.copyFileSync(absPath, backupPath);
5490
5568
  return backupPath;
5491
5569
  }
5492
5570
  function applyDvs001Fix(absPath) {
5493
- const rel = import_node_path23.default.basename(absPath);
5494
- const lines = import_node_fs16.default.readFileSync(absPath, "utf-8").split(/\r?\n/);
5571
+ const rel = import_node_path24.default.basename(absPath);
5572
+ const lines = import_node_fs17.default.readFileSync(absPath, "utf-8").split(/\r?\n/);
5495
5573
  const newLines = lines.filter((ln) => !/^\s*user\s+root\s*$/i.test(ln.trim()));
5496
5574
  const hasNonRootUser = newLines.some(
5497
5575
  (ln) => /^\s*user\s+\S+\s*$/i.test(ln.trim()) && !/^\s*user\s+root\s*$/i.test(ln.trim())
@@ -5517,7 +5595,7 @@ function applyDvs001Fix(absPath) {
5517
5595
  };
5518
5596
  }
5519
5597
  const backupPath = writeBackup(absPath);
5520
- import_node_fs16.default.writeFileSync(absPath, `${newLines.join("\n")}
5598
+ import_node_fs17.default.writeFileSync(absPath, `${newLines.join("\n")}
5521
5599
  `, "utf-8");
5522
5600
  return {
5523
5601
  status: "fixed",
@@ -5594,7 +5672,7 @@ function applyRemediation(args) {
5594
5672
  fix_template: row.fix_template
5595
5673
  };
5596
5674
  }
5597
- const original = import_node_fs16.default.readFileSync(resolved.abs, "utf-8");
5675
+ const original = import_node_fs17.default.readFileSync(resolved.abs, "utf-8");
5598
5676
  const { index, count } = locateTargetInFile(original, target);
5599
5677
  if (count === 0) {
5600
5678
  return {
@@ -5621,7 +5699,7 @@ function applyRemediation(args) {
5621
5699
  const backupPath = writeBackup(resolved.abs);
5622
5700
  const ending = original.includes("\r\n") ? "\r\n" : "\n";
5623
5701
  const outText = updated.split("\n").join(ending);
5624
- import_node_fs16.default.writeFileSync(resolved.abs, outText.endsWith(ending) || !original.endsWith(ending) ? outText : outText + ending, "utf-8");
5702
+ import_node_fs17.default.writeFileSync(resolved.abs, outText.endsWith(ending) || !original.endsWith(ending) ? outText : outText + ending, "utf-8");
5625
5703
  return {
5626
5704
  status: "fixed",
5627
5705
  message: "Remediation applied after exact target verification; backup created",
@@ -5823,7 +5901,7 @@ function isRemediationTool(name) {
5823
5901
  }
5824
5902
 
5825
5903
  // src/hubUploadUrl.ts
5826
- var import_node_fs17 = __toESM(require("fs"));
5904
+ var import_node_fs18 = __toESM(require("fs"));
5827
5905
  var HUB_UPLOAD_REPORT_PATH = "/api/mcp/upload-report";
5828
5906
  var DEFAULT_PRODUCTION_HUB_ORIGIN = "https://runsec.io";
5829
5907
  var DEFAULT_DOCKER_INTERNAL_HUB_ORIGIN = "http://frontend:3000";
@@ -5860,7 +5938,7 @@ function colocatedHubOrigin() {
5860
5938
  }
5861
5939
  function isDockerRuntime() {
5862
5940
  try {
5863
- return import_node_fs17.default.existsSync("/.dockerenv");
5941
+ return import_node_fs18.default.existsSync("/.dockerenv");
5864
5942
  } catch {
5865
5943
  return false;
5866
5944
  }
@@ -5947,6 +6025,10 @@ function dockerInternalAlternateUploadUrl(url) {
5947
6025
  }
5948
6026
  }
5949
6027
 
6028
+ // src/telemetryClient.ts
6029
+ var import_node_http = __toESM(require("http"));
6030
+ var import_node_https = __toESM(require("https"));
6031
+
5950
6032
  // src/complianceScores.ts
5951
6033
  var SEVERITY_PENALTY = {
5952
6034
  CRITICAL: 15,
@@ -6055,14 +6137,78 @@ function logHubSyncFailure(message, error, targetUrl) {
6055
6137
  }
6056
6138
  console.error(`[runsec] Hub telemetry upload error (scan saved locally): ${message}`);
6057
6139
  }
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)
6140
+ function postHubUploadNodeHttp(url, apiKey, payload) {
6141
+ return new Promise((resolve, reject) => {
6142
+ const body = JSON.stringify(payload);
6143
+ let parsed;
6144
+ try {
6145
+ parsed = new URL(url);
6146
+ } catch (err) {
6147
+ reject(err);
6148
+ return;
6149
+ }
6150
+ const headers = {
6151
+ ...hubAuthHeaders(apiKey),
6152
+ "Content-Length": String(Buffer.byteLength(body))
6153
+ };
6154
+ const lib = parsed.protocol === "https:" ? import_node_https.default : import_node_http.default;
6155
+ const port = parsed.port !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
6156
+ const req = lib.request(
6157
+ {
6158
+ hostname: parsed.hostname,
6159
+ port,
6160
+ path: `${parsed.pathname}${parsed.search}`,
6161
+ method: "POST",
6162
+ headers,
6163
+ timeout: HUB_UPLOAD_TIMEOUT_MS
6164
+ },
6165
+ (res) => {
6166
+ const chunks = [];
6167
+ res.on("data", (chunk) => chunks.push(chunk));
6168
+ res.on("end", () => {
6169
+ const text = Buffer.concat(chunks).toString("utf8");
6170
+ const status = res.statusCode ?? 0;
6171
+ resolve({
6172
+ ok: status >= 200 && status < 300,
6173
+ status,
6174
+ statusText: res.statusMessage ?? "",
6175
+ text: async () => text,
6176
+ json: async () => JSON.parse(text || "{}")
6177
+ });
6178
+ });
6179
+ }
6180
+ );
6181
+ req.on("error", reject);
6182
+ req.on("timeout", () => {
6183
+ req.destroy();
6184
+ reject(Object.assign(new Error("Hub upload request timeout"), { code: "ETIMEDOUT" }));
6185
+ });
6186
+ req.write(body);
6187
+ req.end();
6064
6188
  });
6065
6189
  }
6190
+ function preferNodeHttpUpload() {
6191
+ if (process.platform === "win32") return true;
6192
+ if (process.env.RUNSEC_HUB_USE_NODE_HTTP === "1") return true;
6193
+ return false;
6194
+ }
6195
+ async function postHubUpload(url, apiKey, payload) {
6196
+ if (preferNodeHttpUpload()) {
6197
+ return postHubUploadNodeHttp(url, apiKey, payload);
6198
+ }
6199
+ try {
6200
+ return await fetch(url, {
6201
+ method: "POST",
6202
+ headers: hubAuthHeaders(apiKey),
6203
+ body: JSON.stringify(payload),
6204
+ signal: AbortSignal.timeout(HUB_UPLOAD_TIMEOUT_MS)
6205
+ });
6206
+ } catch (fetchError) {
6207
+ console.error("[runsec] fetch() failed \u2014 retrying Hub upload via node:http(s)");
6208
+ dumpHubNetworkError(fetchError, url);
6209
+ return postHubUploadNodeHttp(url, apiKey, payload);
6210
+ }
6211
+ }
6066
6212
  function countSeverityMetrics(findings) {
6067
6213
  const metrics = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
6068
6214
  for (const f of findings) {
@@ -6516,7 +6662,9 @@ CRITICAL INSTRUCTIONS FOR LLM:
6516
6662
  async function main() {
6517
6663
  const key = getApiKey();
6518
6664
  process.env.RUNSEC_API_KEY = key;
6519
- console.error(`[runsec] MCP @runsec/mcp starting \u2014 Hub upload target: ${resolveHubUploadUrl()}`);
6665
+ console.error(
6666
+ `[runsec] MCP @runsec/mcp v${RUNSEC_MCP_VERSION} starting \u2014 Hub upload target: ${resolveHubUploadUrl()}`
6667
+ );
6520
6668
  await verifyApiKey(key);
6521
6669
  const summary = validateRules();
6522
6670
  console.error("Rules registry validated:", summary);
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@runsec/mcp",
3
- "version": "1.0.79",
3
+ "version": "1.0.82",
4
4
  "main": "dist/index.js",
5
5
  "files": [
6
+ "package.json",
6
7
  "dist",
7
8
  "bin/runsec-mcp.cjs",
8
9
  "bin/ensure-binaries.cjs",