mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 12:48:10 +00:00
feat: purge scan data for deleted images and stacks (#1467)
Vulnerability scan rows were never cleaned up when their image was removed from Docker or their stack was deleted, so the Security Overview (including the Top exploit-risk findings card) kept surfacing findings for artifacts that no longer exist. Scan results now reflect what is still on the host: - Deleting a stack immediately purges its stack:<name> compose-config scan. - A background reconciliation in the monitor janitor removes scans whose image is gone from the node, or whose stack folder no longer exists. It is fail-safe: a scan is only removed when its artifact is positively known to be gone, the Docker image list is read with a timeout (skipped on failure), and stack scans are reconciled only when the stack list is non-empty. - An opt-out "Remove scans for deleted images and stacks" setting (on by default, per-node) lets operators retain scan history for removed artifacts. Scan deletes remove child findings explicitly, since SQLite foreign-key cascade is not enabled on the connection.
This commit is contained in:
@@ -241,6 +241,46 @@ describe('prune_on_update (auto-prune after updates)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('prune_orphaned_scans (purge scans for deleted images/stacks)', () => {
|
||||
it('defaults to ON in a freshly seeded database', () => {
|
||||
expect(DatabaseService.getInstance().getGlobalSettings().prune_orphaned_scans).toBe('1');
|
||||
});
|
||||
|
||||
it('is exposed through the settings GET projection', async () => {
|
||||
const res = await request(app).get('/api/settings').set('Cookie', adminCookie);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.prune_orphaned_scans).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects a non-admin write with 403', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/settings')
|
||||
.set('Cookie', viewerCookie)
|
||||
.send({ key: 'prune_orphaned_scans', value: '0' });
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it('accepts a well-formed write and rejects a non-enum value', async () => {
|
||||
const ok = await request(app)
|
||||
.post('/api/settings')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ key: 'prune_orphaned_scans', value: '0' });
|
||||
expect(ok.status).toBe(200);
|
||||
expect(DatabaseService.getInstance().getGlobalSettings().prune_orphaned_scans).toBe('0');
|
||||
|
||||
const bad = await request(app)
|
||||
.post('/api/settings')
|
||||
.set('Cookie', adminCookie)
|
||||
.send({ key: 'prune_orphaned_scans', value: 'banana' });
|
||||
expect(bad.status).toBe(400);
|
||||
expect(bad.body.error).toBe('Validation failed');
|
||||
expect(DatabaseService.getInstance().getGlobalSettings().prune_orphaned_scans).toBe('0');
|
||||
|
||||
// Restore the seeded default so later suites observe the shipped behavior.
|
||||
DatabaseService.getInstance().updateGlobalSetting('prune_orphaned_scans', '1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('health gate settings', () => {
|
||||
it('seeds enabled with a 90 second window in a fresh database', () => {
|
||||
const settings = DatabaseService.getInstance().getGlobalSettings();
|
||||
|
||||
Reference in New Issue
Block a user