package datastore import ( "context" "fmt" "time" domain2 "epigas.gitea.cloud/RiskRancher/core/pkg/domain" ) func (s *SQLiteStore) GetSheriffAnalytics(ctx context.Context) (domain2.SheriffAnalytics, error) { var metrics domain2.SheriffAnalytics s.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM tickets WHERE is_cisa_kev = 1 AND status NOT IN ('Patched', 'Risk Accepted', 'False Positive')").Scan(&metrics.ActiveKEVs) s.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM tickets WHERE severity = 'Critical' AND status NOT IN ('Patched', 'Risk Accepted', 'False Positive')").Scan(&metrics.OpenCriticals) s.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM tickets WHERE remediation_due_date < CURRENT_TIMESTAMP AND status NOT IN ('Patched', 'Risk Accepted', 'False Positive')").Scan(&metrics.TotalOverdue) mttrQuery := ` SELECT COALESCE(AVG(julianday(t.patched_at) - julianday(t.created_at)), 0) FROM tickets t WHERE t.status = 'Patched' ` var mttrFloat float64 s.DB.QueryRowContext(ctx, mttrQuery).Scan(&mttrFloat) metrics.GlobalMTTRDays = int(mttrFloat) sourceQuery := ` SELECT t.source, SUM(CASE WHEN t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive') THEN 1 ELSE 0 END) as total_open, SUM(CASE WHEN t.severity = 'Critical' AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive') THEN 1 ELSE 0 END) as criticals, SUM(CASE WHEN t.is_cisa_kev = 1 AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive') THEN 1 ELSE 0 END) as cisa_kevs, SUM(CASE WHEN t.status = 'Waiting to be Triaged' THEN 1 ELSE 0 END) as untriaged, SUM(CASE WHEN t.remediation_due_date < CURRENT_TIMESTAMP AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive') THEN 1 ELSE 0 END) as patch_overdue, SUM(CASE WHEN t.status = 'Pending Risk Approval' THEN 1 ELSE 0 END) as pending_risk, SUM(CASE WHEN t.status IN ('Patched', 'Risk Accepted', 'False Positive') THEN 1 ELSE 0 END) as total_closed, SUM(CASE WHEN t.status = 'Patched' THEN 1 ELSE 0 END) as patched, SUM(CASE WHEN t.status = 'Risk Accepted' THEN 1 ELSE 0 END) as risk_accepted, SUM(CASE WHEN t.status = 'False Positive' THEN 1 ELSE 0 END) as false_positive FROM tickets t GROUP BY t.source ORDER BY criticals DESC, patch_overdue DESC ` rows, err := s.DB.QueryContext(ctx, sourceQuery) if err == nil { defer rows.Close() for rows.Next() { var sm domain2.SourceMetrics rows.Scan(&sm.Source, &sm.TotalOpen, &sm.Criticals, &sm.CisaKEVs, &sm.Untriaged, &sm.PatchOverdue, &sm.PendingRisk, &sm.TotalClosed, &sm.Patched, &sm.RiskAccepted, &sm.FalsePositive) topAssigneeQ := ` SELECT COALESCE(ta.assignee, 'Unassigned'), COUNT(t.id) as c FROM tickets t LEFT JOIN ticket_assignments ta ON t.id = ta.ticket_id WHERE t.source = ? AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive') GROUP BY ta.assignee ORDER BY c DESC LIMIT 1` var assignee string var count int s.DB.QueryRowContext(ctx, topAssigneeQ, sm.Source).Scan(&assignee, &count) if count > 0 { sm.TopAssignee = fmt.Sprintf("%s (%d)", assignee, count) } else { sm.TopAssignee = "N/A" } if sm.PatchOverdue > 0 { sm.StrategicNote = "🚨 SLA Breach (Escalate to IT Managers)" } else if sm.Untriaged > 0 { sm.StrategicNote = "⚠️ Triage Bottleneck (Check Analysts)" } else if sm.PendingRisk > 0 { sm.StrategicNote = "⚖️ Blocked by Exec Adjudication" } else if sm.Criticals > 0 { sm.StrategicNote = "🔥 High Risk (Monitor closely)" } else if sm.RiskAccepted > sm.Patched && sm.TotalClosed > 0 { sm.StrategicNote = "👀 High Risk Acceptance Rate (Audit Required)" } else if sm.FalsePositive > sm.Patched && sm.TotalClosed > 0 { sm.StrategicNote = "🔧 Noisy Source (Scanner needs tuning)" } else if sm.TotalClosed > 0 { sm.StrategicNote = "✅ Healthy Resolution Velocity" } else { sm.StrategicNote = "✅ Routine Processing" } metrics.SourceHealth = append(metrics.SourceHealth, sm) } } sevQuery := `SELECT severity, COUNT(id) FROM tickets WHERE status NOT IN ('Patched', 'Risk Accepted', 'False Positive') GROUP BY severity` rowsSev, err := s.DB.QueryContext(ctx, sevQuery) if err == nil { defer rowsSev.Close() for rowsSev.Next() { var sev string var count int rowsSev.Scan(&sev, &count) metrics.Severity.Total += count switch sev { case "Critical": metrics.Severity.Critical = count case "High": metrics.Severity.High = count case "Medium": metrics.Severity.Medium = count case "Low": metrics.Severity.Low = count case "Info": metrics.Severity.Info = count } } if metrics.Severity.Total > 0 { metrics.Severity.CritPct = int((float64(metrics.Severity.Critical) / float64(metrics.Severity.Total)) * 100) metrics.Severity.HighPct = int((float64(metrics.Severity.High) / float64(metrics.Severity.Total)) * 100) metrics.Severity.MedPct = int((float64(metrics.Severity.Medium) / float64(metrics.Severity.Total)) * 100) metrics.Severity.LowPct = int((float64(metrics.Severity.Low) / float64(metrics.Severity.Total)) * 100) metrics.Severity.InfoPct = int((float64(metrics.Severity.Info) / float64(metrics.Severity.Total)) * 100) } } resQuery := `SELECT status, COUNT(id) FROM tickets WHERE status IN ('Patched', 'Risk Accepted', 'False Positive') GROUP BY status` rowsRes, err := s.DB.QueryContext(ctx, resQuery) if err == nil { defer rowsRes.Close() for rowsRes.Next() { var status string var count int rowsRes.Scan(&status, &count) metrics.Resolution.Total += count switch status { case "Patched": metrics.Resolution.Patched = count case "Risk Accepted": metrics.Resolution.RiskAccepted = count case "False Positive": metrics.Resolution.FalsePositive = count } } if metrics.Resolution.Total > 0 { metrics.Resolution.PatchedPct = int((float64(metrics.Resolution.Patched) / float64(metrics.Resolution.Total)) * 100) metrics.Resolution.RiskAccPct = int((float64(metrics.Resolution.RiskAccepted) / float64(metrics.Resolution.Total)) * 100) metrics.Resolution.FalsePosPct = int((float64(metrics.Resolution.FalsePositive) / float64(metrics.Resolution.Total)) * 100) } } assetQuery := `SELECT asset_identifier, COUNT(id) as c FROM tickets WHERE status NOT IN ('Patched', 'Risk Accepted', 'False Positive') GROUP BY asset_identifier ORDER BY c DESC LIMIT 5` rowsAsset, err := s.DB.QueryContext(ctx, assetQuery) if err == nil { defer rowsAsset.Close() var maxAssetCount int for rowsAsset.Next() { var am domain2.AssetMetric rowsAsset.Scan(&am.Asset, &am.Count) if maxAssetCount == 0 { maxAssetCount = am.Count } if maxAssetCount > 0 { am.Percentage = int((float64(am.Count) / float64(maxAssetCount)) * 100) } metrics.TopAssets = append(metrics.TopAssets, am) } } return metrics, nil } func (s *SQLiteStore) GetDashboardTickets(ctx context.Context, tabStatus, filter, assetFilter, userEmail, userRole string, limit, offset int) ([]domain2.Ticket, int, map[string]int, error) { metrics := map[string]int{ "critical": 0, "overdue": 0, "mine": 0, "verification": 0, "returned": 0, } scope := "" var scopeArgs []any if userRole == "Wrangler" { scope = ` AND LOWER(t.assignee) = LOWER(?)` scopeArgs = append(scopeArgs, userEmail) } if userRole != "Sheriff" { var critCount, overCount, mineCount, verifyCount, returnedCount int critQ := "SELECT COUNT(t.id) FROM tickets t WHERE t.severity = 'Critical' AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive')" + scope s.DB.QueryRowContext(ctx, critQ, scopeArgs...).Scan(&critCount) metrics["critical"] = critCount overQ := "SELECT COUNT(t.id) FROM tickets t WHERE t.remediation_due_date < CURRENT_TIMESTAMP AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive')" + scope s.DB.QueryRowContext(ctx, overQ, scopeArgs...).Scan(&overCount) metrics["overdue"] = overCount mineQ := "SELECT COUNT(t.id) FROM tickets t WHERE LOWER(t.assignee) = LOWER(?) AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive')" s.DB.QueryRowContext(ctx, mineQ, userEmail).Scan(&mineCount) metrics["mine"] = mineCount verifyQ := "SELECT COUNT(t.id) FROM tickets t WHERE t.status = 'Pending Verification'" + scope s.DB.QueryRowContext(ctx, verifyQ, scopeArgs...).Scan(&verifyCount) metrics["verification"] = verifyCount retQ := "SELECT COUNT(t.id) FROM tickets t WHERE t.status = 'Returned to Security'" + scope s.DB.QueryRowContext(ctx, retQ, scopeArgs...).Scan(&returnedCount) metrics["returned"] = returnedCount } baseQ := "FROM tickets t WHERE 1=1" + scope var args []any args = append(args, scopeArgs...) if assetFilter != "" { baseQ += " AND t.asset_identifier = ?" args = append(args, assetFilter) } if tabStatus == "Waiting to be Triaged" || tabStatus == "holding_pen" { baseQ += " AND t.status IN ('Waiting to be Triaged', 'Returned to Security', 'Triaged')" } else if tabStatus == "Exceptions" { baseQ += " AND t.status NOT IN ('Patched', 'Risk Accepted', 'False Positive')" } else if tabStatus == "archives" { baseQ += " AND t.status IN ('Patched', 'Risk Accepted', 'False Positive')" } else if tabStatus != "" { baseQ += " AND t.status = ?" args = append(args, tabStatus) } if filter == "critical" { baseQ += " AND t.severity = 'Critical'" } else if filter == "overdue" { baseQ += " AND t.remediation_due_date < CURRENT_TIMESTAMP" } else if filter == "mine" { baseQ += " AND LOWER(t.assignee) = LOWER(?)" args = append(args, userEmail) } else if tabStatus == "archives" && filter != "" && filter != "all" { baseQ += " AND t.status = ?" args = append(args, filter) } var total int s.DB.QueryRowContext(ctx, "SELECT COUNT(t.id) "+baseQ, args...).Scan(&total) orderClause := "ORDER BY (CASE WHEN t.status = 'Returned to Security' THEN 0 ELSE 1 END) ASC, t.id DESC" query := ` WITH PaginatedIDs AS ( SELECT t.id ` + baseQ + ` ` + orderClause + ` LIMIT ? OFFSET ? ) SELECT t.id, t.source, t.asset_identifier, t.title, COALESCE(t.description, ''), COALESCE(t.recommended_remediation, ''), t.severity, t.status, t.triage_due_date, t.remediation_due_date, COALESCE(t.patch_evidence, ''), t.assignee as current_assignee, t.owner_viewed_at, t.updated_at, CAST(julianday(COALESCE(t.patched_at, t.updated_at)) - julianday(t.created_at) AS INTEGER) as days_to_resolve, COALESCE(t.latest_comment, '') as latest_comment FROM PaginatedIDs p JOIN tickets t ON t.id = p.id ` + orderClause args = append(args, limit, offset) rows, err := s.DB.QueryContext(ctx, query, args...) if err != nil { return nil, 0, metrics, err } defer rows.Close() var tickets []domain2.Ticket for rows.Next() { var t domain2.Ticket var assignee string err := rows.Scan( &t.ID, &t.Source, &t.AssetIdentifier, &t.Title, &t.Description, &t.RecommendedRemediation, &t.Severity, &t.Status, &t.TriageDueDate, &t.RemediationDueDate, &t.PatchEvidence, &assignee, &t.OwnerViewedAt, &t.UpdatedAt, &t.DaysToResolve, &t.LatestComment, ) if err == nil { t.Assignee = assignee t.IsOverdue = !t.RemediationDueDate.IsZero() && t.RemediationDueDate.Before(time.Now()) && t.Status != "Patched" && t.Status != "Risk Accepted" if tabStatus == "archives" { if t.DaysToResolve != nil { t.SLAString = fmt.Sprintf("%d days", *t.DaysToResolve) } else { t.SLAString = "Unknown" } } else { t.SLAString = t.RemediationDueDate.Format("Jan 02, 2006") } tickets = append(tickets, t) } } return tickets, total, metrics, nil } func (s *SQLiteStore) GetGlobalActivityFeed(ctx context.Context, limit int) ([]domain2.FeedItem, error) { return []domain2.FeedItem{ { Actor: "System", ActivityType: "Info", NewValue: "Detailed Immutable Audit Logging is a RiskRancher Pro feature. Upgrade to track all ticket lifecycle events.", TimeAgo: "Just now", }, }, nil } func (s *SQLiteStore) GetAnalyticsSummary(ctx context.Context) (map[string]int, error) { summary := make(map[string]int) var total int err := s.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM tickets WHERE status != 'Patched' AND status != 'Risk Accepted'`).Scan(&total) if err != nil { return nil, err } summary["Total_Open"] = total sourceRows, err := s.DB.QueryContext(ctx, `SELECT source, COUNT(*) FROM tickets WHERE status != 'Patched' AND status != 'Risk Accepted' GROUP BY source`) if err == nil { defer sourceRows.Close() for sourceRows.Next() { var source string var count int if err := sourceRows.Scan(&source, &count); err == nil { summary["Source_"+source+"_Open"] = count } } } sevRows, err := s.DB.QueryContext(ctx, `SELECT severity, COUNT(*) FROM tickets WHERE status != 'Patched' AND status != 'Risk Accepted' GROUP BY severity`) if err == nil { defer sevRows.Close() for sevRows.Next() { var sev string var count int if err := sevRows.Scan(&sev, &count); err == nil { summary["Severity_"+sev+"_Open"] = count } } } return summary, nil } func (s *SQLiteStore) GetPaginatedActivityFeed(ctx context.Context, filter string, limit, offset int) ([]domain2.FeedItem, int, error) { return []domain2.FeedItem{}, 0, nil }