147 lines
4.9 KiB
JavaScript
147 lines
4.9 KiB
JavaScript
// lib/detectors.js — 6 violation detectors for Agent Police Department
|
|
|
|
export const DETECTOR_NAMES = [
|
|
'permission_bypass',
|
|
'out_of_scope',
|
|
'self_permissioning',
|
|
'disallowed_tools',
|
|
'redundant_operations',
|
|
'off_task'
|
|
];
|
|
|
|
export async function detectViolations(event, policy) {
|
|
const violations = [];
|
|
const rules = policy?.rules || [];
|
|
|
|
for (const detector of DETECTOR_NAMES) {
|
|
const result = runDetector(detector, event, rules);
|
|
if (result) violations.push(result);
|
|
}
|
|
|
|
return violations;
|
|
}
|
|
|
|
function runDetector(name, event, rules) {
|
|
switch (name) {
|
|
case 'permission_bypass': return detectPermissionBypass(event, rules);
|
|
case 'out_of_scope': return detectOutOfScope(event, rules);
|
|
case 'self_permissioning': return detectSelfPermissioning(event, rules);
|
|
case 'disallowed_tools': return detectDisallowedTools(event, rules);
|
|
case 'redundant_operations': return detectRedundantOperations(event, rules);
|
|
case 'off_task': return detectOffTask(event, rules);
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
function detectPermissionBypass(event, rules) {
|
|
const rule = rules.find(r => r.type === 'allowed_domains' && r.enabled);
|
|
if (!rule) return null;
|
|
const allowed = rule.params?.domains || [];
|
|
if (event.type === 'api_call' || event.type === 'tool_use') {
|
|
const url = event.target || event.url || '';
|
|
try {
|
|
const hostname = new URL(url).hostname;
|
|
const isAllowed = allowed.some(d => hostname.includes(d));
|
|
if (!isAllowed) {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
event_id: event.id,
|
|
detector: 'permission_bypass',
|
|
rule_triggered: rule.id,
|
|
severity: 'high',
|
|
message: `Agent accessed disallowed domain: ${hostname}`,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
} catch { /* invalid URL, skip */ }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function detectOutOfScope(event, rules) {
|
|
const rule = rules.find(r => r.type === 'allowed_operations' && r.enabled);
|
|
if (!rule) return null;
|
|
const allowedOps = rule.params?.operations || ['read', 'chat', 'search'];
|
|
const opType = event.type || '';
|
|
const isAllowed = allowedOps.some(op => opType.includes(op));
|
|
if (!isAllowed && (event.type === 'api_call' || event.type === 'tool_use')) {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
event_id: event.id,
|
|
detector: 'out_of_scope',
|
|
rule_triggered: rule.id,
|
|
severity: 'medium',
|
|
message: `Agent performed out-of-scope operation: ${opType}`,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function detectSelfPermissioning(event, rules) {
|
|
const rule = rules.find(r => r.type === 'permission_change' && r.enabled);
|
|
if (!rule) return null;
|
|
const context = (event.context || '') + (event.target || '') + (event.url || '');
|
|
const permissionKeywords = ['permission', 'grant', 'access control', 'role', 'privilege', 'acl', 'rbac'];
|
|
if (permissionKeywords.some(kw => context.toLowerCase().includes(kw))) {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
event_id: event.id,
|
|
detector: 'self_permissioning',
|
|
rule_triggered: rule.id,
|
|
severity: 'critical',
|
|
message: `Agent attempted to modify permissions or access controls`,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function detectDisallowedTools(event, rules) {
|
|
const rule = rules.find(r => r.type === 'disallowed_tools' && r.enabled);
|
|
if (!rule) return null;
|
|
const disallowed = rule.params?.tools || ['eval', 'exec', 'shell', 'system', 'rm'];
|
|
const toolName = event.tool || '';
|
|
if (disallowed.some(dt => toolName.toLowerCase().includes(dt.toLowerCase()))) {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
event_id: event.id,
|
|
detector: 'disallowed_tools',
|
|
rule_triggered: rule.id,
|
|
severity: 'critical',
|
|
message: `Agent used disallowed tool: ${toolName}`,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function detectRedundantOperations(event, rules) {
|
|
const rule = rules.find(r => r.type === 'redundant_rate' && r.enabled);
|
|
if (!rule) return null;
|
|
const maxRate = rule.params?.max_per_minute || 10;
|
|
// We would need history for real detection; for now, flag if the event itself is flagged
|
|
return null;
|
|
}
|
|
|
|
function detectOffTask(event, rules) {
|
|
const rule = rules.find(r => r.type === 'task_scope' && r.enabled);
|
|
if (!rule) return null;
|
|
const taskKeywords = rule.params?.task_keywords || [];
|
|
if (taskKeywords.length === 0) return null;
|
|
const context = (event.context || '') + (event.target || '');
|
|
const onTask = taskKeywords.some(kw => context.toLowerCase().includes(kw.toLowerCase()));
|
|
if (!onTask && event.type === 'tool_use') {
|
|
return {
|
|
id: crypto.randomUUID(),
|
|
event_id: event.id,
|
|
detector: 'off_task',
|
|
rule_triggered: rule.id,
|
|
severity: 'low',
|
|
message: `Agent action may be off-task: ${event.tool || event.type}`,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
return null;
|
|
}
|