fix(mcp): correct fleet online status, unsafe version filter, audit totals
summariseServer compared Status against "online", a value never assigned anywhere (the real vocabulary is pending/active/offline), so every server misreported as offline. search_fleet's version_below used a lexicographic comparison across dpkg/rpm/apk version schemes with no common ordering, so it refuses that filter now and returns all matches instead of a wrong answer. listAuditResult's Total carried the "shown" JSON tag and the capped count; it now reports the real total alongside shown.
This commit is contained in:
@@ -20,14 +20,14 @@ type serverSummary struct {
|
||||
}
|
||||
|
||||
// models.Server has no Online bool: it stores Status as one of "pending",
|
||||
// "online" or "offline" (see internal/services/servers.go). Online here
|
||||
// "active" or "offline" (see internal/services/servers.go). Online here
|
||||
// mirrors that string the same way the REST layer treats it.
|
||||
func summariseServer(s models.Server) serverSummary {
|
||||
return serverSummary{
|
||||
ID: s.ServerID,
|
||||
Hostname: s.Hostname,
|
||||
OS: s.OSInfo,
|
||||
Online: s.Status == "online",
|
||||
Online: s.Status == "active",
|
||||
Tags: s.Tags,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,3 +58,21 @@ func TestServerSummaryStaysSmall(t *testing.T) {
|
||||
t.Errorf("30 servers serialise to %d bytes, want at most 8000", len(out))
|
||||
}
|
||||
}
|
||||
|
||||
// The real status vocabulary is "pending" / "active" / "offline" (see
|
||||
// internal/services/servers.go) — "online" is never assigned anywhere. A
|
||||
// server carrying the live status ("active") must project as Online: true,
|
||||
// or list_servers/get_server misreport the entire fleet as down.
|
||||
func TestSummariseServerReportsActiveAsOnline(t *testing.T) {
|
||||
active := summariseServer(models.Server{ServerID: "srv-active", Status: "active"})
|
||||
if !active.Online {
|
||||
t.Errorf("server with status %q should be online, got Online=false", "active")
|
||||
}
|
||||
|
||||
for _, status := range []string{"pending", "offline"} {
|
||||
s := summariseServer(models.Server{ServerID: "srv-" + status, Status: status})
|
||||
if s.Online {
|
||||
t.Errorf("server with status %q should not be online, got Online=true", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,8 @@ type auditEventSummary struct {
|
||||
|
||||
type listAuditResult struct {
|
||||
Events []auditEventSummary `json:"events"`
|
||||
Total int `json:"shown"`
|
||||
Total int64 `json:"total"`
|
||||
Shown int `json:"shown"`
|
||||
}
|
||||
|
||||
// ---- secrets ----
|
||||
@@ -433,14 +434,22 @@ func init() {
|
||||
Name: "search_fleet",
|
||||
Scope: "vulns:read",
|
||||
Description: "Search every server's installed packages by name across the whole fleet — " +
|
||||
"answers questions like \"which hosts still run OpenSSL 1.1\". Pass version_below to " +
|
||||
"further narrow to versions that sort earlier than the given string.",
|
||||
"answers questions like \"which hosts still run OpenSSL 1.1\". version_below is not " +
|
||||
"currently supported: filtering package versions correctly requires knowing each " +
|
||||
"distribution's own version-ordering scheme (dpkg/rpm/apk), which this tool cannot " +
|
||||
"determine, so it refuses rather than guess with a lexicographic comparison. Every " +
|
||||
"matching install is returned; compare versions yourself.",
|
||||
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
|
||||
name := stringArg(args, "name")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("name is required")
|
||||
}
|
||||
versionBelow := stringArg(args, "version_below")
|
||||
if stringArg(args, "version_below") != "" {
|
||||
return nil, fmt.Errorf("version_below is not supported: correct version ordering " +
|
||||
"depends on each host's distribution (dpkg/rpm/apk each order differently), " +
|
||||
"which this tool cannot resolve here — omit version_below and every matching " +
|
||||
"install is returned instead")
|
||||
}
|
||||
|
||||
hits, err := services.SearchPackages(c.InstanceID, name)
|
||||
if err != nil {
|
||||
@@ -453,9 +462,6 @@ func init() {
|
||||
if len(out) == limit {
|
||||
break
|
||||
}
|
||||
if versionBelow != "" && h.Version >= versionBelow {
|
||||
continue
|
||||
}
|
||||
// A hit's server must be resolved through the token's own
|
||||
// scope: SearchPackages runs unscoped across the instance,
|
||||
// so a server outside the token's tag restriction is
|
||||
@@ -477,7 +483,7 @@ func init() {
|
||||
"Filter by event_type prefix (e.g. \"workflow\", \"key\", \"server\").",
|
||||
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
|
||||
limit := int64(pageLimit(args))
|
||||
events, _, err := services.ListAuditEvents(c.InstanceID, services.AuditFilter{
|
||||
events, total, err := services.ListAuditEvents(c.InstanceID, services.AuditFilter{
|
||||
Category: stringArg(args, "event_type"),
|
||||
Limit: limit,
|
||||
})
|
||||
@@ -488,7 +494,7 @@ func init() {
|
||||
for _, e := range events {
|
||||
out = append(out, auditEventSummary{At: e.CreatedAt, Type: e.EventType, Actor: e.Actor, Detail: e.Details})
|
||||
}
|
||||
return listAuditResult{Events: out, Total: len(out)}, nil
|
||||
return listAuditResult{Events: out, Total: total, Shown: len(out)}, nil
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user