fix(networking): hold unsafe network deletions and stabilize aggregate reads (#1850)

* fix(networking): hold unsafe network deletions and stabilize aggregate reads

Networking audit hardening:

- Fail closed when deleting unlabeled external networks while any stack
  fails to render or the Docker runtime is unreachable: declarations
  cannot be verified, so the backend 409s with a typed code and the
  inventory table holds the delete affordance instead of letting the
  confirm dialog surprise the operator.
- Bound concurrent compose renders during network delete verification
  with the shared render semaphore and mapWithConcurrency.
- Add a short-TTL memo for the node networking aggregate keyed by node
  and request variant, invalidated eagerly on stack, exposure-intent,
  dossier, and network mutations, with stale-on-error serves flagged as
  degradedCache and surfaced in the overview.
- Resolve node-scoped stack edit permissions against the active node in
  the findings action list.
- Developer-mode debug logs for aggregate serving and delete-guard
  outcomes.

Tests: cache unit suite, hardening integration suite, and component
coverage for the permission threading and delete-affordance holds.

* chore(networking): add missing EOF newline in aggregate cache module
This commit is contained in:
Anso
2026-08-28 12:55:19 +00:00
committed by GitHub
parent 392bc15d91
commit cc4a6571c7
19 changed files with 530 additions and 13 deletions
+6
View File
@@ -8,6 +8,7 @@ import { applySuppressions } from '../utils/suppression-filter';
import { SENCHO_ROLLBACK_HOLD_SQL_LIKE } from '../utils/senchoRollbackHold';
import type { AuditStatsInput } from './AuditAnomalyService';
import { EXPOSURE_INTENTS, type ExposureIntent } from './network/types';
import { invalidateNodeNetworkingAggregate } from './network/networkingAggregateCache';
import { HIGH_EPSS_THRESHOLD } from './securityPosture';
import type { BackendScheduledAction } from './scheduledActionRegistry';
import { stackPatternMatches } from '../helpers/stackPattern';
@@ -3852,11 +3853,13 @@ export class DatabaseService {
now,
now
);
invalidateNodeNetworkingAggregate(nodeId);
return this.getStackDossier(nodeId, stackName) as StackDossier;
}
public deleteStackDossier(nodeId: number, stackName: string): void {
this.db.prepare('DELETE FROM stack_dossiers WHERE node_id = ? AND stack_name = ?').run(nodeId, stackName);
invalidateNodeNetworkingAggregate(nodeId);
}
/**
@@ -3947,16 +3950,19 @@ export class DatabaseService {
updated_at = excluded.updated_at,
updated_by = excluded.updated_by`
).run(nodeId, stackName, service, intent, Date.now(), updatedBy);
invalidateNodeNetworkingAggregate(nodeId);
}
/** Clear one intent row, leaving that scope unset; consumers treat a service with no row as inheriting the stack intent. */
public deleteStackExposureIntent(nodeId: number, stackName: string, service: string): void {
this.db.prepare('DELETE FROM stack_exposure_intent WHERE node_id = ? AND stack_name = ? AND service = ?').run(nodeId, stackName, service);
invalidateNodeNetworkingAggregate(nodeId);
}
/** Clear every intent row for a stack (used when the stack is deleted). */
public deleteStackExposureIntents(nodeId: number, stackName: string): void {
this.db.prepare('DELETE FROM stack_exposure_intent WHERE node_id = ? AND stack_name = ?').run(nodeId, stackName);
invalidateNodeNetworkingAggregate(nodeId);
}
// --- Stack Exposure (Compose reachability descriptor) ---