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 SaveLLDPResult(scanID string, result LLDPResult) error ListLLDPResults(scanID string, limit int) []LLDPResult SaveInterfaceResult(scanID string, result InterfaceResult) error ListInterfaceResults(scanID string, filterIP string, limit int) []InterfaceResult SavePortDeviceResult(scanID string, result PortDeviceResult) error ListPortDeviceResults(scanID string, filterIP string, filterIfIndex int, limit int) []PortDeviceResult // ListPortDevicesByMac — строки FDB по точному MAC (mac уже в каноническом виде, см. NormalizeMAC). ListPortDevicesByMac(scanID string, mac string, limit int) []PortDeviceResult // PurgeAllScanData удаляет все сканы и связанные строки (хосты, порты, SNMP, LLDP, интерфейсы). PurgeAllScanData() error } 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 LLDPResult struct { IP string `json:"ip"` LocalPortNum string `json:"local_port_num"` RemoteChassisID string `json:"remote_chassis_id"` RemotePortID string `json:"remote_port_id"` RemoteSysName string `json:"remote_sys_name"` CheckedAt time.Time `json:"checked_at"` } // InterfaceResult — строка IF-MIB (interfaces/ifTable + ifName) для устройства. type InterfaceResult struct { IP string `json:"ip"` IfIndex int `json:"if_index"` IfDescr string `json:"if_descr"` IfName string `json:"if_name"` IfOperStatus string `json:"if_oper_status"` CheckedAt time.Time `json:"checked_at"` } // PortDeviceResult — MAC/IP, обнаруженные за конкретным портом (ifIndex) через BRIDGE-MIB + ARP. type PortDeviceResult struct { IP string `json:"ip"` IfIndex int `json:"if_index"` BridgePort int `json:"bridge_port,omitempty"` Vlan int `json:"vlan"` MAC string `json:"mac"` LearnedIP string `json:"learned_ip"` Hostname string `json:"hostname,omitempty"` // подставляется при ответе API по sysName скана, не хранится в БД 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 lldp map[string][]LLDPResult ifaces map[string][]InterfaceResult fdb map[string][]PortDeviceResult } 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), lldp: make(map[string][]LLDPResult), ifaces: make(map[string][]InterfaceResult), fdb: make(map[string][]PortDeviceResult), } } 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 { s.mu.RLock() defer s.mu.RUnlock() items := s.hosts[scanID] return dedupeHostsLatest(items, limit) } 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 { s.mu.RLock() defer s.mu.RUnlock() items := s.snmp[scanID] return dedupeSNMPLatest(items, limit) } func (s *MemoryStore) SaveLLDPResult(scanID string, result LLDPResult) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.jobs[scanID]; !ok { return errors.New("scan not found") } s.lldp[scanID] = append(s.lldp[scanID], result) return nil } func (s *MemoryStore) ListLLDPResults(scanID string, limit int) []LLDPResult { if limit <= 0 { limit = 2000 } s.mu.RLock() defer s.mu.RUnlock() items := s.lldp[scanID] if len(items) <= limit { out := make([]LLDPResult, len(items)) copy(out, items) return out } out := make([]LLDPResult, limit) copy(out, items[:limit]) return out } func (s *MemoryStore) SaveInterfaceResult(scanID string, result InterfaceResult) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.jobs[scanID]; !ok { return errors.New("scan not found") } s.ifaces[scanID] = append(s.ifaces[scanID], result) return nil } func (s *MemoryStore) PurgeAllScanData() error { s.mu.Lock() defer s.mu.Unlock() s.jobs = make(map[string]ScanJob) s.hosts = make(map[string][]HostResult) s.ports = make(map[string][]OpenPortResult) s.snmp = make(map[string][]SNMPResult) s.lldp = make(map[string][]LLDPResult) s.ifaces = make(map[string][]InterfaceResult) s.fdb = make(map[string][]PortDeviceResult) return nil } func (s *MemoryStore) ListInterfaceResults(scanID string, filterIP string, limit int) []InterfaceResult { if limit <= 0 { limit = 4096 } s.mu.RLock() defer s.mu.RUnlock() items := s.ifaces[scanID] var filtered []InterfaceResult if filterIP != "" { for _, r := range items { if r.IP == filterIP { filtered = append(filtered, r) } } } else { filtered = append(filtered, items...) } sort.Slice(filtered, func(i, j int) bool { return filtered[i].IfIndex < filtered[j].IfIndex }) if len(filtered) <= limit { out := make([]InterfaceResult, len(filtered)) copy(out, filtered) return out } out := make([]InterfaceResult, limit) copy(out, filtered[:limit]) return out } func (s *MemoryStore) SavePortDeviceResult(scanID string, result PortDeviceResult) error { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.jobs[scanID]; !ok { return errors.New("scan not found") } s.fdb[scanID] = append(s.fdb[scanID], result) return nil } func (s *MemoryStore) ListPortDeviceResults(scanID string, filterIP string, filterIfIndex int, limit int) []PortDeviceResult { if limit <= 0 { limit = 20000 } s.mu.RLock() defer s.mu.RUnlock() items := s.fdb[scanID] filtered := make([]PortDeviceResult, 0, len(items)) for _, r := range items { if filterIP != "" && r.IP != filterIP { continue } if filterIfIndex > 0 && r.IfIndex != filterIfIndex && r.BridgePort != filterIfIndex { continue } filtered = append(filtered, r) } sort.Slice(filtered, func(i, j int) bool { if filtered[i].IfIndex != filtered[j].IfIndex { return filtered[i].IfIndex < filtered[j].IfIndex } if filtered[i].Vlan != filtered[j].Vlan { return filtered[i].Vlan < filtered[j].Vlan } if filtered[i].MAC != filtered[j].MAC { return filtered[i].MAC < filtered[j].MAC } return filtered[i].LearnedIP < filtered[j].LearnedIP }) if len(filtered) <= limit { out := make([]PortDeviceResult, len(filtered)) copy(out, filtered) return out } out := make([]PortDeviceResult, limit) copy(out, filtered[:limit]) return out } func (s *MemoryStore) ListPortDevicesByMac(scanID string, mac string, limit int) []PortDeviceResult { if limit <= 0 { limit = 5000 } if mac == "" { return nil } s.mu.RLock() defer s.mu.RUnlock() items := s.fdb[scanID] filtered := make([]PortDeviceResult, 0, 8) for _, r := range items { if NormalizeMAC(r.MAC) == mac { filtered = append(filtered, r) } } sort.Slice(filtered, func(i, j int) bool { if filtered[i].IP != filtered[j].IP { return filtered[i].IP < filtered[j].IP } if filtered[i].IfIndex != filtered[j].IfIndex { return filtered[i].IfIndex < filtered[j].IfIndex } if filtered[i].Vlan != filtered[j].Vlan { return filtered[i].Vlan < filtered[j].Vlan } if filtered[i].MAC != filtered[j].MAC { return filtered[i].MAC < filtered[j].MAC } return filtered[i].LearnedIP < filtered[j].LearnedIP }) if len(filtered) <= limit { out := make([]PortDeviceResult, len(filtered)) copy(out, filtered) return out } out := make([]PortDeviceResult, limit) copy(out, filtered[: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") } expanded, err := expandCIDRSFromRequest(req.CIDRs) if err != nil { return err } req.CIDRs = expanded 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 } func dedupeHostsLatest(items []HostResult, limit int) []HostResult { if limit <= 0 { limit = 200000 } m := make(map[string]HostResult, len(items)) for _, h := range items { cur, ok := m[h.IP] if !ok || h.CheckedAt.After(cur.CheckedAt) { m[h.IP] = h } } out := make([]HostResult, 0, len(m)) for _, h := range m { out = append(out, h) } sort.Slice(out, func(i, j int) bool { return out[i].CheckedAt.After(out[j].CheckedAt) }) if len(out) > limit { out = out[:limit] } return out } func dedupeSNMPLatest(items []SNMPResult, limit int) []SNMPResult { if limit <= 0 { limit = 200000 } m := make(map[string]SNMPResult, len(items)) for _, r := range items { cur, ok := m[r.IP] if !ok || r.CheckedAt.After(cur.CheckedAt) { m[r.IP] = r } } out := make([]SNMPResult, 0, len(m)) for _, r := range m { out = append(out, r) } sort.Slice(out, func(i, j int) bool { return out[i].CheckedAt.After(out[j].CheckedAt) }) if len(out) > limit { out = out[:limit] } return out }