refactor(backend): replica guard helper for security routes (#797)

* refactor(backend): extract replica guard helper for security routes

Adds blockIfReplica(res, resource) in middleware/fleetSyncGuards.ts and
replaces six inline FleetSyncService.getRole() === 'replica' checks
across the security policies and CVE suppressions endpoints.

Error responses now use a uniform shape:
  403 { error: 'Cannot modify <resource> on a replica instance.
    Connect to the primary.', code: 'REPLICA_READ_ONLY' }

The new code field gives callers a stable discriminator without
matching prose.

Closes #750

* test(suppressions): match stable REPLICA_READ_ONLY code instead of prose

The replica guard helper exposes a stable code field for callers to
discriminate without grepping the human-readable error string. Switch
the replica-rejection assertion to use that code so the test no longer
breaks when the unified error template wording is tuned.
This commit is contained in:
Anso
2026-04-27 00:29:38 -04:00
committed by GitHub
parent 82894164e2
commit add3abaece
3 changed files with 31 additions and 25 deletions
@@ -140,7 +140,7 @@ describe('POST /api/security/suppressions', () => {
.set('Authorization', adminAuthHeader)
.send(validBody);
expect(res.status).toBe(403);
expect(res.body.error).toMatch(/control node/i);
expect(res.body.code).toBe('REPLICA_READ_ONLY');
});
it('rejects malformed CVE identifiers', async () => {
+23
View File
@@ -0,0 +1,23 @@
import type { Response } from 'express';
import { FleetSyncService } from '../services/FleetSyncService';
/**
* Reject mutation attempts on a replica Sencho. Returns true (and writes a
* 403 response) when this instance is acting as a replica; the caller
* should `return` immediately. Returns false on a control instance so the
* route handler can continue.
*
* Replicas mirror security configuration (scan policies, CVE suppressions)
* from the control node and reject local writes to keep the fleet
* authoritative source single.
*/
export function blockIfReplica(res: Response, resource: string): boolean {
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({
error: `Cannot modify ${resource} on a replica instance. Connect to the primary.`,
code: 'REPLICA_READ_ONLY',
});
return true;
}
return false;
}
+7 -24
View File
@@ -12,6 +12,7 @@ import { applySuppressions } from '../utils/suppression-filter';
import { generateSarif } from '../services/SarifExporter';
import { getErrorMessage } from '../utils/errors';
import { isDebugEnabled } from '../utils/debug';
import { blockIfReplica } from '../middleware/fleetSyncGuards';
import { FINDING_SEVERITIES, POLICY_SEVERITIES } from '../utils/severity';
const CVE_ID_RE = /^(CVE-\d{4}-\d{4,}|GHSA-[\w-]{14,})$/;
@@ -418,10 +419,7 @@ securityRouter.get('/policies', authMiddleware, (req: Request, res: Response): v
securityRouter.post('/policies', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'Security policies are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'security policies')) return;
const { name, node_id, stack_pattern, max_severity, block_on_deploy, enabled } = req.body ?? {};
if (!name || typeof name !== 'string' || !name.trim()) {
res.status(400).json({ error: 'Policy name is required' }); return;
@@ -452,10 +450,7 @@ securityRouter.post('/policies', authMiddleware, (req: Request, res: Response):
securityRouter.put('/policies/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'Security policies are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'security policies')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid policy id' }); return;
@@ -488,10 +483,7 @@ securityRouter.put('/policies/:id', authMiddleware, (req: Request, res: Response
securityRouter.delete('/policies/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'Security policies are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'security policies')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid policy id' }); return;
@@ -514,10 +506,7 @@ securityRouter.get('/suppressions', authMiddleware, (req: Request, res: Response
securityRouter.post('/suppressions', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'CVE suppressions are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'CVE suppressions')) return;
const body = req.body ?? {};
const cveId = typeof body.cve_id === 'string' ? body.cve_id.trim() : '';
if (!CVE_ID_RE.test(cveId)) {
@@ -570,10 +559,7 @@ securityRouter.post('/suppressions', authMiddleware, (req: Request, res: Respons
securityRouter.put('/suppressions/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'CVE suppressions are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'CVE suppressions')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid suppression id' }); return;
@@ -611,10 +597,7 @@ securityRouter.put('/suppressions/:id', authMiddleware, (req: Request, res: Resp
securityRouter.delete('/suppressions/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'CVE suppressions are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'CVE suppressions')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid suppression id' }); return;