2026-04-02 10:57:36 -04:00
package datastore
import (
"context"
"fmt"
"time"
2026-04-29 11:04:14 -04:00
domain2 "code.riskrancher.com/RiskRancher/core/pkg/domain"
2026-04-02 10:57:36 -04:00
)
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
}