feat(fleet-sync): hide other replicas' identity-scoped policies on a replica (#973)

GET /api/security/policies on a replica now returns only the policies
that apply to THIS replica. Replicated rows with a node_identity
targeting a sibling replica are filtered out so an operator cannot
enumerate the names and rules of policies meant for another node in
the fleet. Defense in depth: the security panel is admin-only, but a
backend filter is bypass-proof and matches Sencho's privacy posture.

Internal evaluators (getMatchingPolicy, evaluateScanAgainstPolicies)
keep using the unfiltered list because they already enforce identity
matching at evaluation time.

CVE suppressions are fleet-wide on every replica (no node_identity
column) so no analogous filter is needed.

Public surface:
- DatabaseService.getScanPoliciesForUi(role, selfIdentity): the
  filtered variant, called from securityRouter.get('/policies').

Tests:
- 3 new vitest cases: control sees full set; replica hides
  other-replica scoped rows; replica always includes locally created
  rows.
- Full backend suite: 1795 pass / 5 skipped.
This commit is contained in:
Anso
2026-05-07 13:48:11 -04:00
committed by GitHub
parent 4007709590
commit a284732a95
3 changed files with 143 additions and 1 deletions
+27
View File
@@ -3526,6 +3526,33 @@ export class DatabaseService {
.all() as ScanPolicy[];
}
/**
* Variant of `getScanPolicies` for the security-settings UI.
*
* On a control instance: returns the full set, identical to
* `getScanPolicies`.
*
* On a replica: returns only the policies that apply to THIS replica.
* Replicated rows scoped to a different replica's identity (the
* `node_identity` of a sibling node in the fleet) are filtered out so
* an operator on Replica A cannot enumerate the names of identity-scoped
* policies meant for Replica B. Internal evaluators
* (`getMatchingPolicy`, `evaluateScanAgainstPolicies`) keep using the
* unfiltered list because they already enforce identity matching at
* evaluation time.
*/
public getScanPoliciesForUi(role: 'control' | 'replica', selfIdentity: string): ScanPolicy[] {
const all = this.getScanPolicies();
if (role === 'control') return all;
return all.filter((p) => {
if (p.replicated_from_control === 0) return true;
// Fleet-wide replicated rows have an empty node_identity and
// apply on every replica.
if (!p.node_identity) return true;
return p.node_identity === selfIdentity;
});
}
public getScanPolicy(id: number): ScanPolicy | null {
return (
(this.db