diff --git a/internal/scans/postgres_store.go b/internal/scans/postgres_store.go index 2ef2932..b9a1998 100644 --- a/internal/scans/postgres_store.go +++ b/internal/scans/postgres_store.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "sort" + "strings" "time" ) @@ -541,16 +542,23 @@ func (s *PostgresStore) PurgeAllScanData() error { func (s *PostgresStore) SavePortDeviceResult(scanID string, result PortDeviceResult) error { query := ` -insert into scan_port_devices (scan_id, ip, if_index, mac, learned_ip, checked_at) -values ($1, $2, $3, $4, $5, $6)` +insert into scan_port_devices (scan_id, ip, if_index, bridge_port, mac, learned_ip, checked_at) +values ($1, $2, $3, $4, $5, $6, $7)` + var learnedIP any + if v := strings.TrimSpace(result.LearnedIP); v != "" { + learnedIP = v + } else { + learnedIP = nil + } _, err := s.db.ExecContext( context.Background(), query, scanID, result.IP, result.IfIndex, + result.BridgePort, result.MAC, - result.LearnedIP, + learnedIP, result.CheckedAt, ) return err @@ -561,11 +569,11 @@ func (s *PostgresStore) ListPortDeviceResults(scanID string, filterIP string, fi limit = 20000 } query := ` -select ip::text, if_index, coalesce(mac, ''), coalesce(learned_ip::text, ''), checked_at +select ip::text, if_index, bridge_port, coalesce(mac, ''), coalesce(learned_ip::text, ''), checked_at from scan_port_devices where scan_id = $1 and ($2 = '' or ip = $2::inet) - and ($3 = 0 or if_index = $3) + and ($3 = 0 or if_index = $3 or bridge_port = $3) order by if_index asc, mac asc, learned_ip asc limit $4` rows, err := s.db.QueryContext(context.Background(), query, scanID, filterIP, filterIfIndex, limit) @@ -577,7 +585,7 @@ limit $4` out := make([]PortDeviceResult, 0, 256) for rows.Next() { var r PortDeviceResult - if err = rows.Scan(&r.IP, &r.IfIndex, &r.MAC, &r.LearnedIP, &r.CheckedAt); err != nil { + if err = rows.Scan(&r.IP, &r.IfIndex, &r.BridgePort, &r.MAC, &r.LearnedIP, &r.CheckedAt); err != nil { return nil } out = append(out, r) diff --git a/internal/scans/runner.go b/internal/scans/runner.go index 54d3f35..8f88870 100644 --- a/internal/scans/runner.go +++ b/internal/scans/runner.go @@ -245,8 +245,8 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P macToIPs := make(map[string]map[string]struct{}) for key, mac := range arpIfIPToMac { - mac = strings.ToLower(strings.TrimSpace(mac)) - if mac == "" || !strings.Contains(mac, ":") { + mac = normalizeMACKey(mac) + if mac == "" { continue } parts := strings.Split(key, ".") @@ -266,11 +266,11 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P seen := make(map[string]struct{}) out := make([]PortDeviceResult, 0, len(combinedFdb)) for macTail, bridgePortRaw := range combinedFdb { - mac := dottedDecimalToMACTail(macTail) + mac := normalizeMACKey(dottedDecimalToMACTail(macTail)) if mac == "" { continue } - bridgePort, err := strconv.Atoi(strings.TrimSpace(bridgePortRaw)) + bridgePort, err := strconv.Atoi(strings.TrimSpace(snmpNumericString(bridgePortRaw))) if err != nil || bridgePort <= 0 { continue } @@ -283,7 +283,7 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P continue } - ipsSet := macToIPs[strings.ToLower(mac)] + ipsSet := macToIPs[mac] if len(ipsSet) == 0 { key := fmt.Sprintf("%d|%s|", ifIndex, mac) if _, ok := seen[key]; ok { @@ -291,11 +291,12 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P } seen[key] = struct{}{} out = append(out, PortDeviceResult{ - IP: ip, - IfIndex: ifIndex, - MAC: mac, - LearnedIP: "", - CheckedAt: checkedAt, + IP: ip, + IfIndex: ifIndex, + BridgePort: bridgePort, + MAC: mac, + LearnedIP: "", + CheckedAt: checkedAt, }) continue } @@ -306,11 +307,12 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P } seen[key] = struct{}{} out = append(out, PortDeviceResult{ - IP: ip, - IfIndex: ifIndex, - MAC: mac, - LearnedIP: learnedIP, - CheckedAt: checkedAt, + IP: ip, + IfIndex: ifIndex, + BridgePort: bridgePort, + MAC: mac, + LearnedIP: learnedIP, + CheckedAt: checkedAt, }) } } @@ -331,6 +333,62 @@ func probePortDevices(client *gosnmp.GoSNMP, ip string, checkedAt time.Time) []P return out } +// snmpNumericString — значение SNMP как строка (int/float/[]byte) для Atoi. +func snmpNumericString(v any) string { + switch x := v.(type) { + case string: + return x + case []byte: + return strings.TrimSpace(string(x)) + default: + return strings.TrimSpace(fmt.Sprint(x)) + } +} + +// 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 "" +} + func dottedDecimalToMACTail(s string) string { p := strings.Split(strings.TrimSpace(s), ".") if len(p) < 6 { diff --git a/internal/scans/store.go b/internal/scans/store.go index 021b83a..b2ec1ce 100644 --- a/internal/scans/store.go +++ b/internal/scans/store.go @@ -119,11 +119,12 @@ type InterfaceResult struct { // PortDeviceResult — MAC/IP, обнаруженные за конкретным портом (ifIndex) через BRIDGE-MIB + ARP. type PortDeviceResult struct { - IP string `json:"ip"` - IfIndex int `json:"if_index"` - MAC string `json:"mac"` - LearnedIP string `json:"learned_ip"` - CheckedAt time.Time `json:"checked_at"` + IP string `json:"ip"` + IfIndex int `json:"if_index"` + BridgePort int `json:"bridge_port,omitempty"` + MAC string `json:"mac"` + LearnedIP string `json:"learned_ip"` + CheckedAt time.Time `json:"checked_at"` } type MemoryStore struct { @@ -409,7 +410,7 @@ func (s *MemoryStore) ListPortDeviceResults(scanID string, filterIP string, filt if filterIP != "" && r.IP != filterIP { continue } - if filterIfIndex > 0 && r.IfIndex != filterIfIndex { + if filterIfIndex > 0 && r.IfIndex != filterIfIndex && r.BridgePort != filterIfIndex { continue } filtered = append(filtered, r) diff --git a/internal/store/migrate.go b/internal/store/migrate.go index ba8ad47..c7e6f82 100644 --- a/internal/store/migrate.go +++ b/internal/store/migrate.go @@ -14,6 +14,9 @@ var scanInterfacesSQL string //go:embed sql/003_scan_port_devices.sql var scanPortDevicesSQL string +//go:embed sql/004_scan_port_devices_bridge_port.sql +var scanPortDevicesBridgePortSQL string + func RunMigrations(db *sql.DB) error { if _, err := db.Exec(initSQL); err != nil { return err @@ -24,5 +27,8 @@ func RunMigrations(db *sql.DB) error { if _, err := db.Exec(scanPortDevicesSQL); err != nil { return err } + if _, err := db.Exec(scanPortDevicesBridgePortSQL); err != nil { + return err + } return nil } diff --git a/internal/store/sql/003_scan_port_devices.sql b/internal/store/sql/003_scan_port_devices.sql index b163e30..61ac0eb 100644 --- a/internal/store/sql/003_scan_port_devices.sql +++ b/internal/store/sql/003_scan_port_devices.sql @@ -3,6 +3,7 @@ create table if not exists scan_port_devices ( scan_id text not null references scan_jobs(id) on delete cascade, ip inet not null, if_index int not null, + bridge_port int not null default 0, mac text not null, learned_ip inet null, checked_at timestamptz not null @@ -13,3 +14,6 @@ create index if not exists idx_scan_port_devices_scan_ip_if create index if not exists idx_scan_port_devices_scan_ip_mac on scan_port_devices (scan_id, ip, mac); + +create index if not exists idx_scan_port_devices_scan_ip_br + on scan_port_devices (scan_id, ip, bridge_port); diff --git a/internal/store/sql/004_scan_port_devices_bridge_port.sql b/internal/store/sql/004_scan_port_devices_bridge_port.sql new file mode 100644 index 0000000..86117b2 --- /dev/null +++ b/internal/store/sql/004_scan_port_devices_bridge_port.sql @@ -0,0 +1,4 @@ +alter table scan_port_devices add column if not exists bridge_port int not null default 0; + +create index if not exists idx_scan_port_devices_scan_ip_br + on scan_port_devices (scan_id, ip, bridge_port);