Agent Police Department v1.0.0 MVP
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// tests/audit.test.js
|
||||
|
||||
require('../__mocks__/chrome.js');
|
||||
const { addEvent, addViolation, getStats, getAuditLog, getViolations, exportAuditLog, searchAuditLog, verifyIntegrity } = require('../lib/audit.js');
|
||||
|
||||
describe('Audit', () => {
|
||||
beforeEach(() => {
|
||||
global.__chromeStore = {};
|
||||
});
|
||||
|
||||
test('addEvent stores event', async () => {
|
||||
const event = { id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() };
|
||||
await addEvent(event);
|
||||
const log = await getAuditLog(10);
|
||||
expect(log).toHaveLength(1);
|
||||
expect(log[0].id).toBe('e1');
|
||||
});
|
||||
|
||||
test('addViolation stores violation', async () => {
|
||||
const violation = { id: 'v1', event_id: 'e1', detector: 'disallowed_tools', severity: 'critical', timestamp: new Date().toISOString() };
|
||||
await addViolation(violation);
|
||||
const list = await getViolations(10);
|
||||
expect(list).toHaveLength(1);
|
||||
expect(list[0].severity).toBe('critical');
|
||||
});
|
||||
|
||||
test('getStats returns correct counts', async () => {
|
||||
await addEvent({ id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() });
|
||||
await addEvent({ id: 'e2', type: 'api_call', timestamp: new Date().toISOString() });
|
||||
await addViolation({ id: 'v1', event_id: 'e1', detector: 'disallowed_tools', severity: 'high', timestamp: new Date().toISOString() });
|
||||
const stats = await getStats();
|
||||
expect(stats.totalEvents).toBe(2);
|
||||
expect(stats.totalViolations).toBe(1);
|
||||
expect(stats.severityCounts.high).toBe(1);
|
||||
});
|
||||
|
||||
test('exportAuditLog includes integrity hash', async () => {
|
||||
await addEvent({ id: 'e1', type: 'tool_use', timestamp: new Date().toISOString() });
|
||||
const exported = await exportAuditLog();
|
||||
expect(exported.totalEvents).toBe(1);
|
||||
expect(exported.integrity).toBeDefined();
|
||||
expect(exported.integrity.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('searchAuditLog filters by query', async () => {
|
||||
await addEvent({ id: 'e1', type: 'tool_use', tool: 'read_file', target: 'app.js', timestamp: new Date().toISOString() });
|
||||
await addEvent({ id: 'e2', type: 'tool_use', tool: 'edit_file', target: 'config.js', timestamp: new Date().toISOString() });
|
||||
const results = await searchAuditLog('config');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].target).toBe('config.js');
|
||||
});
|
||||
|
||||
test('verifyIntegrity returns valid for empty log', async () => {
|
||||
const result = await verifyIntegrity();
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.eventsChecked).toBe(0);
|
||||
});
|
||||
|
||||
test('max events limit enforced', async () => {
|
||||
for (let i = 0; i < 1100; i++) {
|
||||
await addEvent({ id: `e${i}`, type: 'tool_use', timestamp: new Date().toISOString() });
|
||||
}
|
||||
const log = await getAuditLog(2000);
|
||||
expect(log.length).toBeLessThanOrEqual(1000);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
// tests/detectors.test.js
|
||||
|
||||
require('../__mocks__/chrome.js');
|
||||
const { detectViolations, DETECTOR_NAMES } = require('../lib/detectors.js');
|
||||
|
||||
describe('Detectors', () => {
|
||||
beforeEach(() => {
|
||||
global.__chromeStore = {};
|
||||
});
|
||||
|
||||
test('DETECTOR_NAMES has 6 detectors', () => {
|
||||
expect(DETECTOR_NAMES).toHaveLength(6);
|
||||
expect(DETECTOR_NAMES).toContain('permission_bypass');
|
||||
expect(DETECTOR_NAMES).toContain('out_of_scope');
|
||||
expect(DETECTOR_NAMES).toContain('self_permissioning');
|
||||
expect(DETECTOR_NAMES).toContain('disallowed_tools');
|
||||
expect(DETECTOR_NAMES).toContain('redundant_operations');
|
||||
expect(DETECTOR_NAMES).toContain('off_task');
|
||||
});
|
||||
|
||||
test('detects permission bypass for disallowed domain', async () => {
|
||||
const event = {
|
||||
id: 'evt-1',
|
||||
type: 'api_call',
|
||||
tool: 'fetch',
|
||||
target: 'https://malicious.com/api',
|
||||
url: 'https://claude.ai/chat',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['github.com', 'claude.ai'] } }
|
||||
]
|
||||
};
|
||||
const violations = await detectViolations(event, policy);
|
||||
const bypass = violations.find(v => v.detector === 'permission_bypass');
|
||||
expect(bypass).toBeDefined();
|
||||
expect(bypass.severity).toBe('high');
|
||||
expect(bypass.message).toContain('malicious.com');
|
||||
});
|
||||
|
||||
test('allows permitted domain', async () => {
|
||||
const event = {
|
||||
id: 'evt-2',
|
||||
type: 'api_call',
|
||||
tool: 'fetch',
|
||||
target: 'https://github.com/api',
|
||||
url: 'https://claude.ai/chat',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } }
|
||||
]
|
||||
};
|
||||
const violations = await detectViolations(event, policy);
|
||||
const bypass = violations.find(v => v.detector === 'permission_bypass');
|
||||
expect(bypass).toBeUndefined();
|
||||
});
|
||||
|
||||
test('detects disallowed tools', async () => {
|
||||
const event = {
|
||||
id: 'evt-3',
|
||||
type: 'tool_use',
|
||||
tool: 'eval',
|
||||
target: 'some-script.js',
|
||||
url: 'https://claude.ai/chat',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule_disallowed_tools', type: 'disallowed_tools', enabled: true, params: { tools: ['eval', 'exec'] } }
|
||||
]
|
||||
};
|
||||
const violations = await detectViolations(event, policy);
|
||||
const disallowed = violations.find(v => v.detector === 'disallowed_tools');
|
||||
expect(disallowed).toBeDefined();
|
||||
expect(disallowed.severity).toBe('critical');
|
||||
expect(disallowed.message).toContain('eval');
|
||||
});
|
||||
|
||||
test('detects self-permissioning', async () => {
|
||||
const event = {
|
||||
id: 'evt-4',
|
||||
type: 'tool_use',
|
||||
tool: 'edit_file',
|
||||
target: 'permissions.json',
|
||||
context: 'grant admin access control role privilege',
|
||||
url: 'https://claude.ai/chat',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule_permission_change', type: 'permission_change', enabled: true, params: {} }
|
||||
]
|
||||
};
|
||||
const violations = await detectViolations(event, policy);
|
||||
const perm = violations.find(v => v.detector === 'self_permissioning');
|
||||
expect(perm).toBeDefined();
|
||||
expect(perm.severity).toBe('critical');
|
||||
});
|
||||
|
||||
test('detects out-of-scope operations', async () => {
|
||||
const event = {
|
||||
id: 'evt-5',
|
||||
type: 'api_call',
|
||||
tool: 'delete',
|
||||
target: 'https://api.example.com/resource',
|
||||
url: 'https://claude.ai/chat',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule_allowed_operations', type: 'allowed_operations', enabled: true, params: { operations: ['read', 'chat'] } }
|
||||
]
|
||||
};
|
||||
const violations = await detectViolations(event, policy);
|
||||
const scope = violations.find(v => v.detector === 'out_of_scope');
|
||||
expect(scope).toBeDefined();
|
||||
expect(scope.severity).toBe('medium');
|
||||
});
|
||||
|
||||
test('no violations for normal activity', async () => {
|
||||
const event = {
|
||||
id: 'evt-6',
|
||||
type: 'chat',
|
||||
tool: 'chat',
|
||||
target: 'conversation',
|
||||
url: 'https://claude.ai/chat',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule_allowed_domains', type: 'allowed_domains', enabled: true, params: { domains: ['claude.ai'] } },
|
||||
{ id: 'rule_disallowed_tools', type: 'disallowed_tools', enabled: true, params: { tools: ['eval'] } }
|
||||
]
|
||||
};
|
||||
const violations = await detectViolations(event, policy);
|
||||
expect(violations).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
// tests/hash.test.js
|
||||
|
||||
const { hashEvent, hashChain, sha256Async } = require('../lib/hash.js');
|
||||
|
||||
describe('Hash', () => {
|
||||
test('hashEvent returns consistent hash', () => {
|
||||
const event = { id: 'e1', type: 'tool_use' };
|
||||
const h1 = hashEvent(event);
|
||||
const h2 = hashEvent(event);
|
||||
expect(h1).toBe(h2);
|
||||
expect(h1.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('hashEvent returns different hash for different events', () => {
|
||||
const h1 = hashEvent({ id: 'e1', type: 'tool_use' });
|
||||
const h2 = hashEvent({ id: 'e2', type: 'api_call' });
|
||||
expect(h1).not.toBe(h2);
|
||||
});
|
||||
|
||||
test('hashChain returns empty for empty array', () => {
|
||||
expect(hashChain([])).toBe('');
|
||||
});
|
||||
|
||||
test('hashChain returns hash for events', () => {
|
||||
const chain = hashChain([{ id: 'e1' }, { id: 'e2' }]);
|
||||
expect(chain.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('hashChain is order-sensitive', () => {
|
||||
const chain1 = hashChain([{ id: 'e1' }, { id: 'e2' }]);
|
||||
const chain2 = hashChain([{ id: 'e2' }, { id: 'e1' }]);
|
||||
expect(chain1).not.toBe(chain2);
|
||||
});
|
||||
|
||||
test('sha256Async returns hex string', async () => {
|
||||
const hash = await sha256Async('hello');
|
||||
expect(hash).toMatch(/^[a-f0-9]{64}$/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
// tests/policy.test.js
|
||||
|
||||
require('../__mocks__/chrome.js');
|
||||
const { loadPolicy, savePolicy, checkPolicy } = require('../lib/policy.js');
|
||||
|
||||
describe('Policy', () => {
|
||||
beforeEach(() => {
|
||||
global.__chromeStore = {};
|
||||
});
|
||||
|
||||
test('loadPolicy returns default policy when none stored', async () => {
|
||||
const policy = await loadPolicy();
|
||||
expect(policy).toBeDefined();
|
||||
expect(policy.version).toBe(1);
|
||||
expect(policy.rules).toHaveLength(6);
|
||||
expect(policy.rules[0].type).toBe('allowed_domains');
|
||||
});
|
||||
|
||||
test('savePolicy increments version', async () => {
|
||||
const policy = await loadPolicy();
|
||||
const saved = await savePolicy(policy);
|
||||
expect(saved.version).toBe(2);
|
||||
});
|
||||
|
||||
test('checkPolicy allows permitted domains', () => {
|
||||
const event = {
|
||||
type: 'api_call',
|
||||
tool: 'fetch',
|
||||
target: 'https://github.com/api',
|
||||
url: 'https://claude.ai/chat'
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule1', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } }
|
||||
]
|
||||
};
|
||||
const result = checkPolicy(event, policy);
|
||||
expect(result.outcome).toBe('allowed');
|
||||
expect(result.severity).toBe('info');
|
||||
});
|
||||
|
||||
test('checkPolicy flags disallowed domains', () => {
|
||||
const event = {
|
||||
type: 'api_call',
|
||||
tool: 'fetch',
|
||||
target: 'https://evil.com/api',
|
||||
url: 'https://claude.ai/chat'
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule1', type: 'allowed_domains', enabled: true, params: { domains: ['github.com'] } }
|
||||
]
|
||||
};
|
||||
const result = checkPolicy(event, policy);
|
||||
expect(result.outcome).toBe('violation');
|
||||
expect(result.severity).toBe('high');
|
||||
expect(result.ruleId).toBe('rule1');
|
||||
});
|
||||
|
||||
test('checkPolicy flags disallowed tools', () => {
|
||||
const event = {
|
||||
type: 'tool_use',
|
||||
tool: 'eval',
|
||||
target: 'script.js',
|
||||
url: 'https://claude.ai/chat'
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule1', type: 'disallowed_tools', enabled: true, params: { tools: ['eval'] } }
|
||||
]
|
||||
};
|
||||
const result = checkPolicy(event, policy);
|
||||
expect(result.outcome).toBe('violation');
|
||||
expect(result.severity).toBe('critical');
|
||||
});
|
||||
|
||||
test('disabled rules are ignored', () => {
|
||||
const event = {
|
||||
type: 'tool_use',
|
||||
tool: 'eval',
|
||||
target: 'script.js',
|
||||
url: 'https://claude.ai/chat'
|
||||
};
|
||||
const policy = {
|
||||
rules: [
|
||||
{ id: 'rule1', type: 'disallowed_tools', enabled: false, params: { tools: ['eval'] } }
|
||||
]
|
||||
};
|
||||
const result = checkPolicy(event, policy);
|
||||
expect(result.outcome).toBe('allowed');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user