@mcp-audit-gateway/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +21 -0
- package/CONTRIBUTING.md +41 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/attestation/audit-log.d.ts +25 -0
- package/dist/attestation/audit-log.d.ts.map +1 -0
- package/dist/attestation/audit-log.js +95 -0
- package/dist/attestation/audit-log.js.map +1 -0
- package/dist/attestation/bugfix.test.d.ts +2 -0
- package/dist/attestation/bugfix.test.d.ts.map +1 -0
- package/dist/attestation/bugfix.test.js +103 -0
- package/dist/attestation/bugfix.test.js.map +1 -0
- package/dist/attestation/chain.test.d.ts +2 -0
- package/dist/attestation/chain.test.d.ts.map +1 -0
- package/dist/attestation/chain.test.js +153 -0
- package/dist/attestation/chain.test.js.map +1 -0
- package/dist/attestation/keygen.d.ts +5 -0
- package/dist/attestation/keygen.d.ts.map +1 -0
- package/dist/attestation/keygen.js +12 -0
- package/dist/attestation/keygen.js.map +1 -0
- package/dist/attestation/signer.d.ts +25 -0
- package/dist/attestation/signer.d.ts.map +1 -0
- package/dist/attestation/signer.js +93 -0
- package/dist/attestation/signer.js.map +1 -0
- package/dist/attestation/signer.test.d.ts +2 -0
- package/dist/attestation/signer.test.d.ts.map +1 -0
- package/dist/attestation/signer.test.js +62 -0
- package/dist/attestation/signer.test.js.map +1 -0
- package/dist/attestation/verify.d.ts +26 -0
- package/dist/attestation/verify.d.ts.map +1 -0
- package/dist/attestation/verify.js +104 -0
- package/dist/attestation/verify.js.map +1 -0
- package/dist/attestation/verify.test.d.ts +2 -0
- package/dist/attestation/verify.test.d.ts.map +1 -0
- package/dist/attestation/verify.test.js +79 -0
- package/dist/attestation/verify.test.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +364 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/integration.test.d.ts +2 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +88 -0
- package/dist/integration.test.js.map +1 -0
- package/dist/policy/engine.d.ts +24 -0
- package/dist/policy/engine.d.ts.map +1 -0
- package/dist/policy/engine.js +130 -0
- package/dist/policy/engine.js.map +1 -0
- package/dist/policy/engine.test.d.ts +2 -0
- package/dist/policy/engine.test.d.ts.map +1 -0
- package/dist/policy/engine.test.js +80 -0
- package/dist/policy/engine.test.js.map +1 -0
- package/dist/proxy/gateway.d.ts +54 -0
- package/dist/proxy/gateway.d.ts.map +1 -0
- package/dist/proxy/gateway.js +245 -0
- package/dist/proxy/gateway.js.map +1 -0
- package/dist/proxy/gateway.test.d.ts +2 -0
- package/dist/proxy/gateway.test.d.ts.map +1 -0
- package/dist/proxy/gateway.test.js +93 -0
- package/dist/proxy/gateway.test.js.map +1 -0
- package/dist/proxy/mcp-server-adapter.d.ts +21 -0
- package/dist/proxy/mcp-server-adapter.d.ts.map +1 -0
- package/dist/proxy/mcp-server-adapter.js +98 -0
- package/dist/proxy/mcp-server-adapter.js.map +1 -0
- package/dist/proxy/mcp-server-adapter.test.d.ts +2 -0
- package/dist/proxy/mcp-server-adapter.test.d.ts.map +1 -0
- package/dist/proxy/mcp-server-adapter.test.js +160 -0
- package/dist/proxy/mcp-server-adapter.test.js.map +1 -0
- package/dist/proxy/upstream-manager.d.ts +49 -0
- package/dist/proxy/upstream-manager.d.ts.map +1 -0
- package/dist/proxy/upstream-manager.js +246 -0
- package/dist/proxy/upstream-manager.js.map +1 -0
- package/dist/proxy/upstream-manager.test.d.ts +2 -0
- package/dist/proxy/upstream-manager.test.d.ts.map +1 -0
- package/dist/proxy/upstream-manager.test.js +413 -0
- package/dist/proxy/upstream-manager.test.js.map +1 -0
- package/dist/telemetry/logger.d.ts +26 -0
- package/dist/telemetry/logger.d.ts.map +1 -0
- package/dist/telemetry/logger.js +48 -0
- package/dist/telemetry/logger.js.map +1 -0
- package/dist/telemetry/logger.test.d.ts +2 -0
- package/dist/telemetry/logger.test.d.ts.map +1 -0
- package/dist/telemetry/logger.test.js +124 -0
- package/dist/telemetry/logger.test.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +41 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +64 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/metrics.test.d.ts +2 -0
- package/dist/telemetry/metrics.test.d.ts.map +1 -0
- package/dist/telemetry/metrics.test.js +93 -0
- package/dist/telemetry/metrics.test.js.map +1 -0
- package/dist/telemetry/tracer.d.ts +16 -0
- package/dist/telemetry/tracer.d.ts.map +1 -0
- package/dist/telemetry/tracer.js +71 -0
- package/dist/telemetry/tracer.js.map +1 -0
- package/dist/types.d.ts +477 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +68 -0
- package/dist/types.js.map +1 -0
- package/dist/wrap/proxy.d.ts +2 -0
- package/dist/wrap/proxy.d.ts.map +1 -0
- package/dist/wrap/proxy.js +152 -0
- package/dist/wrap/proxy.js.map +1 -0
- package/dist/wrap/proxy.test.d.ts +2 -0
- package/dist/wrap/proxy.test.d.ts.map +1 -0
- package/dist/wrap/proxy.test.js +155 -0
- package/dist/wrap/proxy.test.js.map +1 -0
- package/examples/README.md +33 -0
- package/examples/claude-desktop.json +50 -0
- package/examples/multi-agent-production.json +72 -0
- package/gateway.config.example.json +61 -0
- package/package.json +49 -0
- package/src/attestation/audit-log.ts +126 -0
- package/src/attestation/bugfix.test.ts +113 -0
- package/src/attestation/chain.test.ts +188 -0
- package/src/attestation/keygen.ts +18 -0
- package/src/attestation/signer.test.ts +75 -0
- package/src/attestation/signer.ts +105 -0
- package/src/attestation/verify.test.ts +86 -0
- package/src/attestation/verify.ts +131 -0
- package/src/cli.ts +407 -0
- package/src/index.ts +21 -0
- package/src/integration.test.ts +108 -0
- package/src/policy/engine.test.ts +95 -0
- package/src/policy/engine.ts +162 -0
- package/src/proxy/gateway.test.ts +108 -0
- package/src/proxy/gateway.ts +328 -0
- package/src/proxy/mcp-server-adapter.test.ts +197 -0
- package/src/proxy/mcp-server-adapter.ts +140 -0
- package/src/proxy/upstream-manager.test.ts +534 -0
- package/src/proxy/upstream-manager.ts +326 -0
- package/src/telemetry/logger.test.ts +139 -0
- package/src/telemetry/logger.ts +72 -0
- package/src/telemetry/metrics.test.ts +125 -0
- package/src/telemetry/metrics.ts +83 -0
- package/src/telemetry/tracer.ts +90 -0
- package/src/types.ts +113 -0
- package/src/wrap/proxy.test.ts +175 -0
- package/src/wrap/proxy.ts +192 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build-and-publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-node@v4
|
|
13
|
+
with:
|
|
14
|
+
node-version: 20
|
|
15
|
+
registry-url: https://registry.npmjs.org/
|
|
16
|
+
- run: npm ci
|
|
17
|
+
- run: npm run build
|
|
18
|
+
- run: npm test
|
|
19
|
+
- run: npm publish --access public
|
|
20
|
+
env:
|
|
21
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions welcome. Here's how to get going.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone <repo-url> && cd mcp-audit-gateway
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
npm test
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires Node.js 20+.
|
|
15
|
+
|
|
16
|
+
## Running tests
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm test # all tests
|
|
20
|
+
npx vitest run src/attestation/ # just attestation tests
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Tests use Vitest and run against real filesystem paths (no mocks for the audit log). Temp files go in `/tmp` and are cleaned up in `afterEach`.
|
|
24
|
+
|
|
25
|
+
## Code style
|
|
26
|
+
|
|
27
|
+
TypeScript strict mode. No `any` unless interfacing with the MCP SDK's dynamic types. Keep functions short and avoid class inheritance.
|
|
28
|
+
|
|
29
|
+
## Pull requests
|
|
30
|
+
|
|
31
|
+
One feature or fix per PR. Include a test that fails without the fix and passes with it.
|
|
32
|
+
|
|
33
|
+
If you're changing the attestation or signing logic, run the full verification test suite and include `verify` CLI output in the PR description showing the audit log validates end-to-end.
|
|
34
|
+
|
|
35
|
+
## Architecture decisions
|
|
36
|
+
|
|
37
|
+
The gateway is a transparent proxy. It should never modify tool call arguments or results. Its job is to observe, sign, and enforce policy. If a feature requires mutating upstream data, it belongs in a different layer.
|
|
38
|
+
|
|
39
|
+
## Security
|
|
40
|
+
|
|
41
|
+
If you find a vulnerability in the signing, chain verification, or policy enforcement, please report it privately before opening a public issue.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elankumaran Srinivasan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# mcp-audit
|
|
2
|
+
|
|
3
|
+
Your AI agent made 847 tool calls yesterday. Can you verify what it did?
|
|
4
|
+
|
|
5
|
+
Tamper-evident audit trail for AI agent tool calls.
|
|
6
|
+
|
|
7
|
+
## Setup (10 seconds)
|
|
8
|
+
|
|
9
|
+
Before:
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"command": "npx",
|
|
13
|
+
"args": ["@modelcontextprotocol/server-github"]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
After:
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["mcp-audit", "wrap", "--", "npx", "@modelcontextprotocol/server-github"]
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Every tool call is now cryptographically signed and hash-chained. Nothing else changes. The MCP server works exactly as before.
|
|
26
|
+
|
|
27
|
+
## What it does
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
$ mcp-audit tail
|
|
31
|
+
|
|
32
|
+
✓ 14:32:01 github/create_pr 234ms bf7a2f62
|
|
33
|
+
✓ 14:32:03 github/list_issues 89ms a1c4e890
|
|
34
|
+
✗ 14:32:05 fs/delete_file 12ms c3d9f012
|
|
35
|
+
✓ 14:32:08 github/merge_pr 456ms e5f6a7b8
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every entry is signed with HMAC-SHA256 and chained to the previous record. Tamper with any entry and verification fails. Delete an entry and the chain breaks.
|
|
39
|
+
|
|
40
|
+
## Verify integrity
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
$ mcp-audit verify ~/.mcp-audit/audit.jsonl
|
|
44
|
+
|
|
45
|
+
Results:
|
|
46
|
+
Total records: 847
|
|
47
|
+
Valid: 847
|
|
48
|
+
Invalid: 0
|
|
49
|
+
|
|
50
|
+
All records verified successfully.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
┌────────────┐ ┌───────────┐ ┌────────────┐
|
|
57
|
+
│ MCP Client │──────▶│ mcp-audit │──────▶│ MCP Server │
|
|
58
|
+
│ (Claude, │◀──────│ wrap │◀──────│ (any) │
|
|
59
|
+
│ Cursor) │ └─────┬─────┘ └────────────┘
|
|
60
|
+
└────────────┘ │
|
|
61
|
+
▼
|
|
62
|
+
~/.mcp-audit/
|
|
63
|
+
audit.jsonl
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The wrap command spawns your MCP server as a child process and sits between the client and server on stdio. It forwards ALL messages transparently. Only `tools/call` responses get signed and logged. Everything else passes through untouched.
|
|
67
|
+
|
|
68
|
+
On first run, a signing key is auto-generated in `~/.mcp-audit/key.hex`. No configuration needed.
|
|
69
|
+
|
|
70
|
+
## Audit record format
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"id": "bf7a2f62-4d0f-4cce-afd2-cbfbf7bca2a5",
|
|
75
|
+
"timestamp": "2026-08-16T14:32:01.000Z",
|
|
76
|
+
"method": "tools/call",
|
|
77
|
+
"toolName": "github/create_pr",
|
|
78
|
+
"args": {"title": "Fix bug", "body": "..."},
|
|
79
|
+
"durationMs": 234,
|
|
80
|
+
"success": true,
|
|
81
|
+
"previousHash": "8a3f2b...",
|
|
82
|
+
"attestation": "7c4d9e..."
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The `attestation` is an HMAC-SHA256 signature over the record's canonical fields. The `previousHash` is SHA-256 of the preceding record. Together they detect tampering, ordering, and completeness.
|
|
87
|
+
|
|
88
|
+
## Use with Claude Desktop
|
|
89
|
+
|
|
90
|
+
`claude_desktop_config.json`:
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"github": {
|
|
95
|
+
"command": "npx",
|
|
96
|
+
"args": ["mcp-audit", "wrap", "--", "npx", "@modelcontextprotocol/server-github"]
|
|
97
|
+
},
|
|
98
|
+
"filesystem": {
|
|
99
|
+
"command": "npx",
|
|
100
|
+
"args": ["mcp-audit", "wrap", "--", "npx", "@modelcontextprotocol/server-filesystem", "/tmp"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Use with Claude Code
|
|
107
|
+
|
|
108
|
+
`.claude/hooks/mcp-servers.json` or directly in your MCP server command — prefix with `mcp-audit wrap --`.
|
|
109
|
+
|
|
110
|
+
## CLI
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
mcp-audit wrap -- <cmd> [args] # Wrap any MCP server
|
|
114
|
+
mcp-audit tail # Live stream of tool calls
|
|
115
|
+
mcp-audit verify <log> # Verify chain integrity
|
|
116
|
+
mcp-audit serve [config] # Full gateway (policy + OTel)
|
|
117
|
+
mcp-audit keygen [dir] # Generate Ed25519 key pair
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Full gateway mode
|
|
121
|
+
|
|
122
|
+
For teams that also need access control, rate limiting, and multi-server routing:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
mcp-audit serve gateway.config.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The full gateway adds:
|
|
129
|
+
- Policy engine (glob-based ACLs, per-principal rate limits)
|
|
130
|
+
- Tool namespacing across multiple upstream MCP servers
|
|
131
|
+
- OpenTelemetry traces and metrics export
|
|
132
|
+
- Upstream health management with automatic reconnection
|
|
133
|
+
- Ed25519 signatures (stronger than HMAC, portable verification)
|
|
134
|
+
|
|
135
|
+
See [gateway configuration](gateway.config.example.json) for the full schema.
|
|
136
|
+
|
|
137
|
+
## Install
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm install -g @mcp-gateway/audit
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This installs the `mcp-audit` CLI globally. Or use without installing:
|
|
144
|
+
```bash
|
|
145
|
+
npx @mcp-gateway/audit wrap -- <your mcp server command>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Testing
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm test # 96 tests across 12 test files
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AuditRecord } from "../types.js";
|
|
2
|
+
import type { Signer } from "./signer.js";
|
|
3
|
+
export declare function hashRecord(record: AuditRecord): string;
|
|
4
|
+
export declare class AuditLog {
|
|
5
|
+
private path;
|
|
6
|
+
private signer;
|
|
7
|
+
private rotateAfterBytes;
|
|
8
|
+
private currentSize;
|
|
9
|
+
private lastHash;
|
|
10
|
+
private writeQueue;
|
|
11
|
+
constructor(path: string, signer: Signer, rotateAfterBytes: number);
|
|
12
|
+
record(method: string, opts: {
|
|
13
|
+
toolName?: string;
|
|
14
|
+
namespace?: string;
|
|
15
|
+
upstream?: string;
|
|
16
|
+
principal?: string;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
success: boolean;
|
|
19
|
+
errorCode?: number;
|
|
20
|
+
}): Promise<AuditRecord>;
|
|
21
|
+
private writeRecord;
|
|
22
|
+
private rotate;
|
|
23
|
+
init(): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=audit-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.d.ts","sourceRoot":"","sources":["../../src/attestation/audit-log.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAGtD;AAED,qBAAa,QAAQ;IAMjB,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,gBAAgB;IAP1B,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,UAAU,CAAoC;gBAG5C,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,gBAAgB,EAAE,MAAM;IAG5B,MAAM,CACV,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;QACJ,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,OAAO,EAAE,OAAO,CAAC;QACjB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACA,OAAO,CAAC,WAAW,CAAC;YAaT,WAAW;YAmCX,MAAM;IAUd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAmC5B"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { appendFile, stat, rename, open } from "node:fs/promises";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
export function hashRecord(record) {
|
|
4
|
+
const json = JSON.stringify(record);
|
|
5
|
+
return createHash("sha256").update(json).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
export class AuditLog {
|
|
8
|
+
path;
|
|
9
|
+
signer;
|
|
10
|
+
rotateAfterBytes;
|
|
11
|
+
currentSize = 0;
|
|
12
|
+
lastHash = "genesis";
|
|
13
|
+
writeQueue = Promise.resolve();
|
|
14
|
+
constructor(path, signer, rotateAfterBytes) {
|
|
15
|
+
this.path = path;
|
|
16
|
+
this.signer = signer;
|
|
17
|
+
this.rotateAfterBytes = rotateAfterBytes;
|
|
18
|
+
}
|
|
19
|
+
async record(method, opts) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.writeQueue = this.writeQueue.then(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const result = await this.writeRecord(method, opts);
|
|
24
|
+
resolve(result);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
reject(err);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async writeRecord(method, opts) {
|
|
33
|
+
const record = {
|
|
34
|
+
id: randomUUID(),
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
method,
|
|
37
|
+
...opts,
|
|
38
|
+
previousHash: this.lastHash,
|
|
39
|
+
};
|
|
40
|
+
record.attestation = await this.signer.sign(record);
|
|
41
|
+
const line = JSON.stringify(record) + "\n";
|
|
42
|
+
await appendFile(this.path, line);
|
|
43
|
+
this.currentSize += Buffer.byteLength(line);
|
|
44
|
+
this.lastHash = hashRecord(record);
|
|
45
|
+
if (this.currentSize >= this.rotateAfterBytes) {
|
|
46
|
+
await this.rotate();
|
|
47
|
+
}
|
|
48
|
+
return record;
|
|
49
|
+
}
|
|
50
|
+
async rotate() {
|
|
51
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
52
|
+
const rotatedPath = this.path.replace(/\.jsonl$/, `-${ts}.jsonl`);
|
|
53
|
+
try {
|
|
54
|
+
await rename(this.path, rotatedPath);
|
|
55
|
+
this.currentSize = 0;
|
|
56
|
+
this.lastHash = "genesis";
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}
|
|
60
|
+
async init() {
|
|
61
|
+
let s;
|
|
62
|
+
try {
|
|
63
|
+
s = await stat(this.path);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
this.currentSize = 0;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
this.currentSize = s.size;
|
|
70
|
+
if (s.size > 0) {
|
|
71
|
+
const tailSize = Math.min(s.size, 8192);
|
|
72
|
+
const fh = await open(this.path, "r");
|
|
73
|
+
try {
|
|
74
|
+
const buf = Buffer.alloc(tailSize);
|
|
75
|
+
await fh.read(buf, 0, tailSize, s.size - tailSize);
|
|
76
|
+
const tail = buf.toString("utf-8");
|
|
77
|
+
const lines = tail.trimEnd().split("\n");
|
|
78
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
79
|
+
try {
|
|
80
|
+
const record = JSON.parse(lines[i]);
|
|
81
|
+
this.lastHash = hashRecord(record);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
process.stderr.write(`[audit-log] warning: corrupt record in tail, skipping\n`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
await fh.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=audit-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-log.js","sourceRoot":"","sources":["../../src/attestation/audit-log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bugfix.test.d.ts","sourceRoot":"","sources":["../../src/attestation/bugfix.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { unlink, readFile } from "node:fs/promises";
|
|
3
|
+
import { HmacSigner } from "./signer.js";
|
|
4
|
+
import { AuditLog, hashRecord } from "./audit-log.js";
|
|
5
|
+
import { verifyAuditLog } from "./verify.js";
|
|
6
|
+
const TEST_LOG = "/tmp/bugfix-audit.jsonl";
|
|
7
|
+
const SECRET = "d".repeat(64);
|
|
8
|
+
describe("Bug fix: pipe injection in canonicalization", () => {
|
|
9
|
+
const signer = new HmacSigner(SECRET);
|
|
10
|
+
it("different records with pipe chars produce different signatures", async () => {
|
|
11
|
+
const record1 = {
|
|
12
|
+
id: "uuid-1",
|
|
13
|
+
timestamp: "2026-08-16T10:00:00Z",
|
|
14
|
+
method: "tools/call",
|
|
15
|
+
toolName: "db/read|admin",
|
|
16
|
+
namespace: "db",
|
|
17
|
+
upstream: "evil",
|
|
18
|
+
principal: "alice",
|
|
19
|
+
durationMs: 50,
|
|
20
|
+
success: true,
|
|
21
|
+
};
|
|
22
|
+
const record2 = {
|
|
23
|
+
id: "uuid-1",
|
|
24
|
+
timestamp: "2026-08-16T10:00:00Z",
|
|
25
|
+
method: "tools/call",
|
|
26
|
+
toolName: "db/read",
|
|
27
|
+
namespace: "admin",
|
|
28
|
+
upstream: "db",
|
|
29
|
+
principal: "evil|alice",
|
|
30
|
+
durationMs: 50,
|
|
31
|
+
success: true,
|
|
32
|
+
};
|
|
33
|
+
const sig1 = await signer.sign(record1);
|
|
34
|
+
const sig2 = await signer.sign(record2);
|
|
35
|
+
expect(sig1).not.toBe(sig2);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe("Bug fix: hash chain survives restart", () => {
|
|
39
|
+
const signer = new HmacSigner(SECRET);
|
|
40
|
+
beforeEach(async () => { try {
|
|
41
|
+
await unlink(TEST_LOG);
|
|
42
|
+
}
|
|
43
|
+
catch { } });
|
|
44
|
+
afterEach(async () => { try {
|
|
45
|
+
await unlink(TEST_LOG);
|
|
46
|
+
}
|
|
47
|
+
catch { } });
|
|
48
|
+
it("restores lastHash from existing log on init", async () => {
|
|
49
|
+
const log1 = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
|
|
50
|
+
await log1.init();
|
|
51
|
+
await log1.record("tools/call", { toolName: "test/a", durationMs: 10, success: true });
|
|
52
|
+
await log1.record("tools/call", { toolName: "test/b", durationMs: 10, success: true });
|
|
53
|
+
const log2 = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
|
|
54
|
+
await log2.init();
|
|
55
|
+
await log2.record("tools/call", { toolName: "test/c", durationMs: 10, success: true });
|
|
56
|
+
const result = await verifyAuditLog(TEST_LOG, signer, { verifyChain: true });
|
|
57
|
+
expect(result.total).toBe(3);
|
|
58
|
+
expect(result.valid).toBe(3);
|
|
59
|
+
expect(result.invalid).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe("Bug fix: concurrent writes maintain chain integrity", () => {
|
|
63
|
+
const signer = new HmacSigner(SECRET);
|
|
64
|
+
beforeEach(async () => { try {
|
|
65
|
+
await unlink(TEST_LOG);
|
|
66
|
+
}
|
|
67
|
+
catch { } });
|
|
68
|
+
afterEach(async () => { try {
|
|
69
|
+
await unlink(TEST_LOG);
|
|
70
|
+
}
|
|
71
|
+
catch { } });
|
|
72
|
+
it("serializes concurrent writes correctly", async () => {
|
|
73
|
+
const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
|
|
74
|
+
await log.init();
|
|
75
|
+
await log.record("tools/call", { toolName: "test/setup", durationMs: 10, success: true });
|
|
76
|
+
const concurrent = Array.from({ length: 10 }, (_, i) => log.record("tools/call", { toolName: `test/tool_${i}`, durationMs: i, success: true }));
|
|
77
|
+
await Promise.all(concurrent);
|
|
78
|
+
const content = await readFile(TEST_LOG, "utf-8");
|
|
79
|
+
const records = content.trimEnd().split("\n").map((l) => JSON.parse(l));
|
|
80
|
+
expect(records).toHaveLength(11);
|
|
81
|
+
const previousHashes = records.map((r) => r.previousHash);
|
|
82
|
+
const unique = new Set(previousHashes);
|
|
83
|
+
expect(unique.size).toBe(11);
|
|
84
|
+
for (let i = 1; i < records.length; i++) {
|
|
85
|
+
const expected = hashRecord(records[i - 1]);
|
|
86
|
+
expect(records[i].previousHash).toBe(expected);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe("Bug fix: glob matcher prefix/suffix overlap", () => {
|
|
91
|
+
it("does not match when prefix and suffix overlap", async () => {
|
|
92
|
+
const { PolicyEngine } = await import("../policy/engine.js");
|
|
93
|
+
const engine = new PolicyEngine("allow", []);
|
|
94
|
+
const match = engine.globMatch.bind(engine);
|
|
95
|
+
expect(match("ab*ab", "ab")).toBe(false);
|
|
96
|
+
expect(match("admin*admin", "admin")).toBe(false);
|
|
97
|
+
expect(match("ab*ab", "abab")).toBe(true);
|
|
98
|
+
expect(match("ab*ab", "abXab")).toBe(true);
|
|
99
|
+
expect(match("*/write_*", "fs/write_file")).toBe(true);
|
|
100
|
+
expect(match("agent:readonly-*", "agent:readonly-bot")).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=bugfix.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bugfix.test.js","sourceRoot":"","sources":["../../src/attestation/bugfix.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chain.test.d.ts","sourceRoot":"","sources":["../../src/attestation/chain.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { unlink, readFile } from "node:fs/promises";
|
|
3
|
+
import { HmacSigner } from "./signer.js";
|
|
4
|
+
import { AuditLog, hashRecord } from "./audit-log.js";
|
|
5
|
+
import { verifyChain } from "./verify.js";
|
|
6
|
+
const TEST_LOG = "/tmp/test-chain-audit.jsonl";
|
|
7
|
+
const SECRET = "c".repeat(64);
|
|
8
|
+
async function readRecords(path) {
|
|
9
|
+
const content = await readFile(path, "utf-8");
|
|
10
|
+
return content
|
|
11
|
+
.split("\n")
|
|
12
|
+
.filter((line) => line.trim().length > 0)
|
|
13
|
+
.map((line) => JSON.parse(line));
|
|
14
|
+
}
|
|
15
|
+
describe("hash chain integrity", () => {
|
|
16
|
+
const signer = new HmacSigner(SECRET);
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
try {
|
|
19
|
+
await unlink(TEST_LOG);
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
});
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
try {
|
|
25
|
+
await unlink(TEST_LOG);
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
});
|
|
29
|
+
it("valid chain passes verification", async () => {
|
|
30
|
+
const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
|
|
31
|
+
await log.init();
|
|
32
|
+
await log.record("tools/call", {
|
|
33
|
+
toolName: "test/tool_a",
|
|
34
|
+
namespace: "test",
|
|
35
|
+
upstream: "test-server",
|
|
36
|
+
principal: "agent:bot",
|
|
37
|
+
durationMs: 100,
|
|
38
|
+
success: true,
|
|
39
|
+
});
|
|
40
|
+
await log.record("tools/call", {
|
|
41
|
+
toolName: "test/tool_b",
|
|
42
|
+
namespace: "test",
|
|
43
|
+
upstream: "test-server",
|
|
44
|
+
principal: "agent:bot",
|
|
45
|
+
durationMs: 50,
|
|
46
|
+
success: true,
|
|
47
|
+
});
|
|
48
|
+
await log.record("tools/call", {
|
|
49
|
+
toolName: "test/tool_c",
|
|
50
|
+
namespace: "test",
|
|
51
|
+
upstream: "test-server",
|
|
52
|
+
principal: "agent:bot",
|
|
53
|
+
durationMs: 75,
|
|
54
|
+
success: true,
|
|
55
|
+
});
|
|
56
|
+
const records = await readRecords(TEST_LOG);
|
|
57
|
+
const result = await verifyChain(records);
|
|
58
|
+
expect(result.valid).toBe(true);
|
|
59
|
+
expect(result.total).toBe(3);
|
|
60
|
+
expect(result.errors).toHaveLength(0);
|
|
61
|
+
// Verify first record has genesis
|
|
62
|
+
expect(records[0].previousHash).toBe("genesis");
|
|
63
|
+
// Verify second record references hash of first
|
|
64
|
+
expect(records[1].previousHash).toBe(hashRecord(records[0]));
|
|
65
|
+
// Verify third record references hash of second
|
|
66
|
+
expect(records[2].previousHash).toBe(hashRecord(records[1]));
|
|
67
|
+
});
|
|
68
|
+
it("detects deleted record", async () => {
|
|
69
|
+
const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
|
|
70
|
+
await log.init();
|
|
71
|
+
await log.record("tools/call", {
|
|
72
|
+
toolName: "test/tool_a",
|
|
73
|
+
durationMs: 100,
|
|
74
|
+
success: true,
|
|
75
|
+
});
|
|
76
|
+
await log.record("tools/call", {
|
|
77
|
+
toolName: "test/tool_b",
|
|
78
|
+
durationMs: 50,
|
|
79
|
+
success: true,
|
|
80
|
+
});
|
|
81
|
+
await log.record("tools/call", {
|
|
82
|
+
toolName: "test/tool_c",
|
|
83
|
+
durationMs: 75,
|
|
84
|
+
success: true,
|
|
85
|
+
});
|
|
86
|
+
const records = await readRecords(TEST_LOG);
|
|
87
|
+
// Remove the middle record
|
|
88
|
+
const tampered = [records[0], records[2]];
|
|
89
|
+
const result = await verifyChain(tampered);
|
|
90
|
+
expect(result.valid).toBe(false);
|
|
91
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
92
|
+
expect(result.errors[0].reason).toBe("previousHash does not match hash of prior record");
|
|
93
|
+
});
|
|
94
|
+
it("detects reordered records", async () => {
|
|
95
|
+
const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
|
|
96
|
+
await log.init();
|
|
97
|
+
await log.record("tools/call", {
|
|
98
|
+
toolName: "test/tool_a",
|
|
99
|
+
durationMs: 100,
|
|
100
|
+
success: true,
|
|
101
|
+
});
|
|
102
|
+
await log.record("tools/call", {
|
|
103
|
+
toolName: "test/tool_b",
|
|
104
|
+
durationMs: 50,
|
|
105
|
+
success: true,
|
|
106
|
+
});
|
|
107
|
+
await log.record("tools/call", {
|
|
108
|
+
toolName: "test/tool_c",
|
|
109
|
+
durationMs: 75,
|
|
110
|
+
success: true,
|
|
111
|
+
});
|
|
112
|
+
const records = await readRecords(TEST_LOG);
|
|
113
|
+
// Swap the second and third records
|
|
114
|
+
const reordered = [records[0], records[2], records[1]];
|
|
115
|
+
const result = await verifyChain(reordered);
|
|
116
|
+
expect(result.valid).toBe(false);
|
|
117
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
118
|
+
expect(result.errors[0].reason).toBe("previousHash does not match hash of prior record");
|
|
119
|
+
});
|
|
120
|
+
it("detects inserted record (previousHash won't match)", async () => {
|
|
121
|
+
const log = new AuditLog(TEST_LOG, signer, 100 * 1024 * 1024);
|
|
122
|
+
await log.init();
|
|
123
|
+
await log.record("tools/call", {
|
|
124
|
+
toolName: "test/tool_a",
|
|
125
|
+
durationMs: 100,
|
|
126
|
+
success: true,
|
|
127
|
+
});
|
|
128
|
+
await log.record("tools/call", {
|
|
129
|
+
toolName: "test/tool_b",
|
|
130
|
+
durationMs: 50,
|
|
131
|
+
success: true,
|
|
132
|
+
});
|
|
133
|
+
const records = await readRecords(TEST_LOG);
|
|
134
|
+
// Insert a fake record between the two
|
|
135
|
+
const inserted = {
|
|
136
|
+
id: "fake-inserted-id",
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
method: "tools/call",
|
|
139
|
+
toolName: "test/malicious_tool",
|
|
140
|
+
durationMs: 1,
|
|
141
|
+
success: true,
|
|
142
|
+
previousHash: hashRecord(records[0]),
|
|
143
|
+
};
|
|
144
|
+
const tampered = [records[0], inserted, records[1]];
|
|
145
|
+
const result = await verifyChain(tampered);
|
|
146
|
+
expect(result.valid).toBe(false);
|
|
147
|
+
// The record after the inserted one will fail because its previousHash
|
|
148
|
+
// was computed against the original record[0], not the inserted record
|
|
149
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
150
|
+
expect(result.errors.some((e) => e.reason === "previousHash does not match hash of prior record")).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=chain.test.js.map
|