mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 15:46:43 +00:00
feat(security): enforce scan policies as a pre-deploy gate (#719)
Policies with block_on_deploy=1 now scan every stack image before docker compose up runs and reject the deploy with HTTP 409 on violation. The UI opens a dialog listing offending images; admins can override per deploy with ?ignorePolicy=true, and every bypass is recorded in the audit log with the originating route, actor, policy, and image list. When Trivy is not installed on the target node the gate fails open with a warning notification, so teams are never locked out by tooling state. Post-deploy and scheduled scans still evaluate matching policies and dispatch warnings on violations to surface drift on long-running stacks. Public API additions: policy and suppression CRUD under /api/security, plus the documented 409 block-response shape on all deploy paths.
This commit is contained in:
@@ -11,6 +11,7 @@ import { NodeRegistry } from './NodeRegistry';
|
||||
import { RegistryService } from './RegistryService';
|
||||
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { isPathWithinBase, isValidStackName } from '../utils/validation';
|
||||
|
||||
/**
|
||||
* ComposeService - local docker compose CLI execution.
|
||||
@@ -407,4 +408,56 @@ export class ComposeService {
|
||||
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enumerate image references declared in a stack's compose file.
|
||||
*
|
||||
* Used by the pre-deploy policy gate to decide which images to scan before
|
||||
* `docker compose up` runs. Path traversal is guarded against the node's
|
||||
* compose base directory; missing / unreadable compose files or `.env`
|
||||
* interpolation failures surface as a rejected Promise so the gate can
|
||||
* block the deploy rather than silently allow it.
|
||||
*/
|
||||
public async listStackImages(stackName: string): Promise<string[]> {
|
||||
if (!isValidStackName(stackName)) {
|
||||
throw new Error('Invalid stack path');
|
||||
}
|
||||
const stackDir = path.resolve(this.baseDir, stackName);
|
||||
if (!isPathWithinBase(stackDir, this.baseDir) || path.resolve(this.baseDir) === stackDir) {
|
||||
throw new Error('Invalid stack path');
|
||||
}
|
||||
const stdout = await this.captureCompose(['config', '--images'], stackDir);
|
||||
const seen = new Set<string>();
|
||||
const images: string[] = [];
|
||||
for (const raw of stdout.split(/\r?\n/)) {
|
||||
const line = raw.trim();
|
||||
if (!line) continue;
|
||||
if (line.startsWith('sha256:')) continue;
|
||||
if (seen.has(line)) continue;
|
||||
seen.add(line);
|
||||
images.push(line);
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
private captureCompose(args: string[], cwd: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('docker', ['compose', ...args], {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
|
||||
},
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (data: Buffer) => { stdout += data.toString(); });
|
||||
child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); });
|
||||
child.on('error', (err) => reject(err));
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) resolve(stdout);
|
||||
else reject(new Error(stderr.trim() || `docker compose ${args.join(' ')} failed with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user