Add scan diff endpoint and update spec checklist.

Compare two scans by alive hosts and LLDP-derived links, then reflect completed diff milestone in technical spec.

Made-with: Cursor
This commit is contained in:
Andrey Lutsenko
2026-04-09 22:05:09 +10:00
parent ab94409a52
commit df32d43ac5
3 changed files with 81 additions and 3 deletions
+71
View File
@@ -39,6 +39,15 @@ type TopologyEdge struct {
TargetPort string `json:"target_port,omitempty"`
}
type ScanDiffResponse struct {
FromScanID string `json:"from_scan_id"`
ToScanID string `json:"to_scan_id"`
NewHosts []string `json:"new_hosts"`
MissingHosts []string `json:"missing_hosts"`
NewLinks []string `json:"new_links"`
MissingLinks []string `json:"missing_links"`
}
func NewHandler(store scans.Store, run *scans.Runner) *Handler {
return &Handler{store: store, run: run}
}
@@ -48,6 +57,7 @@ func (h *Handler) Routes() http.Handler {
mux.HandleFunc("GET /health", h.health)
mux.HandleFunc("POST /api/scans", h.createScan)
mux.HandleFunc("GET /api/scans", h.listScans)
mux.HandleFunc("GET /api/scans/diff", h.getScansDiff)
mux.HandleFunc("GET /api/scans/{id}", h.getScan)
mux.HandleFunc("GET /api/scans/{id}/hosts", h.getScanHosts)
mux.HandleFunc("GET /api/scans/{id}/ports", h.getScanPorts)
@@ -108,6 +118,38 @@ func (h *Handler) listScans(w http.ResponseWriter, r *http.Request) {
})
}
func (h *Handler) getScansDiff(w http.ResponseWriter, r *http.Request) {
fromID := strings.TrimSpace(r.URL.Query().Get("from"))
toID := strings.TrimSpace(r.URL.Query().Get("to"))
if fromID == "" || toID == "" {
writeError(w, http.StatusBadRequest, "both from and to are required")
return
}
if _, ok := h.store.GetScan(fromID); !ok {
writeError(w, http.StatusNotFound, "from scan not found")
return
}
if _, ok := h.store.GetScan(toID); !ok {
writeError(w, http.StatusNotFound, "to scan not found")
return
}
fromHosts := hostUpSet(h.store.ListHostResults(fromID, 200000))
toHosts := hostUpSet(h.store.ListHostResults(toID, 200000))
fromLinks := linkSet(h.store.ListLLDPResults(fromID, 200000))
toLinks := linkSet(h.store.ListLLDPResults(toID, 200000))
resp := ScanDiffResponse{
FromScanID: fromID,
ToScanID: toID,
NewHosts: setDiff(toHosts, fromHosts),
MissingHosts: setDiff(fromHosts, toHosts),
NewLinks: setDiff(toLinks, fromLinks),
MissingLinks: setDiff(fromLinks, toLinks),
}
writeJSON(w, http.StatusOK, resp)
}
func (h *Handler) getScan(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
@@ -374,6 +416,35 @@ func withJSON(next http.Handler) http.Handler {
})
}
func hostUpSet(items []scans.HostResult) map[string]struct{} {
out := make(map[string]struct{}, len(items))
for _, h := range items {
if h.IsUp {
out[h.IP] = struct{}{}
}
}
return out
}
func linkSet(items []scans.LLDPResult) map[string]struct{} {
out := make(map[string]struct{}, len(items))
for _, l := range items {
key := l.IP + "|" + l.LocalPortNum + "|" + l.RemoteSysName + "|" + l.RemotePortID + "|" + l.RemoteChassisID
out[key] = struct{}{}
}
return out
}
func setDiff(a, b map[string]struct{}) []string {
out := make([]string, 0)
for k := range a {
if _, ok := b[k]; !ok {
out = append(out, k)
}
}
return out
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)