/** * Docker Security Module Tests * Tests for image digest verification, security modes, and trusted digest management * * Note: These tests focus on the security logic and configuration management. * Docker API integration tests are handled separately. */ const fs = require('fs'); const path = require('path'); // We'll test the module with a temp config file to avoid affecting production const TEST_CONFIG_PATH = path.join(__dirname, 'test-docker-security-config.json'); describe('DockerSecurity Module', () => { let dockerSecurity; const originalEnv = process.env.DOCKER_SECURITY_CONFIG; beforeAll(() => { // Point to test config file process.env.DOCKER_SECURITY_CONFIG = TEST_CONFIG_PATH; }); beforeEach(() => { // Clean up test config before each test if (fs.existsSync(TEST_CONFIG_PATH)) { fs.unlinkSync(TEST_CONFIG_PATH); } // Clear module cache and reload delete require.cache[require.resolve('../docker-security')]; dockerSecurity = require('../docker-security'); // Clear any existing trusted digests dockerSecurity.config.trustedDigests = {}; dockerSecurity.config.verificationMode = 'verify'; dockerSecurity.mode = 'verify'; dockerSecurity.saveConfig(); }); afterEach(() => { // Clean up test config if (fs.existsSync(TEST_CONFIG_PATH)) { fs.unlinkSync(TEST_CONFIG_PATH); } }); afterAll(() => { // Restore original env if (originalEnv) { process.env.DOCKER_SECURITY_CONFIG = originalEnv; } else { delete process.env.DOCKER_SECURITY_CONFIG; } }); describe('Configuration Management', () => { test('should create default config when file does not exist', () => { expect(dockerSecurity.config).toBeDefined(); expect(dockerSecurity.config).toHaveProperty('trustedDigests'); expect(dockerSecurity.config).toHaveProperty('verificationMode'); expect(dockerSecurity.config).toHaveProperty('allowUnverified'); expect(dockerSecurity.config).toHaveProperty('updateTrustedOnPull'); }); test('should save and load config correctly', () => { // Add a trusted digest dockerSecurity.setTrustedDigest('test:v1', 'sha256:test123'); // Reload module delete require.cache[require.resolve('../docker-security')]; const reloaded = require('../docker-security'); // Should have loaded the saved config expect(reloaded.config.trustedDigests['test:v1']).toBe('sha256:test123'); }); test('should handle corrupted config file gracefully', () => { // Write corrupted JSON fs.writeFileSync(TEST_CONFIG_PATH, '{ invalid json'); // Reload - should not crash delete require.cache[require.resolve('../docker-security')]; const fresh = require('../docker-security'); expect(fresh.config).toBeDefined(); expect(fresh.config.trustedDigests).toBeDefined(); }); test('should persist changes to disk', () => { dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:abc123'); // Config file should exist expect(fs.existsSync(TEST_CONFIG_PATH)).toBe(true); // Should be valid JSON const savedData = fs.readFileSync(TEST_CONFIG_PATH, 'utf8'); const parsed = JSON.parse(savedData); expect(parsed.trustedDigests['nginx:latest']).toBe('sha256:abc123'); }); }); describe('Trusted Digest Management', () => { test('should add new trusted digest', () => { dockerSecurity.setTrustedDigest('postgres:14', 'sha256:newdigest'); expect(dockerSecurity.config.trustedDigests['postgres:14']).toBe('sha256:newdigest'); }); test('should update existing trusted digest', () => { dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:original'); dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:updated'); expect(dockerSecurity.config.trustedDigests['nginx:latest']).toBe('sha256:updated'); }); test('should remove trusted digest', () => { dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:test'); expect(dockerSecurity.config.trustedDigests['nginx:latest']).toBeDefined(); dockerSecurity.removeTrustedDigest('nginx:latest'); expect(dockerSecurity.config.trustedDigests['nginx:latest']).toBeUndefined(); }); test('should get all trusted digests', () => { dockerSecurity.setTrustedDigest('image1', 'sha256:digest1'); dockerSecurity.setTrustedDigest('image2', 'sha256:digest2'); const digests = dockerSecurity.getTrustedDigests(); expect(digests).toHaveProperty('image1'); expect(digests).toHaveProperty('image2'); expect(Object.keys(digests).length).toBeGreaterThanOrEqual(2); }); test('should return copy of trusted digests (not reference)', () => { dockerSecurity.setTrustedDigest('original', 'sha256:original'); const digests = dockerSecurity.getTrustedDigests(); digests['modified'] = 'sha256:modified'; expect(dockerSecurity.config.trustedDigests['modified']).toBeUndefined(); expect(dockerSecurity.config.trustedDigests['original']).toBe('sha256:original'); }); test('should handle image names with special characters', () => { const specialName = 'my-app_v2.0:latest'; dockerSecurity.setTrustedDigest(specialName, 'sha256:special'); expect(dockerSecurity.config.trustedDigests[specialName]).toBe('sha256:special'); }); test('should handle very long image names', () => { const longName = `registry.example.com/team/project/${ 'a'.repeat(100) }:v1.2.3`; dockerSecurity.setTrustedDigest(longName, 'sha256:long'); expect(dockerSecurity.config.trustedDigests[longName]).toBe('sha256:long'); }); }); describe('Security Modes', () => { test('should set mode to strict', () => { dockerSecurity.setMode('strict'); expect(dockerSecurity.mode).toBe('strict'); expect(dockerSecurity.config.verificationMode).toBe('strict'); }); test('should set mode to verify', () => { dockerSecurity.setMode('verify'); expect(dockerSecurity.mode).toBe('verify'); expect(dockerSecurity.config.verificationMode).toBe('verify'); }); test('should set mode to permissive', () => { dockerSecurity.setMode('permissive'); expect(dockerSecurity.mode).toBe('permissive'); expect(dockerSecurity.config.verificationMode).toBe('permissive'); }); test('should reject invalid mode', () => { expect(() => dockerSecurity.setMode('invalid')).toThrow('Invalid mode'); expect(() => dockerSecurity.setMode('STRICT')).toThrow('Invalid mode'); expect(() => dockerSecurity.setMode('')).toThrow('Invalid mode'); }); test('should persist mode change to config file', () => { dockerSecurity.setMode('strict'); const savedData = fs.readFileSync(TEST_CONFIG_PATH, 'utf8'); const parsed = JSON.parse(savedData); expect(parsed.verificationMode).toBe('strict'); }); test('should load mode from config on startup', () => { dockerSecurity.setMode('strict'); // Reload module delete require.cache[require.resolve('../docker-security')]; const reloaded = require('../docker-security'); expect(reloaded.mode).toBe('strict'); }); }); describe('Digest Verification Logic', () => { test('should accept 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'); expect(result.reason).toContain('matches trusted value'); }); test('should reject mismatched digest in strict mode', async () => { dockerSecurity.setMode('strict'); dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123'); const result = await dockerSecurity.verifyImageDigest('nginx:latest', 'sha256:different'); expect(result.verified).toBe(false); expect(result.action).toBe('reject'); expect(result.reason).toContain('mismatch'); }); test('should warn on mismatched digest in verify mode', async () => { dockerSecurity.setMode('verify'); dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123'); const result = await dockerSecurity.verifyImageDigest('nginx:latest', 'sha256:different'); expect(result.verified).toBe(false); expect(result.action).toBe('warn'); expect(result.reason).toContain('mismatch'); }); test('should accept mismatched digest in permissive mode', async () => { dockerSecurity.setMode('permissive'); dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123'); const result = await dockerSecurity.verifyImageDigest('nginx:latest', 'sha256:different'); expect(result.verified).toBe(true); expect(result.action).toBe('accept'); expect(result.reason).toContain('permissive mode'); }); test('should reject unknown image in strict mode', async () => { dockerSecurity.setMode('strict'); const result = await dockerSecurity.verifyImageDigest('unknown:latest', 'sha256:anything'); expect(result.verified).toBe(false); expect(result.action).toBe('reject'); expect(result.reason).toContain('No trusted digest'); }); test('should accept unknown image in verify mode', async () => { dockerSecurity.setMode('verify'); const result = await dockerSecurity.verifyImageDigest('unknown:latest', 'sha256:anything'); expect(result.verified).toBe(true); expect(result.action).toBe('accept'); }); test('should accept and auto-trust unknown image when updateTrustedOnPull enabled', async () => { dockerSecurity.setMode('permissive'); dockerSecurity.config.updateTrustedOnPull = true; const result = await dockerSecurity.verifyImageDigest('newimage:v1', 'sha256:new123'); expect(result.verified).toBe(true); expect(result.action).toBe('accept'); expect(dockerSecurity.config.trustedDigests['newimage:v1']).toBe('sha256:new123'); }); test('should not auto-trust when updateTrustedOnPull disabled', async () => { dockerSecurity.config.updateTrustedOnPull = false; const result = await dockerSecurity.verifyImageDigest('newimage:v2', 'sha256:new456'); expect(result.verified).toBe(true); expect(dockerSecurity.config.trustedDigests['newimage:v2']).toBeUndefined(); }); test('should match base image name when tag not in config', async () => { // Config has 'redis' (no tag), test 'redis:alpine' dockerSecurity.setTrustedDigest('redis', 'sha256:base'); const result = await dockerSecurity.verifyImageDigest('redis:alpine', 'sha256:base'); expect(result.verified).toBe(true); expect(result.action).toBe('accept'); }); test('should prefer specific tag over base name', async () => { dockerSecurity.setTrustedDigest('redis', 'sha256:base'); dockerSecurity.setTrustedDigest('redis:alpine', 'sha256:alpine'); const result = await dockerSecurity.verifyImageDigest('redis:alpine', 'sha256:alpine'); expect(result.verified).toBe(true); expect(result.trustedDigest).toBe('sha256:alpine'); }); }); describe('Status Reporting', () => { test('should return security status', () => { const status = dockerSecurity.getStatus(); expect(status).toHaveProperty('mode'); expect(status).toHaveProperty('trustedImagesCount'); expect(status).toHaveProperty('configFile'); expect(status).toHaveProperty('updateTrustedOnPull'); }); test('should count trusted images correctly', () => { const initialCount = dockerSecurity.getStatus().trustedImagesCount; dockerSecurity.setTrustedDigest('count-test-1', 'sha256:1'); dockerSecurity.setTrustedDigest('count-test-2', 'sha256:2'); dockerSecurity.setTrustedDigest('count-test-3', 'sha256:3'); const status = dockerSecurity.getStatus(); expect(status.trustedImagesCount).toBe(initialCount + 3); }); test('should reflect current mode', () => { dockerSecurity.setMode('strict'); expect(dockerSecurity.getStatus().mode).toBe('strict'); dockerSecurity.setMode('verify'); expect(dockerSecurity.getStatus().mode).toBe('verify'); }); }); describe('Edge Cases & Security Boundaries', () => { test('should handle empty digest string in strict mode', async () => { dockerSecurity.setMode('strict'); dockerSecurity.setTrustedDigest('test:latest', 'sha256:trusted'); const result = await dockerSecurity.verifyImageDigest('test:latest', ''); expect(result.verified).toBe(false); expect(result.action).toBe('reject'); }); test('should handle null/undefined digest in strict mode', async () => { dockerSecurity.setMode('strict'); dockerSecurity.setTrustedDigest('test:latest', 'sha256:trusted'); const result1 = await dockerSecurity.verifyImageDigest('test:latest', null); const result2 = await dockerSecurity.verifyImageDigest('test:latest', undefined); expect(result1.verified).toBe(false); expect(result2.verified).toBe(false); }); test('should not expose sensitive data in verification result', async () => { dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:trusted123'); const result = await dockerSecurity.verifyImageDigest('nginx:latest', 'sha256:trusted123'); // Should not contain file paths, internal state, etc const resultJson = JSON.stringify(result); expect(resultJson).not.toContain(TEST_CONFIG_PATH); expect(result).not.toHaveProperty('config'); expect(result).not.toHaveProperty('_internal'); }); test('should handle concurrent digest updates safely', () => { // Add multiple digests rapidly for (let i = 0; i < 10; i++) { dockerSecurity.setTrustedDigest(`image${i}`, `sha256:digest${i}`); } // All should be present for (let i = 0; i < 10; i++) { expect(dockerSecurity.config.trustedDigests[`image${i}`]).toBe(`sha256:digest${i}`); } }); test('should handle removal of non-existent digest', () => { expect(() => { dockerSecurity.removeTrustedDigest('nonexistent:latest'); }).not.toThrow(); }); test('should validate digest format (basic)', async () => { dockerSecurity.setMode('verify'); // These should all work (verification logic doesn't enforce sha256: prefix) const result1 = await dockerSecurity.verifyImageDigest('test:1', 'sha256:abc123'); const result2 = await dockerSecurity.verifyImageDigest('test:2', 'localid123'); expect(result1.actualDigest).toBe('sha256:abc123'); expect(result2.actualDigest).toBe('localid123'); }); }); describe('Verification Result Structure', () => { test('should include all expected fields in result', async () => { dockerSecurity.setTrustedDigest('test:v1', 'sha256:trusted'); const result = await dockerSecurity.verifyImageDigest('test:v1', 'sha256:trusted'); 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'); }); test('should set trustedDigest to null when not configured', async () => { const result = await dockerSecurity.verifyImageDigest('unknown:v1', 'sha256:test'); expect(result.trustedDigest).toBeNull(); }); test('should preserve imageName and actualDigest in result', async () => { const imageName = 'myapp:1.2.3'; const digest = 'sha256:abcdef123456'; const result = await dockerSecurity.verifyImageDigest(imageName, digest); expect(result.imageName).toBe(imageName); expect(result.actualDigest).toBe(digest); }); }); });