mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-26 02:06:49 +00:00
fix(fleet-sync): hygiene pass on receiver behavior and cleanup (#972)
A bundle of small file-local fixes to the receiver path and node-deletion flow. Changes: - F4 receiver audit log: applyIncomingSync now writes a system audit entry on every applied push so mirrored security-rule changes show up in the replica's audit panel with a clear control-side origin. - F7 pilot-agent skip: pushResource explicitly excludes pilot-agent nodes (they have no api_url for HTTP push) and warns once per node id so the operator sees they will not receive replicated policies. - B4 identity-drift notification: when targetIdentity differs from the cached fleet_self_identity, dispatch a warning so the operator can audit any identity-scoped policies that may need re-targeting. - B6 stack_pattern ReDoS guard: reject patterns with 4+ consecutive wildcards or more than 8 wildcards total. Both control-side validators (POST/PUT scan policies) and the receiver-side row validator share the helper. - B9 deleteNode cascade: clear fleet_sync_status rows for the node inside the existing transaction so the sync-status panel does not render ghost entries after a node is removed. - S6 last_error redaction: formatError strips Bearer tokens and JWT-shaped values from error messages and caps at 500 chars before storing in fleet_sync_status.last_error or logging. Tests: - 8 new vitest cases covering audit-log entry, identity-drift alert, pilot-agent warn-once, formatError redaction (Bearer + JWT), ReDoS validator rejection, and a backtracking-time smoke test. - New database-fleet-sync-cascade.test.ts: deleteNode removes fleet_sync_status rows for the deleted node and leaves siblings untouched. - Full backend suite: 1792 pass / 5 skipped.
This commit is contained in:
@@ -48,7 +48,10 @@ function validateScanPolicyRow(row: unknown): string | null {
|
||||
if (typeof r.name !== 'string' || r.name.length === 0 || r.name.length > 200) return 'name must be a non-empty string';
|
||||
if (typeof r.max_severity !== 'string' || !POLICY_SEVERITIES.has(r.max_severity)) return 'max_severity must be CRITICAL, HIGH, MEDIUM, or LOW';
|
||||
if (r.stack_pattern !== null && typeof r.stack_pattern !== 'string') return 'stack_pattern must be a string or null';
|
||||
if (typeof r.stack_pattern === 'string' && r.stack_pattern.length > 200) return 'stack_pattern is too long';
|
||||
if (typeof r.stack_pattern === 'string') {
|
||||
const patternError = validateStackPatternForRedos(r.stack_pattern);
|
||||
if (patternError) return patternError;
|
||||
}
|
||||
if (typeof r.node_identity !== 'string') return 'node_identity must be a string';
|
||||
if (r.node_identity.length > 500) return 'node_identity is too long';
|
||||
if (!isIntFlag(r.block_on_deploy)) return 'block_on_deploy must be 0 or 1';
|
||||
@@ -56,6 +59,24 @@ function validateScanPolicyRow(row: unknown): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject `stack_pattern` inputs that would compile to a backtracking-prone
|
||||
* regex. The matcher in `getMatchingPolicy` substitutes `*` with `.*`, so a
|
||||
* pattern like `***...` becomes a chain of adjacent `.*` runs that exhibit
|
||||
* catastrophic backtracking on long inputs.
|
||||
*
|
||||
* Caps mirror the limit in routes/security.ts so a control creating a policy
|
||||
* sees the same error as a replica receiving one. Length is gated at 200 by
|
||||
* the surrounding row validator.
|
||||
*/
|
||||
export function validateStackPatternForRedos(pattern: string): string | null {
|
||||
if (pattern.length > 200) return 'stack_pattern is too long';
|
||||
const stars = (pattern.match(/\*/g) ?? []).length;
|
||||
if (stars > 8) return 'stack_pattern has too many wildcards (max 8)';
|
||||
if (/\*{4,}/.test(pattern)) return 'stack_pattern must not contain 4+ consecutive wildcards';
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCveSuppressionRow(row: unknown): string | null {
|
||||
if (!row || typeof row !== 'object') return 'row must be an object';
|
||||
const r = row as Record<string, unknown>;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { blockIfReplica } from '../middleware/fleetSyncGuards';
|
||||
import { validateStackPatternForRedos } from './fleet';
|
||||
import { FINDING_SEVERITIES, POLICY_SEVERITIES } from '../utils/severity';
|
||||
|
||||
const CVE_ID_RE = /^(CVE-\d{4}-\d{4,}|GHSA-[\w-]{14,})$/;
|
||||
@@ -423,13 +424,20 @@ securityRouter.post('/policies', authMiddleware, (req: Request, res: Response):
|
||||
if (!POLICY_SEVERITIES.has(max_severity)) {
|
||||
res.status(400).json({ error: 'max_severity must be CRITICAL, HIGH, MEDIUM, or LOW' }); return;
|
||||
}
|
||||
const normalizedPattern = stack_pattern ? String(stack_pattern) : null;
|
||||
if (normalizedPattern !== null) {
|
||||
const patternError = validateStackPatternForRedos(normalizedPattern);
|
||||
if (patternError) {
|
||||
res.status(400).json({ error: patternError }); return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const resolvedNodeId = node_id != null ? Number(node_id) : null;
|
||||
const policy = DatabaseService.getInstance().createScanPolicy({
|
||||
name: name.trim(),
|
||||
node_id: resolvedNodeId,
|
||||
node_identity: FleetSyncService.resolveIdentityForNodeId(resolvedNodeId),
|
||||
stack_pattern: stack_pattern ? String(stack_pattern) : null,
|
||||
stack_pattern: normalizedPattern,
|
||||
max_severity,
|
||||
block_on_deploy: block_on_deploy ? 1 : 0,
|
||||
enabled: enabled === false ? 0 : 1,
|
||||
@@ -459,7 +467,16 @@ securityRouter.put('/policies/:id', authMiddleware, (req: Request, res: Response
|
||||
updates.node_id = resolvedNodeId;
|
||||
updates.node_identity = FleetSyncService.resolveIdentityForNodeId(resolvedNodeId);
|
||||
}
|
||||
if (body.stack_pattern !== undefined) updates.stack_pattern = body.stack_pattern ? String(body.stack_pattern) : null;
|
||||
if (body.stack_pattern !== undefined) {
|
||||
const normalizedPattern = body.stack_pattern ? String(body.stack_pattern) : null;
|
||||
if (normalizedPattern !== null) {
|
||||
const patternError = validateStackPatternForRedos(normalizedPattern);
|
||||
if (patternError) {
|
||||
res.status(400).json({ error: patternError }); return;
|
||||
}
|
||||
}
|
||||
updates.stack_pattern = normalizedPattern;
|
||||
}
|
||||
if (body.max_severity !== undefined) {
|
||||
if (!POLICY_SEVERITIES.has(body.max_severity)) {
|
||||
res.status(400).json({ error: 'max_severity must be CRITICAL, HIGH, MEDIUM, or LOW' }); return;
|
||||
|
||||
Reference in New Issue
Block a user