test: add comprehensive docker-security test suite (41 tests)
This commit is contained in:
@@ -2,414 +2,273 @@
|
||||
* 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.
|
||||
* 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');
|
||||
|
||||
// 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');
|
||||
// Test config file path
|
||||
const TEST_CONFIG_FILE = path.join(__dirname, '../docker-security-config.test.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);
|
||||
// Clean up test config
|
||||
if (fs.existsSync(TEST_CONFIG_FILE)) {
|
||||
fs.unlinkSync(TEST_CONFIG_FILE);
|
||||
}
|
||||
|
||||
// Clear module cache and reload
|
||||
delete require.cache[require.resolve('../docker-security')];
|
||||
// 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');
|
||||
|
||||
// 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;
|
||||
if (fs.existsSync(TEST_CONFIG_FILE)) {
|
||||
fs.unlinkSync(TEST_CONFIG_FILE);
|
||||
}
|
||||
});
|
||||
|
||||
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 load default config when file does not exist', () => {
|
||||
const status = dockerSecurity.getStatus();
|
||||
expect(status.mode).toBe('verify');
|
||||
expect(status.trustedImagesCount).toBe(0);
|
||||
});
|
||||
|
||||
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 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', () => {
|
||||
// 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();
|
||||
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 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');
|
||||
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 new trusted digest', () => {
|
||||
dockerSecurity.setTrustedDigest('postgres:14', 'sha256:newdigest');
|
||||
|
||||
expect(dockerSecurity.config.trustedDigests['postgres:14']).toBe('sha256:newdigest');
|
||||
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('nginx:latest', 'sha256:original');
|
||||
dockerSecurity.setTrustedDigest('nginx:latest', 'sha256:updated');
|
||||
|
||||
expect(dockerSecurity.config.trustedDigests['nginx:latest']).toBe('sha256:updated');
|
||||
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('nginx:latest', 'sha256:test');
|
||||
expect(dockerSecurity.config.trustedDigests['nginx:latest']).toBeDefined();
|
||||
|
||||
dockerSecurity.removeTrustedDigest('nginx:latest');
|
||||
expect(dockerSecurity.config.trustedDigests['nginx:latest']).toBeUndefined();
|
||||
});
|
||||
dockerSecurity.setTrustedDigest('postgres:15', 'sha256:trusted123');
|
||||
dockerSecurity.removeTrustedDigest('postgres:15');
|
||||
|
||||
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);
|
||||
expect(digests['postgres:15']).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return copy of trusted digests (not reference)', () => {
|
||||
dockerSecurity.setTrustedDigest('original', 'sha256:original');
|
||||
|
||||
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();
|
||||
digests['modified'] = 'sha256:modified';
|
||||
|
||||
expect(dockerSecurity.config.trustedDigests['modified']).toBeUndefined();
|
||||
expect(dockerSecurity.config.trustedDigests['original']).toBe('sha256:original');
|
||||
expect(Object.keys(digests)).toHaveLength(2);
|
||||
expect(digests['mysql:8']).toBe('sha256:mysql123');
|
||||
expect(digests['redis:alpine']).toBe('sha256:redis456');
|
||||
});
|
||||
|
||||
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 removal of non-existent digest', () => {
|
||||
dockerSecurity.removeTrustedDigest('nonexistent:latest');
|
||||
|
||||
const digests = dockerSecurity.getTrustedDigests();
|
||||
expect(digests['nonexistent:latest']).toBeUndefined();
|
||||
});
|
||||
|
||||
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');
|
||||
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('Security Modes', () => {
|
||||
describe('Verification Modes', () => {
|
||||
test('should set mode to strict', () => {
|
||||
dockerSecurity.setMode('strict');
|
||||
|
||||
expect(dockerSecurity.mode).toBe('strict');
|
||||
expect(dockerSecurity.config.verificationMode).toBe('strict');
|
||||
const status = dockerSecurity.getStatus();
|
||||
expect(status.mode).toBe('strict');
|
||||
});
|
||||
|
||||
test('should set mode to verify', () => {
|
||||
dockerSecurity.setMode('verify');
|
||||
|
||||
expect(dockerSecurity.mode).toBe('verify');
|
||||
expect(dockerSecurity.config.verificationMode).toBe('verify');
|
||||
const status = dockerSecurity.getStatus();
|
||||
expect(status.mode).toBe('verify');
|
||||
});
|
||||
|
||||
test('should set mode to permissive', () => {
|
||||
dockerSecurity.setMode('permissive');
|
||||
|
||||
expect(dockerSecurity.mode).toBe('permissive');
|
||||
expect(dockerSecurity.config.verificationMode).toBe('permissive');
|
||||
const status = dockerSecurity.getStatus();
|
||||
expect(status.mode).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');
|
||||
expect(() => dockerSecurity.setMode('invalid'))
|
||||
.toThrow('Invalid mode');
|
||||
});
|
||||
|
||||
test('should persist mode change to config file', () => {
|
||||
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 savedData = fs.readFileSync(TEST_CONFIG_PATH, 'utf8');
|
||||
const parsed = JSON.parse(savedData);
|
||||
expect(parsed.verificationMode).toBe('strict');
|
||||
|
||||
const savedConfig = JSON.parse(fs.readFileSync(TEST_CONFIG_FILE, 'utf8'));
|
||||
expect(savedConfig.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', () => {
|
||||
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('Edge Cases & Security Boundaries', () => {
|
||||
test('should handle empty digest string in strict mode', async () => {
|
||||
describe('Digest Verification Logic - Strict Mode', () => {
|
||||
beforeEach(() => {
|
||||
dockerSecurity.setMode('strict');
|
||||
dockerSecurity.setTrustedDigest('test:latest', 'sha256:trusted');
|
||||
|
||||
const result = await dockerSecurity.verifyImageDigest('test:latest', '');
|
||||
|
||||
});
|
||||
|
||||
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 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 () => {
|
||||
test('should accept image with matching digest', 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');
|
||||
|
||||
const result = await dockerSecurity.verifyImageDigest(
|
||||
'nginx:latest',
|
||||
'sha256:trusted123'
|
||||
);
|
||||
|
||||
expect(result.verified).toBe(true);
|
||||
expect(result.action).toBe('accept');
|
||||
});
|
||||
|
||||
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 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 handle removal of non-existent digest', () => {
|
||||
expect(() => {
|
||||
dockerSecurity.removeTrustedDigest('nonexistent:latest');
|
||||
}).not.toThrow();
|
||||
});
|
||||
test('should include all relevant fields in verification result', async () => {
|
||||
dockerSecurity.setTrustedDigest('redis:alpine', 'sha256:expected999');
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
const result = await dockerSecurity.verifyImageDigest(
|
||||
'redis:alpine',
|
||||
'sha256:actual888'
|
||||
);
|
||||
|
||||
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');
|
||||
@@ -418,21 +277,222 @@ describe('DockerSecurity Module', () => {
|
||||
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();
|
||||
describe('Digest Verification Logic - Verify Mode', () => {
|
||||
beforeEach(() => {
|
||||
dockerSecurity.setMode('verify');
|
||||
// Disable auto-update for predictable tests
|
||||
dockerSecurity.config.updateTrustedOnPull = false;
|
||||
});
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user