feat(mac-locations): VLAN-aware port MAC counts, access/trunk thresholds, likely_attachment
Made-with: Cursor
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -60,6 +61,8 @@ type MacLocationRow struct {
|
|||||||
MAC string `json:"mac"`
|
MAC string `json:"mac"`
|
||||||
ClientIP string `json:"client_ip,omitempty"`
|
ClientIP string `json:"client_ip,omitempty"`
|
||||||
ClientName string `json:"client_name,omitempty"`
|
ClientName string `json:"client_name,omitempty"`
|
||||||
|
MacsOnPort int `json:"macs_on_port"`
|
||||||
|
PortKind string `json:"port_kind"` // access | ambiguous | strong_transit
|
||||||
CheckedAt time.Time `json:"checked_at"`
|
CheckedAt time.Time `json:"checked_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +437,35 @@ func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
limit = n
|
limit = n
|
||||||
}
|
}
|
||||||
|
accessMax := 8
|
||||||
|
trunkMin := 32
|
||||||
|
if v := strings.TrimSpace(r.URL.Query().Get("access_max_macs")); v != "" {
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
if err != nil || n < 1 || n > 4096 {
|
||||||
|
writeError(w, http.StatusBadRequest, "access_max_macs must be between 1 and 4096")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accessMax = n
|
||||||
|
}
|
||||||
|
if v := strings.TrimSpace(r.URL.Query().Get("trunk_min_macs")); v != "" {
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
if err != nil || n < 2 || n > 65536 {
|
||||||
|
writeError(w, http.StatusBadRequest, "trunk_min_macs must be between 2 and 65536")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trunkMin = n
|
||||||
|
}
|
||||||
|
if accessMax >= trunkMin {
|
||||||
|
writeError(w, http.StatusBadRequest, "access_max_macs must be less than trunk_min_macs")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
counts, err := h.store.CountDistinctMACsPerSwitchPort(id)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
snmpItems := h.store.ListSNMPResults(id, 200000)
|
snmpItems := h.store.ListSNMPResults(id, 200000)
|
||||||
ipToName := make(map[string]string, len(snmpItems))
|
ipToName := make(map[string]string, len(snmpItems))
|
||||||
for _, s := range snmpItems {
|
for _, s := range snmpItems {
|
||||||
@@ -451,6 +483,11 @@ func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
|
|||||||
items := h.store.ListPortDevicesByMac(id, norm, limit)
|
items := h.store.ListPortDevicesByMac(id, norm, limit)
|
||||||
out := make([]MacLocationRow, 0, len(items))
|
out := make([]MacLocationRow, 0, len(items))
|
||||||
for _, r := range items {
|
for _, r := range items {
|
||||||
|
key := scans.PortFDBLocationKey(r.IP, r.IfIndex, r.BridgePort, r.Vlan)
|
||||||
|
nOn := counts[key]
|
||||||
|
if nOn < 1 {
|
||||||
|
nOn = 1
|
||||||
|
}
|
||||||
row := MacLocationRow{
|
row := MacLocationRow{
|
||||||
SwitchIP: r.IP,
|
SwitchIP: r.IP,
|
||||||
IfIndex: r.IfIndex,
|
IfIndex: r.IfIndex,
|
||||||
@@ -458,6 +495,8 @@ func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
|
|||||||
Vlan: r.Vlan,
|
Vlan: r.Vlan,
|
||||||
MAC: r.MAC,
|
MAC: r.MAC,
|
||||||
ClientIP: strings.TrimSpace(r.LearnedIP),
|
ClientIP: strings.TrimSpace(r.LearnedIP),
|
||||||
|
MacsOnPort: nOn,
|
||||||
|
PortKind: scans.ClassifyPortKind(nOn, accessMax, trunkMin),
|
||||||
CheckedAt: r.CheckedAt,
|
CheckedAt: r.CheckedAt,
|
||||||
}
|
}
|
||||||
if nm, ok := ipToName[r.IP]; ok {
|
if nm, ok := ipToName[r.IP]; ok {
|
||||||
@@ -470,8 +509,50 @@ func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
out = append(out, row)
|
out = append(out, row)
|
||||||
}
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool {
|
||||||
|
ri, rj := scans.PortKindRank(out[i].PortKind), scans.PortKindRank(out[j].PortKind)
|
||||||
|
if ri != rj {
|
||||||
|
return ri < rj
|
||||||
|
}
|
||||||
|
if out[i].MacsOnPort != out[j].MacsOnPort {
|
||||||
|
return out[i].MacsOnPort < out[j].MacsOnPort
|
||||||
|
}
|
||||||
|
if out[i].SwitchIP != out[j].SwitchIP {
|
||||||
|
return out[i].SwitchIP < out[j].SwitchIP
|
||||||
|
}
|
||||||
|
if out[i].IfIndex != out[j].IfIndex {
|
||||||
|
return out[i].IfIndex < out[j].IfIndex
|
||||||
|
}
|
||||||
|
if out[i].Vlan != out[j].Vlan {
|
||||||
|
return out[i].Vlan < out[j].Vlan
|
||||||
|
}
|
||||||
|
return out[i].ClientIP < out[j].ClientIP
|
||||||
|
})
|
||||||
|
|
||||||
|
likely := make([]MacLocationRow, 0)
|
||||||
|
for _, row := range out {
|
||||||
|
if row.PortKind == "access" {
|
||||||
|
likely = append(likely, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(likely, func(i, j int) bool {
|
||||||
|
if likely[i].MacsOnPort != likely[j].MacsOnPort {
|
||||||
|
return likely[i].MacsOnPort < likely[j].MacsOnPort
|
||||||
|
}
|
||||||
|
if likely[i].SwitchIP != likely[j].SwitchIP {
|
||||||
|
return likely[i].SwitchIP < likely[j].SwitchIP
|
||||||
|
}
|
||||||
|
if likely[i].IfIndex != likely[j].IfIndex {
|
||||||
|
return likely[i].IfIndex < likely[j].IfIndex
|
||||||
|
}
|
||||||
|
return likely[i].Vlan < likely[j].Vlan
|
||||||
|
})
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
"mac": norm,
|
"mac": norm,
|
||||||
|
"access_max_macs": accessMax,
|
||||||
|
"trunk_min_macs": trunkMin,
|
||||||
|
"likely_attachment": likely,
|
||||||
"items": out,
|
"items": out,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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 {
|
func (s *PostgresStore) PurgeAllScanData() error {
|
||||||
// Дочерние таблицы ссылаются на scan_jobs — CASCADE очищает всё; RESTART IDENTITY сбрасывает bigserial.
|
// Дочерние таблицы ссылаются на scan_jobs — CASCADE очищает всё; RESTART IDENTITY сбрасывает bigserial.
|
||||||
_, err := s.db.ExecContext(context.Background(), `truncate table scan_jobs restart identity cascade`)
|
_, 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
|
ListPortDeviceResults(scanID string, filterIP string, filterIfIndex int, limit int) []PortDeviceResult
|
||||||
// ListPortDevicesByMac — строки FDB по точному MAC (mac уже в каноническом виде, см. NormalizeMAC).
|
// ListPortDevicesByMac — строки FDB по точному MAC (mac уже в каноническом виде, см. NormalizeMAC).
|
||||||
ListPortDevicesByMac(scanID string, mac string, limit int) []PortDeviceResult
|
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 удаляет все сканы и связанные строки (хосты, порты, SNMP, LLDP, интерфейсы).
|
||||||
PurgeAllScanData() error
|
PurgeAllScanData() error
|
||||||
}
|
}
|
||||||
@@ -482,6 +484,29 @@ func (s *MemoryStore) ListPortDevicesByMac(scanID string, mac string, limit int)
|
|||||||
return out
|
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) {
|
func applyDefaultOptions(opts *ScanOptions) {
|
||||||
if opts.PingTimeoutMS <= 0 {
|
if opts.PingTimeoutMS <= 0 {
|
||||||
opts.PingTimeoutMS = 700
|
opts.PingTimeoutMS = 700
|
||||||
|
|||||||
+51
-14
@@ -56,13 +56,19 @@
|
|||||||
|
|
||||||
<div id="macSearchPanel" style="margin:10px 0;padding:12px 14px;background:#eef2ff;border:2px solid #6366f1;border-radius:10px;box-shadow:0 1px 3px rgba(99,102,241,0.2);">
|
<div id="macSearchPanel" style="margin:10px 0;padding:12px 14px;background:#eef2ff;border:2px solid #6366f1;border-radius:10px;box-shadow:0 1px 3px rgba(99,102,241,0.2);">
|
||||||
<div style="font-weight:700;font-size:15px;color:#1e1b4b;margin-bottom:6px;">Где этот MAC подключён? (таблица FDB по выбранному scan)</div>
|
<div style="font-weight:700;font-size:15px;color:#1e1b4b;margin-bottom:6px;">Где этот MAC подключён? (таблица FDB по выбранному scan)</div>
|
||||||
<p class="hint" style="margin:0 0 10px;">Сначала укажите <strong>scan_id</strong> в поле выше (или «Последний scan» / «Загрузить»). Введите MAC — увидите коммутатор (IP и sysName), порт, VLAN, IP за портом из ARP.</p>
|
<p class="hint" style="margin:0 0 10px;">Сначала укажите <strong>scan_id</strong> в поле выше (или «Последний scan» / «Загрузить»). Введите MAC. Строки сортируются: сначала порты с малым числом уникальных MAC на том же <strong>VLAN</strong> и порту (вероятный доступ), затем «между порогами», затем транк/uplink.</p>
|
||||||
<div class="row" style="align-items:center;flex-wrap:wrap;gap:8px;">
|
<div class="row" style="align-items:center;flex-wrap:wrap;gap:8px;">
|
||||||
<label for="macSearchInput" style="font-weight:600;color:#312e81;">MAC</label>
|
<label for="macSearchInput" style="font-weight:600;color:#312e81;">MAC</label>
|
||||||
<input id="macSearchInput" placeholder="aa:bb:cc:dd:ee:ff" style="min-width:260px;font-family:monospace;" autocomplete="off" />
|
<input id="macSearchInput" placeholder="aa:bb:cc:dd:ee:ff" style="min-width:260px;font-family:monospace;" autocomplete="off" />
|
||||||
<button type="button" id="macSearchBtn" style="background:#4f46e5;color:#fff;border:none;padding:8px 14px;border-radius:8px;cursor:pointer;font-weight:600;">Показать на каком коммутаторе</button>
|
<button type="button" id="macSearchBtn" style="background:#4f46e5;color:#fff;border:none;padding:8px 14px;border-radius:8px;cursor:pointer;font-weight:600;">Показать на каком коммутаторе</button>
|
||||||
<span id="macSearchSummary" class="hint" style="margin:0;"></span>
|
<span id="macSearchSummary" class="hint" style="margin:0;"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row" style="flex-wrap:wrap;gap:10px;align-items:center;font-size:13px;margin-top:4px;">
|
||||||
|
<span style="color:#4338ca;font-weight:600;">Пороги</span>
|
||||||
|
<label title="Если на порту (и VLAN) не больше столько уникальных MAC — считаем порт доступом">доступ ≤ <input type="number" id="macAccessMax" min="1" max="4096" value="8" style="width:56px;padding:4px 6px;" /></label>
|
||||||
|
<label title="Если не меньше столько уникальных MAC — сильный признак транка/uplink">транк ≥ <input type="number" id="macTrunkMin" min="2" max="65536" value="32" style="width:56px;padding:4px 6px;" /></label>
|
||||||
|
<span class="hint" style="margin:0;">(должно быть: доступ < транк)</span>
|
||||||
|
</div>
|
||||||
<div style="overflow:auto; max-height:260px; margin-top:12px; border:1px solid #c7d2fe; border-radius:8px;background:#fff;">
|
<div style="overflow:auto; max-height:260px; margin-top:12px; border:1px solid #c7d2fe; border-radius:8px;background:#fff;">
|
||||||
<table id="macSearchTable" style="width:100%; border-collapse:collapse; font-size:13px;">
|
<table id="macSearchTable" style="width:100%; border-collapse:collapse; font-size:13px;">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -75,10 +81,12 @@
|
|||||||
<th style="text-align:left;padding:8px 10px;">MAC</th>
|
<th style="text-align:left;padding:8px 10px;">MAC</th>
|
||||||
<th style="text-align:left;padding:8px 10px;">IP за портом</th>
|
<th style="text-align:left;padding:8px 10px;">IP за портом</th>
|
||||||
<th style="text-align:left;padding:8px 10px;">Имя клиента</th>
|
<th style="text-align:left;padding:8px 10px;">Имя клиента</th>
|
||||||
|
<th style="text-align:left;padding:8px 10px;" title="Уникальных MAC на этом коммутаторе, ifIndex, bridge, VLAN">MAC на порту</th>
|
||||||
|
<th style="text-align:left;padding:8px 10px;">Оценка порта</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="macSearchTableBody">
|
<tbody id="macSearchTableBody">
|
||||||
<tr><td colspan="8" style="padding:10px;color:#64748b;">Введите MAC и нажмите кнопку (нужен scan_id в поле выше).</td></tr>
|
<tr><td colspan="10" style="padding:10px;color:#64748b;">Введите MAC и нажмите кнопку (нужен scan_id в поле выше).</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -1256,6 +1264,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function macPortKindRu(kind) {
|
||||||
|
if (kind === "access") return "доступ (мало MAC)";
|
||||||
|
if (kind === "strong_transit") return "транк / uplink";
|
||||||
|
if (kind === "ambiguous") return "между порогами";
|
||||||
|
return kind ? String(kind) : "—";
|
||||||
|
}
|
||||||
|
|
||||||
async function runMacSearch() {
|
async function runMacSearch() {
|
||||||
const scanId = scanIdInput.value.trim();
|
const scanId = scanIdInput.value.trim();
|
||||||
const mac = (macSearchInput && macSearchInput.value.trim()) || "";
|
const mac = (macSearchInput && macSearchInput.value.trim()) || "";
|
||||||
@@ -1267,20 +1282,35 @@
|
|||||||
setStatus("Введите MAC-адрес.");
|
setStatus("Введите MAC-адрес.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const amEl = document.getElementById("macAccessMax");
|
||||||
|
const tmEl = document.getElementById("macTrunkMin");
|
||||||
|
const accessMax = amEl && amEl.value !== "" ? parseInt(amEl.value, 10) : 8;
|
||||||
|
const trunkMin = tmEl && tmEl.value !== "" ? parseInt(tmEl.value, 10) : 32;
|
||||||
|
if (!Number.isFinite(accessMax) || !Number.isFinite(trunkMin) || accessMax < 1 || trunkMin < 2) {
|
||||||
|
setStatus("Пороги: введите числа (доступ ≥1, транк ≥2).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (accessMax >= trunkMin) {
|
||||||
|
setStatus("Пороги: «доступ» должно быть строго меньше «транк» (например 8 и 32).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (macSearchSummary) macSearchSummary.textContent = "";
|
if (macSearchSummary) macSearchSummary.textContent = "";
|
||||||
if (macSearchTableBody) {
|
if (macSearchTableBody) {
|
||||||
macSearchTableBody.innerHTML =
|
macSearchTableBody.innerHTML =
|
||||||
'<tr><td colspan="8" style="padding:8px;color:#64748b;">Загрузка…</td></tr>';
|
'<tr><td colspan="10" style="padding:8px;color:#64748b;">Загрузка…</td></tr>';
|
||||||
}
|
}
|
||||||
setStatus(`Поиск MAC в scan ${scanId}…`);
|
setStatus(`Поиск MAC в scan ${scanId}…`);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
let url = `/api/scans/${encodeURIComponent(scanId)}/mac-locations?mac=${encodeURIComponent(
|
||||||
`/api/scans/${encodeURIComponent(scanId)}/mac-locations?mac=${encodeURIComponent(mac)}&limit=2000`
|
mac
|
||||||
);
|
)}&limit=2000&access_max_macs=${encodeURIComponent(String(accessMax))}&trunk_min_macs=${encodeURIComponent(
|
||||||
|
String(trunkMin)
|
||||||
|
)}`;
|
||||||
|
const res = await fetch(url);
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (macSearchTableBody) {
|
if (macSearchTableBody) {
|
||||||
macSearchTableBody.innerHTML = `<tr><td colspan="8" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
macSearchTableBody.innerHTML = `<tr><td colspan="10" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
||||||
text || res.statusText
|
text || res.statusText
|
||||||
)}</td></tr>`;
|
)}</td></tr>`;
|
||||||
}
|
}
|
||||||
@@ -1289,16 +1319,21 @@
|
|||||||
}
|
}
|
||||||
const data = JSON.parse(text);
|
const data = JSON.parse(text);
|
||||||
const items = data.items || [];
|
const items = data.items || [];
|
||||||
|
const likely = data.likely_attachment || [];
|
||||||
const canon = data.mac || mac;
|
const canon = data.mac || mac;
|
||||||
|
const am = data.access_max_macs ?? accessMax;
|
||||||
|
const tm = data.trunk_min_macs ?? trunkMin;
|
||||||
if (macSearchSummary) {
|
if (macSearchSummary) {
|
||||||
macSearchSummary.textContent = items.length
|
if (!items.length) {
|
||||||
? `Нормализовано: ${canon} · записей: ${items.length}`
|
macSearchSummary.textContent = `Нормализовано: ${canon} · в FDB этого скана нет`;
|
||||||
: `Нормализовано: ${canon} · в FDB этого скана нет`;
|
} else {
|
||||||
|
macSearchSummary.textContent = `Пороги: доступ ≤${am}, транк ≥${tm}. Вероятный доступ: ${likely.length} из ${items.length} строк (см. колонки).`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!macSearchTableBody) return;
|
if (!macSearchTableBody) return;
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
macSearchTableBody.innerHTML =
|
macSearchTableBody.innerHTML =
|
||||||
'<tr><td colspan="8" style="padding:8px;color:#64748b;">Нет совпадений в scan_port_devices.</td></tr>';
|
'<tr><td colspan="10" style="padding:8px;color:#64748b;">Нет совпадений в scan_port_devices.</td></tr>';
|
||||||
setStatus(`Поиск MAC: совпадений нет (${canon}).`);
|
setStatus(`Поиск MAC: совпадений нет (${canon}).`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1311,13 +1346,15 @@
|
|||||||
String(row.bridge_port ?? "")
|
String(row.bridge_port ?? "")
|
||||||
)}</td><td>${escapeHtml(String(row.vlan ?? ""))}</td><td>${escapeHtml(
|
)}</td><td>${escapeHtml(String(row.vlan ?? ""))}</td><td>${escapeHtml(
|
||||||
row.mac || "—"
|
row.mac || "—"
|
||||||
)}</td><td>${escapeHtml(row.client_ip || "—")}</td><td>${escapeHtml(row.client_name || "—")}</td></tr>`
|
)}</td><td>${escapeHtml(row.client_ip || "—")}</td><td>${escapeHtml(row.client_name || "—")}</td><td>${escapeHtml(
|
||||||
|
String(row.macs_on_port ?? "")
|
||||||
|
)}</td><td>${escapeHtml(macPortKindRu(row.port_kind))}</td></tr>`
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
setStatus(`Поиск MAC: найдено ${items.length} строк (${canon}).`);
|
setStatus(`Поиск MAC: ${items.length} строк (${canon}), вероятный доступ: ${likely.length}.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (macSearchTableBody) {
|
if (macSearchTableBody) {
|
||||||
macSearchTableBody.innerHTML = `<tr><td colspan="8" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
macSearchTableBody.innerHTML = `<tr><td colspan="10" style="padding:8px;color:#b91c1c;">${escapeHtml(
|
||||||
e.message
|
e.message
|
||||||
)}</td></tr>`;
|
)}</td></tr>`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user