feat: MAC lookup in scan FDB (API mac-locations, UI table, index)
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package scans
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NormalizeMAC приводит ввод пользователя к виду aa:bb:cc:dd:ee:ff (как в FDB/ARP).
|
||||
func NormalizeMAC(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
s = strings.ReplaceAll(s, "-", ":")
|
||||
if strings.Count(s, ":") == 5 {
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != 6 {
|
||||
return ""
|
||||
}
|
||||
var b [6]byte
|
||||
for i, p := range parts {
|
||||
n, err := strconv.ParseUint(strings.TrimSpace(p), 16, 8)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
b[i] = byte(n)
|
||||
}
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b[0], b[1], b[2], b[3], b[4], b[5])
|
||||
}
|
||||
if strings.HasPrefix(s, "0x") {
|
||||
h := strings.TrimPrefix(s, "0x")
|
||||
h = strings.ReplaceAll(h, ":", "")
|
||||
if len(h) != 12 {
|
||||
return ""
|
||||
}
|
||||
raw, err := hex.DecodeString(h)
|
||||
if err != nil || len(raw) != 6 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", raw[0], raw[1], raw[2], raw[3], raw[4], raw[5])
|
||||
}
|
||||
if len(s) == 12 {
|
||||
raw, err := hex.DecodeString(s)
|
||||
if err != nil || len(raw) != 6 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", raw[0], raw[1], raw[2], raw[3], raw[4], raw[5])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -534,6 +534,37 @@ limit $2`,
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *PostgresStore) ListPortDevicesByMac(scanID string, mac string, limit int) []PortDeviceResult {
|
||||
if limit <= 0 {
|
||||
limit = 5000
|
||||
}
|
||||
if mac == "" {
|
||||
return nil
|
||||
}
|
||||
query := `
|
||||
select ip::text, if_index, bridge_port, vlan, coalesce(mac, ''), coalesce(learned_ip::text, ''), checked_at
|
||||
from scan_port_devices
|
||||
where scan_id = $1
|
||||
and lower(replace(trim(coalesce(mac, '')), '-', ':')) = lower(replace(trim($2::text), '-', ':'))
|
||||
order by ip::text asc, if_index asc, vlan asc, mac asc, learned_ip asc
|
||||
limit $3`
|
||||
rows, err := s.db.QueryContext(context.Background(), query, scanID, mac, limit)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]PortDeviceResult, 0, 32)
|
||||
for rows.Next() {
|
||||
var r PortDeviceResult
|
||||
if err = rows.Scan(&r.IP, &r.IfIndex, &r.BridgePort, &r.Vlan, &r.MAC, &r.LearnedIP, &r.CheckedAt); err != nil {
|
||||
return nil
|
||||
}
|
||||
out = append(out, r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *PostgresStore) PurgeAllScanData() error {
|
||||
// Дочерние таблицы ссылаются на scan_jobs — CASCADE очищает всё; RESTART IDENTITY сбрасывает bigserial.
|
||||
_, err := s.db.ExecContext(context.Background(), `truncate table scan_jobs restart identity cascade`)
|
||||
|
||||
@@ -364,48 +364,8 @@ func snmpNumericString(v any) string {
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeMACKey приводит MAC к виду aa:bb:cc:dd:ee:ff для сопоставления FDB и ARP.
|
||||
func normalizeMACKey(s string) string {
|
||||
s = strings.ToLower(strings.TrimSpace(s))
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
s = strings.ReplaceAll(s, "-", ":")
|
||||
if strings.Count(s, ":") == 5 {
|
||||
parts := strings.Split(s, ":")
|
||||
if len(parts) != 6 {
|
||||
return ""
|
||||
}
|
||||
var b [6]byte
|
||||
for i, p := range parts {
|
||||
n, err := strconv.ParseUint(strings.TrimSpace(p), 16, 8)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
b[i] = byte(n)
|
||||
}
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b[0], b[1], b[2], b[3], b[4], b[5])
|
||||
}
|
||||
if strings.HasPrefix(s, "0x") {
|
||||
h := strings.TrimPrefix(s, "0x")
|
||||
h = strings.ReplaceAll(h, ":", "")
|
||||
if len(h) != 12 {
|
||||
return ""
|
||||
}
|
||||
raw, err := hex.DecodeString(h)
|
||||
if err != nil || len(raw) != 6 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", raw[0], raw[1], raw[2], raw[3], raw[4], raw[5])
|
||||
}
|
||||
if len(s) == 12 {
|
||||
raw, err := hex.DecodeString(s)
|
||||
if err != nil || len(raw) != 6 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", raw[0], raw[1], raw[2], raw[3], raw[4], raw[5])
|
||||
}
|
||||
return ""
|
||||
return NormalizeMAC(s)
|
||||
}
|
||||
|
||||
func dottedDecimalToMACTail(s string) string {
|
||||
|
||||
@@ -71,6 +71,8 @@ type Store interface {
|
||||
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
|
||||
}
|
||||
@@ -439,6 +441,47 @@ func (s *MemoryStore) ListPortDeviceResults(scanID string, filterIP string, filt
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user