vaspera 2.12.0 → 2.14.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/CHANGELOG.md +79 -0
- package/dist/__tests__/antagonist-integration.test.d.ts +6 -0
- package/dist/__tests__/antagonist-integration.test.d.ts.map +1 -0
- package/dist/__tests__/antagonist-integration.test.js +239 -0
- package/dist/__tests__/antagonist-integration.test.js.map +1 -0
- package/dist/__tests__/siem-integration.test.d.ts +7 -0
- package/dist/__tests__/siem-integration.test.d.ts.map +1 -0
- package/dist/__tests__/siem-integration.test.js +285 -0
- package/dist/__tests__/siem-integration.test.js.map +1 -0
- package/dist/agents/antagonist/challenger.d.ts +46 -0
- package/dist/agents/antagonist/challenger.d.ts.map +1 -0
- package/dist/agents/antagonist/challenger.js +257 -0
- package/dist/agents/antagonist/challenger.js.map +1 -0
- package/dist/agents/antagonist/index.d.ts +31 -0
- package/dist/agents/antagonist/index.d.ts.map +1 -0
- package/dist/agents/antagonist/index.js +175 -0
- package/dist/agents/antagonist/index.js.map +1 -0
- package/dist/agents/antagonist/prioritizer.d.ts +27 -0
- package/dist/agents/antagonist/prioritizer.d.ts.map +1 -0
- package/dist/agents/antagonist/prioritizer.js +181 -0
- package/dist/agents/antagonist/prioritizer.js.map +1 -0
- package/dist/agents/antagonist/prompts.d.ts +12 -0
- package/dist/agents/antagonist/prompts.d.ts.map +1 -0
- package/dist/agents/antagonist/prompts.js +155 -0
- package/dist/agents/antagonist/prompts.js.map +1 -0
- package/dist/agents/antagonist/synthesizer.d.ts +34 -0
- package/dist/agents/antagonist/synthesizer.d.ts.map +1 -0
- package/dist/agents/antagonist/synthesizer.js +451 -0
- package/dist/agents/antagonist/synthesizer.js.map +1 -0
- package/dist/agents/antagonist/types.d.ts +145 -0
- package/dist/agents/antagonist/types.d.ts.map +1 -0
- package/dist/agents/antagonist/types.js +63 -0
- package/dist/agents/antagonist/types.js.map +1 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/certification/consensus.test.js +2 -0
- package/dist/certification/consensus.test.js.map +1 -1
- package/dist/certification/store.d.ts.map +1 -1
- package/dist/certification/store.js +6 -1
- package/dist/certification/store.js.map +1 -1
- package/dist/certification/types.d.ts +1 -1
- package/dist/certification/types.d.ts.map +1 -1
- package/dist/certification/types.js +2 -0
- package/dist/certification/types.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +460 -16
- package/dist/index.js.map +1 -1
- package/dist/persistence/__tests__/json-fallback.test.d.ts +5 -0
- package/dist/persistence/__tests__/json-fallback.test.d.ts.map +1 -0
- package/dist/persistence/__tests__/json-fallback.test.js +249 -0
- package/dist/persistence/__tests__/json-fallback.test.js.map +1 -0
- package/dist/persistence/__tests__/persistence.test.js.map +1 -1
- package/dist/persistence/db.d.ts +15 -0
- package/dist/persistence/db.d.ts.map +1 -1
- package/dist/persistence/db.js +59 -10
- package/dist/persistence/db.js.map +1 -1
- package/dist/persistence/index.d.ts +13 -4
- package/dist/persistence/index.d.ts.map +1 -1
- package/dist/persistence/index.js +139 -14
- package/dist/persistence/index.js.map +1 -1
- package/dist/persistence/json-fallback.d.ts +52 -0
- package/dist/persistence/json-fallback.d.ts.map +1 -0
- package/dist/persistence/json-fallback.js +283 -0
- package/dist/persistence/json-fallback.js.map +1 -0
- package/dist/sbom/provenance.test.js +2 -2
- package/dist/sbom/provenance.test.js.map +1 -1
- package/dist/sbom/signing.d.ts.map +1 -1
- package/dist/sbom/signing.js +5 -3
- package/dist/sbom/signing.js.map +1 -1
- package/dist/scanners/ai-code/index.d.ts.map +1 -1
- package/dist/scanners/ai-code/index.js +90 -2
- package/dist/scanners/ai-code/index.js.map +1 -1
- package/dist/scanners/ai-code/types.d.ts +12 -0
- package/dist/scanners/ai-code/types.d.ts.map +1 -1
- package/dist/scanners/eslint.d.ts.map +1 -1
- package/dist/scanners/eslint.js +45 -3
- package/dist/scanners/eslint.js.map +1 -1
- package/dist/scanners/scale/bottleneck-detector.d.ts +13 -2
- package/dist/scanners/scale/bottleneck-detector.d.ts.map +1 -1
- package/dist/scanners/scale/bottleneck-detector.js +199 -72
- package/dist/scanners/scale/bottleneck-detector.js.map +1 -1
- package/dist/scanners/types.d.ts +18 -1
- package/dist/scanners/types.d.ts.map +1 -1
- package/dist/scanners/types.js.map +1 -1
- package/dist/scanners/typescript.d.ts.map +1 -1
- package/dist/scanners/typescript.js +36 -4
- package/dist/scanners/typescript.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,6 +56,8 @@ import { validateProjectPath, PathValidationError } from "./util/paths.js";
|
|
|
56
56
|
// Telemetry and scan registry
|
|
57
57
|
import { trackCertificationStarted, trackCertificationCompleted, trackScannerRun, } from "./telemetry/usage.js";
|
|
58
58
|
import { getRegistry } from "./telemetry/registry.js";
|
|
59
|
+
// False positive feedback
|
|
60
|
+
import { submitFeedback, generateFeedbackReport, getSuppressionSuggestions, FP_REASON_DESCRIPTIONS, } from "./scanners/fp-feedback.js";
|
|
59
61
|
// ---------------------------------------------------------------------------
|
|
60
62
|
// Config
|
|
61
63
|
// ---------------------------------------------------------------------------
|
|
@@ -871,6 +873,64 @@ server.registerTool("certification_scan", {
|
|
|
871
873
|
};
|
|
872
874
|
});
|
|
873
875
|
// ---------------------------------------------------------------------------
|
|
876
|
+
// Tool: Diff-Aware Scanning (CI Integration)
|
|
877
|
+
// ---------------------------------------------------------------------------
|
|
878
|
+
server.registerTool("certification_scan_diff", {
|
|
879
|
+
title: "Diff-Aware Certification Scan",
|
|
880
|
+
description: `Scan only changed files for faster CI/PR workflows.
|
|
881
|
+
|
|
882
|
+
Uses \`git diff\` to detect files changed since a base commit (e.g., origin/main).
|
|
883
|
+
Reduces scan time by 50-90% in typical PR scenarios.
|
|
884
|
+
|
|
885
|
+
**Security-critical files** (auth, middleware, API routes) are always included
|
|
886
|
+
regardless of changes when \`always_scan_security\` is true.`,
|
|
887
|
+
inputSchema: {
|
|
888
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
889
|
+
base_sha: z.string().optional().describe("Base SHA for comparison (default: origin/main)"),
|
|
890
|
+
head_sha: z.string().optional().describe("Head SHA to compare (default: HEAD)"),
|
|
891
|
+
always_scan_security: z.boolean().optional().default(true).describe("Always include security-critical files"),
|
|
892
|
+
exclude: z.array(z.string()).optional().describe("Regex patterns to exclude from scanning"),
|
|
893
|
+
},
|
|
894
|
+
annotations: {
|
|
895
|
+
readOnlyHint: true,
|
|
896
|
+
destructiveHint: false,
|
|
897
|
+
idempotentHint: true,
|
|
898
|
+
openWorldHint: false,
|
|
899
|
+
},
|
|
900
|
+
}, async ({ project_path, base_sha, head_sha, always_scan_security, exclude }) => {
|
|
901
|
+
const { configureIncrementalScan } = await import("./action/diff-mode.js");
|
|
902
|
+
const result = await configureIncrementalScan({
|
|
903
|
+
projectPath: project_path,
|
|
904
|
+
baseSha: base_sha,
|
|
905
|
+
headSha: head_sha,
|
|
906
|
+
alwaysScanSecurityFiles: always_scan_security,
|
|
907
|
+
exclude,
|
|
908
|
+
});
|
|
909
|
+
const fileList = result.filesToScan.map((f) => f.path);
|
|
910
|
+
const securityFiles = fileList.filter((f) => f.includes("auth") ||
|
|
911
|
+
f.includes("security") ||
|
|
912
|
+
f.includes("middleware") ||
|
|
913
|
+
f.includes("api/"));
|
|
914
|
+
return jsonResponse({
|
|
915
|
+
isIncremental: result.isIncremental,
|
|
916
|
+
filesToScan: fileList,
|
|
917
|
+
fileCount: fileList.length,
|
|
918
|
+
securityFileCount: securityFiles.length,
|
|
919
|
+
removedFiles: result.removedFiles,
|
|
920
|
+
estimatedSavings: `${result.estimatedSavingsPercent}%`,
|
|
921
|
+
diff: {
|
|
922
|
+
baseSha: result.diff.baseSha,
|
|
923
|
+
headSha: result.diff.headSha,
|
|
924
|
+
additions: result.diff.additions,
|
|
925
|
+
deletions: result.diff.deletions,
|
|
926
|
+
changedFilesTotal: result.diff.changedFiles.length,
|
|
927
|
+
},
|
|
928
|
+
note: result.isIncremental
|
|
929
|
+
? `Scanning ${fileList.length} changed files (${result.estimatedSavingsPercent}% reduction)`
|
|
930
|
+
: "Full scan recommended (diff detection failed or no changes)",
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
// ---------------------------------------------------------------------------
|
|
874
934
|
// Tool: Check Scanner Availability
|
|
875
935
|
// ---------------------------------------------------------------------------
|
|
876
936
|
server.registerTool("certification_scanners_available", {
|
|
@@ -1527,6 +1587,170 @@ server.registerTool("redteam_challenge", {
|
|
|
1527
1587
|
};
|
|
1528
1588
|
});
|
|
1529
1589
|
// ---------------------------------------------------------------------------
|
|
1590
|
+
// Tool: Antagonist Synthesize
|
|
1591
|
+
// ---------------------------------------------------------------------------
|
|
1592
|
+
server.registerTool("antagonist_synthesize", {
|
|
1593
|
+
title: "Synthesize Attack Narratives",
|
|
1594
|
+
description: `Run antagonist analysis after all agents complete. Synthesizes findings into attack narratives and challenges assumptions. Two modes: "synthesis" (attack narratives), "challenger" (internal critic), or "both".`,
|
|
1595
|
+
inputSchema: {
|
|
1596
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
1597
|
+
certification_id: z.string().describe("Certification ID"),
|
|
1598
|
+
mode: z.enum(["synthesis", "challenger", "both"]).optional().default("both").describe("Analysis mode"),
|
|
1599
|
+
include_prioritization: z.boolean().optional().default(true).describe("Include remediation prioritization"),
|
|
1600
|
+
use_llm: z.boolean().optional().default(true).describe("Use LLM for enhanced analysis"),
|
|
1601
|
+
},
|
|
1602
|
+
annotations: {
|
|
1603
|
+
readOnlyHint: false,
|
|
1604
|
+
destructiveHint: false,
|
|
1605
|
+
idempotentHint: true,
|
|
1606
|
+
openWorldHint: false,
|
|
1607
|
+
},
|
|
1608
|
+
}, async ({ project_path, certification_id, mode = "both", include_prioritization = true, use_llm = true }) => {
|
|
1609
|
+
const { runAntagonistAnalysis } = await import("./agents/antagonist/index.js");
|
|
1610
|
+
const { analyzeExploitChains } = await import("./agents/exploit-chain.js");
|
|
1611
|
+
const certification = await getCertification(project_path, certification_id);
|
|
1612
|
+
if (!certification) {
|
|
1613
|
+
return errorResponse(`Certification ${certification_id} not found`);
|
|
1614
|
+
}
|
|
1615
|
+
const ready = await allAgentsCompleted(project_path, certification_id);
|
|
1616
|
+
if (!ready) {
|
|
1617
|
+
return errorResponse("Not all agents have completed. Run antagonist after all agents finish.");
|
|
1618
|
+
}
|
|
1619
|
+
const allFindings = [];
|
|
1620
|
+
const agentSummaries = {};
|
|
1621
|
+
for (const [agentName, agentData] of Object.entries(certification.agents)) {
|
|
1622
|
+
if (agentData) {
|
|
1623
|
+
allFindings.push(...agentData.findings);
|
|
1624
|
+
agentSummaries[agentName] = {
|
|
1625
|
+
completed: agentData.status === "completed",
|
|
1626
|
+
findingCount: agentData.findings.length,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const chainResult = await analyzeExploitChains(allFindings);
|
|
1631
|
+
const result = await runAntagonistAnalysis({
|
|
1632
|
+
projectPath: project_path,
|
|
1633
|
+
certificationId: certification_id,
|
|
1634
|
+
findings: allFindings,
|
|
1635
|
+
exploitChains: chainResult.chains,
|
|
1636
|
+
exfilPaths: [],
|
|
1637
|
+
agentSummaries: agentSummaries,
|
|
1638
|
+
}, {
|
|
1639
|
+
mode,
|
|
1640
|
+
includePrioritization: include_prioritization,
|
|
1641
|
+
useLlm: use_llm,
|
|
1642
|
+
});
|
|
1643
|
+
return jsonResponse({
|
|
1644
|
+
success: result.success,
|
|
1645
|
+
analysisId: result.analysisId,
|
|
1646
|
+
narrativesFound: result.attackNarratives.length,
|
|
1647
|
+
challengesFound: result.challengerAssessments.length,
|
|
1648
|
+
coverageScore: result.gapAnalysis.coverageScore,
|
|
1649
|
+
summary: result.summary,
|
|
1650
|
+
attackNarratives: result.attackNarratives.slice(0, 5).map((n) => ({
|
|
1651
|
+
name: n.name,
|
|
1652
|
+
likelihood: n.likelihood,
|
|
1653
|
+
difficulty: n.difficulty,
|
|
1654
|
+
impact: n.impact,
|
|
1655
|
+
findingCount: n.findingIds.length,
|
|
1656
|
+
narrative: n.narrative.slice(0, 300),
|
|
1657
|
+
})),
|
|
1658
|
+
challenges: result.challengerAssessments.slice(0, 5).map((c) => ({
|
|
1659
|
+
type: c.type,
|
|
1660
|
+
targetAgent: c.targetAgent,
|
|
1661
|
+
challenge: c.challenge,
|
|
1662
|
+
severity: c.severity,
|
|
1663
|
+
})),
|
|
1664
|
+
prioritization: result.prioritization.slice(0, 5).map((p) => ({
|
|
1665
|
+
order: p.order,
|
|
1666
|
+
findingId: p.findingId,
|
|
1667
|
+
reason: p.reason,
|
|
1668
|
+
effort: p.effort,
|
|
1669
|
+
impact: p.impact,
|
|
1670
|
+
})),
|
|
1671
|
+
gapAnalysis: {
|
|
1672
|
+
coverageScore: result.gapAnalysis.coverageScore,
|
|
1673
|
+
untestedVectors: result.gapAnalysis.untestedAttackVectors.slice(0, 5),
|
|
1674
|
+
recommendations: result.gapAnalysis.recommendations.slice(0, 3),
|
|
1675
|
+
},
|
|
1676
|
+
duration: result.duration,
|
|
1677
|
+
tokensUsed: result.tokensUsed,
|
|
1678
|
+
});
|
|
1679
|
+
});
|
|
1680
|
+
// ---------------------------------------------------------------------------
|
|
1681
|
+
// Tool: Antagonist Challenge
|
|
1682
|
+
// ---------------------------------------------------------------------------
|
|
1683
|
+
server.registerTool("antagonist_challenge", {
|
|
1684
|
+
title: "Challenge Finding",
|
|
1685
|
+
description: `Challenge a specific finding from another agent. Used to flag potential false positives, missing context, or wrong severity.`,
|
|
1686
|
+
inputSchema: {
|
|
1687
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
1688
|
+
certification_id: z.string().describe("Certification ID"),
|
|
1689
|
+
target_finding_id: z.string().describe("Finding ID to challenge"),
|
|
1690
|
+
challenge_type: z.enum(["false_positive", "missing_context", "wrong_severity", "wrong_assumption"]).describe("Type of challenge"),
|
|
1691
|
+
evidence: z.string().describe("Evidence supporting the challenge"),
|
|
1692
|
+
},
|
|
1693
|
+
annotations: {
|
|
1694
|
+
readOnlyHint: false,
|
|
1695
|
+
destructiveHint: false,
|
|
1696
|
+
idempotentHint: false,
|
|
1697
|
+
openWorldHint: false,
|
|
1698
|
+
},
|
|
1699
|
+
}, async ({ project_path, certification_id, target_finding_id, challenge_type, evidence }) => {
|
|
1700
|
+
const certification = await getCertification(project_path, certification_id);
|
|
1701
|
+
if (!certification) {
|
|
1702
|
+
return errorResponse(`Certification ${certification_id} not found`);
|
|
1703
|
+
}
|
|
1704
|
+
let targetFinding = null;
|
|
1705
|
+
let targetAgent = null;
|
|
1706
|
+
for (const [agentName, agentData] of Object.entries(certification.agents)) {
|
|
1707
|
+
if (agentData) {
|
|
1708
|
+
const found = agentData.findings.find((f) => f.id === target_finding_id);
|
|
1709
|
+
if (found) {
|
|
1710
|
+
targetFinding = found;
|
|
1711
|
+
targetAgent = agentName;
|
|
1712
|
+
break;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (!targetFinding || !targetAgent) {
|
|
1717
|
+
return errorResponse(`Finding ${target_finding_id} not found`);
|
|
1718
|
+
}
|
|
1719
|
+
const challengeTypeMap = {
|
|
1720
|
+
false_positive: "false_positive_likely",
|
|
1721
|
+
missing_context: "insufficient_evidence",
|
|
1722
|
+
wrong_severity: "wrong_severity",
|
|
1723
|
+
wrong_assumption: "wrong_assumption",
|
|
1724
|
+
};
|
|
1725
|
+
const assessment = {
|
|
1726
|
+
id: `chal-manual-${Date.now().toString(36)}`,
|
|
1727
|
+
type: challengeTypeMap[challenge_type] || challenge_type,
|
|
1728
|
+
targetAgent,
|
|
1729
|
+
targetFindingId: target_finding_id,
|
|
1730
|
+
challenge: `Manual challenge: ${challenge_type}`,
|
|
1731
|
+
evidence,
|
|
1732
|
+
suggestedAction: challenge_type === "false_positive"
|
|
1733
|
+
? "Review and potentially dismiss this finding"
|
|
1734
|
+
: challenge_type === "wrong_severity"
|
|
1735
|
+
? "Re-evaluate severity level"
|
|
1736
|
+
: "Provide additional context or evidence",
|
|
1737
|
+
severity: targetFinding.severity,
|
|
1738
|
+
confidence: 90,
|
|
1739
|
+
};
|
|
1740
|
+
await addCrossVerification(project_path, certification_id, {
|
|
1741
|
+
finding_id: target_finding_id,
|
|
1742
|
+
verifying_agent: "antagonist",
|
|
1743
|
+
verdict: "disputed",
|
|
1744
|
+
evidence: `[${challenge_type}] ${evidence}`,
|
|
1745
|
+
});
|
|
1746
|
+
return jsonResponse({
|
|
1747
|
+
success: true,
|
|
1748
|
+
challenge: assessment,
|
|
1749
|
+
findingDisputed: target_finding_id,
|
|
1750
|
+
message: `Finding ${target_finding_id} has been challenged and marked as disputed.`,
|
|
1751
|
+
});
|
|
1752
|
+
});
|
|
1753
|
+
// ---------------------------------------------------------------------------
|
|
1530
1754
|
// Tool: Calculate Consensus
|
|
1531
1755
|
// ---------------------------------------------------------------------------
|
|
1532
1756
|
server.registerTool("certification_consensus", {
|
|
@@ -1789,11 +2013,17 @@ server.registerTool("certification_dashboard", {
|
|
|
1789
2013
|
// ---------------------------------------------------------------------------
|
|
1790
2014
|
server.registerTool("autofix_preview", {
|
|
1791
2015
|
title: "Preview Auto-Fix for Finding",
|
|
1792
|
-
description: `Preview an automatic fix
|
|
2016
|
+
description: `Preview an automatic fix without applying it. Can be used two ways:
|
|
2017
|
+
1. With certification_id + finding_id to preview a fix from a certification scan
|
|
2018
|
+
2. Standalone with file + pattern_id to preview a fix without running a full scan first`,
|
|
1793
2019
|
inputSchema: {
|
|
1794
2020
|
project_path: z.string().describe("Absolute path to the project root"),
|
|
1795
|
-
certification_id: z.string().describe("Certification ID"),
|
|
1796
|
-
finding_id: z.string().describe("Finding ID to preview fix for"),
|
|
2021
|
+
certification_id: z.string().optional().describe("Certification ID (optional if using standalone mode)"),
|
|
2022
|
+
finding_id: z.string().optional().describe("Finding ID to preview fix for (required with certification_id)"),
|
|
2023
|
+
file: z.string().optional().describe("File path relative to project root (standalone mode)"),
|
|
2024
|
+
line: z.number().optional().describe("Line number where the issue is located (standalone mode)"),
|
|
2025
|
+
pattern_id: z.string().optional().describe("Pattern ID to apply (e.g., 'perf-async-foreach', 'sec-hardcoded-secret'). Use autofix_list_patterns to see available patterns."),
|
|
2026
|
+
category: z.string().optional().describe("Finding category (alternative to pattern_id, e.g., 'async-foreach', 'hardcoded')"),
|
|
1797
2027
|
},
|
|
1798
2028
|
annotations: {
|
|
1799
2029
|
readOnlyHint: true,
|
|
@@ -1801,24 +2031,51 @@ server.registerTool("autofix_preview", {
|
|
|
1801
2031
|
idempotentHint: true,
|
|
1802
2032
|
openWorldHint: false,
|
|
1803
2033
|
},
|
|
1804
|
-
}, async ({ project_path, certification_id, finding_id }) => {
|
|
1805
|
-
const certification = await getCertification(project_path, certification_id);
|
|
1806
|
-
if (!certification) {
|
|
1807
|
-
return errorResponse(`Certification ${certification_id} not found`);
|
|
1808
|
-
}
|
|
1809
|
-
// Find the finding
|
|
2034
|
+
}, async ({ project_path, certification_id, finding_id, file, line, pattern_id, category }) => {
|
|
1810
2035
|
let finding = null;
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2036
|
+
if (certification_id && finding_id) {
|
|
2037
|
+
const certification = await getCertification(project_path, certification_id);
|
|
2038
|
+
if (!certification) {
|
|
2039
|
+
return errorResponse(`Certification ${certification_id} not found`);
|
|
2040
|
+
}
|
|
2041
|
+
for (const agentData of Object.values(certification.agents)) {
|
|
2042
|
+
if (agentData) {
|
|
2043
|
+
finding = agentData.findings.find((f) => f.id === finding_id) || null;
|
|
2044
|
+
if (finding)
|
|
2045
|
+
break;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (!finding) {
|
|
2049
|
+
return errorResponse(`Finding ${finding_id} not found`);
|
|
1816
2050
|
}
|
|
1817
2051
|
}
|
|
1818
|
-
if (
|
|
1819
|
-
|
|
2052
|
+
else if (file) {
|
|
2053
|
+
if (!pattern_id && !category) {
|
|
2054
|
+
return errorResponse("Standalone mode requires either pattern_id or category. Use autofix_list_patterns to see available patterns.");
|
|
2055
|
+
}
|
|
2056
|
+
finding = {
|
|
2057
|
+
id: pattern_id || `standalone-${category || "unknown"}`,
|
|
2058
|
+
severity: "medium",
|
|
2059
|
+
category: category || pattern_id || "unknown",
|
|
2060
|
+
file,
|
|
2061
|
+
line: line || 1,
|
|
2062
|
+
description: "Standalone autofix preview",
|
|
2063
|
+
evidence: "",
|
|
2064
|
+
confidence: 100,
|
|
2065
|
+
verifications: [],
|
|
2066
|
+
created_at: new Date().toISOString(),
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
else {
|
|
2070
|
+
return errorResponse("Provide either (certification_id + finding_id) or (file + pattern_id/category) for standalone mode.");
|
|
1820
2071
|
}
|
|
1821
2072
|
const preview = await previewFix(project_path, finding);
|
|
2073
|
+
if (!preview.canAutoFix && !certification_id) {
|
|
2074
|
+
return jsonResponse({
|
|
2075
|
+
...preview,
|
|
2076
|
+
hint: "No matching pattern found. Use autofix_list_patterns to see available fix patterns and their IDs.",
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
1822
2079
|
return jsonResponse({ ...preview });
|
|
1823
2080
|
});
|
|
1824
2081
|
// ---------------------------------------------------------------------------
|
|
@@ -6094,6 +6351,193 @@ server.registerTool("ai_code_hallucinations", {
|
|
|
6094
6351
|
}
|
|
6095
6352
|
});
|
|
6096
6353
|
// ---------------------------------------------------------------------------
|
|
6354
|
+
// False Positive Feedback Tools
|
|
6355
|
+
// ---------------------------------------------------------------------------
|
|
6356
|
+
server.registerTool("feedback_submit", {
|
|
6357
|
+
description: "Submit feedback marking a finding as true positive (TP) or false positive (FP). Helps improve scanner accuracy over time.",
|
|
6358
|
+
inputSchema: {
|
|
6359
|
+
project_path: z.string().describe("Path to the project"),
|
|
6360
|
+
scanner: z.enum([
|
|
6361
|
+
"semgrep", "npm-audit", "gitleaks", "tsc", "eslint",
|
|
6362
|
+
"bandit", "gosec", "brakeman", "trivy", "binary-analysis",
|
|
6363
|
+
"memory-safety", "race-condition", "healthcare", "logic",
|
|
6364
|
+
"dast", "zap", "nuclei", "terraform", "tfsec", "checkov",
|
|
6365
|
+
"openapi", "spectral", "rust", "cargo-audit", "clippy",
|
|
6366
|
+
"detection", "plugin"
|
|
6367
|
+
]).describe("Scanner that generated the finding"),
|
|
6368
|
+
rule_id: z.string().describe("Rule ID (e.g., 'semgrep:owasp.sql-injection')"),
|
|
6369
|
+
file: z.string().describe("File path where the finding was reported"),
|
|
6370
|
+
line: z.number().optional().describe("Line number of the finding"),
|
|
6371
|
+
verdict: z.enum(["tp", "fp"]).describe("Your verdict: 'tp' for true positive, 'fp' for false positive"),
|
|
6372
|
+
reason: z.enum([
|
|
6373
|
+
"test-code", "false-pattern-match", "sanitized-elsewhere",
|
|
6374
|
+
"intentional", "vendor-code", "generated-code",
|
|
6375
|
+
"example-code", "configuration", "other"
|
|
6376
|
+
]).optional().describe("Reason for marking as FP (required if verdict is 'fp')"),
|
|
6377
|
+
details: z.string().optional().describe("Additional context or notes"),
|
|
6378
|
+
},
|
|
6379
|
+
annotations: {
|
|
6380
|
+
readOnlyHint: false,
|
|
6381
|
+
destructiveHint: false,
|
|
6382
|
+
idempotentHint: true,
|
|
6383
|
+
openWorldHint: false,
|
|
6384
|
+
},
|
|
6385
|
+
}, async ({ project_path, scanner, rule_id, file, line, verdict, reason, details }) => {
|
|
6386
|
+
try {
|
|
6387
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
6388
|
+
const finding = {
|
|
6389
|
+
scanner: scanner,
|
|
6390
|
+
ruleId: rule_id,
|
|
6391
|
+
file,
|
|
6392
|
+
line: line ?? 0,
|
|
6393
|
+
message: "",
|
|
6394
|
+
severity: "medium",
|
|
6395
|
+
confidence: 100,
|
|
6396
|
+
};
|
|
6397
|
+
const entry = await submitFeedback(validatedPath, finding, verdict, {
|
|
6398
|
+
reason: reason,
|
|
6399
|
+
details,
|
|
6400
|
+
});
|
|
6401
|
+
return jsonResponse({
|
|
6402
|
+
success: true,
|
|
6403
|
+
feedbackId: entry.id,
|
|
6404
|
+
scanner: entry.scanner,
|
|
6405
|
+
ruleId: entry.ruleId,
|
|
6406
|
+
file: entry.file,
|
|
6407
|
+
verdict: entry.verdict,
|
|
6408
|
+
reason: entry.reason,
|
|
6409
|
+
submittedAt: entry.submittedAt,
|
|
6410
|
+
message: verdict === "fp"
|
|
6411
|
+
? `Marked as false positive. Reason: ${FP_REASON_DESCRIPTIONS[reason] || reason}`
|
|
6412
|
+
: "Marked as true positive. This helps confirm the finding is valid.",
|
|
6413
|
+
});
|
|
6414
|
+
}
|
|
6415
|
+
catch (error) {
|
|
6416
|
+
if (error instanceof PathValidationError) {
|
|
6417
|
+
return errorResponse(`Path validation failed: ${error.message}`);
|
|
6418
|
+
}
|
|
6419
|
+
return errorResponse(error instanceof Error ? error.message : String(error));
|
|
6420
|
+
}
|
|
6421
|
+
});
|
|
6422
|
+
server.registerTool("feedback_report", {
|
|
6423
|
+
description: "Generate a report of all feedback submitted for a project, including FP rates by scanner and top FP reasons.",
|
|
6424
|
+
inputSchema: {
|
|
6425
|
+
project_path: z.string().describe("Path to the project"),
|
|
6426
|
+
},
|
|
6427
|
+
annotations: {
|
|
6428
|
+
readOnlyHint: true,
|
|
6429
|
+
destructiveHint: false,
|
|
6430
|
+
idempotentHint: true,
|
|
6431
|
+
openWorldHint: false,
|
|
6432
|
+
},
|
|
6433
|
+
}, async ({ project_path }) => {
|
|
6434
|
+
try {
|
|
6435
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
6436
|
+
const report = await generateFeedbackReport(validatedPath);
|
|
6437
|
+
return jsonResponse({
|
|
6438
|
+
overview: {
|
|
6439
|
+
totalFeedback: report.overview.totalFeedback,
|
|
6440
|
+
truePositives: report.overview.tpCount,
|
|
6441
|
+
falsePositives: report.overview.fpCount,
|
|
6442
|
+
overallFPRate: `${(report.overview.overallFPRate * 100).toFixed(1)}%`,
|
|
6443
|
+
},
|
|
6444
|
+
byScanner: report.byScanner.map((s) => ({
|
|
6445
|
+
scanner: s.scanner,
|
|
6446
|
+
total: s.total,
|
|
6447
|
+
tp: s.tp,
|
|
6448
|
+
fp: s.fp,
|
|
6449
|
+
fpRate: `${(s.fpRate * 100).toFixed(1)}%`,
|
|
6450
|
+
})),
|
|
6451
|
+
topFPReasons: report.topFPReasons.map((r) => ({
|
|
6452
|
+
reason: r.reason,
|
|
6453
|
+
description: FP_REASON_DESCRIPTIONS[r.reason],
|
|
6454
|
+
count: r.count,
|
|
6455
|
+
percentage: `${r.percentage.toFixed(1)}%`,
|
|
6456
|
+
})),
|
|
6457
|
+
recentFeedback: report.recentFeedback.slice(0, 5).map((f) => ({
|
|
6458
|
+
scanner: f.scanner,
|
|
6459
|
+
ruleId: f.ruleId,
|
|
6460
|
+
file: f.file,
|
|
6461
|
+
verdict: f.verdict,
|
|
6462
|
+
reason: f.reason,
|
|
6463
|
+
submittedAt: f.submittedAt,
|
|
6464
|
+
})),
|
|
6465
|
+
suppressionSuggestions: report.suppressionSuggestions.length,
|
|
6466
|
+
message: report.overview.totalFeedback === 0
|
|
6467
|
+
? "No feedback submitted yet. Use feedback_submit to mark findings as TP or FP."
|
|
6468
|
+
: `Analyzed ${report.overview.totalFeedback} feedback entries. FP rate: ${(report.overview.overallFPRate * 100).toFixed(1)}%`,
|
|
6469
|
+
});
|
|
6470
|
+
}
|
|
6471
|
+
catch (error) {
|
|
6472
|
+
if (error instanceof PathValidationError) {
|
|
6473
|
+
return errorResponse(`Path validation failed: ${error.message}`);
|
|
6474
|
+
}
|
|
6475
|
+
return errorResponse(error instanceof Error ? error.message : String(error));
|
|
6476
|
+
}
|
|
6477
|
+
});
|
|
6478
|
+
server.registerTool("feedback_suppressions", {
|
|
6479
|
+
description: "Get suggestions for rules to disable based on accumulated false positive feedback. Rules with >50% FP rate (min 5 samples) are flagged.",
|
|
6480
|
+
inputSchema: {
|
|
6481
|
+
project_path: z.string().describe("Path to the project"),
|
|
6482
|
+
min_fp_rate: z.number().min(0).max(1).optional().default(0.5).describe("Minimum FP rate to suggest suppression (0.0-1.0, default: 0.5)"),
|
|
6483
|
+
min_sample_size: z.number().min(1).optional().default(5).describe("Minimum number of feedback entries required (default: 5)"),
|
|
6484
|
+
},
|
|
6485
|
+
annotations: {
|
|
6486
|
+
readOnlyHint: true,
|
|
6487
|
+
destructiveHint: false,
|
|
6488
|
+
idempotentHint: true,
|
|
6489
|
+
openWorldHint: false,
|
|
6490
|
+
},
|
|
6491
|
+
}, async ({ project_path, min_fp_rate, min_sample_size }) => {
|
|
6492
|
+
try {
|
|
6493
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
6494
|
+
const suggestions = await getSuppressionSuggestions(validatedPath, {
|
|
6495
|
+
minFPRate: min_fp_rate,
|
|
6496
|
+
minSampleSize: min_sample_size,
|
|
6497
|
+
});
|
|
6498
|
+
if (suggestions.length === 0) {
|
|
6499
|
+
return jsonResponse({
|
|
6500
|
+
suggestions: [],
|
|
6501
|
+
message: "No rules meet the criteria for suppression. Either not enough feedback or FP rates are acceptable.",
|
|
6502
|
+
criteria: {
|
|
6503
|
+
minFPRate: `${(min_fp_rate ?? 0.5) * 100}%`,
|
|
6504
|
+
minSampleSize: min_sample_size ?? 5,
|
|
6505
|
+
},
|
|
6506
|
+
});
|
|
6507
|
+
}
|
|
6508
|
+
return jsonResponse({
|
|
6509
|
+
suggestions: suggestions.map((s) => ({
|
|
6510
|
+
scanner: s.scanner,
|
|
6511
|
+
ruleId: s.ruleId,
|
|
6512
|
+
fpRate: `${(s.fpRate * 100).toFixed(1)}%`,
|
|
6513
|
+
sampleSize: s.sampleSize,
|
|
6514
|
+
recommendation: s.suggestion,
|
|
6515
|
+
recommendationText: s.suggestion === "disable"
|
|
6516
|
+
? "High FP rate (≥80%) - consider disabling this rule"
|
|
6517
|
+
: s.suggestion === "review"
|
|
6518
|
+
? "Moderate FP rate (≥50%) - review rule configuration"
|
|
6519
|
+
: "FP rate acceptable - keep rule enabled",
|
|
6520
|
+
commonReasons: s.commonReasons.map((r) => ({
|
|
6521
|
+
reason: r,
|
|
6522
|
+
description: FP_REASON_DESCRIPTIONS[r],
|
|
6523
|
+
})),
|
|
6524
|
+
})),
|
|
6525
|
+
summary: {
|
|
6526
|
+
totalSuggestions: suggestions.length,
|
|
6527
|
+
disableRecommendations: suggestions.filter((s) => s.suggestion === "disable").length,
|
|
6528
|
+
reviewRecommendations: suggestions.filter((s) => s.suggestion === "review").length,
|
|
6529
|
+
},
|
|
6530
|
+
message: `Found ${suggestions.length} rule(s) with high false positive rates based on your feedback.`,
|
|
6531
|
+
});
|
|
6532
|
+
}
|
|
6533
|
+
catch (error) {
|
|
6534
|
+
if (error instanceof PathValidationError) {
|
|
6535
|
+
return errorResponse(`Path validation failed: ${error.message}`);
|
|
6536
|
+
}
|
|
6537
|
+
return errorResponse(error instanceof Error ? error.message : String(error));
|
|
6538
|
+
}
|
|
6539
|
+
});
|
|
6540
|
+
// ---------------------------------------------------------------------------
|
|
6097
6541
|
// Exports (for HTTP server and client integration)
|
|
6098
6542
|
// ---------------------------------------------------------------------------
|
|
6099
6543
|
export { server, textResponse, jsonResponse, errorResponse };
|