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:
@@ -8,6 +8,7 @@
|
|||||||
- `POST /api/scans` - создание задания скана.
|
- `POST /api/scans` - создание задания скана.
|
||||||
- `GET /api/scans` - список сканов.
|
- `GET /api/scans` - список сканов.
|
||||||
- `GET /api/scans/{id}` - просмотр созданного задания.
|
- `GET /api/scans/{id}` - просмотр созданного задания.
|
||||||
|
- `GET /api/scans/diff?from=&to=` - сравнение двух сканов (hosts/links).
|
||||||
- `GET /api/scans/{id}/hosts` - результаты проверки хостов по скану.
|
- `GET /api/scans/{id}/hosts` - результаты проверки хостов по скану.
|
||||||
- `GET /api/scans/{id}/ports` - результаты проверки TCP-портов по скану.
|
- `GET /api/scans/{id}/ports` - результаты проверки TCP-портов по скану.
|
||||||
- `GET /api/scans/{id}/snmp` - результаты SNMP v2c (`sysName`, `sysDescr`, `sysObjectID`).
|
- `GET /api/scans/{id}/snmp` - результаты SNMP v2c (`sysName`, `sysDescr`, `sysObjectID`).
|
||||||
@@ -131,6 +132,12 @@ curl -s http://localhost:8080/api/scans/<scan_id>
|
|||||||
curl -s http://localhost:8080/api/scans?limit=20
|
curl -s http://localhost:8080/api/scans?limit=20
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Сравнить два scan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s "http://localhost:8080/api/scans/diff?from=<scan_id_1>&to=<scan_id_2>"
|
||||||
|
```
|
||||||
|
|
||||||
В ответе у scan есть:
|
В ответе у scan есть:
|
||||||
|
|
||||||
- `status`: `queued`, `running`, `done`, `failed`
|
- `status`: `queued`, `running`, `done`, `failed`
|
||||||
|
|||||||
+3
-3
@@ -25,7 +25,7 @@
|
|||||||
- [x] сбор LLDP соседей по SNMP;
|
- [x] сбор LLDP соседей по SNMP;
|
||||||
- [x] сохранение результатов скана в БД;
|
- [x] сохранение результатов скана в БД;
|
||||||
- [x] построение базового графа связей устройств (`/links`, `/topology`);
|
- [x] построение базового графа связей устройств (`/links`, `/topology`);
|
||||||
- [ ] история сканов и сравнение изменений.
|
- [x] история сканов и сравнение изменений (`/api/scans/diff?from=&to=`).
|
||||||
|
|
||||||
## Нефункциональные требования
|
## Нефункциональные требования
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
- [x] `POST /api/scans`
|
- [x] `POST /api/scans`
|
||||||
- [x] `GET /api/scans/{id}`
|
- [x] `GET /api/scans/{id}`
|
||||||
- [x] `GET /api/scans/{id}/topology`
|
- [x] `GET /api/scans/{id}/topology`
|
||||||
- [ ] `GET /api/topology/diff?from=&to=`
|
- [x] `GET /api/scans/diff?from=&to=`
|
||||||
|
|
||||||
## Модель данных (минимум)
|
## Модель данных (минимум)
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
3. [x] Ping + port scan
|
3. [x] Ping + port scan
|
||||||
4. [x] SNMP system + LLDP
|
4. [x] SNMP system + LLDP
|
||||||
5. [ ] Граф топологии в UI
|
5. [ ] Граф топологии в UI
|
||||||
6. [ ] История и diff
|
6. [x] История и diff
|
||||||
|
|
||||||
## Критерии приемки v1
|
## Критерии приемки v1
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ type TopologyEdge struct {
|
|||||||
TargetPort string `json:"target_port,omitempty"`
|
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 {
|
func NewHandler(store scans.Store, run *scans.Runner) *Handler {
|
||||||
return &Handler{store: store, run: run}
|
return &Handler{store: store, run: run}
|
||||||
}
|
}
|
||||||
@@ -48,6 +57,7 @@ func (h *Handler) Routes() http.Handler {
|
|||||||
mux.HandleFunc("GET /health", h.health)
|
mux.HandleFunc("GET /health", h.health)
|
||||||
mux.HandleFunc("POST /api/scans", h.createScan)
|
mux.HandleFunc("POST /api/scans", h.createScan)
|
||||||
mux.HandleFunc("GET /api/scans", h.listScans)
|
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}", h.getScan)
|
||||||
mux.HandleFunc("GET /api/scans/{id}/hosts", h.getScanHosts)
|
mux.HandleFunc("GET /api/scans/{id}/hosts", h.getScanHosts)
|
||||||
mux.HandleFunc("GET /api/scans/{id}/ports", h.getScanPorts)
|
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) {
|
func (h *Handler) getScan(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
if 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) {
|
func writeJSON(w http.ResponseWriter, status int, payload any) {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
_ = json.NewEncoder(w).Encode(payload)
|
_ = json.NewEncoder(w).Encode(payload)
|
||||||
|
|||||||
Reference in New Issue
Block a user