feat(mac-locations): VLAN-aware port MAC counts, access/trunk thresholds, likely_attachment
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package scans
|
||||
|
||||
import "strconv"
|
||||
|
||||
// PortFDBLocationKey — ключ «где в FDB»: IP коммутатора, порт и VLAN (Q-BRIDGE).
|
||||
func PortFDBLocationKey(ip string, ifIndex, bridgePort, vlan int) string {
|
||||
return ip + "|" + strconv.Itoa(ifIndex) + "|" + strconv.Itoa(bridgePort) + "|" + strconv.Itoa(vlan)
|
||||
}
|
||||
|
||||
// ClassifyPortKind по числу уникальных MAC на порту и порогам (ожидается accessMax < trunkMin).
|
||||
func ClassifyPortKind(macsOnPort, accessMax, trunkMin int) string {
|
||||
if macsOnPort <= accessMax {
|
||||
return "access"
|
||||
}
|
||||
if macsOnPort >= trunkMin {
|
||||
return "strong_transit"
|
||||
}
|
||||
return "ambiguous"
|
||||
}
|
||||
|
||||
// PortKindRank для сортировки: access → ambiguous → strong_transit.
|
||||
func PortKindRank(kind string) int {
|
||||
switch kind {
|
||||
case "access":
|
||||
return 0
|
||||
case "ambiguous":
|
||||
return 1
|
||||
case "strong_transit":
|
||||
return 2
|
||||
default:
|
||||
return 9
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package scans
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestClassifyPortKind(t *testing.T) {
|
||||
const accessMax, trunkMin = 8, 32
|
||||
if g := ClassifyPortKind(1, accessMax, trunkMin); g != "access" {
|
||||
t.Fatalf("1 -> access, got %q", g)
|
||||
}
|
||||
if g := ClassifyPortKind(8, accessMax, trunkMin); g != "access" {
|
||||
t.Fatalf("8 -> access, got %q", g)
|
||||
}
|
||||
if g := ClassifyPortKind(9, accessMax, trunkMin); g != "ambiguous" {
|
||||
t.Fatalf("9 -> ambiguous, got %q", g)
|
||||
}
|
||||
if g := ClassifyPortKind(31, accessMax, trunkMin); g != "ambiguous" {
|
||||
t.Fatalf("31 -> ambiguous, got %q", g)
|
||||
}
|
||||
if g := ClassifyPortKind(32, accessMax, trunkMin); g != "strong_transit" {
|
||||
t.Fatalf("32 -> strong_transit, got %q", g)
|
||||
}
|
||||
}
|
||||
@@ -570,6 +570,36 @@ limit $3`
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *PostgresStore) CountDistinctMACsPerSwitchPort(scanID string) (map[string]int, error) {
|
||||
query := `
|
||||
select ip::text, if_index, bridge_port, vlan,
|
||||
count(distinct lower(replace(trim(coalesce(mac, '')), '-', ':')))::int as c
|
||||
from scan_port_devices
|
||||
where scan_id = $1
|
||||
and trim(coalesce(mac, '')) <> ''
|
||||
group by ip, if_index, bridge_port, vlan`
|
||||
rows, err := s.db.QueryContext(context.Background(), query, scanID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make(map[string]int, 4096)
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
var ifIndex, bridgePort, vlan, c int
|
||||
if err = rows.Scan(&ip, &ifIndex, &bridgePort, &vlan, &c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
k := PortFDBLocationKey(ip, ifIndex, bridgePort, vlan)
|
||||
out[k] = c
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *PostgresStore) PurgeAllScanData() error {
|
||||
// Дочерние таблицы ссылаются на scan_jobs — CASCADE очищает всё; RESTART IDENTITY сбрасывает bigserial.
|
||||
_, err := s.db.ExecContext(context.Background(), `truncate table scan_jobs restart identity cascade`)
|
||||
|
||||
@@ -73,6 +73,8 @@ type Store interface {
|
||||
ListPortDeviceResults(scanID string, filterIP string, filterIfIndex int, limit int) []PortDeviceResult
|
||||
// ListPortDevicesByMac — строки FDB по точному MAC (mac уже в каноническом виде, см. NormalizeMAC).
|
||||
ListPortDevicesByMac(scanID string, mac string, limit int) []PortDeviceResult
|
||||
// CountDistinctMACsPerSwitchPort — число уникальных MAC на каждом (коммутатор, ifIndex, bridge_port, vlan) в скане.
|
||||
CountDistinctMACsPerSwitchPort(scanID string) (map[string]int, error)
|
||||
// PurgeAllScanData удаляет все сканы и связанные строки (хосты, порты, SNMP, LLDP, интерфейсы).
|
||||
PurgeAllScanData() error
|
||||
}
|
||||
@@ -482,6 +484,29 @@ func (s *MemoryStore) ListPortDevicesByMac(scanID string, mac string, limit int)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *MemoryStore) CountDistinctMACsPerSwitchPort(scanID string) (map[string]int, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
items := s.fdb[scanID]
|
||||
per := make(map[string]map[string]struct{})
|
||||
for _, r := range items {
|
||||
mk := NormalizeMAC(r.MAC)
|
||||
if mk == "" {
|
||||
continue
|
||||
}
|
||||
k := PortFDBLocationKey(r.IP, r.IfIndex, r.BridgePort, r.Vlan)
|
||||
if per[k] == nil {
|
||||
per[k] = make(map[string]struct{})
|
||||
}
|
||||
per[k][mk] = struct{}{}
|
||||
}
|
||||
out := make(map[string]int, len(per))
|
||||
for k, set := range per {
|
||||
out[k] = len(set)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func applyDefaultOptions(opts *ScanOptions) {
|
||||
if opts.PingTimeoutMS <= 0 {
|
||||
opts.PingTimeoutMS = 700
|
||||
|
||||
Reference in New Issue
Block a user