secufusion-mcp 1.0.60 → 1.1.0

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/index.js +136 -26
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -2794,6 +2794,7 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
2794
2794
  // ══════════════════════════════════════════════════════════════════════
2795
2795
  // ── P1: TENANT_ISOLATION ──────────────────────────────────────────────
2796
2796
  const p1 = [];
2797
+ const p1Suppressed = [];
2797
2798
  if (!skip_checks.includes("tenant_isolation")) {
2798
2799
  for (const f of fileRecords.filter(f => f.isRepo)) {
2799
2800
  const queryAnnotationLines = [];
@@ -2804,23 +2805,69 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
2804
2805
  f.lines.forEach((line, idx) => {
2805
2806
  const dm = line.match(/(?:find|delete|count|exists)\w+By(\w+)/);
2806
2807
  if (dm && !dm[1].toLowerCase().includes("tenant")) {
2807
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2808
- message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
2809
- suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
2808
+ let suppressionReason = "";
2809
+ for (let i = Math.max(0, idx - 5); i < idx; i++) {
2810
+ const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
2811
+ if (match)
2812
+ suppressionReason = match[1];
2813
+ }
2814
+ if (suppressionReason) {
2815
+ p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: dm[0] });
2816
+ }
2817
+ else {
2818
+ p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: idx + 1,
2819
+ message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
2820
+ suggestion: "Rename to include AndTenantId, e.g. findByIdAndTenantId(UUID id, UUID tenantId)" });
2821
+ }
2810
2822
  }
2811
2823
  });
2812
2824
  queryAnnotationLines.forEach(ai => {
2813
2825
  const block = f.lines.slice(ai, ai + 5).join(" ");
2814
2826
  if (!/tenantId|tenant_id|:tenantId/i.test(block)) {
2815
- p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
2816
- message: `@Query at line ${ai + 1} has no tenantId filter`,
2817
- suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
2827
+ let suppressionReason = "";
2828
+ for (let i = Math.max(0, ai - 5); i < ai; i++) {
2829
+ const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
2830
+ if (match)
2831
+ suppressionReason = match[1];
2832
+ }
2833
+ if (suppressionReason) {
2834
+ p1Suppressed.push({ file: f.file_path, line: ai + 1, reason: suppressionReason, method: "@Query method" });
2835
+ }
2836
+ else {
2837
+ p1.push({ check: "TENANT_ISOLATION", severity: "BLOCKING", file: f.file_path, line: ai + 1,
2838
+ message: `@Query at line ${ai + 1} has no tenantId filter`,
2839
+ suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query" });
2840
+ }
2818
2841
  }
2819
2842
  });
2820
2843
  }
2821
2844
  }
2822
2845
  mechanical.push(...p1);
2823
- passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length });
2846
+ passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1.length === 0, count: p1.length, suppressedCount: p1Suppressed.length });
2847
+ // Auto-log suppressions to decisions.json
2848
+ if (p1Suppressed.length > 0) {
2849
+ const tasksRoot = resolve(TASKS_DIR);
2850
+ const dir = resolveTaskFolder(work_item_id, tasksRoot);
2851
+ if (dir) {
2852
+ const decisionsPath = path.join(dir, "decisions.json");
2853
+ const decisions = readJson(decisionsPath) || [];
2854
+ let modified = false;
2855
+ for (const sup of p1Suppressed) {
2856
+ const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
2857
+ if (!decisions.some(d => d.decision === desc)) {
2858
+ decisions.push({
2859
+ id: decisions.length + 1,
2860
+ decision: desc,
2861
+ rationale: sup.reason,
2862
+ logged_at: new Date().toISOString()
2863
+ });
2864
+ modified = true;
2865
+ }
2866
+ }
2867
+ if (modified)
2868
+ writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
2869
+ }
2870
+ }
2824
2871
  // ── P2: N_PLUS_ONE ────────────────────────────────────────────────────
2825
2872
  const p2 = [];
2826
2873
  if (!skip_checks.includes("n_plus_one")) {
@@ -3278,7 +3325,8 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
3278
3325
  for (let i = 0; i < passResults.length; i++) {
3279
3326
  const p = passResults[i];
3280
3327
  const icon = p.skipped ? "⏭️ SKIP" : p.passed ? "✅ PASS" : "❌ FAIL";
3281
- out += `| P${i + 1} | ${p.name} | ${icon} | ${p.skipped ? "—" : p.count} |\n`;
3328
+ const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
3329
+ out += `| P${i + 1} | ${p.name} | ${icon} | ${issueText} |\n`;
3282
3330
  }
3283
3331
  if (blockingMechanical.length > 0) {
3284
3332
  out += `\n#### ❌ Blocking Mechanical Findings\n`;
@@ -3287,6 +3335,13 @@ server.tool("run_pre_pr_checks_with_reviewer_agent", "Unified, single-call PR re
3287
3335
  out += `> ${f.message}\n> 💡 *${f.suggestion}*\n`;
3288
3336
  }
3289
3337
  }
3338
+ if (p1Suppressed.length > 0) {
3339
+ out += `\n#### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
3340
+ for (const sup of p1Suppressed) {
3341
+ out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
3342
+ out += `> Suppressed — ${sup.reason}\n`;
3343
+ }
3344
+ }
3290
3345
  const warnings1 = mechanical.filter(f => f.severity === "WARNING");
3291
3346
  if (warnings1.length > 0) {
3292
3347
  out += `\n#### ⚠️ Warnings\n`;
@@ -3487,6 +3542,7 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
3487
3542
  // Rule: every Repository method must filter by tenantId
3488
3543
  // ══════════════════════════════════════════════════════════════════════
3489
3544
  const p1Findings = [];
3545
+ const p1Suppressed = [];
3490
3546
  if (!skip_checks.includes("tenant_isolation")) {
3491
3547
  for (const f of fileRecords.filter(f => f.isRepo)) {
3492
3548
  // Find method signatures in repository interface/class
@@ -3502,14 +3558,25 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
3502
3558
  if (derivedMatch) {
3503
3559
  const byClause = derivedMatch[1].toLowerCase();
3504
3560
  if (!byClause.includes("tenantid") && !byClause.includes("tenant")) {
3505
- p1Findings.push({
3506
- check: "TENANT_ISOLATION",
3507
- severity: "BLOCKING",
3508
- file: f.file_path,
3509
- line: idx + 1,
3510
- message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
3511
- suggestion: "Rename to include AndTenantId: e.g. findByIdAndTenantId(UUID id, UUID tenantId)",
3512
- });
3561
+ let suppressionReason = "";
3562
+ for (let i = Math.max(0, idx - 5); i < idx; i++) {
3563
+ const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
3564
+ if (match)
3565
+ suppressionReason = match[1];
3566
+ }
3567
+ if (suppressionReason) {
3568
+ p1Suppressed.push({ file: f.file_path, line: idx + 1, reason: suppressionReason, method: derivedMatch[0] });
3569
+ }
3570
+ else {
3571
+ p1Findings.push({
3572
+ check: "TENANT_ISOLATION",
3573
+ severity: "BLOCKING",
3574
+ file: f.file_path,
3575
+ line: idx + 1,
3576
+ message: `Derived query \`${line.trim()}\` does not filter by tenantId`,
3577
+ suggestion: "Rename to include AndTenantId: e.g. findByIdAndTenantId(UUID id, UUID tenantId)",
3578
+ });
3579
+ }
3513
3580
  }
3514
3581
  }
3515
3582
  });
@@ -3518,20 +3585,55 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
3518
3585
  // Look at this line and next 3 lines for the query body
3519
3586
  const queryBlock = f.lines.slice(annotIdx, annotIdx + 5).join(" ");
3520
3587
  if (!/tenantId|tenant_id|:tenantId/i.test(queryBlock)) {
3521
- p1Findings.push({
3522
- check: "TENANT_ISOLATION",
3523
- severity: "BLOCKING",
3524
- file: f.file_path,
3525
- line: annotIdx + 1,
3526
- message: `@Query at line ${annotIdx + 1} does not include tenantId filter`,
3527
- suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query",
3528
- });
3588
+ let suppressionReason = "";
3589
+ for (let i = Math.max(0, annotIdx - 5); i < annotIdx; i++) {
3590
+ const match = f.lines[i].match(/@TenantScopeException\s*\(\s*"([^"]+)"\s*\)/i);
3591
+ if (match)
3592
+ suppressionReason = match[1];
3593
+ }
3594
+ if (suppressionReason) {
3595
+ p1Suppressed.push({ file: f.file_path, line: annotIdx + 1, reason: suppressionReason, method: "@Query method" });
3596
+ }
3597
+ else {
3598
+ p1Findings.push({
3599
+ check: "TENANT_ISOLATION",
3600
+ severity: "BLOCKING",
3601
+ file: f.file_path,
3602
+ line: annotIdx + 1,
3603
+ message: `@Query at line ${annotIdx + 1} does not include tenantId filter`,
3604
+ suggestion: "Add WHERE e.tenantId = :tenantId to the JPQL/SQL query",
3605
+ });
3606
+ }
3529
3607
  }
3530
3608
  });
3531
3609
  }
3532
3610
  }
3533
3611
  allFindings.push(...p1Findings);
3534
- passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1Findings.length === 0, count: p1Findings.length });
3612
+ passResults.push({ name: "TENANT_ISOLATION", skipped: skip_checks.includes("tenant_isolation"), passed: p1Findings.length === 0, count: p1Findings.length, suppressedCount: p1Suppressed.length });
3613
+ // Auto-log suppressions to decisions.json
3614
+ if (p1Suppressed.length > 0 && work_item_id) {
3615
+ const tasksRoot = resolve(TASKS_DIR);
3616
+ const dir = resolveTaskFolder(work_item_id, tasksRoot);
3617
+ if (dir) {
3618
+ const decisionsPath = path.join(dir, "decisions.json");
3619
+ const decisions = readJson(decisionsPath) || [];
3620
+ let modified = false;
3621
+ for (const sup of p1Suppressed) {
3622
+ const desc = `Exempted TENANT_ISOLATION on ${sup.method}`;
3623
+ if (!decisions.some(d => d.decision === desc)) {
3624
+ decisions.push({
3625
+ id: decisions.length + 1,
3626
+ decision: desc,
3627
+ rationale: sup.reason,
3628
+ logged_at: new Date().toISOString()
3629
+ });
3630
+ modified = true;
3631
+ }
3632
+ }
3633
+ if (modified)
3634
+ writeFile(decisionsPath, JSON.stringify(decisions, null, 2));
3635
+ }
3636
+ }
3535
3637
  // ══════════════════════════════════════════════════════════════════════
3536
3638
  // PASS 2 — N_PLUS_ONE
3537
3639
  // Rule: no repository method calls inside for/forEach loops
@@ -3958,7 +4060,8 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
3958
4060
  out += `| Pass | Check | Status | Findings |\n|---|---|---|---|\n`;
3959
4061
  for (const p of passResults) {
3960
4062
  const statusIcon = p.skipped ? "⏭️ SKIPPED" : p.passed ? "✅ PASS" : "❌ FAIL";
3961
- out += `| P${passResults.indexOf(p) + 1} | ${p.name} | ${statusIcon} | ${p.skipped ? "—" : p.count} |\n`;
4063
+ const issueText = p.skipped ? "—" : (p.suppressedCount ? `${p.count} (${p.suppressedCount} suppressed)` : `${p.count}`);
4064
+ out += `| P${passResults.indexOf(p) + 1} | ${p.name} | ${statusIcon} | ${issueText} |\n`;
3962
4065
  }
3963
4066
  // ── Blocking findings detail ──────────────────────────────────────────
3964
4067
  if (blockingFindings.length > 0) {
@@ -3969,6 +4072,13 @@ server.tool("run_pre_pr_checks", "Full-spectrum PR review system — runs BEFORE
3969
4072
  out += `> 💡 *${f.suggestion}*\n`;
3970
4073
  }
3971
4074
  }
4075
+ if (p1Suppressed.length > 0) {
4076
+ out += `\n### 🛡️ Suppressed Findings (${p1Suppressed.length})\n`;
4077
+ for (const sup of p1Suppressed) {
4078
+ out += `\n**[TENANT_ISOLATION]** \`${sup.file}\` line ${sup.line}\n`;
4079
+ out += `> Suppressed — ${sup.reason}\n`;
4080
+ }
4081
+ }
3972
4082
  // ── Warning findings detail ───────────────────────────────────────────
3973
4083
  const warnings = allFindings.filter(f => f.severity === "WARNING");
3974
4084
  if (warnings.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secufusion-mcp",
3
- "version": "1.0.60",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "SecuFusion MCP server - developer workflow tooling with guardrails",
6
6
  "main": "index.js",