From 1cb31a4e45c6e3fb9f379276010a0769d91e16a4 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Thu, 25 Jun 2026 23:27:33 -0700 Subject: [PATCH 1/2] fix: always tag audit event source --- docs/features/audit-log.mdx | 16 +++++++++++----- server/lib/audit.ts | 12 +++++++++--- src/pages/AuditLog.tsx | 2 +- tests/audit.test.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 9 deletions(-) diff --git a/docs/features/audit-log.mdx b/docs/features/audit-log.mdx index 4551842..650471e 100644 --- a/docs/features/audit-log.mdx +++ b/docs/features/audit-log.mdx @@ -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" } ``` @@ -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 @@ -79,7 +81,8 @@ 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" } ``` @@ -96,7 +99,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" } ``` @@ -109,7 +113,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:` | @@ -125,7 +129,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" } ``` @@ -136,6 +141,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 diff --git a/server/lib/audit.ts b/server/lib/audit.ts index 9c08f95..131c1c2 100644 --- a/server/lib/audit.ts +++ b/server/lib/audit.ts @@ -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 { @@ -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) */ @@ -43,6 +45,7 @@ interface DataExportEvent extends BaseEvent { sql: string row_count: number format: string + source: 'web' } export type AuditEvent = AuthLoginEvent | AuthLogoutEvent | SQLExecuteEvent | DataExportEvent @@ -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) @@ -107,6 +111,7 @@ export function auditLogout(actor: string): void { ts: now(), action: 'auth.logout', actor, + source: 'web', }) } @@ -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 } @@ -161,6 +166,7 @@ export function auditExport( sql, row_count, format, + source: 'web', }) } diff --git a/src/pages/AuditLog.tsx b/src/pages/AuditLog.tsx index 4b306b2..9a625b8 100644 --- a/src/pages/AuditLog.tsx +++ b/src/pages/AuditLog.tsx @@ -156,7 +156,7 @@ export default function AuditLog({ connectionId }: AuditLogProps) { {new Date(entry.timestamp).toLocaleString()} {entry.actor} - + {entry.provider || '—'} {entry.ip || '—'} diff --git a/tests/audit.test.ts b/tests/audit.test.ts index db0eb6e..918ea61 100644 --- a/tests/audit.test.ts +++ b/tests/audit.test.ts @@ -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('source' in entries[0] ? entries[0].source : '').toBe('web') expect(entries[1].action).toBe('sql.execute') + expect('source' in entries[1] ? 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('source' in entries[0] ? 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('source' in entries[1] ? 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) => 'source' in event && event.source === 'web')).toBe(true) }) it('applies the response limit', () => { From 4ea01e0f2cb2dd828eaa09708fd66e4ca0b38792 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Fri, 26 Jun 2026 00:15:06 -0700 Subject: [PATCH 2/2] chore: address audit source review comments --- docs/features/audit-log.mdx | 5 +++++ tests/audit.test.ts | 10 +++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/features/audit-log.mdx b/docs/features/audit-log.mdx index 650471e..f7e2e88 100644 --- a/docs/features/audit-log.mdx +++ b/docs/features/audit-log.mdx @@ -86,6 +86,11 @@ All audit events are JSON objects with a common structure: } ``` +| Field | Description | +|-------|-------------| +| `actor` | User who logged out | +| `source` | Event origin: currently `"web"` | + ### sql.execute ```json diff --git a/tests/audit.test.ts b/tests/audit.test.ts index 918ea61..6b3c481 100644 --- a/tests/audit.test.ts +++ b/tests/audit.test.ts @@ -56,9 +56,9 @@ describe('audit event store', () => { const entries = listAuditEvents('prod', 10) expect(entries).toHaveLength(2) expect(entries[0].action).toBe('data.export') - expect('source' in entries[0] ? entries[0].source : '').toBe('web') + expect(entries[0].source).toBe('web') expect(entries[1].action).toBe('sql.execute') - expect('source' in entries[1] ? entries[1].source : '').toBe('web') + expect(entries[1].source).toBe('web') }) it('records source for web and MCP SQL events', () => { @@ -71,10 +71,10 @@ describe('audit event store', () => { const entries = listAuditEvents('prod', 10) expect(entries).toHaveLength(2) - expect('source' in entries[0] ? entries[0].source : '').toBe('mcp') + 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('source' in entries[1] ? entries[1].source : '').toBe('web') + expect(entries[1].source).toBe('web') }) it('records source for system auth events', () => { @@ -83,7 +83,7 @@ describe('audit event store', () => { const system = listSystemAuditEvents(10) expect(system).toHaveLength(2) - expect(system.every((event) => 'source' in event && event.source === 'web')).toBe(true) + expect(system.every((event) => event.source === 'web')).toBe(true) }) it('applies the response limit', () => {