Audit Events
Overview
Every action through the Behavry proxy writes one row to audit_events. This table is a TimescaleDB hypertable partitioned by timestamp, which enables efficient time-range queries and automatic compression/retention policies.
The audit log is append-only by convention. No event should ever be modified or deleted (use retention policies to drop old chunks after the configured window).
Schema
CREATE TABLE audit_events (
id UUID PRIMARY KEY,
tenant_id UUID REFERENCES tenants(id), -- null for legacy rows
timestamp TIMESTAMPTZ NOT NULL, -- hypertable partition key
-- Who
agent_id UUID,
session_id UUID,
-- What
action VARCHAR(255), -- "read", "write", "delete", "initialize", etc.
target TEXT, -- resource path, URL, table name, etc.
tool_name VARCHAR(255), -- MCP tool name, e.g. "read_file"
mcp_server VARCHAR(255), -- server identifier, e.g. "filesystem"
-- Policy decision
policy_result VARCHAR(50), -- "allow" | "deny" | "escalate"
policy_id VARCHAR(255), -- which rule fired, e.g. "filesystem.read"
policy_reason TEXT, -- human-readable explanation
-- Behavioral
behavioral_score FLOAT, -- anomaly z-score at time of event (if computed)
-- DLP
dlp_findings JSONB, -- array of {pattern, severity, field} objects
dlp_action VARCHAR(50), -- "allow" | "warn" | "block"
-- Audit integrity chain
input_hash VARCHAR(64), -- SHA-256 of tool call inputs
previous_hash VARCHAR(64), -- hash of preceding event (chain)
event_hash VARCHAR(64), -- SHA-256 of this event's content
-- Outcome
response_code INTEGER, -- HTTP/MCP response code
latency_ms FLOAT, -- end-to-end latency in milliseconds
error TEXT, -- error message if outcome was an error
-- Extra context
extra JSONB -- arbitrary structured context
);
Example Records
Allowed read
{
"id": "e1f2a3b4-0000-0000-0000-000000000001",
"tenant_id": "t-uuid-...",
"timestamp": "2025-02-15T14:23:11.456Z",
"agent_id": "a1b2c3d4-...",
"session_id": "s-uuid-...",
"action": "read",
"target": "/home/projects/report.pdf",
"tool_name": "read_file",
"mcp_server": "filesystem",
"policy_result": "allow",
"policy_id": "filesystem.read",
"policy_reason": null,
"dlp_findings": null,
"dlp_action": null,
"response_code": 200,
"latency_ms": 42.3,
"error": null
}
Denied — blocked path
{
"id": "e1f2a3b4-0000-0000-0000-000000000002",
"timestamp": "2025-02-15T14:24:00.123Z",
"agent_id": "a1b2c3d4-...",
"session_id": "s-uuid-...",
"action": "read",
"target": "/home/user/.ssh/id_rsa",
"tool_name": "read_file",
"mcp_server": "filesystem",
"policy_result": "deny",
"policy_id": "filesystem.blocked_paths",
"policy_reason": "Access to sensitive files is not permitted",
"response_code": null,
"latency_ms": 3.1
}
DLP block
{
"id": "e1f2a3b4-0000-0000-0000-000000000003",
"timestamp": "2025-02-15T14:25:00.789Z",
"agent_id": "a1b2c3d4-...",
"action": "write",
"target": "/tmp/output.txt",
"tool_name": "write_file",
"mcp_server": "filesystem",
"policy_result": "deny",
"policy_id": "dlp",
"policy_reason": "DLP block: sensitive data detected in request (critical)",
"dlp_findings": [
{"pattern": "aws_key", "severity": "critical", "field": "content", "match": "AKIA..."},
{"pattern": "email", "severity": "medium", "field": "content", "match": "user@example.com"}
],
"dlp_action": "block"
}
Escalated and approved
{
"id": "e1f2a3b4-0000-0000-0000-000000000004",
"timestamp": "2025-02-15T14:26:00.000Z",
"agent_id": "a1b2c3d4-...",
"action": "delete",
"target": "/home/projects/old_report.pdf",
"tool_name": "delete_file",
"policy_result": "escalate",
"policy_id": "filesystem.escalate_delete",
"policy_reason": "File deletion requires human approval"
}
{
"id": "e1f2a3b4-0000-0000-0000-000000000005",
"timestamp": "2025-02-15T14:27:45.000Z",
"agent_id": "a1b2c3d4-...",
"action": "delete",
"target": "/home/projects/old_report.pdf",
"tool_name": "delete_file",
"policy_result": "allow",
"policy_id": "filesystem.escalate_delete",
"policy_reason": "Escalation approved",
"response_code": 200,
"latency_ms": 105234.2
}
Querying Audit Events
REST API
# Recent events (default: last 100)
GET /api/v1/audit/events?limit=100
# Filter by agent
GET /api/v1/audit/events?agent_id=a1b2c3d4-...
# Filter by outcome
GET /api/v1/audit/events?policy_result=deny
# Filter by time range
GET /api/v1/audit/events?from=2025-02-15T00:00:00Z&to=2025-02-15T23:59:59Z
# Full filter example
GET /api/v1/audit/events?agent_id=...&policy_result=deny&limit=50
Direct SQL (for admin/reporting)
-- All denials in the last 24 hours
SELECT timestamp, agent_id, tool_name, target, policy_reason
FROM audit_events
WHERE policy_result = 'deny'
AND timestamp > NOW() - INTERVAL '24 hours'
ORDER BY timestamp DESC;
-- Denial rate by agent
SELECT agent_id,
COUNT(*) FILTER (WHERE policy_result = 'deny') AS denials,
COUNT(*) AS total,
ROUND(100.0 * COUNT(*) FILTER (WHERE policy_result = 'deny') / COUNT(*), 1) AS deny_pct
FROM audit_events
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY agent_id
ORDER BY deny_pct DESC;
-- DLP findings in last hour
SELECT timestamp, agent_id, tool_name, dlp_findings
FROM audit_events
WHERE dlp_action IS NOT NULL
AND timestamp > NOW() - INTERVAL '1 hour'
ORDER BY timestamp DESC;
SSE Live Feed
The dashboard subscribes to GET /api/v1/audit/stream (Server-Sent Events). Events arrive in real time as:
event: audit_event
data: {"event_type": "tool_call", "agent_id": "...", "timestamp": "...", "policy_result": "allow", ...}
event: policy_deny
data: {"event_type": "policy_deny", "agent_id": "...", "policy_reason": "...", ...}
event: escalation
data: {"event_type": "escalation", "escalation_id": "...", "agent_id": "...", ...}
Retention and Compression (Production)
TimescaleDB enables efficient retention and compression:
-- Compress chunks older than 7 days (saves ~90% storage)
SELECT add_compression_policy('audit_events', INTERVAL '7 days');
-- Drop chunks older than 90 days
SELECT add_retention_policy('audit_events', INTERVAL '90 days');
-- Check policy status
SELECT * FROM timescaledb_information.jobs;
Adjust retention to match your compliance requirements (e.g., 1 year for SOC 2, 7 years for financial records).
Export to SIEM
Audit events can be exported via webhook delivery (JSON or CEF format). See Alerts & Escalations for webhook configuration.
For bulk export:
GET /api/v1/audit/export?format=json&from=2025-01-01T00:00:00Z&to=2025-02-01T00:00:00Z
GET /api/v1/audit/export?format=cef&from=...
Audit Integrity Chain
Each event stores:
input_hash: SHA-256 of the tool call input parametersevent_hash: SHA-256 of the event's content fieldsprevious_hash:event_hashof the preceding event for the same agent
This forms a per-agent hash chain that makes it detectable if events are modified or deleted. Full verification is not yet automated in the UI — raw chain validation can be done by querying events in timestamp order and recomputing hashes.