package scans import ( "crypto/rand" "encoding/hex" "errors" "net" "sort" "sync" "time" ) type CreateScanRequest struct { Name string `json:"name"` CIDRs []string `json:"cidrs"` ExcludeIPs []string `json:"exclude_ips"` SNMPCredentialsID string `json:"snmp_credentials_id"` Options ScanOptions `json:"options"` } type ScanOptions struct { PingTimeoutMS int `json:"ping_timeout_ms"` PingRetries int `json:"ping_retries"` MaxParallelHosts int `json:"max_parallel_hosts"` PortScanEnabled bool `json:"port_scan_enabled"` Ports []int `json:"ports"` } type ScanJob struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` CIDRs []string `json:"cidrs"` 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) SaveHostResult(scanID string, host HostResult) error ListHostResults(scanID string, limit int) []HostResult SaveOpenPortResult(scanID string, result OpenPortResult) error ListOpenPortResults(scanID string, limit int) []OpenPortResult SaveSNMPResult(scanID string, result SNMPResult) error ListSNMPResults(scanID string, limit int) []SNMPResult } type HostResult struct { IP string `json:"ip"` IsUp bool `json:"is_up"` CheckedAt time.Time `json:"checked_at"` } type OpenPortResult struct { IP string `json:"ip"` Port int `json:"port"` IsOpen bool `json:"is_open"` CheckedAt time.Time `json:"checked_at"` } type SNMPResult struct { IP string `json:"ip"` Success bool `json:"success"` SysName string `json:"sys_name"` SysDescr string `json:"sys_descr"` SysObjectID string `json:"sys_object_id"` Error string `json:"error,omitempty"` CheckedAt time.Time `json:"checked_at"` } type MemoryStore struct { mu sync.RWMutex jobs map[string]ScanJob hosts map[string][]HostResult ports map[string][]OpenPortResult snmp map[string][]SNMPResult } func NewMemoryStore() *MemoryStore { return &MemoryStore{ jobs: make(map[string]ScanJob), hosts: make(map[string][]HostResult), ports: make(map[string][]OpenPortResult), snmp: make(map[string][]SNMPResult), } } func (s *MemoryStore) CreateScan(req CreateScanRequest) (ScanJob, error) { if err := validateCreateScanRequest(req); err != nil { return ScanJob{}, err } applyDefaultOptions(&req.Options) id, err := newID() if err != nil { return ScanJob{}, err } now := time.Now().UTC() job := ScanJob{ ID: id, Name: req.Name, Status: "queued", CIDRs: req.CIDRs, ExcludeIPs: req.ExcludeIPs, SNMPCredentialsID: req.SNMPCredentialsID, Options: req.Options, Progress: 0, Stats: ScanStats{}, CreatedAt: now, } s.mu.Lock() s.jobs[id] = job s.mu.Unlock() return job, nil } func (s *MemoryStore) GetScan(id string) (ScanJob, bool) { s.mu.RLock() job, ok := s.jobs[id] s.mu.RUnlock() 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 (s *MemoryStore) SaveHostResult(scanID string, host HostResult) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.jobs[scanID]; !ok { return errors.New("scan not found") } s.hosts[scanID] = append(s.hosts[scanID], host) return nil } func (s *MemoryStore) ListHostResults(scanID string, limit int) []HostResult { if limit <= 0 { limit = 500 } s.mu.RLock() defer s.mu.RUnlock() items := s.hosts[scanID] if len(items) <= limit { out := make([]HostResult, len(items)) copy(out, items) return out } out := make([]HostResult, limit) copy(out, items[:limit]) return out } func (s *MemoryStore) SaveOpenPortResult(scanID string, result OpenPortResult) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.jobs[scanID]; !ok { return errors.New("scan not found") } s.ports[scanID] = append(s.ports[scanID], result) return nil } func (s *MemoryStore) ListOpenPortResults(scanID string, limit int) []OpenPortResult { if limit <= 0 { limit = 1000 } s.mu.RLock() defer s.mu.RUnlock() items := s.ports[scanID] if len(items) <= limit { out := make([]OpenPortResult, len(items)) copy(out, items) return out } out := make([]OpenPortResult, limit) copy(out, items[:limit]) return out } func (s *MemoryStore) SaveSNMPResult(scanID string, result SNMPResult) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.jobs[scanID]; !ok { return errors.New("scan not found") } s.snmp[scanID] = append(s.snmp[scanID], result) return nil } func (s *MemoryStore) ListSNMPResults(scanID string, limit int) []SNMPResult { if limit <= 0 { limit = 1000 } s.mu.RLock() defer s.mu.RUnlock() items := s.snmp[scanID] if len(items) <= limit { out := make([]SNMPResult, len(items)) copy(out, items) return out } out := make([]SNMPResult, limit) copy(out, items[:limit]) return out } func applyDefaultOptions(opts *ScanOptions) { if opts.PingTimeoutMS <= 0 { opts.PingTimeoutMS = 700 } if opts.PingRetries < 0 { opts.PingRetries = 0 } if opts.MaxParallelHosts <= 0 { opts.MaxParallelHosts = 128 } if len(opts.Ports) == 0 { opts.Ports = []int{22, 80, 443, 161} } } 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 { return "", err } return hex.EncodeToString(b[:]), nil }