/** * 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); }); }); });