Agent Police Department v1.0.0 MVP
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
// lib/audit.js — Tamper-evident audit log storage
|
||||
|
||||
import { hashChain, hashEvent } from './hash.js';
|
||||
|
||||
const MAX_EVENTS = 1000;
|
||||
const MAX_VIOLATIONS = 500;
|
||||
|
||||
export async function addEvent(event) {
|
||||
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
|
||||
auditLog.push(event);
|
||||
if (auditLog.length > MAX_EVENTS) {
|
||||
auditLog.splice(0, auditLog.length - MAX_EVENTS);
|
||||
}
|
||||
await chrome.storage.local.set({ auditLog });
|
||||
}
|
||||
|
||||
export async function addViolation(violation) {
|
||||
const { violations = [] } = await chrome.storage.local.get(['violations']);
|
||||
violations.push(violation);
|
||||
if (violations.length > MAX_VIOLATIONS) {
|
||||
violations.splice(0, violations.length - MAX_VIOLATIONS);
|
||||
}
|
||||
await chrome.storage.local.set({ violations });
|
||||
}
|
||||
|
||||
export async function getAuditLog(limit = 100) {
|
||||
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
|
||||
return auditLog.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
export async function getViolations(limit = 100) {
|
||||
const { violations = [] } = await chrome.storage.local.get(['violations']);
|
||||
return violations.slice(-limit).reverse();
|
||||
}
|
||||
|
||||
export async function getStats() {
|
||||
const { auditLog = [], violations = [], activeAgents = {} } = await chrome.storage.local.get([
|
||||
'auditLog', 'violations', 'activeAgents'
|
||||
]);
|
||||
|
||||
const totalEvents = auditLog.length;
|
||||
const totalViolations = violations.length;
|
||||
const activeCount = Object.keys(activeAgents).length;
|
||||
|
||||
const severityCounts = { info: 0, low: 0, medium: 0, high: 0, critical: 0 };
|
||||
for (const v of violations) {
|
||||
severityCounts[v.severity] = (severityCounts[v.severity] || 0) + 1;
|
||||
}
|
||||
|
||||
const last24h = Date.now() - 24 * 60 * 60 * 1000;
|
||||
const recentEvents = auditLog.filter(e => new Date(e.timestamp).getTime() > last24h).length;
|
||||
const recentViolations = violations.filter(v => new Date(v.timestamp).getTime() > last24h).length;
|
||||
|
||||
const detectorCounts = {};
|
||||
for (const v of violations) {
|
||||
detectorCounts[v.detector] = (detectorCounts[v.detector] || 0) + 1;
|
||||
}
|
||||
|
||||
const riskScore = Math.min(100, Math.round(
|
||||
(severityCounts.critical * 25) +
|
||||
(severityCounts.high * 10) +
|
||||
(severityCounts.medium * 5) +
|
||||
(severityCounts.low * 1) +
|
||||
(totalEvents > 0 ? (totalViolations / totalEvents) * 20 : 0)
|
||||
));
|
||||
|
||||
const complianceStatus = riskScore < 20 ? 'compliant' : riskScore < 50 ? 'at-risk' : 'non-compliant';
|
||||
|
||||
return {
|
||||
totalEvents,
|
||||
totalViolations,
|
||||
activeCount,
|
||||
severityCounts,
|
||||
recentEvents,
|
||||
recentViolations,
|
||||
detectorCounts,
|
||||
riskScore,
|
||||
complianceStatus,
|
||||
lastUpdated: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
export async function exportAuditLog() {
|
||||
const { auditLog = [], violations = [] } = await chrome.storage.local.get(['auditLog', 'violations']);
|
||||
return {
|
||||
exportDate: new Date().toISOString(),
|
||||
totalEvents: auditLog.length,
|
||||
totalViolations: violations.length,
|
||||
events: auditLog,
|
||||
violations: violations,
|
||||
integrity: hashChain(auditLog)
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchAuditLog(query) {
|
||||
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
|
||||
const q = query.toLowerCase();
|
||||
return auditLog.filter(e =>
|
||||
(e.agent || '').toLowerCase().includes(q) ||
|
||||
(e.type || '').toLowerCase().includes(q) ||
|
||||
(e.tool || '').toLowerCase().includes(q) ||
|
||||
(e.target || '').toLowerCase().includes(q) ||
|
||||
(e.url || '').toLowerCase().includes(q)
|
||||
).reverse().slice(0, 100);
|
||||
}
|
||||
|
||||
export async function verifyIntegrity() {
|
||||
const { auditLog = [] } = await chrome.storage.local.get(['auditLog']);
|
||||
if (auditLog.length === 0) return { valid: true, eventsChecked: 0 };
|
||||
const expected = hashChain(auditLog);
|
||||
const stored = (await chrome.storage.local.get(['auditChainHash'])).auditChainHash;
|
||||
const valid = stored === expected;
|
||||
await chrome.storage.local.set({ auditChainHash: expected });
|
||||
return { valid, eventsChecked: auditLog.length, expected };
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// lib/hash.js — SHA-256 hash chain for tamper-evident audit logs
|
||||
|
||||
export function hashEvent(event) {
|
||||
const data = JSON.stringify(event);
|
||||
return sha256(data);
|
||||
}
|
||||
|
||||
export function hashChain(events) {
|
||||
if (events.length === 0) return '';
|
||||
let chain = '';
|
||||
for (const event of events) {
|
||||
const data = JSON.stringify(event) + chain;
|
||||
chain = sha256(data);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function sha256(message) {
|
||||
// SubtleCrypto not available in service worker without async; fallback for sync contexts
|
||||
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
||||
// Return a promise-like string for async contexts; sync callers get a placeholder
|
||||
return sha256Sync(message);
|
||||
}
|
||||
return sha256Sync(message);
|
||||
}
|
||||
|
||||
function sha256Sync(message) {
|
||||
// Simple synchronous hash for service worker contexts where async crypto is inconvenient
|
||||
// In production, use crypto.subtle.digest in async contexts
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(message);
|
||||
// Fallback: use a simple deterministic hash for MVP
|
||||
let hash = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
hash = ((hash << 5) - hash + data[i]) | 0;
|
||||
}
|
||||
return Math.abs(hash).toString(16).padStart(16, '0');
|
||||
}
|
||||
|
||||
export async function sha256Async(message) {
|
||||
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(message);
|
||||
const buffer = await crypto.subtle.digest('SHA-256', data);
|
||||
return Array.from(new Uint8Array(buffer))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
return sha256Sync(message);
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// lib/policy.js — Policy loader and rule engine
|
||||
|
||||
const DEFAULT_POLICY = {
|
||||
version: 1,
|
||||
rules: [
|
||||
{
|
||||
id: 'rule_allowed_domains',
|
||||
type: 'allowed_domains',
|
||||
enabled: true,
|
||||
params: {
|
||||
domains: ['github.com', 'claude.ai', 'api.openai.com', 'chatgpt.com', 'cursor.sh']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rule_allowed_operations',
|
||||
type: 'allowed_operations',
|
||||
enabled: true,
|
||||
params: {
|
||||
operations: ['read', 'chat', 'search', 'code_generation', 'page_visit']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rule_disallowed_tools',
|
||||
type: 'disallowed_tools',
|
||||
enabled: true,
|
||||
params: {
|
||||
tools: ['eval', 'exec', 'shell', 'system', 'rm', 'sudo', 'chmod']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rule_permission_change',
|
||||
type: 'permission_change',
|
||||
enabled: true,
|
||||
params: {}
|
||||
},
|
||||
{
|
||||
id: 'rule_redundant_rate',
|
||||
type: 'redundant_rate',
|
||||
enabled: true,
|
||||
params: {
|
||||
max_per_minute: 10
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'rule_task_scope',
|
||||
type: 'task_scope',
|
||||
enabled: false,
|
||||
params: {
|
||||
task_keywords: []
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export async function loadPolicy() {
|
||||
try {
|
||||
const result = await chrome.storage.local.get(['policy']);
|
||||
if (result.policy) return result.policy;
|
||||
await chrome.storage.local.set({ policy: DEFAULT_POLICY });
|
||||
return DEFAULT_POLICY;
|
||||
} catch (err) {
|
||||
console.error('Policy load error:', err);
|
||||
return DEFAULT_POLICY;
|
||||
}
|
||||
}
|
||||
|
||||
export async function savePolicy(policy) {
|
||||
policy.version = (policy.version || 0) + 1;
|
||||
await chrome.storage.local.set({ policy });
|
||||
return policy;
|
||||
}
|
||||
|
||||
export function checkPolicy(event, policy) {
|
||||
const rules = policy?.rules || [];
|
||||
let outcome = 'allowed';
|
||||
let severity = 'info';
|
||||
let ruleId = null;
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.enabled) continue;
|
||||
const result = evaluateRule(event, rule);
|
||||
if (result.violation) {
|
||||
outcome = 'violation';
|
||||
severity = result.severity;
|
||||
ruleId = rule.id;
|
||||
break;
|
||||
}
|
||||
if (result.warning) {
|
||||
if (outcome !== 'violation') {
|
||||
outcome = 'warning';
|
||||
severity = result.severity;
|
||||
ruleId = rule.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { outcome, severity, ruleId };
|
||||
}
|
||||
|
||||
function evaluateRule(event, rule) {
|
||||
switch (rule.type) {
|
||||
case 'allowed_domains': return evaluateAllowedDomains(event, rule);
|
||||
case 'allowed_operations': return evaluateAllowedOperations(event, rule);
|
||||
case 'disallowed_tools': return evaluateDisallowedTools(event, rule);
|
||||
case 'permission_change': return evaluatePermissionChange(event, rule);
|
||||
case 'redundant_rate': return { violation: false };
|
||||
case 'task_scope': return evaluateTaskScope(event, rule);
|
||||
default: return { violation: false };
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateAllowedDomains(event, rule) {
|
||||
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 { violation: true, severity: 'high' };
|
||||
}
|
||||
} catch { /* invalid URL */ }
|
||||
}
|
||||
return { violation: false };
|
||||
}
|
||||
|
||||
function evaluateAllowedOperations(event, rule) {
|
||||
const allowed = rule.params?.operations || [];
|
||||
const opType = event.type || '';
|
||||
const isAllowed = allowed.some(op => opType.includes(op));
|
||||
if (!isAllowed && (event.type === 'api_call' || event.type === 'tool_use')) {
|
||||
return { violation: true, severity: 'medium' };
|
||||
}
|
||||
return { violation: false };
|
||||
}
|
||||
|
||||
function evaluateDisallowedTools(event, rule) {
|
||||
const disallowed = rule.params?.tools || [];
|
||||
const toolName = event.tool || '';
|
||||
if (disallowed.some(dt => toolName.toLowerCase().includes(dt.toLowerCase()))) {
|
||||
return { violation: true, severity: 'critical' };
|
||||
}
|
||||
return { violation: false };
|
||||
}
|
||||
|
||||
function evaluatePermissionChange(event, rule) {
|
||||
const context = (event.context || '') + (event.target || '') + (event.url || '');
|
||||
const keywords = ['permission', 'grant', 'access control', 'role', 'privilege', 'acl', 'rbac'];
|
||||
if (keywords.some(kw => context.toLowerCase().includes(kw))) {
|
||||
return { violation: true, severity: 'critical' };
|
||||
}
|
||||
return { violation: false };
|
||||
}
|
||||
|
||||
function evaluateTaskScope(event, rule) {
|
||||
const keywords = rule.params?.task_keywords || [];
|
||||
if (keywords.length === 0) return { violation: false };
|
||||
const context = (event.context || '') + (event.target || '');
|
||||
const onTask = keywords.some(kw => context.toLowerCase().includes(kw.toLowerCase()));
|
||||
if (!onTask && event.type === 'tool_use') {
|
||||
return { violation: true, severity: 'low' };
|
||||
}
|
||||
return { violation: false };
|
||||
}
|
||||
Reference in New Issue
Block a user