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
+57 -3
View File
@@ -119,6 +119,11 @@ type FindingFilter struct {
// patchable"; false is the unfixable set — remove the package, disable the
// service, or accept it, but do not wait for an update.
HasFix *bool
// TokenScope is the acting credential's tag restriction, nil meaning
// unrestricted. A finding on a server outside it is dropped: a CVE row
// names a server ID and a hostname, and a count that includes invisible
// hosts is itself a statement about a fleet the caller must not see.
TokenScope map[string]string
}
// ListInstanceFindings returns findings across the whole fleet.
@@ -166,6 +171,36 @@ func ListInstanceFindings(instanceID string, f FindingFilter) ([]models.VulnFind
filter["server_id"] = bson.M{"$in": ids}
}
// The token restriction is applied the same way the Tags selector above
// is — by narrowing server_id — rather than by a post-pass, so the two
// cannot disagree and the query keeps one shape. IntersectSelectors is
// not used here because Tags has already been resolved to IDs by this
// point; intersecting the ID sets is the same operation one level down.
if len(f.TokenScope) > 0 {
visible, restricted, err := VisibleServerIDs(instanceID, f.TokenScope)
if err != nil {
return nil, err
}
if restricted {
ids := make([]string, 0, len(visible))
for id := range visible {
ids = append(ids, id)
}
if len(ids) == 0 {
return []models.VulnFinding{}, nil
}
if existing, ok := filter["server_id"]; ok {
filter["$and"] = bson.A{
bson.M{"server_id": existing},
bson.M{"server_id": bson.M{"$in": ids}},
}
delete(filter, "server_id")
} else {
filter["server_id"] = bson.M{"$in": ids}
}
}
}
cur, err := db.Col("vuln_findings").Find(ctx, filter)
if err != nil {
return nil, err
@@ -182,12 +217,31 @@ func ListInstanceFindings(instanceID string, f FindingFilter) ([]models.VulnFind
// CountOpenFindingsBySeverity powers the summary tiles. Accepted findings are
// excluded: they are suppressed from counts until their expiry, which is the
// whole point of accepting one.
func CountOpenFindingsBySeverity(instanceID string) (map[string]int, error) {
// tokenScope is the acting credential's tag restriction, nil meaning
// unrestricted: a summary tile counting findings on hosts the caller cannot
// see is the same aggregate leak as an unfiltered affected-host count.
func CountOpenFindingsBySeverity(instanceID string, tokenScope map[string]string) (map[string]int, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
match := bson.M{"instance_id": instanceID, "state": models.FindingOpen}
visible, restricted, err := VisibleServerIDs(instanceID, tokenScope)
if err != nil {
return nil, err
}
if restricted {
ids := make([]string, 0, len(visible))
for id := range visible {
ids = append(ids, id)
}
if len(ids) == 0 {
return map[string]int{}, nil
}
match["server_id"] = bson.M{"$in": ids}
}
cur, err := db.Col("vuln_findings").Aggregate(ctx, []bson.M{
{"$match": bson.M{"instance_id": instanceID, "state": models.FindingOpen}},
{"$match": match},
{"$group": bson.M{"_id": "$severity", "n": bson.M{"$sum": 1}}},
})
if err != nil {
@@ -464,4 +518,4 @@ func sweepFixedFindings(ctx context.Context) {
log.Printf("vuln sweeper: removed %d fixed findings for %s", res.DeletedCount, instanceID)
}
}
}
}
+18 -1
View File
@@ -93,7 +93,16 @@ type PackageHit struct {
// The Mongo filter narrows to documents containing the name; the second pass is
// needed because a multikey match returns the whole document, not the matching
// array element.
func SearchPackages(instanceID, name string) ([]PackageHit, error) {
//
// tokenScope is the acting credential's tag restriction, nil meaning
// unrestricted; a hit on a server outside it is dropped before it is returned.
// The filtering is done with VisibleServerIDs — one membership set resolved
// once — rather than by resolving each hit's server individually the way
// search_fleet does, because a package search can return one hit per host in
// the fleet and the query shape must not depend on how many matched. The Mongo
// query itself is unchanged: server_packages carries no tags to filter on, so
// the narrowing is necessarily a second pass either way.
func SearchPackages(instanceID, name string, tokenScope map[string]string) ([]PackageHit, error) {
ctx := context.Background()
cur, err := db.Col("server_packages").Find(ctx, bson.M{
"instance_id": instanceID,
@@ -109,8 +118,16 @@ func SearchPackages(instanceID, name string) ([]PackageHit, error) {
return nil, err
}
visible, restricted, err := VisibleServerIDs(instanceID, tokenScope)
if err != nil {
return nil, err
}
hits := []PackageHit{}
for _, d := range docs {
if restricted && !visible[d.ServerID] {
continue
}
for _, p := range d.Packages {
if p.Name == name {
hits = append(hits, PackageHit{ServerID: d.ServerID, Name: p.Name, Version: p.Version})
+15 -1
View File
@@ -93,7 +93,13 @@ type WorkloadHit struct {
// SearchWorkloads answers "which servers run image X" — the reason the snapshot
// is stored rather than fetched on demand and discarded.
func SearchWorkloads(instanceID, image, stack, state string) ([]WorkloadHit, error) {
//
// tokenScope is the acting credential's tag restriction, nil meaning
// unrestricted. A WorkloadHit names a server ID, so an unfiltered fleet-wide
// search enumerates hosts a restricted token must not see. server_workloads
// carries no tags of its own, so the narrowing is a membership test against
// VisibleServerIDs resolved once — the same shape SearchPackages uses.
func SearchWorkloads(instanceID, image, stack, state string, tokenScope map[string]string) ([]WorkloadHit, error) {
ctx := context.Background()
filter := bson.M{"instance_id": instanceID}
@@ -112,8 +118,16 @@ func SearchWorkloads(instanceID, image, stack, state string) ([]WorkloadHit, err
return nil, err
}
visible, restricted, err := VisibleServerIDs(instanceID, tokenScope)
if err != nil {
return nil, err
}
hits := []WorkloadHit{}
for _, d := range docs {
if restricted && !visible[d.ServerID] {
continue
}
for _, w := range d.Workloads {
if image != "" && w.Image != image {
continue