mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-09-04 14:45:41 +00:00
feat(security): surface Compose internet-reachability exposure in posture (#1442)
* feat(security): surface Compose internet-reachability exposure in posture Builds a per-stack per-service exposure descriptor from the rendered effective Compose model, cached at deploy/update time, and joins it into the Security action posture. A service is publicly exposed when it publishes a port on a non-loopback host IP or uses host networking. The exposure cache lives in a new stack_exposure table, refreshed inside ComposeService.deployStack and updateStack (covering all funneled paths: manual, scheduler, mesh, templates, labels, App Store, Git, webhooks). Cleanup runs on stack delete, blueprint withdrawal, and node delete. The overview route intersects the exposed image set with the existing per-image suppression-aware Critical/High tally, so a clean public nginx does not escalate posture. The scan sheet shows a "Published service" or "Internal only" evidence badge per image. * fix(test): provide fresh auto-close proc for exposure spawn in stall tests Two deployStack idle-stall tests used mockSpawn.mockReturnValue(proc) which returned the same already-closed process for the new config spawn added by the exposure refresh. The renderConfig promise hung waiting for a close event that had already fired. The fix uses mockImplementation to return the controlled proc for the first spawn (up) and a fresh auto-closing proc for the second spawn (config via refreshExposureCache). * fix(security): tighten loopback detection, clarify exposure semantics, drop internal-only badge - Expand isLoopback to cover full 127.0.0.0/8 range (127.0.0.2 etc) - Clarify that exposure is configured (Compose model), not live topology - Remove "Internal only" badge: false is not proof of non-exposure when other stacks using the same image may lack a cached descriptor
This commit is contained in:
@@ -958,7 +958,18 @@ describe('ComposeService - idle-output stall backstop', () => {
|
||||
process.env.SENCHO_COMPOSE_STALL_TIMEOUT_MS = '1000';
|
||||
mockListContainers.mockResolvedValue([]);
|
||||
const proc = createMockProcess();
|
||||
mockSpawn.mockReturnValue(proc);
|
||||
// deployStack now spawns a second child (exposure refresh via renderConfig)
|
||||
// after the up command closes. Return the controlled proc for the up spawn,
|
||||
// and a fresh auto-closing proc for the config spawn so the test does not
|
||||
// hang on the already-closed proc.
|
||||
let spawnCount = 0;
|
||||
mockSpawn.mockImplementation(() => {
|
||||
spawnCount += 1;
|
||||
if (spawnCount === 1) return proc;
|
||||
const configProc = createMockProcess();
|
||||
Promise.resolve().then(() => configProc.emit('close', 0));
|
||||
return configProc;
|
||||
});
|
||||
|
||||
const svc = ComposeService.getInstance(1);
|
||||
// deployStack spawns a single `up`; emit output every 600ms (< 1s window)
|
||||
@@ -997,7 +1008,16 @@ describe('ComposeService - idle-output stall backstop', () => {
|
||||
process.env.SENCHO_COMPOSE_STALL_TIMEOUT_MS = '0'; // invalid → default (10min)
|
||||
mockListContainers.mockResolvedValue([]);
|
||||
const proc = createMockProcess();
|
||||
mockSpawn.mockReturnValue(proc);
|
||||
// Same pattern as the stall test above: the second spawn (exposure refresh)
|
||||
// needs its own auto-closing proc so deployStack can resolve after close.
|
||||
let spawnCount = 0;
|
||||
mockSpawn.mockImplementation(() => {
|
||||
spawnCount += 1;
|
||||
if (spawnCount === 1) return proc;
|
||||
const configProc = createMockProcess();
|
||||
Promise.resolve().then(() => configProc.emit('close', 0));
|
||||
return configProc;
|
||||
});
|
||||
|
||||
const svc = ComposeService.getInstance(1);
|
||||
const promise = svc.deployStack('my-stack');
|
||||
|
||||
Reference in New Issue
Block a user