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.
- package/index.js +136 -26
- 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
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
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
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
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
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
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
|
-
|
|
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) {
|