vaspera 2.11.0 → 2.13.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 +46 -0
- package/dist/__tests__/audit-trail.test.d.ts +7 -0
- package/dist/__tests__/audit-trail.test.d.ts.map +1 -0
- package/dist/__tests__/audit-trail.test.js +336 -0
- package/dist/__tests__/audit-trail.test.js.map +1 -0
- package/dist/__tests__/property-test-helpers.d.ts +1 -1
- 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/action/pr-comment.test.js +1 -0
- package/dist/action/pr-comment.test.js.map +1 -1
- package/dist/action/sarif-upload.test.js +1 -0
- package/dist/action/sarif-upload.test.js.map +1 -1
- package/dist/autofix/ast/__tests__/typescript.test.d.ts +5 -0
- package/dist/autofix/ast/__tests__/typescript.test.d.ts.map +1 -0
- package/dist/autofix/ast/__tests__/typescript.test.js +210 -0
- package/dist/autofix/ast/__tests__/typescript.test.js.map +1 -0
- package/dist/autofix/ast/index.d.ts +11 -0
- package/dist/autofix/ast/index.d.ts.map +1 -0
- package/dist/autofix/ast/index.js +11 -0
- package/dist/autofix/ast/index.js.map +1 -0
- package/dist/autofix/ast/types.d.ts +77 -0
- package/dist/autofix/ast/types.d.ts.map +1 -0
- package/dist/autofix/ast/types.js +9 -0
- package/dist/autofix/ast/types.js.map +1 -0
- package/dist/autofix/ast/typescript.d.ts +17 -0
- package/dist/autofix/ast/typescript.d.ts.map +1 -0
- package/dist/autofix/ast/typescript.js +427 -0
- package/dist/autofix/ast/typescript.js.map +1 -0
- package/dist/autofix/constitution.schema.d.ts +21 -21
- package/dist/autofix/index.d.ts +1 -0
- package/dist/autofix/index.d.ts.map +1 -1
- package/dist/autofix/index.js +2 -0
- package/dist/autofix/index.js.map +1 -1
- package/dist/config/flags.d.ts +6 -6
- package/dist/history/store.d.ts +55 -1
- package/dist/history/store.d.ts.map +1 -1
- package/dist/history/store.js +152 -4
- package/dist/history/store.js.map +1 -1
- package/dist/history/types.d.ts +9 -5
- package/dist/history/types.d.ts.map +1 -1
- package/dist/history/verify.d.ts.map +1 -1
- package/dist/history/verify.js +5 -3
- package/dist/history/verify.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +923 -16
- package/dist/index.js.map +1 -1
- package/dist/integrations/siem/datadog.d.ts +44 -0
- package/dist/integrations/siem/datadog.d.ts.map +1 -0
- package/dist/integrations/siem/datadog.js +211 -0
- package/dist/integrations/siem/datadog.js.map +1 -0
- package/dist/integrations/siem/format.d.ts +59 -0
- package/dist/integrations/siem/format.d.ts.map +1 -0
- package/dist/integrations/siem/format.js +360 -0
- package/dist/integrations/siem/format.js.map +1 -0
- package/dist/integrations/siem/index.d.ts +56 -0
- package/dist/integrations/siem/index.d.ts.map +1 -0
- package/dist/integrations/siem/index.js +117 -0
- package/dist/integrations/siem/index.js.map +1 -0
- package/dist/integrations/siem/sentinel.d.ts +53 -0
- package/dist/integrations/siem/sentinel.d.ts.map +1 -0
- package/dist/integrations/siem/sentinel.js +231 -0
- package/dist/integrations/siem/sentinel.js.map +1 -0
- package/dist/integrations/siem/splunk.d.ts +46 -0
- package/dist/integrations/siem/splunk.d.ts.map +1 -0
- package/dist/integrations/siem/splunk.js +210 -0
- package/dist/integrations/siem/splunk.js.map +1 -0
- package/dist/integrations/siem/types.d.ts +210 -0
- package/dist/integrations/siem/types.d.ts.map +1 -0
- package/dist/integrations/siem/types.js +9 -0
- package/dist/integrations/siem/types.js.map +1 -0
- 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.d.ts +5 -0
- package/dist/persistence/__tests__/persistence.test.d.ts.map +1 -0
- package/dist/persistence/__tests__/persistence.test.js +369 -0
- package/dist/persistence/__tests__/persistence.test.js.map +1 -0
- package/dist/persistence/db.d.ts +30 -0
- package/dist/persistence/db.d.ts.map +1 -0
- package/dist/persistence/db.js +128 -0
- package/dist/persistence/db.js.map +1 -0
- package/dist/persistence/index.d.ts +75 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +268 -0
- package/dist/persistence/index.js.map +1 -0
- 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/persistence/migrations/index.d.ts +10 -0
- package/dist/persistence/migrations/index.d.ts.map +1 -0
- package/dist/persistence/migrations/index.js +125 -0
- package/dist/persistence/migrations/index.js.map +1 -0
- package/dist/persistence/repositories/findings.d.ts +41 -0
- package/dist/persistence/repositories/findings.d.ts.map +1 -0
- package/dist/persistence/repositories/findings.js +238 -0
- package/dist/persistence/repositories/findings.js.map +1 -0
- package/dist/persistence/repositories/projects.d.ts +22 -0
- package/dist/persistence/repositories/projects.d.ts.map +1 -0
- package/dist/persistence/repositories/projects.js +71 -0
- package/dist/persistence/repositories/projects.js.map +1 -0
- package/dist/persistence/repositories/scans.d.ts +30 -0
- package/dist/persistence/repositories/scans.d.ts.map +1 -0
- package/dist/persistence/repositories/scans.js +107 -0
- package/dist/persistence/repositories/scans.js.map +1 -0
- package/dist/persistence/repositories/trends.d.ts +42 -0
- package/dist/persistence/repositories/trends.d.ts.map +1 -0
- package/dist/persistence/repositories/trends.js +178 -0
- package/dist/persistence/repositories/trends.js.map +1 -0
- package/dist/persistence/types.d.ts +105 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +13 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/plugins/types.d.ts +2 -2
- 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 +24 -12
- package/dist/scanners/ai-code/types.d.ts.map +1 -1
- package/dist/scanners/cache.d.ts.map +1 -1
- package/dist/scanners/cache.js +1 -0
- package/dist/scanners/cache.js.map +1 -1
- package/dist/scanners/deploy/types.d.ts +13 -13
- package/dist/scanners/detection/__tests__/detection.test.d.ts +5 -0
- package/dist/scanners/detection/__tests__/detection.test.d.ts.map +1 -0
- package/dist/scanners/detection/__tests__/detection.test.js +265 -0
- package/dist/scanners/detection/__tests__/detection.test.js.map +1 -0
- package/dist/scanners/detection/engines/ast-query.d.ts +23 -0
- package/dist/scanners/detection/engines/ast-query.d.ts.map +1 -0
- package/dist/scanners/detection/engines/ast-query.js +232 -0
- package/dist/scanners/detection/engines/ast-query.js.map +1 -0
- package/dist/scanners/detection/engines/data-flow.d.ts +12 -0
- package/dist/scanners/detection/engines/data-flow.d.ts.map +1 -0
- package/dist/scanners/detection/engines/data-flow.js +269 -0
- package/dist/scanners/detection/engines/data-flow.js.map +1 -0
- package/dist/scanners/detection/index.d.ts +29 -0
- package/dist/scanners/detection/index.d.ts.map +1 -0
- package/dist/scanners/detection/index.js +140 -0
- package/dist/scanners/detection/index.js.map +1 -0
- package/dist/scanners/detection/rules/builtin.d.ts +14 -0
- package/dist/scanners/detection/rules/builtin.d.ts.map +1 -0
- package/dist/scanners/detection/rules/builtin.js +307 -0
- package/dist/scanners/detection/rules/builtin.js.map +1 -0
- package/dist/scanners/detection/rules/loader.d.ts +19 -0
- package/dist/scanners/detection/rules/loader.d.ts.map +1 -0
- package/dist/scanners/detection/rules/loader.js +111 -0
- package/dist/scanners/detection/rules/loader.js.map +1 -0
- package/dist/scanners/detection/types.d.ts +171 -0
- package/dist/scanners/detection/types.d.ts.map +1 -0
- package/dist/scanners/detection/types.js +36 -0
- package/dist/scanners/detection/types.js.map +1 -0
- 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/index.d.ts +9 -1
- package/dist/scanners/index.d.ts.map +1 -1
- package/dist/scanners/index.js +64 -0
- package/dist/scanners/index.js.map +1 -1
- package/dist/scanners/index.test.js +6 -6
- package/dist/scanners/index.test.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/scale/types.d.ts +3 -3
- package/dist/scanners/types.d.ts +19 -2
- package/dist/scanners/types.d.ts.map +1 -1
- package/dist/scanners/types.js +1 -0
- 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 +5 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,10 @@ generateSummary, filterFindings, getActionItems,
|
|
|
17
17
|
exportToSarif, exportForGitHub,
|
|
18
18
|
// Rules
|
|
19
19
|
loadCustomRules, checkFileAgainstRules, matchesToFindings, listRuleTemplates, generateSampleConfig, } from "./certification/index.js";
|
|
20
|
+
// Autofix PR generation
|
|
21
|
+
import { createAutofixPRs, isGhCliAvailable, isGhAuthenticated, } from "./autofix/index.js";
|
|
22
|
+
// Persistence layer
|
|
23
|
+
import { getProjectTrends, markFindingsFixed, getOpenFindings, } from "./persistence/index.js";
|
|
20
24
|
// Deterministic scanners
|
|
21
25
|
import { runAllScanners, checkScannersAvailable, scannerFindingsToCertificationFindings, generateScannerSummary, getScannerInstallCommands,
|
|
22
26
|
// Mythos-class scanners
|
|
@@ -38,6 +42,9 @@ runSingleFrameworkAssessment, runComplianceAssessment, generateComplianceSummary
|
|
|
38
42
|
// Evidence collection and audit trail verification
|
|
39
43
|
import { collectEvidence, storeEvidenceBundle, formatEvidenceBundleAsMarkdown, } from "./evidence/index.js";
|
|
40
44
|
import { verifyHistoryIntegrity, formatVerificationResultAsMarkdown, } from "./history/verify.js";
|
|
45
|
+
import { exportAuditTrail, } from "./history/store.js";
|
|
46
|
+
// SIEM Integration
|
|
47
|
+
import { createSIEMClient, getSIEMRegistry, createFindingEvent, } from "./integrations/siem/index.js";
|
|
41
48
|
// SBOM, Provenance, and Sigstore Signing (uses @sigstore/sign for real signing)
|
|
42
49
|
import { generateSBOM, generateSBOMSummary, generateProvenance, generateProvenanceSummary, verifyProvenance, signContent, isSigningAvailable, generateSigningSummary, detectCIEnvironment, } from "./sbom/index.js";
|
|
43
50
|
// Cost tracking
|
|
@@ -49,6 +56,8 @@ import { validateProjectPath, PathValidationError } from "./util/paths.js";
|
|
|
49
56
|
// Telemetry and scan registry
|
|
50
57
|
import { trackCertificationStarted, trackCertificationCompleted, trackScannerRun, } from "./telemetry/usage.js";
|
|
51
58
|
import { getRegistry } from "./telemetry/registry.js";
|
|
59
|
+
// False positive feedback
|
|
60
|
+
import { submitFeedback, generateFeedbackReport, getSuppressionSuggestions, FP_REASON_DESCRIPTIONS, } from "./scanners/fp-feedback.js";
|
|
52
61
|
// ---------------------------------------------------------------------------
|
|
53
62
|
// Config
|
|
54
63
|
// ---------------------------------------------------------------------------
|
|
@@ -864,6 +873,64 @@ server.registerTool("certification_scan", {
|
|
|
864
873
|
};
|
|
865
874
|
});
|
|
866
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
|
+
// ---------------------------------------------------------------------------
|
|
867
934
|
// Tool: Check Scanner Availability
|
|
868
935
|
// ---------------------------------------------------------------------------
|
|
869
936
|
server.registerTool("certification_scanners_available", {
|
|
@@ -1782,11 +1849,17 @@ server.registerTool("certification_dashboard", {
|
|
|
1782
1849
|
// ---------------------------------------------------------------------------
|
|
1783
1850
|
server.registerTool("autofix_preview", {
|
|
1784
1851
|
title: "Preview Auto-Fix for Finding",
|
|
1785
|
-
description: `Preview an automatic fix
|
|
1852
|
+
description: `Preview an automatic fix without applying it. Can be used two ways:
|
|
1853
|
+
1. With certification_id + finding_id to preview a fix from a certification scan
|
|
1854
|
+
2. Standalone with file + pattern_id to preview a fix without running a full scan first`,
|
|
1786
1855
|
inputSchema: {
|
|
1787
1856
|
project_path: z.string().describe("Absolute path to the project root"),
|
|
1788
|
-
certification_id: z.string().describe("Certification ID"),
|
|
1789
|
-
finding_id: z.string().describe("Finding ID to preview fix for"),
|
|
1857
|
+
certification_id: z.string().optional().describe("Certification ID (optional if using standalone mode)"),
|
|
1858
|
+
finding_id: z.string().optional().describe("Finding ID to preview fix for (required with certification_id)"),
|
|
1859
|
+
file: z.string().optional().describe("File path relative to project root (standalone mode)"),
|
|
1860
|
+
line: z.number().optional().describe("Line number where the issue is located (standalone mode)"),
|
|
1861
|
+
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."),
|
|
1862
|
+
category: z.string().optional().describe("Finding category (alternative to pattern_id, e.g., 'async-foreach', 'hardcoded')"),
|
|
1790
1863
|
},
|
|
1791
1864
|
annotations: {
|
|
1792
1865
|
readOnlyHint: true,
|
|
@@ -1794,24 +1867,51 @@ server.registerTool("autofix_preview", {
|
|
|
1794
1867
|
idempotentHint: true,
|
|
1795
1868
|
openWorldHint: false,
|
|
1796
1869
|
},
|
|
1797
|
-
}, async ({ project_path, certification_id, finding_id }) => {
|
|
1798
|
-
const certification = await getCertification(project_path, certification_id);
|
|
1799
|
-
if (!certification) {
|
|
1800
|
-
return errorResponse(`Certification ${certification_id} not found`);
|
|
1801
|
-
}
|
|
1802
|
-
// Find the finding
|
|
1870
|
+
}, async ({ project_path, certification_id, finding_id, file, line, pattern_id, category }) => {
|
|
1803
1871
|
let finding = null;
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1872
|
+
if (certification_id && finding_id) {
|
|
1873
|
+
const certification = await getCertification(project_path, certification_id);
|
|
1874
|
+
if (!certification) {
|
|
1875
|
+
return errorResponse(`Certification ${certification_id} not found`);
|
|
1876
|
+
}
|
|
1877
|
+
for (const agentData of Object.values(certification.agents)) {
|
|
1878
|
+
if (agentData) {
|
|
1879
|
+
finding = agentData.findings.find((f) => f.id === finding_id) || null;
|
|
1880
|
+
if (finding)
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (!finding) {
|
|
1885
|
+
return errorResponse(`Finding ${finding_id} not found`);
|
|
1809
1886
|
}
|
|
1810
1887
|
}
|
|
1811
|
-
if (
|
|
1812
|
-
|
|
1888
|
+
else if (file) {
|
|
1889
|
+
if (!pattern_id && !category) {
|
|
1890
|
+
return errorResponse("Standalone mode requires either pattern_id or category. Use autofix_list_patterns to see available patterns.");
|
|
1891
|
+
}
|
|
1892
|
+
finding = {
|
|
1893
|
+
id: pattern_id || `standalone-${category || "unknown"}`,
|
|
1894
|
+
severity: "medium",
|
|
1895
|
+
category: category || pattern_id || "unknown",
|
|
1896
|
+
file,
|
|
1897
|
+
line: line || 1,
|
|
1898
|
+
description: "Standalone autofix preview",
|
|
1899
|
+
evidence: "",
|
|
1900
|
+
confidence: 100,
|
|
1901
|
+
verifications: [],
|
|
1902
|
+
created_at: new Date().toISOString(),
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
else {
|
|
1906
|
+
return errorResponse("Provide either (certification_id + finding_id) or (file + pattern_id/category) for standalone mode.");
|
|
1813
1907
|
}
|
|
1814
1908
|
const preview = await previewFix(project_path, finding);
|
|
1909
|
+
if (!preview.canAutoFix && !certification_id) {
|
|
1910
|
+
return jsonResponse({
|
|
1911
|
+
...preview,
|
|
1912
|
+
hint: "No matching pattern found. Use autofix_list_patterns to see available fix patterns and their IDs.",
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1815
1915
|
return jsonResponse({ ...preview });
|
|
1816
1916
|
});
|
|
1817
1917
|
// ---------------------------------------------------------------------------
|
|
@@ -1954,6 +2054,307 @@ server.registerTool("autofix_batch", {
|
|
|
1954
2054
|
});
|
|
1955
2055
|
});
|
|
1956
2056
|
// ---------------------------------------------------------------------------
|
|
2057
|
+
// Tool: Create Autofix Pull Requests
|
|
2058
|
+
// ---------------------------------------------------------------------------
|
|
2059
|
+
server.registerTool("autofix_create_prs", {
|
|
2060
|
+
title: "Create Autofix Pull Requests",
|
|
2061
|
+
description: `Create GitHub pull requests with automated security fixes. Groups findings by severity/file/pattern and generates separate PRs for each group. Requires gh CLI to be installed and authenticated.
|
|
2062
|
+
|
|
2063
|
+
**Prerequisites:**
|
|
2064
|
+
- gh CLI installed and authenticated
|
|
2065
|
+
- Git repository with a remote
|
|
2066
|
+
- Clean working tree (uncommitted changes will be stashed)
|
|
2067
|
+
|
|
2068
|
+
**Grouping strategies:**
|
|
2069
|
+
- severity: One PR per severity level (critical, high, medium, low)
|
|
2070
|
+
- file: One PR per affected file
|
|
2071
|
+
- pattern: One PR per fix pattern type
|
|
2072
|
+
- single: One PR per individual finding`,
|
|
2073
|
+
inputSchema: {
|
|
2074
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
2075
|
+
certification_id: z.string().optional().describe("Certification ID to get findings from (if not providing findings directly)"),
|
|
2076
|
+
findings: z.array(z.object({
|
|
2077
|
+
id: z.string(),
|
|
2078
|
+
severity: z.enum(["critical", "high", "medium", "low", "info"]),
|
|
2079
|
+
category: z.string(),
|
|
2080
|
+
file: z.string().optional(),
|
|
2081
|
+
line: z.number().optional(),
|
|
2082
|
+
description: z.string(),
|
|
2083
|
+
evidence: z.string().optional(),
|
|
2084
|
+
confidence: z.number().optional(),
|
|
2085
|
+
})).optional().describe("Direct findings array (alternative to certification_id)"),
|
|
2086
|
+
group_by: z.enum(["severity", "file", "pattern", "single"]).default("severity").describe("How to group fixes into PRs"),
|
|
2087
|
+
dry_run: z.boolean().default(true).describe("Preview mode - show what PRs would be created without actually creating them"),
|
|
2088
|
+
include_unsafe: z.boolean().default(false).describe("Include high-risk fixes that may require manual review"),
|
|
2089
|
+
branch_prefix: z.string().default("vaspera/autofix").describe("Prefix for branch names"),
|
|
2090
|
+
base_branch: z.string().optional().describe("Base branch to create PRs against (defaults to main/master)"),
|
|
2091
|
+
},
|
|
2092
|
+
annotations: {
|
|
2093
|
+
readOnlyHint: false,
|
|
2094
|
+
destructiveHint: true,
|
|
2095
|
+
idempotentHint: false,
|
|
2096
|
+
openWorldHint: true,
|
|
2097
|
+
},
|
|
2098
|
+
}, async ({ project_path, certification_id, findings: directFindings, group_by, dry_run, include_unsafe, branch_prefix, base_branch }) => {
|
|
2099
|
+
// Check prerequisites
|
|
2100
|
+
if (!(await isGhCliAvailable())) {
|
|
2101
|
+
return errorResponse("gh CLI is not installed. Install from: https://cli.github.com/");
|
|
2102
|
+
}
|
|
2103
|
+
if (!(await isGhAuthenticated())) {
|
|
2104
|
+
return errorResponse("gh CLI is not authenticated. Run: gh auth login");
|
|
2105
|
+
}
|
|
2106
|
+
// Get findings from certification or direct input
|
|
2107
|
+
let findings = [];
|
|
2108
|
+
if (directFindings && directFindings.length > 0) {
|
|
2109
|
+
findings = directFindings;
|
|
2110
|
+
}
|
|
2111
|
+
else if (certification_id) {
|
|
2112
|
+
const certification = await getCertification(project_path, certification_id);
|
|
2113
|
+
if (!certification) {
|
|
2114
|
+
return errorResponse(`Certification ${certification_id} not found`);
|
|
2115
|
+
}
|
|
2116
|
+
// Collect all findings from all agents
|
|
2117
|
+
for (const agentData of Object.values(certification.agents)) {
|
|
2118
|
+
if (agentData) {
|
|
2119
|
+
findings.push(...agentData.findings);
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
else {
|
|
2124
|
+
return errorResponse("Either certification_id or findings must be provided");
|
|
2125
|
+
}
|
|
2126
|
+
if (findings.length === 0) {
|
|
2127
|
+
return jsonResponse({
|
|
2128
|
+
message: "No findings to fix",
|
|
2129
|
+
totalFindings: 0,
|
|
2130
|
+
prsCreated: [],
|
|
2131
|
+
unfixable: [],
|
|
2132
|
+
success: true,
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
// Create PRs
|
|
2136
|
+
const result = await createAutofixPRs({
|
|
2137
|
+
projectPath: project_path,
|
|
2138
|
+
findings: findings,
|
|
2139
|
+
groupBy: group_by,
|
|
2140
|
+
dryRun: dry_run,
|
|
2141
|
+
includeUnsafe: include_unsafe,
|
|
2142
|
+
branchPrefix: branch_prefix,
|
|
2143
|
+
baseBranch: base_branch || "main",
|
|
2144
|
+
});
|
|
2145
|
+
// Format output
|
|
2146
|
+
const lines = [
|
|
2147
|
+
`# Autofix PR ${dry_run ? "Preview" : "Results"}`,
|
|
2148
|
+
"",
|
|
2149
|
+
`**Total Findings**: ${result.totalFindings}`,
|
|
2150
|
+
`**PRs ${dry_run ? "Would Create" : "Created"}**: ${result.prsCreated.length}`,
|
|
2151
|
+
`**Unfixable**: ${result.unfixable.length}`,
|
|
2152
|
+
"",
|
|
2153
|
+
];
|
|
2154
|
+
if (result.prsCreated.length > 0) {
|
|
2155
|
+
lines.push("## Pull Requests");
|
|
2156
|
+
lines.push("");
|
|
2157
|
+
for (const pr of result.prsCreated) {
|
|
2158
|
+
const fixCount = pr.fixesApplied.length;
|
|
2159
|
+
const fileList = pr.filesModified.slice(0, 3).join(", ");
|
|
2160
|
+
const moreFiles = pr.filesModified.length > 3 ? ` +${pr.filesModified.length - 3} more` : "";
|
|
2161
|
+
if (dry_run) {
|
|
2162
|
+
lines.push(`- **${pr.branch}**: ${fixCount} fixes (${fileList}${moreFiles})`);
|
|
2163
|
+
}
|
|
2164
|
+
else {
|
|
2165
|
+
lines.push(`- [PR #${pr.prNumber}](${pr.prUrl}): ${fixCount} fixes`);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
lines.push("");
|
|
2169
|
+
}
|
|
2170
|
+
if (result.unfixable.length > 0) {
|
|
2171
|
+
lines.push("## Unfixable Findings");
|
|
2172
|
+
lines.push("");
|
|
2173
|
+
for (const uf of result.unfixable.slice(0, 10)) {
|
|
2174
|
+
lines.push(`- ${uf.findingId}: ${uf.reason}`);
|
|
2175
|
+
}
|
|
2176
|
+
if (result.unfixable.length > 10) {
|
|
2177
|
+
lines.push(`- ... and ${result.unfixable.length - 10} more`);
|
|
2178
|
+
}
|
|
2179
|
+
lines.push("");
|
|
2180
|
+
}
|
|
2181
|
+
if (result.summary && Object.keys(result.summary.byPattern).length > 0) {
|
|
2182
|
+
lines.push("## Summary by Pattern");
|
|
2183
|
+
lines.push("");
|
|
2184
|
+
for (const [pattern, count] of Object.entries(result.summary.byPattern)) {
|
|
2185
|
+
lines.push(`- ${pattern}: ${count} fixes`);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return {
|
|
2189
|
+
content: [
|
|
2190
|
+
{
|
|
2191
|
+
type: "text",
|
|
2192
|
+
text: lines.join("\n"),
|
|
2193
|
+
},
|
|
2194
|
+
],
|
|
2195
|
+
result,
|
|
2196
|
+
};
|
|
2197
|
+
});
|
|
2198
|
+
// ---------------------------------------------------------------------------
|
|
2199
|
+
// Tool: Persistence - Get Project Trends
|
|
2200
|
+
// ---------------------------------------------------------------------------
|
|
2201
|
+
server.registerTool("persistence_get_trends", {
|
|
2202
|
+
title: "Get Project Security Trends",
|
|
2203
|
+
description: `Get historical security trends for a project including new/fixed findings over time, MTTR, and fix velocity. Requires SQLite persistence to be enabled.`,
|
|
2204
|
+
inputSchema: {
|
|
2205
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
2206
|
+
period: z.enum(["day", "week", "month"]).default("week").describe("Trend aggregation period"),
|
|
2207
|
+
lookback_periods: z.number().default(12).describe("Number of periods to look back"),
|
|
2208
|
+
},
|
|
2209
|
+
annotations: {
|
|
2210
|
+
readOnlyHint: true,
|
|
2211
|
+
destructiveHint: false,
|
|
2212
|
+
idempotentHint: true,
|
|
2213
|
+
openWorldHint: false,
|
|
2214
|
+
},
|
|
2215
|
+
}, async ({ project_path, period, lookback_periods }) => {
|
|
2216
|
+
try {
|
|
2217
|
+
const trends = await getProjectTrends(project_path, period, lookback_periods);
|
|
2218
|
+
const lines = [
|
|
2219
|
+
`# Security Trends for ${trends.project.name}`,
|
|
2220
|
+
"",
|
|
2221
|
+
`**Period**: ${period} (last ${lookback_periods} periods)`,
|
|
2222
|
+
`**Last Scan**: ${trends.project.lastScanAt || "Never"}`,
|
|
2223
|
+
"",
|
|
2224
|
+
"## Current Stats",
|
|
2225
|
+
"",
|
|
2226
|
+
`- **Open Findings**: ${trends.stats.openFindings}`,
|
|
2227
|
+
`- **Fixed Findings**: ${trends.stats.fixedFindings}`,
|
|
2228
|
+
`- **False Positives**: ${trends.stats.falsePositives}`,
|
|
2229
|
+
`- **Avg MTTR**: ${trends.stats.avgMttrHours ? `${trends.stats.avgMttrHours.toFixed(1)} hours` : "N/A"}`,
|
|
2230
|
+
`- **Fix Velocity**: ${trends.stats.fixVelocityPerWeek ? `${trends.stats.fixVelocityPerWeek.toFixed(1)}/week` : "N/A"}`,
|
|
2231
|
+
"",
|
|
2232
|
+
"## By Severity",
|
|
2233
|
+
"",
|
|
2234
|
+
];
|
|
2235
|
+
for (const [sev, count] of Object.entries(trends.stats.bySeverity)) {
|
|
2236
|
+
if (count > 0) {
|
|
2237
|
+
lines.push(`- **${sev}**: ${count}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
lines.push("", "## Trend Data", "");
|
|
2241
|
+
lines.push("| Period | New | Fixed | Open |");
|
|
2242
|
+
lines.push("|--------|-----|-------|------|");
|
|
2243
|
+
for (const t of trends.trends.slice(-10)) {
|
|
2244
|
+
lines.push(`| ${t.period} | ${t.newFindings} | ${t.fixedFindings} | ${t.openFindings} |`);
|
|
2245
|
+
}
|
|
2246
|
+
return {
|
|
2247
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2248
|
+
trends,
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
catch (error) {
|
|
2252
|
+
return errorResponse(`Failed to get trends: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2253
|
+
}
|
|
2254
|
+
});
|
|
2255
|
+
// ---------------------------------------------------------------------------
|
|
2256
|
+
// Tool: Persistence - Get Open Findings
|
|
2257
|
+
// ---------------------------------------------------------------------------
|
|
2258
|
+
server.registerTool("persistence_get_findings", {
|
|
2259
|
+
title: "Get Persisted Findings",
|
|
2260
|
+
description: `Get open security findings from the persistence layer with filtering options.`,
|
|
2261
|
+
inputSchema: {
|
|
2262
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
2263
|
+
severity: z.enum(["critical", "high", "medium", "low", "info"]).optional().describe("Filter by severity"),
|
|
2264
|
+
category: z.string().optional().describe("Filter by category"),
|
|
2265
|
+
file: z.string().optional().describe("Filter by file path (partial match)"),
|
|
2266
|
+
limit: z.number().default(50).describe("Maximum findings to return"),
|
|
2267
|
+
},
|
|
2268
|
+
annotations: {
|
|
2269
|
+
readOnlyHint: true,
|
|
2270
|
+
destructiveHint: false,
|
|
2271
|
+
idempotentHint: true,
|
|
2272
|
+
openWorldHint: false,
|
|
2273
|
+
},
|
|
2274
|
+
}, async ({ project_path, severity, category, file, limit }) => {
|
|
2275
|
+
try {
|
|
2276
|
+
const findings = await getOpenFindings(project_path, {
|
|
2277
|
+
severity: severity,
|
|
2278
|
+
category,
|
|
2279
|
+
file,
|
|
2280
|
+
limit,
|
|
2281
|
+
});
|
|
2282
|
+
if (findings.length === 0) {
|
|
2283
|
+
return {
|
|
2284
|
+
content: [{ type: "text", text: "No open findings found matching the criteria." }],
|
|
2285
|
+
findings: [],
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
const lines = [
|
|
2289
|
+
`# Open Findings (${findings.length})`,
|
|
2290
|
+
"",
|
|
2291
|
+
];
|
|
2292
|
+
const bySeverity = {};
|
|
2293
|
+
for (const f of findings) {
|
|
2294
|
+
if (!bySeverity[f.severity])
|
|
2295
|
+
bySeverity[f.severity] = [];
|
|
2296
|
+
bySeverity[f.severity].push(f);
|
|
2297
|
+
}
|
|
2298
|
+
for (const sev of ["critical", "high", "medium", "low", "info"]) {
|
|
2299
|
+
const sevFindings = bySeverity[sev];
|
|
2300
|
+
if (!sevFindings || sevFindings.length === 0)
|
|
2301
|
+
continue;
|
|
2302
|
+
lines.push(`## ${sev.toUpperCase()} (${sevFindings.length})`);
|
|
2303
|
+
lines.push("");
|
|
2304
|
+
for (const f of sevFindings.slice(0, 10)) {
|
|
2305
|
+
lines.push(`- **${f.file}:${f.line}** - ${f.description.slice(0, 80)}`);
|
|
2306
|
+
lines.push(` - Category: ${f.category}, Scanner: ${f.scannerSource}`);
|
|
2307
|
+
lines.push(` - First seen: ${f.firstSeenAt.split("T")[0]}`);
|
|
2308
|
+
}
|
|
2309
|
+
if (sevFindings.length > 10) {
|
|
2310
|
+
lines.push(`- ... and ${sevFindings.length - 10} more ${sev} findings`);
|
|
2311
|
+
}
|
|
2312
|
+
lines.push("");
|
|
2313
|
+
}
|
|
2314
|
+
return {
|
|
2315
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
2316
|
+
findings,
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
catch (error) {
|
|
2320
|
+
return errorResponse(`Failed to get findings: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
// ---------------------------------------------------------------------------
|
|
2324
|
+
// Tool: Persistence - Mark Findings Fixed
|
|
2325
|
+
// ---------------------------------------------------------------------------
|
|
2326
|
+
server.registerTool("persistence_mark_fixed", {
|
|
2327
|
+
title: "Mark Findings as Fixed",
|
|
2328
|
+
description: `Mark one or more findings as fixed in the persistence layer. This updates the finding status and records fix history.`,
|
|
2329
|
+
inputSchema: {
|
|
2330
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
2331
|
+
finding_ids: z.array(z.string()).describe("Finding IDs to mark as fixed"),
|
|
2332
|
+
fixed_by: z.string().optional().describe("Who fixed the findings"),
|
|
2333
|
+
},
|
|
2334
|
+
annotations: {
|
|
2335
|
+
readOnlyHint: false,
|
|
2336
|
+
destructiveHint: false,
|
|
2337
|
+
idempotentHint: true,
|
|
2338
|
+
openWorldHint: false,
|
|
2339
|
+
},
|
|
2340
|
+
}, async ({ project_path, finding_ids, fixed_by }) => {
|
|
2341
|
+
try {
|
|
2342
|
+
const count = await markFindingsFixed(project_path, finding_ids, fixed_by);
|
|
2343
|
+
return {
|
|
2344
|
+
content: [
|
|
2345
|
+
{
|
|
2346
|
+
type: "text",
|
|
2347
|
+
text: `Marked ${count} finding(s) as fixed.${fixed_by ? ` Fixed by: ${fixed_by}` : ""}`,
|
|
2348
|
+
},
|
|
2349
|
+
],
|
|
2350
|
+
fixedCount: count,
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
catch (error) {
|
|
2354
|
+
return errorResponse(`Failed to mark findings: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2355
|
+
}
|
|
2356
|
+
});
|
|
2357
|
+
// ---------------------------------------------------------------------------
|
|
1957
2358
|
// Tool: Certification Summary
|
|
1958
2359
|
// ---------------------------------------------------------------------------
|
|
1959
2360
|
server.registerTool("certification_summary", {
|
|
@@ -2664,6 +3065,325 @@ server.registerTool("verify_audit_trail", {
|
|
|
2664
3065
|
}
|
|
2665
3066
|
});
|
|
2666
3067
|
// ---------------------------------------------------------------------------
|
|
3068
|
+
// Tool: Audit Export
|
|
3069
|
+
// ---------------------------------------------------------------------------
|
|
3070
|
+
server.registerTool("audit_export", {
|
|
3071
|
+
title: "Export Audit Trail",
|
|
3072
|
+
description: `Export the tamper-evident audit trail for compliance review. Creates a portable, verifiable export suitable for external auditors and compliance teams. Supports JSON, JSONL, and CSV formats.`,
|
|
3073
|
+
inputSchema: {
|
|
3074
|
+
project_path: z.string().describe("Absolute path to the project root"),
|
|
3075
|
+
format: z.enum(["json", "jsonl", "csv"]).optional().describe("Export format. Default: json"),
|
|
3076
|
+
start_date: z.string().optional().describe("Start date filter (ISO 8601)"),
|
|
3077
|
+
end_date: z.string().optional().describe("End date filter (ISO 8601)"),
|
|
3078
|
+
types: z.array(z.enum([
|
|
3079
|
+
"certification_started",
|
|
3080
|
+
"certification_completed",
|
|
3081
|
+
"scan_completed",
|
|
3082
|
+
"finding_submitted",
|
|
3083
|
+
"finding_fixed",
|
|
3084
|
+
"compliance_report",
|
|
3085
|
+
"model_run",
|
|
3086
|
+
])).optional().describe("Entry types to include. Default: all"),
|
|
3087
|
+
include_integrity: z.boolean().optional().describe("Include integrity proofs. Default: true"),
|
|
3088
|
+
output_path: z.string().optional().describe("Output file path. If omitted, returns content directly"),
|
|
3089
|
+
output_format: z.enum(["markdown", "json"]).optional().describe("Response format. Default: json"),
|
|
3090
|
+
},
|
|
3091
|
+
annotations: {
|
|
3092
|
+
readOnlyHint: true,
|
|
3093
|
+
destructiveHint: false,
|
|
3094
|
+
idempotentHint: true,
|
|
3095
|
+
openWorldHint: false,
|
|
3096
|
+
},
|
|
3097
|
+
}, async ({ project_path, format, start_date, end_date, types, include_integrity, output_path, output_format }) => {
|
|
3098
|
+
try {
|
|
3099
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
3100
|
+
const result = await exportAuditTrail(validatedPath, {
|
|
3101
|
+
format: format,
|
|
3102
|
+
startDate: start_date,
|
|
3103
|
+
endDate: end_date,
|
|
3104
|
+
types: types,
|
|
3105
|
+
includeIntegrity: include_integrity !== false,
|
|
3106
|
+
outputPath: output_path,
|
|
3107
|
+
});
|
|
3108
|
+
if (output_format === "markdown") {
|
|
3109
|
+
const lines = [
|
|
3110
|
+
"# Audit Trail Export",
|
|
3111
|
+
"",
|
|
3112
|
+
`**Project**: ${result.projectPath}`,
|
|
3113
|
+
`**Exported At**: ${result.exportedAt}`,
|
|
3114
|
+
`**Entry Count**: ${result.entryCount}`,
|
|
3115
|
+
"",
|
|
3116
|
+
"## Date Range",
|
|
3117
|
+
"",
|
|
3118
|
+
`| Field | Value |`,
|
|
3119
|
+
`|-------|-------|`,
|
|
3120
|
+
`| Start | ${result.dateRange.start} |`,
|
|
3121
|
+
`| End | ${result.dateRange.end} |`,
|
|
3122
|
+
"",
|
|
3123
|
+
"## Chain Integrity",
|
|
3124
|
+
"",
|
|
3125
|
+
`| Field | Value |`,
|
|
3126
|
+
`|-------|-------|`,
|
|
3127
|
+
`| Genesis Hash | \`${result.chainIntegrity.genesisHash?.slice(0, 16) || "N/A"}...\` |`,
|
|
3128
|
+
`| Head Hash | \`${result.chainIntegrity.headHash?.slice(0, 16) || "N/A"}...\` |`,
|
|
3129
|
+
`| Entries with Integrity | ${result.chainIntegrity.entriesWithIntegrity} |`,
|
|
3130
|
+
];
|
|
3131
|
+
if (result.outputPath) {
|
|
3132
|
+
lines.push("", `**Output File**: ${result.outputPath}`);
|
|
3133
|
+
}
|
|
3134
|
+
return textResponse(lines.join("\n"));
|
|
3135
|
+
}
|
|
3136
|
+
return jsonResponse({
|
|
3137
|
+
exportedAt: result.exportedAt,
|
|
3138
|
+
projectPath: result.projectPath,
|
|
3139
|
+
entryCount: result.entryCount,
|
|
3140
|
+
dateRange: result.dateRange,
|
|
3141
|
+
chainIntegrity: result.chainIntegrity,
|
|
3142
|
+
outputPath: result.outputPath,
|
|
3143
|
+
content: result.content,
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
catch (error) {
|
|
3147
|
+
if (error instanceof PathValidationError) {
|
|
3148
|
+
return errorResponse(error.message);
|
|
3149
|
+
}
|
|
3150
|
+
return errorResponse(`Audit export failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3151
|
+
}
|
|
3152
|
+
});
|
|
3153
|
+
// ---------------------------------------------------------------------------
|
|
3154
|
+
// Tool: SIEM Configure
|
|
3155
|
+
// ---------------------------------------------------------------------------
|
|
3156
|
+
server.registerTool("siem_configure", {
|
|
3157
|
+
title: "Configure SIEM Connection",
|
|
3158
|
+
description: `Configure a connection to a SIEM platform (Splunk, Microsoft Sentinel, or Datadog). The connection is stored in memory for the session and can be used to export findings and events.`,
|
|
3159
|
+
inputSchema: {
|
|
3160
|
+
name: z.string().describe("Unique name for this SIEM connection"),
|
|
3161
|
+
provider: z.enum(["splunk", "sentinel", "datadog"]).describe("SIEM provider type"),
|
|
3162
|
+
endpoint: z.string().optional().describe("SIEM endpoint URL (required for Splunk)"),
|
|
3163
|
+
token: z.string().describe("Authentication token/API key"),
|
|
3164
|
+
workspace_id: z.string().optional().describe("Log Analytics workspace ID (Sentinel only)"),
|
|
3165
|
+
index: z.string().optional().describe("Splunk index name"),
|
|
3166
|
+
source_type: z.string().optional().describe("Splunk source type"),
|
|
3167
|
+
site: z.string().optional().describe("Datadog site (e.g., datadoghq.com, datadoghq.eu)"),
|
|
3168
|
+
service: z.string().optional().describe("Service name for tagging"),
|
|
3169
|
+
env: z.string().optional().describe("Environment tag"),
|
|
3170
|
+
tags: z.array(z.string()).optional().describe("Additional tags (Datadog)"),
|
|
3171
|
+
},
|
|
3172
|
+
annotations: {
|
|
3173
|
+
readOnlyHint: false,
|
|
3174
|
+
destructiveHint: false,
|
|
3175
|
+
idempotentHint: true,
|
|
3176
|
+
openWorldHint: true,
|
|
3177
|
+
},
|
|
3178
|
+
}, async ({ name, provider, endpoint, token, workspace_id, index, source_type, site, service, env, tags }) => {
|
|
3179
|
+
try {
|
|
3180
|
+
const registry = getSIEMRegistry();
|
|
3181
|
+
let client;
|
|
3182
|
+
switch (provider) {
|
|
3183
|
+
case "splunk":
|
|
3184
|
+
if (!endpoint) {
|
|
3185
|
+
return errorResponse("Splunk requires an endpoint URL");
|
|
3186
|
+
}
|
|
3187
|
+
client = createSIEMClient({
|
|
3188
|
+
provider: "splunk",
|
|
3189
|
+
enabled: true,
|
|
3190
|
+
endpoint,
|
|
3191
|
+
token,
|
|
3192
|
+
options: { index, sourceType: source_type, source: service },
|
|
3193
|
+
});
|
|
3194
|
+
break;
|
|
3195
|
+
case "sentinel":
|
|
3196
|
+
if (!workspace_id) {
|
|
3197
|
+
return errorResponse("Sentinel requires a workspace_id");
|
|
3198
|
+
}
|
|
3199
|
+
client = createSIEMClient({
|
|
3200
|
+
provider: "sentinel",
|
|
3201
|
+
enabled: true,
|
|
3202
|
+
endpoint: `https://${workspace_id}.ods.opinsights.azure.com`,
|
|
3203
|
+
token,
|
|
3204
|
+
options: { workspaceId: workspace_id },
|
|
3205
|
+
});
|
|
3206
|
+
break;
|
|
3207
|
+
case "datadog":
|
|
3208
|
+
client = createSIEMClient({
|
|
3209
|
+
provider: "datadog",
|
|
3210
|
+
enabled: true,
|
|
3211
|
+
endpoint: "",
|
|
3212
|
+
token,
|
|
3213
|
+
options: { site, service, env, tags },
|
|
3214
|
+
});
|
|
3215
|
+
break;
|
|
3216
|
+
}
|
|
3217
|
+
registry.register(name, client);
|
|
3218
|
+
return jsonResponse({
|
|
3219
|
+
success: true,
|
|
3220
|
+
name,
|
|
3221
|
+
provider,
|
|
3222
|
+
message: `SIEM connection '${name}' configured successfully`,
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
catch (error) {
|
|
3226
|
+
return errorResponse(`Failed to configure SIEM: ${error instanceof Error ? error.message : String(error)}`);
|
|
3227
|
+
}
|
|
3228
|
+
});
|
|
3229
|
+
// ---------------------------------------------------------------------------
|
|
3230
|
+
// Tool: SIEM Test
|
|
3231
|
+
// ---------------------------------------------------------------------------
|
|
3232
|
+
server.registerTool("siem_test", {
|
|
3233
|
+
title: "Test SIEM Connection",
|
|
3234
|
+
description: `Test connectivity to a configured SIEM platform. Sends a test event and reports latency and status.`,
|
|
3235
|
+
inputSchema: {
|
|
3236
|
+
name: z.string().describe("Name of the configured SIEM connection"),
|
|
3237
|
+
},
|
|
3238
|
+
annotations: {
|
|
3239
|
+
readOnlyHint: true,
|
|
3240
|
+
destructiveHint: false,
|
|
3241
|
+
idempotentHint: true,
|
|
3242
|
+
openWorldHint: true,
|
|
3243
|
+
},
|
|
3244
|
+
}, async ({ name }) => {
|
|
3245
|
+
try {
|
|
3246
|
+
const registry = getSIEMRegistry();
|
|
3247
|
+
const client = registry.get(name);
|
|
3248
|
+
if (!client) {
|
|
3249
|
+
return errorResponse(`SIEM connection '${name}' not found. Use siem_configure first.`);
|
|
3250
|
+
}
|
|
3251
|
+
const result = await client.testConnection();
|
|
3252
|
+
if (result.success) {
|
|
3253
|
+
return jsonResponse({
|
|
3254
|
+
success: true,
|
|
3255
|
+
provider: result.provider,
|
|
3256
|
+
endpoint: result.endpoint,
|
|
3257
|
+
latencyMs: result.latencyMs,
|
|
3258
|
+
message: `Connection to ${result.provider} successful`,
|
|
3259
|
+
details: result.details,
|
|
3260
|
+
});
|
|
3261
|
+
}
|
|
3262
|
+
return errorResponse(`Connection test failed: ${result.error}`);
|
|
3263
|
+
}
|
|
3264
|
+
catch (error) {
|
|
3265
|
+
return errorResponse(`Failed to test SIEM: ${error instanceof Error ? error.message : String(error)}`);
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
// ---------------------------------------------------------------------------
|
|
3269
|
+
// Tool: SIEM Export Findings
|
|
3270
|
+
// ---------------------------------------------------------------------------
|
|
3271
|
+
server.registerTool("siem_export_findings", {
|
|
3272
|
+
title: "Export Findings to SIEM",
|
|
3273
|
+
description: `Export security findings to a configured SIEM platform. Sends finding events for SOC visibility and incident response integration.`,
|
|
3274
|
+
inputSchema: {
|
|
3275
|
+
name: z.string().describe("Name of the configured SIEM connection"),
|
|
3276
|
+
project_path: z.string().describe("Absolute path to the project"),
|
|
3277
|
+
certification_id: z.string().optional().describe("Certification ID to filter findings"),
|
|
3278
|
+
severity: z.array(z.enum(["critical", "high", "medium", "low", "info"])).optional().describe("Filter by severity"),
|
|
3279
|
+
event_type: z.enum(["finding.new", "finding.fixed", "scan.completed", "certification.completed"]).optional().describe("Event type to generate"),
|
|
3280
|
+
dry_run: z.boolean().optional().describe("Preview events without sending. Default: false"),
|
|
3281
|
+
},
|
|
3282
|
+
annotations: {
|
|
3283
|
+
readOnlyHint: false,
|
|
3284
|
+
destructiveHint: false,
|
|
3285
|
+
idempotentHint: false,
|
|
3286
|
+
openWorldHint: true,
|
|
3287
|
+
},
|
|
3288
|
+
}, async ({ name, project_path, certification_id, severity, event_type, dry_run }) => {
|
|
3289
|
+
try {
|
|
3290
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
3291
|
+
const registry = getSIEMRegistry();
|
|
3292
|
+
const client = registry.get(name);
|
|
3293
|
+
if (!client) {
|
|
3294
|
+
return errorResponse(`SIEM connection '${name}' not found. Use siem_configure first.`);
|
|
3295
|
+
}
|
|
3296
|
+
// Get findings from certification
|
|
3297
|
+
if (!certification_id) {
|
|
3298
|
+
return errorResponse("certification_id is required to export findings");
|
|
3299
|
+
}
|
|
3300
|
+
const certification = await getCertification(validatedPath, certification_id);
|
|
3301
|
+
if (!certification) {
|
|
3302
|
+
return errorResponse("No certification data found. Run certification_scan first.");
|
|
3303
|
+
}
|
|
3304
|
+
// Extract findings from all agents
|
|
3305
|
+
let allFindings = [];
|
|
3306
|
+
for (const agentFindings of Object.values(certification.agents)) {
|
|
3307
|
+
if (agentFindings?.findings) {
|
|
3308
|
+
allFindings.push(...agentFindings.findings);
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
// Filter by severity if provided
|
|
3312
|
+
if (severity && severity.length > 0) {
|
|
3313
|
+
allFindings = allFindings.filter((f) => severity.includes(f.severity));
|
|
3314
|
+
}
|
|
3315
|
+
// Generate events
|
|
3316
|
+
const events = allFindings.map((finding) => createFindingEvent(validatedPath, event_type || "finding.new", {
|
|
3317
|
+
findingId: finding.id,
|
|
3318
|
+
severity: finding.severity,
|
|
3319
|
+
category: finding.category,
|
|
3320
|
+
file: finding.file,
|
|
3321
|
+
line: finding.line,
|
|
3322
|
+
scanner: finding.scannerSource,
|
|
3323
|
+
ruleId: finding.ruleId,
|
|
3324
|
+
cweIds: finding.cweIds,
|
|
3325
|
+
description: finding.description,
|
|
3326
|
+
}, certification_id));
|
|
3327
|
+
if (dry_run) {
|
|
3328
|
+
return jsonResponse({
|
|
3329
|
+
dryRun: true,
|
|
3330
|
+
eventCount: events.length,
|
|
3331
|
+
events: events.slice(0, 10),
|
|
3332
|
+
message: `Would send ${events.length} events to ${client.provider}`,
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
// Send events
|
|
3336
|
+
const result = await client.sendEvents(events);
|
|
3337
|
+
return jsonResponse({
|
|
3338
|
+
success: result.success,
|
|
3339
|
+
provider: client.provider,
|
|
3340
|
+
totalEvents: result.totalEvents,
|
|
3341
|
+
successCount: result.successCount,
|
|
3342
|
+
failureCount: result.failureCount,
|
|
3343
|
+
errors: result.errors,
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
catch (error) {
|
|
3347
|
+
if (error instanceof PathValidationError) {
|
|
3348
|
+
return errorResponse(error.message);
|
|
3349
|
+
}
|
|
3350
|
+
return errorResponse(`Failed to export findings: ${error instanceof Error ? error.message : String(error)}`);
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
// ---------------------------------------------------------------------------
|
|
3354
|
+
// Tool: SIEM List
|
|
3355
|
+
// ---------------------------------------------------------------------------
|
|
3356
|
+
server.registerTool("siem_list", {
|
|
3357
|
+
title: "List SIEM Connections",
|
|
3358
|
+
description: `List all configured SIEM connections in the current session.`,
|
|
3359
|
+
inputSchema: {},
|
|
3360
|
+
annotations: {
|
|
3361
|
+
readOnlyHint: true,
|
|
3362
|
+
destructiveHint: false,
|
|
3363
|
+
idempotentHint: true,
|
|
3364
|
+
openWorldHint: false,
|
|
3365
|
+
},
|
|
3366
|
+
}, async () => {
|
|
3367
|
+
try {
|
|
3368
|
+
const registry = getSIEMRegistry();
|
|
3369
|
+
const connections = registry.list();
|
|
3370
|
+
if (connections.length === 0) {
|
|
3371
|
+
return textResponse("No SIEM connections configured. Use siem_configure to add one.");
|
|
3372
|
+
}
|
|
3373
|
+
const lines = [
|
|
3374
|
+
"# Configured SIEM Connections",
|
|
3375
|
+
"",
|
|
3376
|
+
"| Name | Provider |",
|
|
3377
|
+
"|------|----------|",
|
|
3378
|
+
...connections.map((c) => `| ${c.name} | ${c.provider} |`),
|
|
3379
|
+
];
|
|
3380
|
+
return textResponse(lines.join("\n"));
|
|
3381
|
+
}
|
|
3382
|
+
catch (error) {
|
|
3383
|
+
return errorResponse(`Failed to list SIEM connections: ${error instanceof Error ? error.message : String(error)}`);
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
// ---------------------------------------------------------------------------
|
|
2667
3387
|
// Tool: Collect Evidence Bundle
|
|
2668
3388
|
// ---------------------------------------------------------------------------
|
|
2669
3389
|
server.registerTool("collect_evidence_bundle", {
|
|
@@ -5467,6 +6187,193 @@ server.registerTool("ai_code_hallucinations", {
|
|
|
5467
6187
|
}
|
|
5468
6188
|
});
|
|
5469
6189
|
// ---------------------------------------------------------------------------
|
|
6190
|
+
// False Positive Feedback Tools
|
|
6191
|
+
// ---------------------------------------------------------------------------
|
|
6192
|
+
server.registerTool("feedback_submit", {
|
|
6193
|
+
description: "Submit feedback marking a finding as true positive (TP) or false positive (FP). Helps improve scanner accuracy over time.",
|
|
6194
|
+
inputSchema: {
|
|
6195
|
+
project_path: z.string().describe("Path to the project"),
|
|
6196
|
+
scanner: z.enum([
|
|
6197
|
+
"semgrep", "npm-audit", "gitleaks", "tsc", "eslint",
|
|
6198
|
+
"bandit", "gosec", "brakeman", "trivy", "binary-analysis",
|
|
6199
|
+
"memory-safety", "race-condition", "healthcare", "logic",
|
|
6200
|
+
"dast", "zap", "nuclei", "terraform", "tfsec", "checkov",
|
|
6201
|
+
"openapi", "spectral", "rust", "cargo-audit", "clippy",
|
|
6202
|
+
"detection", "plugin"
|
|
6203
|
+
]).describe("Scanner that generated the finding"),
|
|
6204
|
+
rule_id: z.string().describe("Rule ID (e.g., 'semgrep:owasp.sql-injection')"),
|
|
6205
|
+
file: z.string().describe("File path where the finding was reported"),
|
|
6206
|
+
line: z.number().optional().describe("Line number of the finding"),
|
|
6207
|
+
verdict: z.enum(["tp", "fp"]).describe("Your verdict: 'tp' for true positive, 'fp' for false positive"),
|
|
6208
|
+
reason: z.enum([
|
|
6209
|
+
"test-code", "false-pattern-match", "sanitized-elsewhere",
|
|
6210
|
+
"intentional", "vendor-code", "generated-code",
|
|
6211
|
+
"example-code", "configuration", "other"
|
|
6212
|
+
]).optional().describe("Reason for marking as FP (required if verdict is 'fp')"),
|
|
6213
|
+
details: z.string().optional().describe("Additional context or notes"),
|
|
6214
|
+
},
|
|
6215
|
+
annotations: {
|
|
6216
|
+
readOnlyHint: false,
|
|
6217
|
+
destructiveHint: false,
|
|
6218
|
+
idempotentHint: true,
|
|
6219
|
+
openWorldHint: false,
|
|
6220
|
+
},
|
|
6221
|
+
}, async ({ project_path, scanner, rule_id, file, line, verdict, reason, details }) => {
|
|
6222
|
+
try {
|
|
6223
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
6224
|
+
const finding = {
|
|
6225
|
+
scanner: scanner,
|
|
6226
|
+
ruleId: rule_id,
|
|
6227
|
+
file,
|
|
6228
|
+
line: line ?? 0,
|
|
6229
|
+
message: "",
|
|
6230
|
+
severity: "medium",
|
|
6231
|
+
confidence: 100,
|
|
6232
|
+
};
|
|
6233
|
+
const entry = await submitFeedback(validatedPath, finding, verdict, {
|
|
6234
|
+
reason: reason,
|
|
6235
|
+
details,
|
|
6236
|
+
});
|
|
6237
|
+
return jsonResponse({
|
|
6238
|
+
success: true,
|
|
6239
|
+
feedbackId: entry.id,
|
|
6240
|
+
scanner: entry.scanner,
|
|
6241
|
+
ruleId: entry.ruleId,
|
|
6242
|
+
file: entry.file,
|
|
6243
|
+
verdict: entry.verdict,
|
|
6244
|
+
reason: entry.reason,
|
|
6245
|
+
submittedAt: entry.submittedAt,
|
|
6246
|
+
message: verdict === "fp"
|
|
6247
|
+
? `Marked as false positive. Reason: ${FP_REASON_DESCRIPTIONS[reason] || reason}`
|
|
6248
|
+
: "Marked as true positive. This helps confirm the finding is valid.",
|
|
6249
|
+
});
|
|
6250
|
+
}
|
|
6251
|
+
catch (error) {
|
|
6252
|
+
if (error instanceof PathValidationError) {
|
|
6253
|
+
return errorResponse(`Path validation failed: ${error.message}`);
|
|
6254
|
+
}
|
|
6255
|
+
return errorResponse(error instanceof Error ? error.message : String(error));
|
|
6256
|
+
}
|
|
6257
|
+
});
|
|
6258
|
+
server.registerTool("feedback_report", {
|
|
6259
|
+
description: "Generate a report of all feedback submitted for a project, including FP rates by scanner and top FP reasons.",
|
|
6260
|
+
inputSchema: {
|
|
6261
|
+
project_path: z.string().describe("Path to the project"),
|
|
6262
|
+
},
|
|
6263
|
+
annotations: {
|
|
6264
|
+
readOnlyHint: true,
|
|
6265
|
+
destructiveHint: false,
|
|
6266
|
+
idempotentHint: true,
|
|
6267
|
+
openWorldHint: false,
|
|
6268
|
+
},
|
|
6269
|
+
}, async ({ project_path }) => {
|
|
6270
|
+
try {
|
|
6271
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
6272
|
+
const report = await generateFeedbackReport(validatedPath);
|
|
6273
|
+
return jsonResponse({
|
|
6274
|
+
overview: {
|
|
6275
|
+
totalFeedback: report.overview.totalFeedback,
|
|
6276
|
+
truePositives: report.overview.tpCount,
|
|
6277
|
+
falsePositives: report.overview.fpCount,
|
|
6278
|
+
overallFPRate: `${(report.overview.overallFPRate * 100).toFixed(1)}%`,
|
|
6279
|
+
},
|
|
6280
|
+
byScanner: report.byScanner.map((s) => ({
|
|
6281
|
+
scanner: s.scanner,
|
|
6282
|
+
total: s.total,
|
|
6283
|
+
tp: s.tp,
|
|
6284
|
+
fp: s.fp,
|
|
6285
|
+
fpRate: `${(s.fpRate * 100).toFixed(1)}%`,
|
|
6286
|
+
})),
|
|
6287
|
+
topFPReasons: report.topFPReasons.map((r) => ({
|
|
6288
|
+
reason: r.reason,
|
|
6289
|
+
description: FP_REASON_DESCRIPTIONS[r.reason],
|
|
6290
|
+
count: r.count,
|
|
6291
|
+
percentage: `${r.percentage.toFixed(1)}%`,
|
|
6292
|
+
})),
|
|
6293
|
+
recentFeedback: report.recentFeedback.slice(0, 5).map((f) => ({
|
|
6294
|
+
scanner: f.scanner,
|
|
6295
|
+
ruleId: f.ruleId,
|
|
6296
|
+
file: f.file,
|
|
6297
|
+
verdict: f.verdict,
|
|
6298
|
+
reason: f.reason,
|
|
6299
|
+
submittedAt: f.submittedAt,
|
|
6300
|
+
})),
|
|
6301
|
+
suppressionSuggestions: report.suppressionSuggestions.length,
|
|
6302
|
+
message: report.overview.totalFeedback === 0
|
|
6303
|
+
? "No feedback submitted yet. Use feedback_submit to mark findings as TP or FP."
|
|
6304
|
+
: `Analyzed ${report.overview.totalFeedback} feedback entries. FP rate: ${(report.overview.overallFPRate * 100).toFixed(1)}%`,
|
|
6305
|
+
});
|
|
6306
|
+
}
|
|
6307
|
+
catch (error) {
|
|
6308
|
+
if (error instanceof PathValidationError) {
|
|
6309
|
+
return errorResponse(`Path validation failed: ${error.message}`);
|
|
6310
|
+
}
|
|
6311
|
+
return errorResponse(error instanceof Error ? error.message : String(error));
|
|
6312
|
+
}
|
|
6313
|
+
});
|
|
6314
|
+
server.registerTool("feedback_suppressions", {
|
|
6315
|
+
description: "Get suggestions for rules to disable based on accumulated false positive feedback. Rules with >50% FP rate (min 5 samples) are flagged.",
|
|
6316
|
+
inputSchema: {
|
|
6317
|
+
project_path: z.string().describe("Path to the project"),
|
|
6318
|
+
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)"),
|
|
6319
|
+
min_sample_size: z.number().min(1).optional().default(5).describe("Minimum number of feedback entries required (default: 5)"),
|
|
6320
|
+
},
|
|
6321
|
+
annotations: {
|
|
6322
|
+
readOnlyHint: true,
|
|
6323
|
+
destructiveHint: false,
|
|
6324
|
+
idempotentHint: true,
|
|
6325
|
+
openWorldHint: false,
|
|
6326
|
+
},
|
|
6327
|
+
}, async ({ project_path, min_fp_rate, min_sample_size }) => {
|
|
6328
|
+
try {
|
|
6329
|
+
const validatedPath = await validateProjectPath(project_path);
|
|
6330
|
+
const suggestions = await getSuppressionSuggestions(validatedPath, {
|
|
6331
|
+
minFPRate: min_fp_rate,
|
|
6332
|
+
minSampleSize: min_sample_size,
|
|
6333
|
+
});
|
|
6334
|
+
if (suggestions.length === 0) {
|
|
6335
|
+
return jsonResponse({
|
|
6336
|
+
suggestions: [],
|
|
6337
|
+
message: "No rules meet the criteria for suppression. Either not enough feedback or FP rates are acceptable.",
|
|
6338
|
+
criteria: {
|
|
6339
|
+
minFPRate: `${(min_fp_rate ?? 0.5) * 100}%`,
|
|
6340
|
+
minSampleSize: min_sample_size ?? 5,
|
|
6341
|
+
},
|
|
6342
|
+
});
|
|
6343
|
+
}
|
|
6344
|
+
return jsonResponse({
|
|
6345
|
+
suggestions: suggestions.map((s) => ({
|
|
6346
|
+
scanner: s.scanner,
|
|
6347
|
+
ruleId: s.ruleId,
|
|
6348
|
+
fpRate: `${(s.fpRate * 100).toFixed(1)}%`,
|
|
6349
|
+
sampleSize: s.sampleSize,
|
|
6350
|
+
recommendation: s.suggestion,
|
|
6351
|
+
recommendationText: s.suggestion === "disable"
|
|
6352
|
+
? "High FP rate (≥80%) - consider disabling this rule"
|
|
6353
|
+
: s.suggestion === "review"
|
|
6354
|
+
? "Moderate FP rate (≥50%) - review rule configuration"
|
|
6355
|
+
: "FP rate acceptable - keep rule enabled",
|
|
6356
|
+
commonReasons: s.commonReasons.map((r) => ({
|
|
6357
|
+
reason: r,
|
|
6358
|
+
description: FP_REASON_DESCRIPTIONS[r],
|
|
6359
|
+
})),
|
|
6360
|
+
})),
|
|
6361
|
+
summary: {
|
|
6362
|
+
totalSuggestions: suggestions.length,
|
|
6363
|
+
disableRecommendations: suggestions.filter((s) => s.suggestion === "disable").length,
|
|
6364
|
+
reviewRecommendations: suggestions.filter((s) => s.suggestion === "review").length,
|
|
6365
|
+
},
|
|
6366
|
+
message: `Found ${suggestions.length} rule(s) with high false positive rates based on your feedback.`,
|
|
6367
|
+
});
|
|
6368
|
+
}
|
|
6369
|
+
catch (error) {
|
|
6370
|
+
if (error instanceof PathValidationError) {
|
|
6371
|
+
return errorResponse(`Path validation failed: ${error.message}`);
|
|
6372
|
+
}
|
|
6373
|
+
return errorResponse(error instanceof Error ? error.message : String(error));
|
|
6374
|
+
}
|
|
6375
|
+
});
|
|
6376
|
+
// ---------------------------------------------------------------------------
|
|
5470
6377
|
// Exports (for HTTP server and client integration)
|
|
5471
6378
|
// ---------------------------------------------------------------------------
|
|
5472
6379
|
export { server, textResponse, jsonResponse, errorResponse };
|