Agent Police Department v1.0.0 MVP
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user