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

439 lines
16 KiB
JavaScript

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