Files
flipstone-radar/server/security.test.ts

198 lines
6.3 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Config } from './config';
// Mock the config and server modules
const mockConfig: Config = {
github: {
token: 'ghp_super_secret_token_123',
repositories: [
{ owner: 'test-owner', name: 'test-repo' },
{ owner: 'another-owner', name: 'another-repo' }
]
},
server: {
port: 3001,
host: '0.0.0.0'
},
cache: {
timeoutSeconds: 300
}
};
// Import the getPublicConfig function (we'll need to export it for testing)
// For now, let's simulate what the public config should look like
function getPublicConfig(config: Config) {
return {
repositories: config.github.repositories.map((repo) => ({
owner: repo.owner,
name: repo.name,
full_name: `${repo.owner}/${repo.name}`
})),
cache: {
timeoutSeconds: config.cache?.timeoutSeconds || 300
},
repositoryCount: config.github.repositories.length
};
}
describe('Security Tests', () => {
describe('Config API Security', () => {
it('should not expose sensitive information in public config', () => {
const publicConfig = getPublicConfig(mockConfig);
// Ensure sensitive data is not included
expect(publicConfig).not.toHaveProperty('github');
expect(publicConfig).not.toHaveProperty('server');
expect(publicConfig).not.toHaveProperty('token');
// Ensure no token is accidentally included anywhere
const configString = JSON.stringify(publicConfig);
expect(configString).not.toContain('ghp_');
expect(configString).not.toContain('token');
expect(configString).not.toContain('secret');
});
it('should only expose safe repository information', () => {
const publicConfig = getPublicConfig(mockConfig);
expect(publicConfig.repositories).toHaveLength(2);
expect(publicConfig.repositories[0]).toEqual({
owner: 'test-owner',
name: 'test-repo',
full_name: 'test-owner/test-repo'
});
expect(publicConfig.repositories[1]).toEqual({
owner: 'another-owner',
name: 'another-repo',
full_name: 'another-owner/another-repo'
});
});
it('should expose safe cache configuration', () => {
const publicConfig = getPublicConfig(mockConfig);
expect(publicConfig.cache).toEqual({
timeoutSeconds: 300
});
});
it('should include repository count for UI', () => {
const publicConfig = getPublicConfig(mockConfig);
expect(publicConfig.repositoryCount).toBe(2);
});
it('should handle missing cache configuration safely', () => {
const configWithoutCache = {
...mockConfig,
cache: undefined
};
const publicConfig = getPublicConfig(configWithoutCache);
expect(publicConfig.cache).toEqual({
timeoutSeconds: 300
});
});
it('should never expose server configuration', () => {
const publicConfig = getPublicConfig(mockConfig);
expect(publicConfig).not.toHaveProperty('port');
expect(publicConfig).not.toHaveProperty('host');
expect(publicConfig).not.toHaveProperty('server');
});
it('should never expose GitHub token', () => {
const publicConfig = getPublicConfig(mockConfig);
// Check that token is never exposed in any form
const configString = JSON.stringify(publicConfig);
expect(configString).not.toContain('ghp_super_secret_token_123');
expect(configString).not.toContain('token');
});
it('should handle repository with potential sensitive data', () => {
const configWithSensitiveRepo = {
...mockConfig,
github: {
...mockConfig.github,
repositories: [
{
owner: 'test-owner',
name: 'test-repo',
// @ts-ignore - testing potential sensitive data
token: 'per-repo-token-123'
}
]
}
};
const publicConfig = getPublicConfig(configWithSensitiveRepo);
expect(publicConfig.repositories[0]).toEqual({
owner: 'test-owner',
name: 'test-repo',
full_name: 'test-owner/test-repo'
});
// Ensure repository-specific sensitive data is not exposed
expect(publicConfig.repositories[0]).not.toHaveProperty('token');
});
});
describe('Safe Logging', () => {
it('should log config changes without sensitive data', () => {
const consoleSpy = vi.spyOn(console, 'log');
// Simulate the logConfigChange function
function logConfigChange(config: Config) {
console.log(`📊 Configuration loaded: ${config.github.repositories.length} repositories`);
console.log(`💾 Cache timeout: ${config.cache?.timeoutSeconds || 300} seconds`);
}
logConfigChange(mockConfig);
const logCalls = consoleSpy.mock.calls.flat();
const allLogs = logCalls.join(' ');
// Ensure no sensitive data is logged
expect(allLogs).not.toContain('ghp_super_secret_token_123');
expect(allLogs).not.toContain('token');
expect(allLogs).not.toContain('3001'); // port
expect(allLogs).not.toContain('0.0.0.0'); // host
// Ensure safe data is logged
expect(allLogs).toContain('2 repositories');
expect(allLogs).toContain('300 seconds');
consoleSpy.mockRestore();
});
});
describe('Data Sanitization', () => {
it('should sanitize any potential sensitive data in repository names', () => {
const configWithSensitiveNames = {
...mockConfig,
github: {
...mockConfig.github,
repositories: [
{ owner: 'test-owner', name: 'repo-with-token-ghp123' },
{ owner: 'user', name: 'secret-project' }
]
}
};
const publicConfig = getPublicConfig(configWithSensitiveNames);
// Repository names should be preserved as-is (they're not sensitive themselves)
// but we should ensure the filtering process doesn't accidentally expose other data
expect(publicConfig.repositories[0].name).toBe('repo-with-token-ghp123');
expect(publicConfig.repositories[1].name).toBe('secret-project');
// But the actual token should never be exposed
const configString = JSON.stringify(publicConfig);
expect(configString).not.toContain('ghp_super_secret_token_123');
});
});
});