122 lines
3.1 KiB
Go
122 lines
3.1 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"dip-ids/internal/config"
|
|
"dip-ids/internal/storage"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
)
|
|
|
|
// portScanDetector обнаруживает сканирование портов методом sliding window.
|
|
// Алгоритм: если один источник обращается к N уникальным портам назначения
|
|
// за период T — это потенциальное сканирование.
|
|
type portScanDetector struct {
|
|
mu sync.Mutex
|
|
cfg config.PortScanConfig
|
|
// srcIP -> map[dstPort][]time.Time
|
|
tracker map[string]map[uint16][]time.Time
|
|
alerted map[string]time.Time // последний алерт по srcIP
|
|
}
|
|
|
|
func NewPortScanDetector(cfg config.PortScanConfig) Detector {
|
|
return &portScanDetector{
|
|
cfg: cfg,
|
|
tracker: make(map[string]map[uint16][]time.Time),
|
|
alerted: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
func (d *portScanDetector) Name() string { return "port_scan" }
|
|
|
|
func (d *portScanDetector) Analyze(pkt gopacket.Packet) []storage.Alert {
|
|
if !d.cfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
netLayer := pkt.NetworkLayer()
|
|
tcpLayer := pkt.Layer(layers.LayerTypeTCP)
|
|
if netLayer == nil || tcpLayer == nil {
|
|
return nil
|
|
}
|
|
|
|
tcp := tcpLayer.(*layers.TCP)
|
|
// Интересуют только SYN-пакеты (начало соединения)
|
|
if !tcp.SYN || tcp.ACK {
|
|
return nil
|
|
}
|
|
|
|
srcIP := netLayer.NetworkFlow().Src().String()
|
|
dstIP := netLayer.NetworkFlow().Dst().String()
|
|
dstPort := uint16(tcp.DstPort)
|
|
now := pkt.Metadata().Timestamp
|
|
if now.IsZero() {
|
|
now = time.Now()
|
|
}
|
|
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
if _, ok := d.tracker[srcIP]; !ok {
|
|
d.tracker[srcIP] = make(map[uint16][]time.Time)
|
|
}
|
|
|
|
window := time.Duration(d.cfg.WindowSeconds) * time.Second
|
|
cutoff := now.Add(-window)
|
|
|
|
// Добавляем текущий порт и очищаем устаревшие записи
|
|
d.tracker[srcIP][dstPort] = append(d.tracker[srcIP][dstPort], now)
|
|
|
|
uniquePorts := 0
|
|
for port, times := range d.tracker[srcIP] {
|
|
filtered := filterTimes(times, cutoff)
|
|
if len(filtered) == 0 {
|
|
delete(d.tracker[srcIP], port)
|
|
} else {
|
|
d.tracker[srcIP][port] = filtered
|
|
uniquePorts++
|
|
}
|
|
}
|
|
|
|
if uniquePorts < d.cfg.Threshold {
|
|
return nil
|
|
}
|
|
|
|
// Дедупликация: не спамить алертами чаще чем раз в 30 сек на один IP
|
|
if last, ok := d.alerted[srcIP]; ok && now.Sub(last) < 30*time.Second {
|
|
return nil
|
|
}
|
|
d.alerted[srcIP] = now
|
|
|
|
return []storage.Alert{{
|
|
CreatedAt: now,
|
|
Type: "port_scan",
|
|
Severity: d.cfg.Severity,
|
|
SrcIP: srcIP,
|
|
DstIP: dstIP,
|
|
DstPort: dstPort,
|
|
Protocol: "TCP",
|
|
Description: fmt.Sprintf("Port scan detected: %d unique ports probed in %ds window", uniquePorts, d.cfg.WindowSeconds),
|
|
Count: uniquePorts,
|
|
}}
|
|
}
|
|
|
|
func (d *portScanDetector) Reset() {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.tracker = make(map[string]map[uint16][]time.Time)
|
|
d.alerted = make(map[string]time.Time)
|
|
}
|
|
|
|
func filterTimes(times []time.Time, cutoff time.Time) []time.Time {
|
|
i := 0
|
|
for i < len(times) && times[i].Before(cutoff) {
|
|
i++
|
|
}
|
|
return times[i:]
|
|
}
|