mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 04:38:11 +00:00
fix: unlock Community deploy policy hard-blocking (#1643)
Remove the leftover paid-only blockingEnabled switch so enabled block-on-deploy policies enforce on every tier, matching the documented every-tier security surface. Existing Community policies begin blocking immediately with no migration.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Community deploy-policy route regressions: a matching block_on_deploy policy
|
||||
* must hard-block via the stack deploy route on Community (local session and
|
||||
* trusted node_proxy with x-sencho-tier: community), returning the established
|
||||
* 409 payload and never calling deployStack.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';
|
||||
import request, { type Response } from 'supertest';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { setupTestDb, cleanupTestDb, loginAsTestAdmin, TEST_JWT_SECRET } from './helpers/setupTestDb';
|
||||
import { PROXY_TIER_HEADER } from '../services/license-headers';
|
||||
|
||||
vi.mock('../services/FileSystemService', () => ({
|
||||
FileSystemService: {
|
||||
getInstance: () => ({
|
||||
getStacks: vi.fn().mockResolvedValue([]),
|
||||
getBaseDir: () => '/tmp/compose',
|
||||
readComposeFile: vi.fn().mockResolvedValue(''),
|
||||
hasComposeFile: vi.fn().mockResolvedValue(true),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
let tmpDir: string;
|
||||
let app: import('express').Express;
|
||||
let authCookie: string;
|
||||
let listImagesSpy: ReturnType<typeof vi.spyOn>;
|
||||
let deploySpy: ReturnType<typeof vi.spyOn>;
|
||||
let trivyAvailableSpy: ReturnType<typeof vi.spyOn>;
|
||||
let scanSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
function expectPolicyHardBlock(res: Response): void {
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body.error).toContain('community-block-critical');
|
||||
expect(res.body.policy).toMatchObject({ name: 'community-block-critical' });
|
||||
expect(res.body.violations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ imageRef: 'nginx:bad' }),
|
||||
]),
|
||||
);
|
||||
expect(scanSpy).toHaveBeenCalled();
|
||||
expect(deploySpy).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = await setupTestDb();
|
||||
({ app } = await import('../index'));
|
||||
authCookie = await loginAsTestAdmin(app);
|
||||
|
||||
const { ComposeService } = await import('../services/ComposeService');
|
||||
listImagesSpy = vi.spyOn(ComposeService.prototype, 'listStackImages').mockResolvedValue(['nginx:bad']);
|
||||
deploySpy = vi.spyOn(ComposeService.prototype, 'deployStack').mockResolvedValue(undefined);
|
||||
|
||||
const TrivyService = (await import('../services/TrivyService')).default;
|
||||
const trivy = TrivyService.getInstance();
|
||||
trivyAvailableSpy = vi.spyOn(trivy, 'isTrivyAvailable').mockReturnValue(true);
|
||||
scanSpy = vi.spyOn(trivy, 'scanImagePreflight').mockResolvedValue({
|
||||
id: 42,
|
||||
node_id: 1,
|
||||
image_ref: 'nginx:bad',
|
||||
image_digest: null,
|
||||
scanned_at: Date.now(),
|
||||
total_vulnerabilities: 1,
|
||||
critical_count: 1,
|
||||
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: 'CRITICAL',
|
||||
os_info: null,
|
||||
trivy_version: '0.50.0',
|
||||
scan_duration_ms: null,
|
||||
triggered_by: 'deploy-preflight',
|
||||
status: 'completed',
|
||||
error: null,
|
||||
stack_context: 'community-block-app',
|
||||
policy_evaluation: null,
|
||||
});
|
||||
|
||||
const { DatabaseService } = await import('../services/DatabaseService');
|
||||
DatabaseService.getInstance().createScanPolicy({
|
||||
name: 'community-block-critical',
|
||||
node_id: null,
|
||||
node_identity: '',
|
||||
stack_pattern: 'community-block-*',
|
||||
max_severity: 'HIGH',
|
||||
block_on_deploy: 1,
|
||||
block_on_severity: 1,
|
||||
block_on_kev: 0,
|
||||
block_on_fixable: 0,
|
||||
enabled: 1,
|
||||
replicated_from_control: 0,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
listImagesSpy.mockRestore();
|
||||
deploySpy.mockRestore();
|
||||
trivyAvailableSpy.mockRestore();
|
||||
scanSpy.mockRestore();
|
||||
vi.restoreAllMocks();
|
||||
cleanupTestDb(tmpDir);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
deploySpy.mockClear();
|
||||
listImagesSpy.mockClear();
|
||||
scanSpy.mockClear();
|
||||
});
|
||||
|
||||
describe('Community deploy policy hard-block (route)', () => {
|
||||
it('returns 409 and skips deployStack for a local Community session', async () => {
|
||||
const res = await request(app)
|
||||
.post('/api/stacks/community-block-app/deploy')
|
||||
.set('Cookie', authCookie);
|
||||
|
||||
expectPolicyHardBlock(res);
|
||||
});
|
||||
|
||||
it('returns 409 and skips deployStack for trusted node_proxy Community tier', async () => {
|
||||
const token = jwt.sign({ scope: 'node_proxy' }, TEST_JWT_SECRET, { expiresIn: '1m' });
|
||||
|
||||
const res = await request(app)
|
||||
.post('/api/stacks/community-block-proxy/deploy')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.set(PROXY_TIER_HEADER, 'community');
|
||||
|
||||
expectPolicyHardBlock(res);
|
||||
});
|
||||
});
|
||||
@@ -1216,12 +1216,10 @@ describe('GitSourceService.apply', () => {
|
||||
const validateSpy = vi.spyOn(svc, 'validateCompose').mockResolvedValue({ ok: true });
|
||||
const { FileSystemService } = await import('../services/FileSystemService');
|
||||
const { ComposeService } = await import('../services/ComposeService');
|
||||
const { LicenseService } = await import('../services/LicenseService');
|
||||
const TrivyService = (await import('../services/TrivyService')).default;
|
||||
const saveSpy = vi.spyOn(FileSystemService.prototype, 'saveStackContent').mockResolvedValue();
|
||||
const listImagesSpy = vi.spyOn(ComposeService.prototype, 'listStackImages').mockResolvedValue(['nginx:bad']);
|
||||
const deploySpy = vi.spyOn(ComposeService.prototype, 'deployStack').mockResolvedValue();
|
||||
const tierSpy = vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('paid');
|
||||
const trivy = TrivyService.getInstance();
|
||||
const trivyAvailableSpy = vi.spyOn(trivy, 'isTrivyAvailable').mockReturnValue(true);
|
||||
const scanSpy = vi.spyOn(trivy, 'scanImagePreflight').mockResolvedValue({
|
||||
@@ -1267,13 +1265,13 @@ describe('GitSourceService.apply', () => {
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.deployed).toBe(false);
|
||||
expect(result.deployError).toContain('Policy "block-high" blocked deploy');
|
||||
expect(scanSpy).toHaveBeenCalled();
|
||||
expect(deploySpy).not.toHaveBeenCalled();
|
||||
|
||||
validateSpy.mockRestore();
|
||||
saveSpy.mockRestore();
|
||||
listImagesSpy.mockRestore();
|
||||
deploySpy.mockRestore();
|
||||
tierSpy.mockRestore();
|
||||
trivyAvailableSpy.mockRestore();
|
||||
scanSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -175,21 +175,6 @@ describe('enforcePolicyPreDeploy', () => {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user