9afbbbb762
Store checked hosts for each scan and add API to fetch host-level up/down probe results. Made-with: Cursor
245 lines
5.3 KiB
Go
245 lines
5.3 KiB
Go
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
|
|
}
|
|
|
|
type HostResult struct {
|
|
IP string `json:"ip"`
|
|
IsUp bool `json:"is_up"`
|
|
CheckedAt time.Time `json:"checked_at"`
|
|
}
|
|
|
|
type MemoryStore struct {
|
|
mu sync.RWMutex
|
|
jobs map[string]ScanJob
|
|
hosts map[string][]HostResult
|
|
}
|
|
|
|
func NewMemoryStore() *MemoryStore {
|
|
return &MemoryStore{
|
|
jobs: make(map[string]ScanJob),
|
|
hosts: make(map[string][]HostResult),
|
|
}
|
|
}
|
|
|
|
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 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
|
|
}
|