Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions docs/features/audit-log.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ All audit events are JSON objects with a common structure:
"actor": "alice@example.com",
"provider": "google",
"ip": "192.168.1.100",
"success": true
"success": true,
"source": "web"
}
```

Expand All @@ -71,6 +72,7 @@ All audit events are JSON objects with a common structure:
| `ip` | Client IP address |
| `success` | Whether login succeeded |
| `error` | Error message (if failed) |
| `source` | Event origin: currently `"web"` |

### auth.logout

Expand All @@ -79,10 +81,16 @@ All audit events are JSON objects with a common structure:
"type": "audit",
"ts": "2024-01-15T10:35:00.000Z",
"action": "auth.logout",
"actor": "alice@example.com"
"actor": "alice@example.com",
"source": "web"
}
```
Comment thread
tianzhou marked this conversation as resolved.

| Field | Description |
|-------|-------------|
| `actor` | User who logged out |
| `source` | Event origin: currently `"web"` |

### sql.execute

```json
Expand All @@ -96,7 +104,8 @@ All audit events are JSON objects with a common structure:
"sql": "SELECT * FROM users WHERE active = true",
"success": true,
"duration_ms": 45,
"row_count": 150
"row_count": 150,
"source": "web"
}
```

Expand All @@ -109,7 +118,7 @@ All audit events are JSON objects with a common structure:
| `duration_ms` | Execution time in milliseconds |
| `row_count` | Number of rows returned (optional) |
| `error` | Error message (if failed) |
| `source` | `"mcp"` when the query came from the [MCP Server](/features/mcp-server) (omitted otherwise) |
| `source` | Query origin: `"web"` for the web app, `"mcp"` for the [MCP Server](/features/mcp-server) |
| `tool` | MCP tool name that ran the query (only when `source` is `"mcp"`) |
| `agent` | The [agent](/configuration/config#agents) id that ran the query (only when `source` is `"mcp"`). For a delegated agent the `actor` is the user it acts for; for a pure agent the `actor` is `agent:<id>` |

Expand All @@ -125,7 +134,8 @@ All audit events are JSON objects with a common structure:
"database": "postgres",
"sql": "SELECT * FROM users WHERE active = true",
"row_count": 150,
"format": "csv"
"format": "csv",
"source": "web"
}
```

Expand All @@ -136,6 +146,7 @@ All audit events are JSON objects with a common structure:
| `sql` | SQL query that produced the exported data |
| `row_count` | Number of rows exported |
| `format` | Export format (`csv`) |
| `source` | Export origin: currently `"web"` |

## Capturing Logs

Expand Down
12 changes: 9 additions & 3 deletions server/lib/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ interface AuthLoginEvent extends BaseEvent {
provider: string
ip: string
success: boolean
source: 'web'
error?: string
}

interface AuthLogoutEvent extends BaseEvent {
action: 'auth.logout'
source: 'web'
}

interface SQLExecuteEvent extends BaseEvent {
Expand All @@ -28,8 +30,8 @@ interface SQLExecuteEvent extends BaseEvent {
duration_ms: number
row_count?: number
error?: string
/** Set to 'mcp' when the query originated from the MCP server */
source?: 'mcp'
/** Origin channel of the query: web app or MCP server */
source: 'web' | 'mcp'
/** MCP tool name (only when source is 'mcp') */
tool?: string
/** Agent id that ran the query, when source is 'mcp' (present for both pure and delegated agents) */
Expand All @@ -43,6 +45,7 @@ interface DataExportEvent extends BaseEvent {
sql: string
row_count: number
format: string
source: 'web'
}

export type AuditEvent = AuthLoginEvent | AuthLogoutEvent | SQLExecuteEvent | DataExportEvent
Expand Down Expand Up @@ -96,6 +99,7 @@ export function auditLogin(actor: string, provider: string, ip: string, success:
provider,
ip,
success,
source: 'web',
}
if (error) event.error = error
emit(event)
Expand All @@ -107,6 +111,7 @@ export function auditLogout(actor: string): void {
ts: now(),
action: 'auth.logout',
actor,
source: 'web',
})
}

Expand All @@ -132,11 +137,11 @@ export function auditSQL(
sql,
success,
duration_ms,
source: opts?.source ?? 'web',
}
if (row_count !== undefined) event.row_count = row_count
if (error) event.error = error
if (opts) {
event.source = opts.source
event.tool = opts.tool
event.agent = opts.agent
}
Expand All @@ -161,6 +166,7 @@ export function auditExport(
sql,
row_count,
format,
source: 'web',
})
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/AuditLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default function AuditLog({ connectionId }: AuditLogProps) {
<TableRow key={`${entry.timestamp}-${entry.action}-${entry.actor}-${idx}`}>
<TableCell className="text-gray-600">{new Date(entry.timestamp).toLocaleString()}</TableCell>
<TableCell>{entry.actor}</TableCell>
<TableCell><ActionCell action={entry.action} /></TableCell>
<TableCell><ActionCell action={entry.action} source={entry.source} /></TableCell>
<TableCell><StatusBadge success={entry.success} /></TableCell>
<TableCell>{entry.provider || '—'}</TableCell>
<TableCell className="font-mono text-xs">{entry.ip || '—'}</TableCell>
Expand Down
27 changes: 27 additions & 0 deletions tests/audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,34 @@ describe('audit event store', () => {
const entries = listAuditEvents('prod', 10)
expect(entries).toHaveLength(2)
expect(entries[0].action).toBe('data.export')
expect(entries[0].source).toBe('web')
expect(entries[1].action).toBe('sql.execute')
expect(entries[1].source).toBe('web')
})

it('records source for web and MCP SQL events', () => {
auditSQL('alice@example.com', 'prod', 'postgres', 'SELECT 1', true, 1, 1)
auditSQL('agent:bot', 'prod', 'postgres', 'SELECT 2', true, 1, 1, undefined, {
source: 'mcp',
tool: 'query',
agent: 'bot',
})

const entries = listAuditEvents('prod', 10)
expect(entries).toHaveLength(2)
expect(entries[0].source).toBe('mcp')
expect('tool' in entries[0] ? entries[0].tool : '').toBe('query')
expect('agent' in entries[0] ? entries[0].agent : '').toBe('bot')
expect(entries[1].source).toBe('web')
})

it('records source for system auth events', () => {
auditLogin('alice@example.com', 'password', '10.0.0.1', true)
auditLogout('alice@example.com')

const system = listSystemAuditEvents(10)
expect(system).toHaveLength(2)
expect(system.every((event) => event.source === 'web')).toBe(true)
})

it('applies the response limit', () => {
Expand Down
Loading