@agent-wall/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.
@@ -0,0 +1,225 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ import * as crypto from "node:crypto";
6
+ import { AuditLogger, checkFilePermissions } from "./audit-logger.js";
7
+
8
+ describe("AuditLogger Security", () => {
9
+ const tmpDir = path.join(os.tmpdir(), `aw-audit-test-${crypto.randomUUID()}`);
10
+
11
+ function tmpFile(name: string): string {
12
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
13
+ return path.join(tmpDir, name);
14
+ }
15
+
16
+ afterEach(() => {
17
+ try {
18
+ const files = fs.readdirSync(tmpDir);
19
+ for (const f of files) fs.unlinkSync(path.join(tmpDir, f));
20
+ fs.rmdirSync(tmpDir);
21
+ } catch { /* ignore */ }
22
+ });
23
+
24
+ describe("HMAC-SHA256 signing", () => {
25
+ it("should add _sig and _seq when signing is enabled", () => {
26
+ const logFile = tmpFile("signed.jsonl");
27
+ const logger = new AuditLogger({
28
+ filePath: logFile,
29
+ signing: true,
30
+ signingKey: "test-key-123",
31
+ stdout: false,
32
+ silent: true,
33
+ });
34
+
35
+ logger.log({
36
+ timestamp: "2026-01-01T00:00:00Z",
37
+ sessionId: "test",
38
+ direction: "request",
39
+ method: "tools/call",
40
+ tool: "read_file",
41
+ });
42
+
43
+ logger.close();
44
+
45
+ const content = fs.readFileSync(logFile, "utf-8").trim();
46
+ const entry = JSON.parse(content);
47
+ expect(entry._sig).toBeDefined();
48
+ expect(entry._seq).toBe(1);
49
+ expect(typeof entry._sig).toBe("string");
50
+ expect(entry._sig.length).toBe(64); // SHA256 hex = 64 chars
51
+ });
52
+
53
+ it("should chain signatures (each depends on previous)", () => {
54
+ const logFile = tmpFile("chain.jsonl");
55
+ const key = "chain-test-key";
56
+ const logger = new AuditLogger({
57
+ filePath: logFile,
58
+ signing: true,
59
+ signingKey: key,
60
+ stdout: false,
61
+ silent: true,
62
+ });
63
+
64
+ logger.log({
65
+ timestamp: "2026-01-01T00:00:01Z",
66
+ sessionId: "test",
67
+ direction: "request",
68
+ method: "tools/call",
69
+ tool: "tool_a",
70
+ });
71
+ logger.log({
72
+ timestamp: "2026-01-01T00:00:02Z",
73
+ sessionId: "test",
74
+ direction: "request",
75
+ method: "tools/call",
76
+ tool: "tool_b",
77
+ });
78
+ logger.close();
79
+
80
+ const lines = fs.readFileSync(logFile, "utf-8").trim().split("\n");
81
+ const entry1 = JSON.parse(lines[0]);
82
+ const entry2 = JSON.parse(lines[1]);
83
+
84
+ expect(entry1._seq).toBe(1);
85
+ expect(entry2._seq).toBe(2);
86
+ // Signatures should be different (different entries + chained)
87
+ expect(entry1._sig).not.toBe(entry2._sig);
88
+ });
89
+
90
+ it("should verify a valid chain", () => {
91
+ const logFile = tmpFile("verify.jsonl");
92
+ const key = "verify-key-abc";
93
+ const logger = new AuditLogger({
94
+ filePath: logFile,
95
+ signing: true,
96
+ signingKey: key,
97
+ stdout: false,
98
+ silent: true,
99
+ });
100
+
101
+ for (let i = 0; i < 5; i++) {
102
+ logger.log({
103
+ timestamp: new Date().toISOString(),
104
+ sessionId: "test",
105
+ direction: "request",
106
+ method: "tools/call",
107
+ tool: `tool_${i}`,
108
+ });
109
+ }
110
+ logger.close();
111
+
112
+ const result = AuditLogger.verifyChain(logFile, key);
113
+ expect(result.valid).toBe(true);
114
+ expect(result.entries).toBe(5);
115
+ expect(result.firstBroken).toBeNull();
116
+ });
117
+
118
+ it("should detect tampered entries", () => {
119
+ const logFile = tmpFile("tampered.jsonl");
120
+ const key = "tamper-key-xyz";
121
+ const logger = new AuditLogger({
122
+ filePath: logFile,
123
+ signing: true,
124
+ signingKey: key,
125
+ stdout: false,
126
+ silent: true,
127
+ });
128
+
129
+ for (let i = 0; i < 3; i++) {
130
+ logger.log({
131
+ timestamp: new Date().toISOString(),
132
+ sessionId: "test",
133
+ direction: "request",
134
+ method: "tools/call",
135
+ tool: `tool_${i}`,
136
+ });
137
+ }
138
+ logger.close();
139
+
140
+ // Tamper with the second entry
141
+ const lines = fs.readFileSync(logFile, "utf-8").trim().split("\n");
142
+ const entry = JSON.parse(lines[1]);
143
+ entry.tool = "TAMPERED";
144
+ lines[1] = JSON.stringify(entry);
145
+ fs.writeFileSync(logFile, lines.join("\n") + "\n");
146
+
147
+ const result = AuditLogger.verifyChain(logFile, key);
148
+ expect(result.valid).toBe(false);
149
+ expect(result.firstBroken).toBe(1);
150
+ });
151
+ });
152
+
153
+ describe("log rotation", () => {
154
+ it("should rotate when file exceeds max size", () => {
155
+ const logFile = tmpFile("rotate.jsonl");
156
+ const logger = new AuditLogger({
157
+ filePath: logFile,
158
+ maxFileSize: 200, // Very small — triggers rotation quickly
159
+ maxFiles: 3,
160
+ stdout: false,
161
+ silent: true,
162
+ });
163
+
164
+ // Write enough entries to trigger rotation
165
+ for (let i = 0; i < 20; i++) {
166
+ logger.log({
167
+ timestamp: new Date().toISOString(),
168
+ sessionId: "test",
169
+ direction: "request",
170
+ method: "tools/call",
171
+ tool: `tool_${i}`,
172
+ });
173
+ }
174
+ logger.close();
175
+
176
+ // Check that rotated files exist
177
+ expect(fs.existsSync(logFile)).toBe(true);
178
+ expect(fs.existsSync(`${logFile}.1`)).toBe(true);
179
+ });
180
+ });
181
+
182
+ describe("file permission checks", () => {
183
+ it("should report safe for normal files", () => {
184
+ const testFile = tmpFile("safe-policy.yaml");
185
+ fs.writeFileSync(testFile, "version: 1\nrules: []");
186
+
187
+ const result = checkFilePermissions(testFile);
188
+ // On Windows/MINGW, permission checks are best-effort
189
+ // The function should at least not throw
190
+ expect(result).toBeDefined();
191
+ expect(Array.isArray(result.warnings)).toBe(true);
192
+ });
193
+
194
+ it("should handle non-existent files", () => {
195
+ const result = checkFilePermissions("/nonexistent/file.yaml");
196
+ expect(result.safe).toBe(false);
197
+ expect(result.warnings.length).toBeGreaterThan(0);
198
+ });
199
+ });
200
+
201
+ describe("unsigned logging still works", () => {
202
+ it("should log without signatures when signing is disabled", () => {
203
+ const logFile = tmpFile("unsigned.jsonl");
204
+ const logger = new AuditLogger({
205
+ filePath: logFile,
206
+ signing: false,
207
+ stdout: false,
208
+ silent: true,
209
+ });
210
+
211
+ logger.log({
212
+ timestamp: "2026-01-01T00:00:00Z",
213
+ sessionId: "test",
214
+ direction: "request",
215
+ method: "tools/call",
216
+ });
217
+ logger.close();
218
+
219
+ const content = fs.readFileSync(logFile, "utf-8").trim();
220
+ const entry = JSON.parse(content);
221
+ expect(entry._sig).toBeUndefined();
222
+ expect(entry._seq).toBeUndefined();
223
+ });
224
+ });
225
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Tests for AuditLogger — structured logging.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, vi } from "vitest";
6
+ import { AuditLogger } from "./audit-logger.js";
7
+
8
+ describe("AuditLogger", () => {
9
+ it("should store logged entries", () => {
10
+ const logger = new AuditLogger({ stdout: false, silent: true });
11
+
12
+ logger.logAllow("sess-1", "read_file", { path: "/test" }, "allow-read", "Allowed");
13
+ logger.logDeny("sess-1", "shell_exec", { command: "rm -rf /" }, "block-shell", "Blocked");
14
+
15
+ const entries = logger.getEntries();
16
+ expect(entries).toHaveLength(2);
17
+ expect(entries[0].verdict?.action).toBe("allow");
18
+ expect(entries[1].verdict?.action).toBe("deny");
19
+ });
20
+
21
+ it("should compute stats correctly", () => {
22
+ const logger = new AuditLogger({ stdout: false, silent: true });
23
+
24
+ logger.logAllow("s", "a", {}, null, "");
25
+ logger.logAllow("s", "b", {}, null, "");
26
+ logger.logDeny("s", "c", {}, null, "");
27
+
28
+ const stats = logger.getStats();
29
+ expect(stats.total).toBe(3);
30
+ expect(stats.allowed).toBe(2);
31
+ expect(stats.denied).toBe(1);
32
+ expect(stats.prompted).toBe(0);
33
+ });
34
+
35
+ it("should redact sensitive argument keys", () => {
36
+ const logger = new AuditLogger({ stdout: false, silent: true, redact: true });
37
+
38
+ logger.log({
39
+ timestamp: new Date().toISOString(),
40
+ sessionId: "s",
41
+ direction: "request",
42
+ method: "tools/call",
43
+ tool: "api_call",
44
+ arguments: {
45
+ api_key: "sk-12345secret",
46
+ url: "https://example.com",
47
+ password: "hunter2",
48
+ },
49
+ });
50
+
51
+ const entries = logger.getEntries();
52
+ expect(entries[0].arguments?.api_key).toBe("[REDACTED]");
53
+ expect(entries[0].arguments?.password).toBe("[REDACTED]");
54
+ expect(entries[0].arguments?.url).toBe("https://example.com");
55
+ });
56
+
57
+ it("should truncate long argument values", () => {
58
+ const logger = new AuditLogger({
59
+ stdout: false,
60
+ silent: true,
61
+ redact: true,
62
+ maxArgLength: 50,
63
+ });
64
+
65
+ const longValue = "a".repeat(200);
66
+ logger.log({
67
+ timestamp: new Date().toISOString(),
68
+ sessionId: "s",
69
+ direction: "request",
70
+ method: "tools/call",
71
+ arguments: { content: longValue },
72
+ });
73
+
74
+ const entries = logger.getEntries();
75
+ const content = entries[0].arguments?.content as string;
76
+ expect(content.length).toBeLessThan(200);
77
+ expect(content).toContain("[truncated]");
78
+ });
79
+
80
+ it("should not redact when disabled", () => {
81
+ const logger = new AuditLogger({ stdout: false, silent: true, redact: false });
82
+
83
+ logger.log({
84
+ timestamp: new Date().toISOString(),
85
+ sessionId: "s",
86
+ direction: "request",
87
+ method: "tools/call",
88
+ arguments: { api_key: "visible" },
89
+ });
90
+
91
+ expect(logger.getEntries()[0].arguments?.api_key).toBe("visible");
92
+ });
93
+ });