Files
dashcaddy/dashcaddy-api/__tests__/docker-security.test.js

499 lines
16 KiB
JavaScript

/**
* Docker Security Module Tests
* Tests for image digest verification, security modes, and trusted digest management
*
* Note: Tests that call getImageDigest() require a real Docker daemon running.
* These are marked with .skip() and should be run as integration tests separately.
*/
const fs = require('fs');
const path = require('path');
// Test config file path
const TEST_CONFIG_FILE = path.join(__dirname, '../docker-security-config.test.json');
describe('DockerSecurity Module', () => {
let dockerSecurity;
beforeEach(() => {
// Clean up test config
if (fs.existsSync(TEST_CONFIG_FILE)) {
fs.unlinkSync(TEST_CONFIG_FILE);
}
// Set test environment
process.env.DOCKER_SECURITY_CONFIG = TEST_CONFIG_FILE;
process.env.DOCKER_VERIFICATION_MODE = 'verify';
// Reset modules to get fresh instance
jest.resetModules();
dockerSecurity = require('../docker-security');
});
afterEach(() => {
// Clean up test config
if (fs.existsSync(TEST_CONFIG_FILE)) {
fs.unlinkSync(TEST_CONFIG_FILE);
}
});
describe('Configuration Management', () => {
test('should load default config when file does not exist', () => {
const status = dockerSecurity.getStatus();
expect(status.mode).toBe('verify');
expect(status.trustedImagesCount).toBe(0);
});
test('should load existing config file', () => {
const testConfig = {
trustedDigests: {
'nginx:latest': 'sha256:abc123'
},
verificationMode: 'strict',
allowUnverified: false,
updateTrustedOnPull: false
};
fs.writeFileSync(TEST_CONFIG_FILE, JSON.stringify(testConfig));
// Force module reload
jest.resetModules();
const freshInstance = require('../docker-security');
const status = freshInstance.getStatus();
expect(status.trustedImagesCount).toBe(1);
});
test('should save config to disk', () => {
dockerSecurity.setTrustedDigest('redis:alpine', 'sha256:def456');
expect(fs.existsSync(TEST_CONFIG_FILE)).toBe(true);
const savedConfig = JSON.parse(fs.readFileSync(TEST_CONFIG_FILE, 'utf8'));
expect(savedConfig.trustedDigests['redis:alpine']).toBe('sha256:def456');
});
test('should handle corrupted config file gracefully', () => {
fs.writeFileSync(TEST_CONFIG_FILE, 'INVALID JSON{{{');
jest.resetModules();
const freshInstance = require('../docker-security');
const status = freshInstance.getStatus();
// Should fall back to default config
expect(status.trustedImagesCount).toBe(0);
});
test('should handle missing config file directory', () => {
// Use a non-existent directory
process.env.DOCKER_SECURITY_CONFIG = '/nonexistent/path/config.json';
jest.resetModules();
const freshInstance = require('../docker-security');
const status = freshInstance.getStatus();
// Should fall back to default config
expect(status.mode).toBe('verify');
expect(status.trustedImagesCount).toBe(0);
});
});
describe('Trusted Digest Management', () => {
test('should add trusted digest', () => {
dockerSecurity.setTrustedDigest('postgres:15', 'sha256:trusted123');
const digests = dockerSecurity.getTrustedDigests();
expect(digests['postgres:15']).toBe('sha256:trusted123');
});
test('should update existing trusted digest', () => {
dockerSecurity.setTrustedDigest('postgres:15', 'sha256:old123');
dockerSecurity.setTrustedDigest('postgres:15', 'sha256:new456');
const digests = dockerSecurity.getTrustedDigests();
expect(digests['postgres:15']).toBe('sha256:new456');
});
test('should remove trusted digest', () => {
dockerSecurity.setTrustedDigest('postgres:15', 'sha256:trusted123');
dockerSecurity.removeTrustedDigest('postgres:15');
const digests = dockerSecurity.getTrustedDigests();
expect(digests['postgres:15']).toBeUndefined();
});
test('should return copy of trusted digests (immutable)', () => {
dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:abc123');
const digests1 = dockerSecurity.getTrustedDigests();
const digests2 = dockerSecurity.getTrustedDigests();
// Modify copy
digests1['nginx:latest'] = 'sha256:modified';
// Original should be unchanged
expect(digests2['nginx:latest']).toBe('sha256:abc123');
});
test('should persist trusted digests across operations', () => {
dockerSecurity.setTrustedDigest('mysql:8', 'sha256:mysql123');
dockerSecurity.setTrustedDigest('redis:alpine', 'sha256:redis456');
const digests = dockerSecurity.getTrustedDigests();
expect(Object.keys(digests)).toHaveLength(2);
expect(digests['mysql:8']).toBe('sha256:mysql123');
expect(digests['redis:alpine']).toBe('sha256:redis456');
});
test('should handle removal of non-existent digest', () => {
dockerSecurity.removeTrustedDigest('nonexistent:latest');
const digests = dockerSecurity.getTrustedDigests();
expect(digests['nonexistent:latest']).toBeUndefined();
});
test('should handle multiple removals', () => {
dockerSecurity.setTrustedDigest('img1:latest', 'sha256:aaa111');
dockerSecurity.setTrustedDigest('img2:latest', 'sha256:bbb222');
dockerSecurity.setTrustedDigest('img3:latest', 'sha256:ccc333');
dockerSecurity.removeTrustedDigest('img1:latest');
dockerSecurity.removeTrustedDigest('img3:latest');
const digests = dockerSecurity.getTrustedDigests();
expect(Object.keys(digests)).toHaveLength(1);
expect(digests['img2:latest']).toBe('sha256:bbb222');
});
});
describe('Verification Modes', () => {
test('should set mode to strict', () => {
dockerSecurity.setMode('strict');
const status = dockerSecurity.getStatus();
expect(status.mode).toBe('strict');
});
test('should set mode to verify', () => {
dockerSecurity.setMode('verify');
const status = dockerSecurity.getStatus();
expect(status.mode).toBe('verify');
});
test('should set mode to permissive', () => {
dockerSecurity.setMode('permissive');
const status = dockerSecurity.getStatus();
expect(status.mode).toBe('permissive');
});
test('should reject invalid mode', () => {
expect(() => dockerSecurity.setMode('invalid'))
.toThrow('Invalid mode');
});
test('should reject empty mode string', () => {
expect(() => dockerSecurity.setMode(''))
.toThrow('Invalid mode');
});
test('should reject null mode', () => {
expect(() => dockerSecurity.setMode(null))
.toThrow('Invalid mode');
});
test('should persist mode changes to config', () => {
dockerSecurity.setMode('strict');
const savedConfig = JSON.parse(fs.readFileSync(TEST_CONFIG_FILE, 'utf8'));
expect(savedConfig.verificationMode).toBe('strict');
});
test('should allow mode changes multiple times', () => {
dockerSecurity.setMode('strict');
expect(dockerSecurity.getStatus().mode).toBe('strict');
dockerSecurity.setMode('permissive');
expect(dockerSecurity.getStatus().mode).toBe('permissive');
dockerSecurity.setMode('verify');
expect(dockerSecurity.getStatus().mode).toBe('verify');
});
});
describe('Digest Verification Logic - Strict Mode', () => {
beforeEach(() => {
dockerSecurity.setMode('strict');
});
test('should reject image with no trusted digest in strict mode', async () => {
const result = await dockerSecurity.verifyImageDigest(
'nginx:latest',
'sha256:actual123'
);
expect(result.verified).toBe(false);
expect(result.action).toBe('reject');
expect(result.reason).toContain('strict mode');
});
test('should accept image with matching digest', async () => {
dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123');
const result = await dockerSecurity.verifyImageDigest(
'nginx:latest',
'sha256:trusted123'
);
expect(result.verified).toBe(true);
expect(result.action).toBe('accept');
});
test('should reject image with mismatched digest in strict mode', async () => {
dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123');
const result = await dockerSecurity.verifyImageDigest(
'nginx:latest',
'sha256:different456'
);
expect(result.verified).toBe(false);
expect(result.action).toBe('reject');
expect(result.actualDigest).toBe('sha256:different456');
expect(result.trustedDigest).toBe('sha256:trusted123');
});
test('should include all relevant fields in verification result', async () => {
dockerSecurity.setTrustedDigest('redis:alpine', 'sha256:expected999');
const result = await dockerSecurity.verifyImageDigest(
'redis:alpine',
'sha256:actual888'
);
expect(result).toHaveProperty('verified');
expect(result).toHaveProperty('mode');
expect(result).toHaveProperty('imageName');
expect(result).toHaveProperty('actualDigest');
expect(result).toHaveProperty('trustedDigest');
expect(result).toHaveProperty('action');
expect(result).toHaveProperty('reason');
});
});
describe('Digest Verification Logic - Verify Mode', () => {
beforeEach(() => {
dockerSecurity.setMode('verify');
// Disable auto-update for predictable tests
dockerSecurity.config.updateTrustedOnPull = false;
});
test('should warn on digest mismatch in verify mode', async () => {
dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123');
const result = await dockerSecurity.verifyImageDigest(
'nginx:latest',
'sha256:different456'
);
expect(result.verified).toBe(false);
expect(result.action).toBe('warn');
expect(result.reason).toContain('verify mode');
});
test('should accept image with no trusted digest in verify mode', async () => {
const result = await dockerSecurity.verifyImageDigest(
'redis:alpine',
'sha256:actual123'
);
expect(result.verified).toBe(true);
expect(result.action).toBe('accept');
});
test('should accept matching digests', async () => {
dockerSecurity.setTrustedDigest('postgres:15', 'sha256:match777');
const result = await dockerSecurity.verifyImageDigest(
'postgres:15',
'sha256:match777'
);
expect(result.verified).toBe(true);
expect(result.action).toBe('accept');
});
});
describe('Digest Verification Logic - Permissive Mode', () => {
beforeEach(() => {
dockerSecurity.setMode('permissive');
dockerSecurity.config.updateTrustedOnPull = false;
});
test('should accept image with mismatched digest in permissive mode', async () => {
dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123');
const result = await dockerSecurity.verifyImageDigest(
'nginx:latest',
'sha256:different456'
);
expect(result.verified).toBe(true);
expect(result.action).toBe('accept');
expect(result.reason).toContain('permissive mode');
});
test('should accept any image without trusted digest', async () => {
const result = await dockerSecurity.verifyImageDigest(
'unknown:latest',
'sha256:anything123'
);
expect(result.verified).toBe(true);
expect(result.action).toBe('accept');
});
test('should accept matching digests', async () => {
dockerSecurity.setTrustedDigest('mysql:8', 'sha256:match555');
const result = await dockerSecurity.verifyImageDigest(
'mysql:8',
'sha256:match555'
);
expect(result.verified).toBe(true);
expect(result.action).toBe('accept');
});
});
describe('Auto-Update Trusted Digests', () => {
test('should auto-add trusted digest on first pull', async () => {
dockerSecurity.config.updateTrustedOnPull = true;
const result = await dockerSecurity.verifyImageDigest(
'newimage:latest',
'sha256:first123'
);
expect(result.verified).toBe(true);
const digests = dockerSecurity.getTrustedDigests();
expect(digests['newimage:latest']).toBe('sha256:first123');
});
test('should not auto-update when disabled', async () => {
dockerSecurity.config.updateTrustedOnPull = false;
await dockerSecurity.verifyImageDigest(
'newimage:latest',
'sha256:first123'
);
const digests = dockerSecurity.getTrustedDigests();
expect(digests['newimage:latest']).toBeUndefined();
});
test('should not overwrite existing trusted digest', async () => {
dockerSecurity.config.updateTrustedOnPull = true;
dockerSecurity.setTrustedDigest('existing:latest', 'sha256:original888');
await dockerSecurity.verifyImageDigest(
'existing:latest',
'sha256:new999'
);
const digests = dockerSecurity.getTrustedDigests();
expect(digests['existing:latest']).toBe('sha256:original888');
});
});
describe('Status Reporting', () => {
test('should return correct status', () => {
dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:abc123');
dockerSecurity.setTrustedDigest('redis:alpine', 'sha256:def456');
dockerSecurity.setMode('strict');
const status = dockerSecurity.getStatus();
expect(status.mode).toBe('strict');
expect(status.trustedImagesCount).toBe(2);
expect(status.configFile).toBe(TEST_CONFIG_FILE);
});
test('should report updateTrustedOnPull setting', () => {
dockerSecurity.config.updateTrustedOnPull = true;
const status = dockerSecurity.getStatus();
expect(status.updateTrustedOnPull).toBe(true);
});
test('should reflect config changes in status', () => {
dockerSecurity.setMode('permissive');
dockerSecurity.setTrustedDigest('img1:latest', 'sha256:aaa');
dockerSecurity.setTrustedDigest('img2:latest', 'sha256:bbb');
dockerSecurity.setTrustedDigest('img3:latest', 'sha256:ccc');
const status = dockerSecurity.getStatus();
expect(status.mode).toBe('permissive');
expect(status.trustedImagesCount).toBe(3);
});
});
describe('Edge Cases', () => {
test('should handle concurrent digest updates', () => {
dockerSecurity.setTrustedDigest('image1:latest', 'sha256:aaa111');
dockerSecurity.setTrustedDigest('image2:latest', 'sha256:bbb222');
dockerSecurity.setTrustedDigest('image3:latest', 'sha256:ccc333');
const digests = dockerSecurity.getTrustedDigests();
expect(digests['image1:latest']).toBe('sha256:aaa111');
expect(digests['image2:latest']).toBe('sha256:bbb222');
expect(digests['image3:latest']).toBe('sha256:ccc333');
});
test('should handle empty digest string', async () => {
const result = await dockerSecurity.verifyImageDigest(
'nginx:latest',
''
);
expect(result.verified).toBe(true); // Permissive by default
});
test('should handle very long image names', async () => {
const longImageName = 'registry.example.com/namespace/project/subproject/image:v1.2.3-beta-20261231';
dockerSecurity.setTrustedDigest(longImageName, 'sha256:abc123');
const result = await dockerSecurity.verifyImageDigest(
longImageName,
'sha256:abc123'
);
expect(result.verified).toBe(true);
expect(result.imageName).toBe(longImageName);
});
test('should handle digest verification with null digest', async () => {
const result = await dockerSecurity.verifyImageDigest(
'nginx:latest',
null
);
// Null digest should be accepted in permissive mode (default)
expect(result.action).toBe('accept');
});
test('should handle image name with multiple colons', async () => {
dockerSecurity.setTrustedDigest('registry.io:5000/app:v1', 'sha256:xyz789');
const result = await dockerSecurity.verifyImageDigest(
'registry.io:5000/app:v1',
'sha256:xyz789'
);
expect(result.verified).toBe(true);
});
});
});