fix(security): scope package search, run documents, workloads and vulnerability reads to the token's tags

This commit is contained in:
2026-09-09 08:36:04 +00:00
parent 6a48dd5d73
commit ac8e957859
9 changed files with 208 additions and 17 deletions
+34 -6
View File
@@ -3,6 +3,7 @@ package mcp
import (
"context"
"fmt"
"sort"
"strings"
"time"
@@ -403,6 +404,15 @@ func init() {
f := services.FindingFilter{
Severity: stringArg(args, "severity"),
State: stringArg(args, "status"),
// The service layer drops findings on servers outside this
// token's scope, so the affected-host count below is over
// visible servers only. An unfiltered count is the same
// aggregate leak ListKeys.AssignedCount was fixed for: it
// says something exists on a machine the caller must not
// know about. A CVE affecting only out-of-scope hosts
// disappears from the list rather than showing a zero.
TokenScope: c.TokenScope,
}
findings, err := services.ListInstanceFindings(c.InstanceID, f)
if err != nil {
@@ -420,12 +430,29 @@ func init() {
}
}
// Go randomises map iteration order, so truncating a ranged map
// to a page made two identical calls return different CVEs — a
// model comparing its own two answers would see the fleet change
// under it. Sorting by CVE ID (then package, since the key is a
// pair) makes the page deterministic.
keys := make([]key, 0, len(meta))
for k := range meta {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].cve != keys[j].cve {
return keys[i].cve < keys[j].cve
}
return keys[i].pkg < keys[j].pkg
})
limit := pageLimit(args)
out := make([]vulnSummary, 0, limit)
for k, v := range meta {
for _, k := range keys {
if len(out) == limit {
break
}
v := meta[k]
v.AffectedNum = counts[k]
out = append(out, v)
}
@@ -490,7 +517,7 @@ func init() {
"install is returned instead")
}
hits, err := services.SearchPackages(c.InstanceID, name)
hits, err := services.SearchPackages(c.InstanceID, name, c.TokenScope)
if err != nil {
return nil, fmt.Errorf("could not search packages: %w", err)
}
@@ -501,10 +528,11 @@ func init() {
if len(out) == limit {
break
}
// 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
// dropped here rather than named to the caller.
// SearchPackages now filters by the token's scope itself,
// so this resolve is how the hostname is obtained rather
// than the only scope check. It stays scoped anyway: this
// loop is what turns a server ID into a name the model
// sees, and a second check costs nothing.
srv, err := services.GetServerScoped(c.InstanceID, h.ServerID, c.TokenScope)
if err != nil {
continue