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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user