Files
sencho/backend/src/__tests__/policy-enforcement.test.ts
T
Anso b1c5fe8391 fix: harden deploy enforcement paths (#1030)
* fix: harden deploy enforcement paths

* fix: update Docker toolchain to Go 1.26.3

* fix: repair Dockerfile tr argument split across lines

* fix: bump protobufjs to clear npm audit high-severity advisories

* fix(test): add execFile to child_process mock in compose-images test

* fix: resolve merge conflicts with main

* fix: resolve merge conflicts with main

* fix: resolve merge conflicts with main
2026-05-12 19:30:49 -04:00

333 lines
12 KiB
TypeScript

/**
* Covers the pre-deploy policy gate across the six behaviours defined in the
* PR 1 plan: no-policy, disabled-policy, Trivy-missing (fail open),
* violation, admin bypass (audit-logged), compose-parse-failure (fail closed).
*
* The gate is the only code path that can block a `docker compose up`, so
* regressions here are high-impact. We stub the four collaborators the helper
* talks to rather than spinning up the real services, since the contracts
* between them are stable and already exercised by their own tests.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { ScanPolicy, VulnerabilityScan } from '../services/DatabaseService';
interface TrivyStub {
isTrivyAvailable: ReturnType<typeof vi.fn>;
scanImagePreflight: ReturnType<typeof vi.fn>;
}
interface ComposeStub {
listStackImages: ReturnType<typeof vi.fn>;
}
interface DbStub {
getMatchingPolicy: ReturnType<typeof vi.fn>;
insertAuditLog: ReturnType<typeof vi.fn>;
}
interface NotificationStub {
dispatchAlert: ReturnType<typeof vi.fn>;
}
const trivyStub: TrivyStub = {
isTrivyAvailable: vi.fn(),
scanImagePreflight: vi.fn(),
};
const composeStub: ComposeStub = {
listStackImages: vi.fn(),
};
const dbStub: DbStub = {
getMatchingPolicy: vi.fn(),
insertAuditLog: vi.fn(),
};
const notificationStub: NotificationStub = {
dispatchAlert: vi.fn(),
};
vi.mock('../services/TrivyService', () => ({
default: { getInstance: () => trivyStub },
}));
vi.mock('../services/ComposeService', () => ({
ComposeService: { getInstance: () => composeStub },
}));
vi.mock('../services/DatabaseService', () => ({
DatabaseService: { getInstance: () => dbStub },
}));
vi.mock('../services/NotificationService', () => ({
NotificationService: { getInstance: () => notificationStub },
}));
vi.mock('../services/FleetSyncService', () => ({
FleetSyncService: { getSelfIdentity: () => 'self-node' },
}));
import { enforcePolicyForImageRefs, enforcePolicyPreDeploy } from '../services/PolicyEnforcement';
function mkPolicy(overrides: Partial<ScanPolicy> = {}): ScanPolicy {
return {
id: 1,
name: 'block-high',
node_id: null,
node_identity: 'self-node',
stack_pattern: '*',
max_severity: 'HIGH',
block_on_deploy: 1,
enabled: 1,
replicated_from_control: 0,
created_at: Date.now(),
updated_at: Date.now(),
...overrides,
};
}
function mkScan(overrides: Partial<VulnerabilityScan> = {}): VulnerabilityScan {
return {
id: 1,
node_id: 1,
image_ref: 'nginx:1.14',
image_digest: null,
scanned_at: Date.now(),
total_vulnerabilities: 0,
critical_count: 0,
high_count: 0,
medium_count: 0,
low_count: 0,
unknown_count: 0,
fixable_count: 0,
secret_count: 0,
misconfig_count: 0,
scanners_used: 'vuln',
highest_severity: 'LOW',
os_info: null,
trivy_version: '0.50.0',
scan_duration_ms: null,
triggered_by: 'deploy-preflight',
status: 'completed',
error: null,
stack_context: 'web',
policy_evaluation: null,
...overrides,
};
}
describe('enforcePolicyPreDeploy', () => {
beforeEach(() => {
trivyStub.isTrivyAvailable.mockReset();
trivyStub.scanImagePreflight.mockReset();
composeStub.listStackImages.mockReset();
dbStub.getMatchingPolicy.mockReset();
dbStub.insertAuditLog.mockReset();
notificationStub.dispatchAlert.mockReset();
});
it('allows deploy when no matching policy exists', async () => {
dbStub.getMatchingPolicy.mockReturnValue(null);
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(true);
expect(result.bypassed).toBe(false);
expect(result.violations).toEqual([]);
expect(result.policy).toBeUndefined();
expect(trivyStub.isTrivyAvailable).not.toHaveBeenCalled();
expect(composeStub.listStackImages).not.toHaveBeenCalled();
});
it('allows deploy when the matching policy is disabled', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ enabled: 0 }));
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(true);
expect(result.bypassed).toBe(false);
expect(result.violations).toEqual([]);
expect(composeStub.listStackImages).not.toHaveBeenCalled();
});
it('allows deploy when the matching policy does not block on deploy', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ block_on_deploy: 0 }));
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(true);
expect(result.violations).toEqual([]);
expect(composeStub.listStackImages).not.toHaveBeenCalled();
});
it('allows deploy without scanning when paid-tier blocking is disabled', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
const result = await enforcePolicyPreDeploy('web', 1, {
bypass: false,
actor: 'u',
blockingEnabled: false,
});
expect(result.ok).toBe(true);
expect(result.violations).toEqual([]);
expect(trivyStub.isTrivyAvailable).not.toHaveBeenCalled();
expect(composeStub.listStackImages).not.toHaveBeenCalled();
});
it('fails open with a warning alert when Trivy is not installed', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(false);
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(true);
expect(result.trivyMissing).toBe(true);
expect(result.violations).toEqual([]);
expect(notificationStub.dispatchAlert).toHaveBeenCalledTimes(1);
expect(notificationStub.dispatchAlert.mock.calls[0][0]).toBe('warning');
expect(notificationStub.dispatchAlert.mock.calls[0][1]).toBe('scan_finding');
expect(notificationStub.dispatchAlert.mock.calls[0][2]).toContain('Trivy not installed');
expect(composeStub.listStackImages).not.toHaveBeenCalled();
});
it('blocks deploy when a scanned image exceeds the policy severity', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(true);
composeStub.listStackImages.mockResolvedValue(['nginx:1.14']);
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({
id: 99,
highest_severity: 'CRITICAL',
critical_count: 2,
high_count: 5,
}));
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(false);
expect(result.bypassed).toBe(false);
expect(result.violations).toHaveLength(1);
expect(result.violations[0]).toMatchObject({
imageRef: 'nginx:1.14',
severity: 'CRITICAL',
criticalCount: 2,
highCount: 5,
scanId: 99,
});
expect(dbStub.insertAuditLog).not.toHaveBeenCalled();
});
it('allows deploy on admin bypass and records a policy.bypass audit entry', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(true);
composeStub.listStackImages.mockResolvedValue(['nginx:1.14']);
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({
id: 99,
highest_severity: 'CRITICAL',
critical_count: 2,
}));
const result = await enforcePolicyPreDeploy('web', 1, {
bypass: true,
actor: 'admin',
ip: '10.0.0.1',
});
expect(result.ok).toBe(true);
expect(result.bypassed).toBe(true);
expect(result.violations).toHaveLength(1);
expect(dbStub.insertAuditLog).toHaveBeenCalledTimes(1);
const entry = dbStub.insertAuditLog.mock.calls[0][0];
expect(entry.username).toBe('admin');
expect(entry.ip_address).toBe('10.0.0.1');
expect(entry.node_id).toBe(1);
expect(entry.summary).toContain('policy.bypass');
expect(entry.summary).toContain('nginx:1.14');
});
it('fails closed with a synthetic violation when compose parse fails', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(true);
composeStub.listStackImages.mockRejectedValue(new Error('compose file missing'));
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(false);
expect(result.violations).toHaveLength(1);
expect(result.violations[0].imageRef).toBe('(compose parse error)');
expect(result.violations[0].severity).toBe('UNKNOWN');
expect(trivyStub.scanImagePreflight).not.toHaveBeenCalled();
});
it('records a violation when an individual image scan throws', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(true);
composeStub.listStackImages.mockResolvedValue(['nginx:1.14']);
trivyStub.scanImagePreflight.mockRejectedValue(new Error('trivy crashed'));
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(false);
expect(result.violations).toHaveLength(1);
expect(result.violations[0].imageRef).toBe('nginx:1.14');
expect(result.violations[0].severity).toBe('UNKNOWN');
expect(result.violations[0].scanId).toBe(0);
});
it('skips image refs that fail validation without calling the scanner', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(true);
composeStub.listStackImages.mockResolvedValue(['not a valid ref!!!', 'nginx:1.14']);
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ highest_severity: 'LOW' }));
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(true);
expect(result.violations).toEqual([]);
expect(trivyStub.scanImagePreflight).toHaveBeenCalledTimes(1);
expect(trivyStub.scanImagePreflight).toHaveBeenCalledWith('nginx:1.14', 1, 'web');
});
it('allows deploy when all scans fall below the policy threshold', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy({ max_severity: 'CRITICAL' }));
trivyStub.isTrivyAvailable.mockReturnValue(true);
composeStub.listStackImages.mockResolvedValue(['nginx:1.14', 'redis:7']);
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({ highest_severity: 'HIGH' }));
const result = await enforcePolicyPreDeploy('web', 1, { bypass: false, actor: 'u' });
expect(result.ok).toBe(true);
expect(result.violations).toEqual([]);
expect(trivyStub.scanImagePreflight).toHaveBeenCalledTimes(2);
});
it('enforces a supplied image list without reading compose from disk', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(true);
trivyStub.scanImagePreflight.mockResolvedValue(mkScan({
id: 42,
highest_severity: 'HIGH',
high_count: 1,
}));
const result = await enforcePolicyForImageRefs('blueprint-web', 1, ['nginx:1.27-alpine'], { bypass: false, actor: 'u' });
expect(result.ok).toBe(false);
expect(result.violations).toHaveLength(1);
expect(result.violations[0]).toMatchObject({
imageRef: 'nginx:1.27-alpine',
severity: 'HIGH',
highCount: 1,
scanId: 42,
});
expect(composeStub.listStackImages).not.toHaveBeenCalled();
});
it('fails closed on invalid supplied image refs when requested', async () => {
dbStub.getMatchingPolicy.mockReturnValue(mkPolicy());
trivyStub.isTrivyAvailable.mockReturnValue(true);
const result = await enforcePolicyForImageRefs('blueprint-web', 1, ['${IMAGE}'], { bypass: false, actor: 'u' }, undefined, true);
expect(result.ok).toBe(false);
expect(result.violations).toHaveLength(1);
expect(result.violations[0]).toMatchObject({
imageRef: '${IMAGE}',
severity: 'UNKNOWN',
scanId: 0,
});
expect(trivyStub.scanImagePreflight).not.toHaveBeenCalled();
expect(composeStub.listStackImages).not.toHaveBeenCalled();
});
});