55 "fmt"
66 "sort"
77 "strings"
8+ "sync"
89 "time"
910
1011 "go.uber.org/zap"
@@ -72,6 +73,10 @@ type Service struct {
7273 secretStore SecretStore
7374 queue * ScanQueue
7475 logger * zap.Logger
76+
77+ // In-memory scan summary cache — avoids expensive BBolt reads per server
78+ summaryCache map [string ]* ScanSummary
79+ summaryCacheMu sync.RWMutex
7580}
7681
7782// NewService creates a new SecurityService
@@ -85,6 +90,7 @@ func NewService(storage Storage, registry *Registry, docker *DockerRunner, dataD
8590 emitter : & NoopEmitter {},
8691 sourceResolver : NewSourceResolver (logger ),
8792 queue : NewScanQueue (logger ),
93+ summaryCache : make (map [string ]* ScanSummary ),
8894 logger : logger ,
8995 }
9096 // Restore installed scanner state from storage (survives restart)
@@ -328,6 +334,8 @@ type scanCallbackAdapter struct {
328334func (a * scanCallbackAdapter ) OnScanStarted (job * ScanJob ) {
329335 _ = a .service .storage .SaveScanJob (job )
330336 a .service .emitter .EmitSecurityScanStarted (job .ServerName , job .Scanners , job .ID )
337+ // Invalidate cached summary so server list shows "scanning"
338+ a .service .invalidateScanSummaryCache (job .ServerName )
331339}
332340
333341func (a * scanCallbackAdapter ) OnScannerStarted (job * ScanJob , scannerID string ) {
@@ -348,6 +356,8 @@ func (a *scanCallbackAdapter) OnScannerFailed(job *ScanJob, scannerID string, er
348356
349357func (a * scanCallbackAdapter ) OnScanCompleted (job * ScanJob , reports []* ScanReport ) {
350358 _ = a .service .storage .SaveScanJob (job )
359+ // Invalidate cached summary so next read gets fresh results
360+ a .service .invalidateScanSummaryCache (job .ServerName )
351361 // Aggregate findings summary for event
352362 summary := make (map [string ]int )
353363 for _ , r := range reports {
@@ -368,6 +378,8 @@ func (a *scanCallbackAdapter) OnScanCompleted(job *ScanJob, reports []*ScanRepor
368378
369379func (a * scanCallbackAdapter ) OnScanFailed (job * ScanJob , err error ) {
370380 _ = a .service .storage .SaveScanJob (job )
381+ // Invalidate cached summary
382+ a .service .invalidateScanSummaryCache (job .ServerName )
371383 // Cleanup auto-resolved source directory
372384 if a .cleanup != nil {
373385 a .cleanup ()
@@ -857,6 +869,13 @@ type IntegrityViolation struct {
857869// Returns nil if no scans have been run for this server.
858870// Considers both Pass 1 and Pass 2 results when computing status.
859871func (s * Service ) GetScanSummary (ctx context.Context , serverName string ) * ScanSummary {
872+ // Check cache first (avoids expensive BBolt reads for every server)
873+ s .summaryCacheMu .RLock ()
874+ if cached , ok := s .summaryCache [serverName ]; ok {
875+ s .summaryCacheMu .RUnlock ()
876+ return cached
877+ }
878+ s .summaryCacheMu .RUnlock ()
860879
861880 // Check for active scan (Pass 1 takes priority in status display)
862881 if active := s .engine .GetActiveJob (serverName ); active != nil {
@@ -889,6 +908,7 @@ func (s *Service) GetScanSummary(ctx context.Context, serverName string) *ScanSu
889908 // Check if the primary job failed
890909 if primaryJob .Status == ScanJobStatusFailed {
891910 summary .Status = "failed"
911+ s .cacheScanSummary (serverName , summary )
892912 return summary
893913 }
894914
@@ -903,6 +923,7 @@ func (s *Service) GetScanSummary(ctx context.Context, serverName string) *ScanSu
903923 }
904924 if allFailed {
905925 summary .Status = "failed"
926+ s .cacheScanSummary (serverName , summary )
906927 return summary
907928 }
908929 }
@@ -936,6 +957,7 @@ func (s *Service) GetScanSummary(ctx context.Context, serverName string) *ScanSu
936957 summary .Status = "failed"
937958 }
938959 }
960+ s .cacheScanSummary (serverName , summary )
939961 return summary
940962 }
941963
@@ -967,6 +989,8 @@ func (s *Service) GetScanSummary(ctx context.Context, serverName string) *ScanSu
967989 summary .Status = "clean" // Only informational findings
968990 }
969991
992+ // Cache for fast subsequent reads
993+ s .cacheScanSummary (serverName , summary )
970994 return summary
971995}
972996
@@ -986,6 +1010,24 @@ type FindingCounts struct {
9861010 Total int `json:"total"`
9871011}
9881012
1013+ // invalidateScanSummaryCache removes a server's cached scan summary,
1014+ // forcing the next GetScanSummary call to recompute from storage.
1015+ func (s * Service ) invalidateScanSummaryCache (serverName string ) {
1016+ s .summaryCacheMu .Lock ()
1017+ delete (s .summaryCache , serverName )
1018+ s .summaryCacheMu .Unlock ()
1019+ }
1020+
1021+ // cacheScanSummary stores a computed scan summary in the cache.
1022+ func (s * Service ) cacheScanSummary (serverName string , summary * ScanSummary ) {
1023+ if summary == nil {
1024+ return
1025+ }
1026+ s .summaryCacheMu .Lock ()
1027+ s .summaryCache [serverName ] = summary
1028+ s .summaryCacheMu .Unlock ()
1029+ }
1030+
9891031// pruneOldScans removes old scan jobs and reports beyond MaxScansPerServer
9901032func (s * Service ) pruneOldScans (serverName string ) {
9911033 jobs , err := s .storage .ListScanJobs (serverName )
0 commit comments