Add scan queue execution and listing endpoint.

Run scans in background with progress/status updates and store scan metrics in memory or PostgreSQL.

Made-with: Cursor
This commit is contained in:
Andrey Lutsenko
2026-04-09 21:49:38 +10:00
parent 284794ec8d
commit 43cb339605
12 changed files with 753 additions and 23 deletions
+91 -14
View File
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"net"
"sort"
"sync"
"time"
)
@@ -33,14 +34,31 @@ type ScanJob struct {
ExcludeIPs []string `json:"exclude_ips"`
SNMPCredentialsID string `json:"snmp_credentials_id"`
Options ScanOptions `json:"options"`
Progress int `json:"progress"`
Stats ScanStats `json:"stats"`
CreatedAt time.Time `json:"created_at"`
StartedAt time.Time `json:"started_at,omitempty"`
FinishedAt time.Time `json:"finished_at,omitempty"`
}
type ScanStats struct {
HostsTotal int `json:"hosts_total"`
HostsUp int `json:"hosts_up"`
}
type ScanUpdate struct {
Status string
Progress int
Stats ScanStats
StartedAt *time.Time
FinishedAt *time.Time
}
type Store interface {
CreateScan(CreateScanRequest) (ScanJob, error)
GetScan(id string) (ScanJob, bool)
ListScans(limit int) []ScanJob
UpdateScan(id string, update ScanUpdate) (ScanJob, bool)
}
type MemoryStore struct {
@@ -55,20 +73,8 @@ func NewMemoryStore() *MemoryStore {
}
func (s *MemoryStore) CreateScan(req CreateScanRequest) (ScanJob, error) {
if len(req.CIDRs) == 0 {
return ScanJob{}, errors.New("cidrs must not be empty")
}
for _, cidr := range req.CIDRs {
if _, _, err := net.ParseCIDR(cidr); err != nil {
return ScanJob{}, errors.New("invalid cidr: " + cidr)
}
}
for _, ip := range req.ExcludeIPs {
if net.ParseIP(ip) == nil {
return ScanJob{}, errors.New("invalid exclude ip: " + ip)
}
if err := validateCreateScanRequest(req); err != nil {
return ScanJob{}, err
}
applyDefaultOptions(&req.Options)
@@ -87,6 +93,8 @@ func (s *MemoryStore) CreateScan(req CreateScanRequest) (ScanJob, error) {
ExcludeIPs: req.ExcludeIPs,
SNMPCredentialsID: req.SNMPCredentialsID,
Options: req.Options,
Progress: 0,
Stats: ScanStats{},
CreatedAt: now,
}
@@ -104,6 +112,55 @@ func (s *MemoryStore) GetScan(id string) (ScanJob, bool) {
return job, ok
}
func (s *MemoryStore) ListScans(limit int) []ScanJob {
if limit <= 0 {
limit = 50
}
s.mu.RLock()
jobs := make([]ScanJob, 0, len(s.jobs))
for _, j := range s.jobs {
jobs = append(jobs, j)
}
s.mu.RUnlock()
sort.Slice(jobs, func(i, j int) bool {
return jobs[i].CreatedAt.After(jobs[j].CreatedAt)
})
if len(jobs) > limit {
jobs = jobs[:limit]
}
return jobs
}
func (s *MemoryStore) UpdateScan(id string, update ScanUpdate) (ScanJob, bool) {
s.mu.Lock()
defer s.mu.Unlock()
job, ok := s.jobs[id]
if !ok {
return ScanJob{}, false
}
if update.Status != "" {
job.Status = update.Status
}
if update.Progress >= 0 {
job.Progress = update.Progress
}
job.Stats = update.Stats
if update.StartedAt != nil {
job.StartedAt = *update.StartedAt
}
if update.FinishedAt != nil {
job.FinishedAt = *update.FinishedAt
}
s.jobs[id] = job
return job, true
}
func applyDefaultOptions(opts *ScanOptions) {
if opts.PingTimeoutMS <= 0 {
opts.PingTimeoutMS = 700
@@ -119,6 +176,26 @@ func applyDefaultOptions(opts *ScanOptions) {
}
}
func validateCreateScanRequest(req CreateScanRequest) error {
if len(req.CIDRs) == 0 {
return errors.New("cidrs must not be empty")
}
for _, cidr := range req.CIDRs {
if _, _, err := net.ParseCIDR(cidr); err != nil {
return errors.New("invalid cidr: " + cidr)
}
}
for _, ip := range req.ExcludeIPs {
if net.ParseIP(ip) == nil {
return errors.New("invalid exclude ip: " + ip)
}
}
return nil
}
func newID() (string, error) {
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {