fix: Allow dismissing AI findings without Pro license. Related to #885

Users who accumulated AI patrol findings before the patrol-without-AI
bug was fixed (24c4bb0b) could not dismiss them because the dismiss and
resolve endpoints required a Pro license.

Changes:
- Remove Pro license requirement from /api/ai/patrol/dismiss endpoint
- Remove Pro license requirement from /api/ai/patrol/resolve endpoint
- Add ClearAll() method to FindingsStore for bulk clearing
- Add DELETE /api/ai/patrol/findings endpoint for clearing all findings
- Add "Clear All" button to AI Insights UI

Users can now dismiss or resolve any findings they can see, and admins
can clear all findings at once if needed.
This commit is contained in:
rcourtman
2025-12-25 05:06:26 +00:00
parent 9cebf9cd21
commit 0feb039775
5 changed files with 127 additions and 20 deletions
+14
View File
@@ -584,6 +584,20 @@ func (s *FindingsStore) GetAll(startTime *time.Time) []*Finding {
return result
}
// ClearAll removes all findings from the store
// Returns the number of findings removed
func (s *FindingsStore) ClearAll() int {
s.mu.Lock()
defer s.mu.Unlock()
count := len(s.findings)
s.findings = make(map[string]*Finding)
s.byResource = make(map[string][]string)
s.activeCounts = make(map[FindingSeverity]int)
s.scheduleSave()
return count
}
// Cleanup removes old resolved findings
func (s *FindingsStore) Cleanup(maxAge time.Duration) int {
s.mu.Lock()
+42
View File
@@ -2718,6 +2718,48 @@ func (h *AISettingsHandler) HandleSuppressFinding(w http.ResponseWriter, r *http
}
}
// HandleClearAllFindings clears all AI findings (DELETE /api/ai/patrol/findings)
// This allows users to clear accumulated findings, especially useful for users who
// accumulated findings before the patrol-without-AI bug was fixed.
func (h *AISettingsHandler) HandleClearAllFindings(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Require authentication (already wrapped by RequireAuth in router)
if !CheckAuth(h.config, w, r) {
return
}
// Check for confirm parameter
if r.URL.Query().Get("confirm") != "true" {
http.Error(w, "confirm=true query parameter required", http.StatusBadRequest)
return
}
patrol := h.aiService.GetPatrolService()
if patrol == nil {
http.Error(w, "Patrol service not available", http.StatusServiceUnavailable)
return
}
findings := patrol.GetFindings()
count := findings.ClearAll()
log.Info().Int("count", count).Msg("Cleared all AI findings")
response := map[string]interface{}{
"success": true,
"cleared": count,
"message": fmt.Sprintf("Cleared %d findings", count),
}
if err := utils.WriteJSONResponse(w, response); err != nil {
log.Error().Err(err).Msg("Failed to write clear findings response")
}
}
// HandleGetFindingsHistory returns all findings including resolved for history (GET /api/ai/patrol/history)
func (h *AISettingsHandler) HandleGetFindingsHistory(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
+15 -3
View File
@@ -1161,14 +1161,26 @@ func (r *Router) setupRoutes() {
// Mutation endpoints (run, acknowledge, dismiss, etc.) return 402 to prevent unauthorized actions
r.mux.HandleFunc("/api/ai/patrol/status", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolStatus))
r.mux.HandleFunc("/api/ai/patrol/stream", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandlePatrolStream)))
r.mux.HandleFunc("/api/ai/patrol/findings", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolFindings))
r.mux.HandleFunc("/api/ai/patrol/findings", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
r.aiSettingsHandler.HandleGetPatrolFindings(w, req)
case http.MethodDelete:
// Clear all findings - doesn't require Pro license so users can clean up accumulated findings
r.aiSettingsHandler.HandleClearAllFindings(w, req)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}))
r.mux.HandleFunc("/api/ai/patrol/history", RequireAuth(r.config, r.aiSettingsHandler.HandleGetFindingsHistory))
r.mux.HandleFunc("/api/ai/patrol/run", RequireAdmin(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleForcePatrol)))
r.mux.HandleFunc("/api/ai/patrol/acknowledge", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleAcknowledgeFinding)))
r.mux.HandleFunc("/api/ai/patrol/dismiss", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleDismissFinding)))
// Dismiss and resolve don't require Pro license - users should be able to clear findings they can see
// This is especially important for users who accumulated findings before fixing the patrol-without-AI bug
r.mux.HandleFunc("/api/ai/patrol/dismiss", RequireAuth(r.config, r.aiSettingsHandler.HandleDismissFinding))
r.mux.HandleFunc("/api/ai/patrol/suppress", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleSuppressFinding)))
r.mux.HandleFunc("/api/ai/patrol/snooze", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleSnoozeFinding)))
r.mux.HandleFunc("/api/ai/patrol/resolve", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleResolveFinding)))
r.mux.HandleFunc("/api/ai/patrol/resolve", RequireAuth(r.config, r.aiSettingsHandler.HandleResolveFinding))
r.mux.HandleFunc("/api/ai/patrol/runs", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolRunHistory))
// Suppression rules management (also Pro-only since they control LLM behavior)
// GET returns empty array for unlicensed, POST returns 402