diff --git a/README.md b/README.md index 2081f9d..6ab970e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ - `GET /api/scans/{id}/ports` - результаты проверки TCP-портов по скану. - `GET /api/scans/{id}/snmp` - результаты SNMP v2c (`sysName`, `sysDescr`, `sysObjectID`). - `GET /api/scans/{id}/lldp` - LLDP соседи (если устройство отдает LLDP-MIB по SNMP). +- `GET /api/scans/{id}/links` - базовые связи из LLDP с попыткой сопоставления по `remote_sys_name`. - Валидация CIDR и exclude IP. - Два backend-хранилища: in-memory и PostgreSQL. - После создания scan запускается фоновый discovery с обновлением статуса и прогресса. @@ -159,6 +160,12 @@ curl -s http://localhost:8080/api/scans//snmp?limit=500 curl -s http://localhost:8080/api/scans//lldp?limit=1000 ``` +Получить связи: + +```bash +curl -s http://localhost:8080/api/scans//links?limit=1000 +``` + ## Запуск как сервис на Linux (systemd) 1. Собрать бинарник: diff --git a/internal/api/handler.go b/internal/api/handler.go index 6142b31..cc459c0 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -15,6 +15,16 @@ type Handler struct { run *scans.Runner } +type LinkView struct { + SourceIP string `json:"source_ip"` + SourcePort string `json:"source_port"` + TargetIP string `json:"target_ip,omitempty"` + TargetSysName string `json:"target_sys_name,omitempty"` + TargetPort string `json:"target_port,omitempty"` + RemoteChassis string `json:"remote_chassis_id,omitempty"` + ResolvedBy string `json:"resolved_by"` +} + func NewHandler(store scans.Store, run *scans.Runner) *Handler { return &Handler{store: store, run: run} } @@ -29,6 +39,7 @@ func (h *Handler) Routes() http.Handler { mux.HandleFunc("GET /api/scans/{id}/ports", h.getScanPorts) mux.HandleFunc("GET /api/scans/{id}/snmp", h.getScanSNMP) mux.HandleFunc("GET /api/scans/{id}/lldp", h.getScanLLDP) + mux.HandleFunc("GET /api/scans/{id}/links", h.getScanLinks) return withJSON(mux) } @@ -202,6 +213,69 @@ func (h *Handler) getScanLLDP(w http.ResponseWriter, r *http.Request) { }) } +func (h *Handler) getScanLinks(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + if id == "" { + writeError(w, http.StatusBadRequest, "scan id is required") + return + } + if _, ok := h.store.GetScan(id); !ok { + writeError(w, http.StatusNotFound, "scan not found") + return + } + + limit := 2000 + if raw := r.URL.Query().Get("limit"); raw != "" { + n, err := strconv.Atoi(raw) + if err != nil || n <= 0 || n > 20000 { + writeError(w, http.StatusBadRequest, "limit must be between 1 and 20000") + return + } + limit = n + } + + snmpItems := h.store.ListSNMPResults(id, 10000) + lldpItems := h.store.ListLLDPResults(id, limit) + + sysNameToIP := make(map[string]string, len(snmpItems)) + for _, s := range snmpItems { + name := strings.TrimSpace(strings.ToLower(s.SysName)) + if name == "" { + continue + } + if _, exists := sysNameToIP[name]; !exists { + sysNameToIP[name] = s.IP + } + } + + links := make([]LinkView, 0, len(lldpItems)) + for _, l := range lldpItems { + targetNameNorm := strings.TrimSpace(strings.ToLower(l.RemoteSysName)) + targetIP := "" + resolvedBy := "unresolved" + if targetNameNorm != "" { + if ip, ok := sysNameToIP[targetNameNorm]; ok { + targetIP = ip + resolvedBy = "remote_sys_name" + } + } + + links = append(links, LinkView{ + SourceIP: l.IP, + SourcePort: l.LocalPortNum, + TargetIP: targetIP, + TargetSysName: l.RemoteSysName, + TargetPort: l.RemotePortID, + RemoteChassis: l.RemoteChassisID, + ResolvedBy: resolvedBy, + }) + } + + writeJSON(w, http.StatusOK, map[string]any{ + "items": links, + }) +} + func withJSON(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8")