vaspera 2.12.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/__tests__/siem-integration.test.d.ts +7 -0
  3. package/dist/__tests__/siem-integration.test.d.ts.map +1 -0
  4. package/dist/__tests__/siem-integration.test.js +285 -0
  5. package/dist/__tests__/siem-integration.test.js.map +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +296 -16
  8. package/dist/index.js.map +1 -1
  9. package/dist/persistence/__tests__/json-fallback.test.d.ts +5 -0
  10. package/dist/persistence/__tests__/json-fallback.test.d.ts.map +1 -0
  11. package/dist/persistence/__tests__/json-fallback.test.js +249 -0
  12. package/dist/persistence/__tests__/json-fallback.test.js.map +1 -0
  13. package/dist/persistence/__tests__/persistence.test.js.map +1 -1
  14. package/dist/persistence/db.d.ts +15 -0
  15. package/dist/persistence/db.d.ts.map +1 -1
  16. package/dist/persistence/db.js +59 -10
  17. package/dist/persistence/db.js.map +1 -1
  18. package/dist/persistence/index.d.ts +13 -4
  19. package/dist/persistence/index.d.ts.map +1 -1
  20. package/dist/persistence/index.js +139 -14
  21. package/dist/persistence/index.js.map +1 -1
  22. package/dist/persistence/json-fallback.d.ts +52 -0
  23. package/dist/persistence/json-fallback.d.ts.map +1 -0
  24. package/dist/persistence/json-fallback.js +283 -0
  25. package/dist/persistence/json-fallback.js.map +1 -0
  26. package/dist/scanners/ai-code/index.d.ts.map +1 -1
  27. package/dist/scanners/ai-code/index.js +90 -2
  28. package/dist/scanners/ai-code/index.js.map +1 -1
  29. package/dist/scanners/ai-code/types.d.ts +12 -0
  30. package/dist/scanners/ai-code/types.d.ts.map +1 -1
  31. package/dist/scanners/eslint.d.ts.map +1 -1
  32. package/dist/scanners/eslint.js +45 -3
  33. package/dist/scanners/eslint.js.map +1 -1
  34. package/dist/scanners/scale/bottleneck-detector.d.ts +13 -2
  35. package/dist/scanners/scale/bottleneck-detector.d.ts.map +1 -1
  36. package/dist/scanners/scale/bottleneck-detector.js +199 -72
  37. package/dist/scanners/scale/bottleneck-detector.js.map +1 -1
  38. package/dist/scanners/types.d.ts +18 -1
  39. package/dist/scanners/types.d.ts.map +1 -1
  40. package/dist/scanners/types.js.map +1 -1
  41. package/dist/scanners/typescript.d.ts.map +1 -1
  42. package/dist/scanners/typescript.js +36 -4
  43. package/dist/scanners/typescript.js.map +1 -1
  44. 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", {
@@ -1789,11 +1849,17 @@ server.registerTool("certification_dashboard", {
1789
1849
  // ---------------------------------------------------------------------------
1790
1850
  server.registerTool("autofix_preview", {
1791
1851
  title: "Preview Auto-Fix for Finding",
1792
- description: `Preview an automatic fix for a certification finding without applying it. Returns the before/after diff.`,
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`,
1793
1855
  inputSchema: {
1794
1856
  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"),
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')"),
1797
1863
  },
1798
1864
  annotations: {
1799
1865
  readOnlyHint: true,
@@ -1801,24 +1867,51 @@ server.registerTool("autofix_preview", {
1801
1867
  idempotentHint: true,
1802
1868
  openWorldHint: false,
1803
1869
  },
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
1870
+ }, async ({ project_path, certification_id, finding_id, file, line, pattern_id, category }) => {
1810
1871
  let finding = null;
1811
- for (const agentData of Object.values(certification.agents)) {
1812
- if (agentData) {
1813
- finding = agentData.findings.find((f) => f.id === finding_id);
1814
- if (finding)
1815
- break;
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`);
1816
1886
  }
1817
1887
  }
1818
- if (!finding) {
1819
- return errorResponse(`Finding ${finding_id} not found`);
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.");
1820
1907
  }
1821
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
+ }
1822
1915
  return jsonResponse({ ...preview });
1823
1916
  });
1824
1917
  // ---------------------------------------------------------------------------
@@ -6094,6 +6187,193 @@ server.registerTool("ai_code_hallucinations", {
6094
6187
  }
6095
6188
  });
6096
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
+ // ---------------------------------------------------------------------------
6097
6377
  // Exports (for HTTP server and client integration)
6098
6378
  // ---------------------------------------------------------------------------
6099
6379
  export { server, textResponse, jsonResponse, errorResponse };