499 lines
16 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|