a7f80eac9a
Made-with: Cursor
448 lines
11 KiB
Go
448 lines
11 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
|
|
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
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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),
|
|
}
|
|
}
|
|
|
|
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) 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 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
|
|
}
|