tlc-claude-code 1.2.29 → 1.4.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.
Files changed (182) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/UsagePane.d.ts +13 -0
  14. package/dashboard/dist/components/UsagePane.js +51 -0
  15. package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
  16. package/dashboard/dist/components/UsagePane.test.js +142 -0
  17. package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
  18. package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
  19. package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
  20. package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
  21. package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
  22. package/dashboard/dist/components/WorkspacePane.js +17 -0
  23. package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
  24. package/dashboard/dist/components/WorkspacePane.test.js +84 -0
  25. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  26. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  27. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  28. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  29. package/package.json +1 -1
  30. package/server/lib/access-control-doc.js +541 -0
  31. package/server/lib/access-control-doc.test.js +672 -0
  32. package/server/lib/adr-generator.js +423 -0
  33. package/server/lib/adr-generator.test.js +586 -0
  34. package/server/lib/agent-progress-monitor.js +223 -0
  35. package/server/lib/agent-progress-monitor.test.js +202 -0
  36. package/server/lib/architecture-command.js +450 -0
  37. package/server/lib/architecture-command.test.js +754 -0
  38. package/server/lib/ast-analyzer.js +324 -0
  39. package/server/lib/ast-analyzer.test.js +437 -0
  40. package/server/lib/audit-attribution.js +191 -0
  41. package/server/lib/audit-attribution.test.js +359 -0
  42. package/server/lib/audit-classifier.js +202 -0
  43. package/server/lib/audit-classifier.test.js +209 -0
  44. package/server/lib/audit-command.js +275 -0
  45. package/server/lib/audit-command.test.js +325 -0
  46. package/server/lib/audit-exporter.js +380 -0
  47. package/server/lib/audit-exporter.test.js +464 -0
  48. package/server/lib/audit-logger.js +236 -0
  49. package/server/lib/audit-logger.test.js +364 -0
  50. package/server/lib/audit-query.js +257 -0
  51. package/server/lib/audit-query.test.js +352 -0
  52. package/server/lib/audit-storage.js +269 -0
  53. package/server/lib/audit-storage.test.js +272 -0
  54. package/server/lib/auth-system.test.js +4 -1
  55. package/server/lib/boundary-detector.js +427 -0
  56. package/server/lib/boundary-detector.test.js +320 -0
  57. package/server/lib/budget-alerts.js +138 -0
  58. package/server/lib/budget-alerts.test.js +235 -0
  59. package/server/lib/bulk-repo-init.js +342 -0
  60. package/server/lib/bulk-repo-init.test.js +388 -0
  61. package/server/lib/candidates-tracker.js +210 -0
  62. package/server/lib/candidates-tracker.test.js +300 -0
  63. package/server/lib/checkpoint-manager.js +251 -0
  64. package/server/lib/checkpoint-manager.test.js +474 -0
  65. package/server/lib/circular-detector.js +337 -0
  66. package/server/lib/circular-detector.test.js +353 -0
  67. package/server/lib/cohesion-analyzer.js +310 -0
  68. package/server/lib/cohesion-analyzer.test.js +447 -0
  69. package/server/lib/compliance-checklist.js +866 -0
  70. package/server/lib/compliance-checklist.test.js +476 -0
  71. package/server/lib/compliance-command.js +616 -0
  72. package/server/lib/compliance-command.test.js +551 -0
  73. package/server/lib/compliance-reporter.js +692 -0
  74. package/server/lib/compliance-reporter.test.js +707 -0
  75. package/server/lib/contract-testing.js +625 -0
  76. package/server/lib/contract-testing.test.js +342 -0
  77. package/server/lib/conversion-planner.js +469 -0
  78. package/server/lib/conversion-planner.test.js +361 -0
  79. package/server/lib/convert-command.js +351 -0
  80. package/server/lib/convert-command.test.js +608 -0
  81. package/server/lib/coupling-calculator.js +189 -0
  82. package/server/lib/coupling-calculator.test.js +509 -0
  83. package/server/lib/data-flow-doc.js +665 -0
  84. package/server/lib/data-flow-doc.test.js +659 -0
  85. package/server/lib/dependency-graph.js +367 -0
  86. package/server/lib/dependency-graph.test.js +516 -0
  87. package/server/lib/duplication-detector.js +349 -0
  88. package/server/lib/duplication-detector.test.js +401 -0
  89. package/server/lib/ephemeral-storage.js +249 -0
  90. package/server/lib/ephemeral-storage.test.js +254 -0
  91. package/server/lib/evidence-collector.js +627 -0
  92. package/server/lib/evidence-collector.test.js +901 -0
  93. package/server/lib/example-service.js +616 -0
  94. package/server/lib/example-service.test.js +397 -0
  95. package/server/lib/flow-diagram-generator.js +474 -0
  96. package/server/lib/flow-diagram-generator.test.js +446 -0
  97. package/server/lib/idp-manager.js +626 -0
  98. package/server/lib/idp-manager.test.js +587 -0
  99. package/server/lib/impact-scorer.js +184 -0
  100. package/server/lib/impact-scorer.test.js +211 -0
  101. package/server/lib/memory-exclusion.js +326 -0
  102. package/server/lib/memory-exclusion.test.js +241 -0
  103. package/server/lib/mermaid-generator.js +358 -0
  104. package/server/lib/mermaid-generator.test.js +301 -0
  105. package/server/lib/messaging-patterns.js +750 -0
  106. package/server/lib/messaging-patterns.test.js +213 -0
  107. package/server/lib/mfa-handler.js +452 -0
  108. package/server/lib/mfa-handler.test.js +490 -0
  109. package/server/lib/microservice-template.js +386 -0
  110. package/server/lib/microservice-template.test.js +325 -0
  111. package/server/lib/new-project-microservice.js +450 -0
  112. package/server/lib/new-project-microservice.test.js +600 -0
  113. package/server/lib/oauth-flow.js +375 -0
  114. package/server/lib/oauth-flow.test.js +487 -0
  115. package/server/lib/oauth-registry.js +190 -0
  116. package/server/lib/oauth-registry.test.js +306 -0
  117. package/server/lib/readme-generator.js +490 -0
  118. package/server/lib/readme-generator.test.js +493 -0
  119. package/server/lib/refactor-command.js +326 -0
  120. package/server/lib/refactor-command.test.js +528 -0
  121. package/server/lib/refactor-executor.js +254 -0
  122. package/server/lib/refactor-executor.test.js +305 -0
  123. package/server/lib/refactor-observer.js +292 -0
  124. package/server/lib/refactor-observer.test.js +422 -0
  125. package/server/lib/refactor-progress.js +193 -0
  126. package/server/lib/refactor-progress.test.js +251 -0
  127. package/server/lib/refactor-reporter.js +237 -0
  128. package/server/lib/refactor-reporter.test.js +247 -0
  129. package/server/lib/repo-dependency-tracker.js +261 -0
  130. package/server/lib/repo-dependency-tracker.test.js +350 -0
  131. package/server/lib/retention-policy.js +281 -0
  132. package/server/lib/retention-policy.test.js +486 -0
  133. package/server/lib/role-mapper.js +236 -0
  134. package/server/lib/role-mapper.test.js +395 -0
  135. package/server/lib/saml-provider.js +765 -0
  136. package/server/lib/saml-provider.test.js +643 -0
  137. package/server/lib/security-policy-generator.js +682 -0
  138. package/server/lib/security-policy-generator.test.js +544 -0
  139. package/server/lib/semantic-analyzer.js +198 -0
  140. package/server/lib/semantic-analyzer.test.js +474 -0
  141. package/server/lib/sensitive-detector.js +112 -0
  142. package/server/lib/sensitive-detector.test.js +209 -0
  143. package/server/lib/service-interaction-diagram.js +700 -0
  144. package/server/lib/service-interaction-diagram.test.js +638 -0
  145. package/server/lib/service-scaffold.js +486 -0
  146. package/server/lib/service-scaffold.test.js +373 -0
  147. package/server/lib/service-summary.js +553 -0
  148. package/server/lib/service-summary.test.js +619 -0
  149. package/server/lib/session-purge.js +460 -0
  150. package/server/lib/session-purge.test.js +312 -0
  151. package/server/lib/shared-kernel.js +578 -0
  152. package/server/lib/shared-kernel.test.js +255 -0
  153. package/server/lib/sso-command.js +544 -0
  154. package/server/lib/sso-command.test.js +552 -0
  155. package/server/lib/sso-session.js +492 -0
  156. package/server/lib/sso-session.test.js +670 -0
  157. package/server/lib/traefik-config.js +282 -0
  158. package/server/lib/traefik-config.test.js +312 -0
  159. package/server/lib/usage-command.js +218 -0
  160. package/server/lib/usage-command.test.js +391 -0
  161. package/server/lib/usage-formatter.js +192 -0
  162. package/server/lib/usage-formatter.test.js +267 -0
  163. package/server/lib/usage-history.js +122 -0
  164. package/server/lib/usage-history.test.js +206 -0
  165. package/server/lib/workspace-command.js +249 -0
  166. package/server/lib/workspace-command.test.js +264 -0
  167. package/server/lib/workspace-config.js +270 -0
  168. package/server/lib/workspace-config.test.js +312 -0
  169. package/server/lib/workspace-docs-command.js +547 -0
  170. package/server/lib/workspace-docs-command.test.js +692 -0
  171. package/server/lib/workspace-memory.js +451 -0
  172. package/server/lib/workspace-memory.test.js +403 -0
  173. package/server/lib/workspace-scanner.js +452 -0
  174. package/server/lib/workspace-scanner.test.js +677 -0
  175. package/server/lib/workspace-test-runner.js +315 -0
  176. package/server/lib/workspace-test-runner.test.js +294 -0
  177. package/server/lib/zero-retention-command.js +439 -0
  178. package/server/lib/zero-retention-command.test.js +448 -0
  179. package/server/lib/zero-retention.js +322 -0
  180. package/server/lib/zero-retention.test.js +258 -0
  181. package/server/package-lock.json +14 -0
  182. package/server/package.json +1 -0
@@ -0,0 +1,464 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { AuditExporter } from './audit-exporter.js';
6
+
7
+ // Mock the AuditQuery class (dependency being built in parallel)
8
+ const mockQuery = vi.fn().mockResolvedValue([]);
9
+ vi.mock('./audit-query.js', () => ({
10
+ AuditQuery: class MockAuditQuery {
11
+ constructor() {
12
+ this.query = mockQuery;
13
+ }
14
+ },
15
+ }));
16
+
17
+ describe('AuditExporter', () => {
18
+ let testDir;
19
+ let exporter;
20
+
21
+ const sampleEntries = [
22
+ {
23
+ timestamp: '2026-01-15T10:00:00.000Z',
24
+ tool: 'Read',
25
+ params: { file_path: '/test/file1.js' },
26
+ classification: 'file:read',
27
+ severity: 'info',
28
+ attribution: { user: 'alice', source: 'claude' },
29
+ sessionId: 'session-123',
30
+ checksum: 'abc123',
31
+ },
32
+ {
33
+ timestamp: '2026-01-15T10:05:00.000Z',
34
+ tool: 'Write',
35
+ params: { file_path: '/test/file2.js', content: 'test content' },
36
+ classification: 'file:write',
37
+ severity: 'warning',
38
+ attribution: { user: 'bob', source: 'human' },
39
+ sessionId: 'session-456',
40
+ checksum: 'def456',
41
+ },
42
+ {
43
+ timestamp: '2026-01-15T10:10:00.000Z',
44
+ tool: 'Bash',
45
+ params: { command: 'rm -rf /tmp/test' },
46
+ classification: 'shell:execute',
47
+ severity: 'critical',
48
+ attribution: { user: 'charlie', source: 'claude' },
49
+ sessionId: 'session-789',
50
+ checksum: 'ghi789',
51
+ },
52
+ ];
53
+
54
+ beforeEach(() => {
55
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-export-test-'));
56
+ exporter = new AuditExporter(testDir);
57
+ mockQuery.mockResolvedValue([...sampleEntries]);
58
+ });
59
+
60
+ afterEach(() => {
61
+ fs.rmSync(testDir, { recursive: true, force: true });
62
+ vi.clearAllMocks();
63
+ });
64
+
65
+ describe('exportJSON returns valid JSON array', () => {
66
+ it('returns a valid JSON array of entries', async () => {
67
+ const result = await exporter.exportJSON();
68
+
69
+ const parsed = JSON.parse(result);
70
+ expect(Array.isArray(parsed)).toBe(true);
71
+ expect(parsed).toHaveLength(3);
72
+ expect(parsed[0].tool).toBe('Read');
73
+ expect(parsed[1].tool).toBe('Write');
74
+ expect(parsed[2].tool).toBe('Bash');
75
+ });
76
+
77
+ it('includes all entry fields in JSON output', async () => {
78
+ const result = await exporter.exportJSON();
79
+ const parsed = JSON.parse(result);
80
+
81
+ expect(parsed[0]).toHaveProperty('timestamp');
82
+ expect(parsed[0]).toHaveProperty('tool');
83
+ expect(parsed[0]).toHaveProperty('params');
84
+ expect(parsed[0]).toHaveProperty('classification');
85
+ expect(parsed[0]).toHaveProperty('severity');
86
+ expect(parsed[0]).toHaveProperty('attribution');
87
+ });
88
+
89
+ it('returns empty array when no entries', async () => {
90
+ mockQuery.mockResolvedValue([]);
91
+
92
+ const result = await exporter.exportJSON();
93
+ const parsed = JSON.parse(result);
94
+
95
+ expect(parsed).toEqual([]);
96
+ });
97
+ });
98
+
99
+ describe('exportCSV returns valid CSV with headers', () => {
100
+ it('returns CSV with header row', async () => {
101
+ const result = await exporter.exportCSV();
102
+ const lines = result.trim().split('\n');
103
+
104
+ expect(lines[0]).toContain('timestamp');
105
+ expect(lines[0]).toContain('tool');
106
+ expect(lines[0]).toContain('classification');
107
+ expect(lines[0]).toContain('severity');
108
+ });
109
+
110
+ it('returns CSV with data rows', async () => {
111
+ const result = await exporter.exportCSV();
112
+ const lines = result.trim().split('\n');
113
+
114
+ // Header + 3 data rows
115
+ expect(lines.length).toBe(4);
116
+ expect(lines[1]).toContain('Read');
117
+ expect(lines[2]).toContain('Write');
118
+ expect(lines[3]).toContain('Bash');
119
+ });
120
+
121
+ it('properly escapes CSV fields with commas', async () => {
122
+ mockQuery.mockResolvedValue([
123
+ {
124
+ timestamp: '2026-01-15T10:00:00.000Z',
125
+ tool: 'Write',
126
+ params: { file_path: '/test/file.js', content: 'hello, world' },
127
+ classification: 'file:write',
128
+ severity: 'warning',
129
+ attribution: { user: 'alice', source: 'claude' },
130
+ sessionId: 'session-123',
131
+ checksum: 'abc123',
132
+ },
133
+ ]);
134
+
135
+ const result = await exporter.exportCSV();
136
+ // Fields with commas should be quoted
137
+ expect(result).toMatch(/"[^"]*,[^"]*"/);
138
+ });
139
+
140
+ it('returns only header when no entries', async () => {
141
+ mockQuery.mockResolvedValue([]);
142
+
143
+ const result = await exporter.exportCSV();
144
+ const lines = result.trim().split('\n');
145
+
146
+ expect(lines.length).toBe(1);
147
+ expect(lines[0]).toContain('timestamp');
148
+ });
149
+ });
150
+
151
+ describe('exportSplunk returns HEC-compatible events', () => {
152
+ it('returns newline-delimited JSON events', async () => {
153
+ const result = await exporter.exportSplunk();
154
+ const lines = result.trim().split('\n');
155
+
156
+ expect(lines.length).toBe(3);
157
+ lines.forEach((line) => {
158
+ const event = JSON.parse(line);
159
+ expect(event).toBeDefined();
160
+ });
161
+ });
162
+
163
+ it('includes required HEC fields', async () => {
164
+ const result = await exporter.exportSplunk();
165
+ const lines = result.trim().split('\n');
166
+ const event = JSON.parse(lines[0]);
167
+
168
+ expect(event).toHaveProperty('time');
169
+ expect(event).toHaveProperty('event');
170
+ expect(event).toHaveProperty('source', 'tlc');
171
+ expect(event).toHaveProperty('sourcetype', 'tlc:audit');
172
+ });
173
+
174
+ it('converts timestamp to epoch seconds', async () => {
175
+ const result = await exporter.exportSplunk();
176
+ const lines = result.trim().split('\n');
177
+ const event = JSON.parse(lines[0]);
178
+
179
+ // 2026-01-15T10:00:00.000Z in epoch seconds
180
+ const expectedEpoch = new Date('2026-01-15T10:00:00.000Z').getTime() / 1000;
181
+ expect(event.time).toBe(expectedEpoch);
182
+ });
183
+
184
+ it('embeds original entry in event field', async () => {
185
+ const result = await exporter.exportSplunk();
186
+ const lines = result.trim().split('\n');
187
+ const event = JSON.parse(lines[0]);
188
+
189
+ expect(event.event.tool).toBe('Read');
190
+ expect(event.event.classification).toBe('file:read');
191
+ });
192
+ });
193
+
194
+ describe('exportCEF returns CEF-formatted lines', () => {
195
+ it('returns CEF formatted lines', async () => {
196
+ const result = await exporter.exportCEF();
197
+ const lines = result.trim().split('\n');
198
+
199
+ expect(lines.length).toBe(3);
200
+ lines.forEach((line) => {
201
+ expect(line.startsWith('CEF:')).toBe(true);
202
+ });
203
+ });
204
+
205
+ it('follows CEF spec format', async () => {
206
+ const result = await exporter.exportCEF();
207
+ const lines = result.trim().split('\n');
208
+
209
+ // CEF:Version|Device Vendor|Device Product|Device Version|Signature ID|Name|Severity|Extension
210
+ const parts = lines[0].split('|');
211
+ expect(parts.length).toBe(8);
212
+ expect(parts[0]).toMatch(/^CEF:\d+$/);
213
+ expect(parts[1]).toBe('TLC'); // Device Vendor
214
+ expect(parts[2]).toBe('AuditLogger'); // Device Product
215
+ });
216
+
217
+ it('maps severity correctly', async () => {
218
+ const result = await exporter.exportCEF();
219
+ const lines = result.trim().split('\n');
220
+
221
+ // CEF severity is 0-10 scale
222
+ // info -> 1-3, warning -> 4-6, critical -> 7-10
223
+ const parts0 = lines[0].split('|'); // severity: info
224
+ const parts1 = lines[1].split('|'); // severity: warning
225
+ const parts2 = lines[2].split('|'); // severity: critical
226
+
227
+ expect(parseInt(parts0[6])).toBeLessThanOrEqual(3); // info
228
+ expect(parseInt(parts1[6])).toBeGreaterThanOrEqual(4); // warning
229
+ expect(parseInt(parts1[6])).toBeLessThanOrEqual(6);
230
+ expect(parseInt(parts2[6])).toBeGreaterThanOrEqual(7); // critical
231
+ });
232
+
233
+ it('includes extension fields', async () => {
234
+ const result = await exporter.exportCEF();
235
+ const lines = result.trim().split('\n');
236
+
237
+ // Extension is the last part
238
+ const extension = lines[0].split('|')[7];
239
+ expect(extension).toContain('rt='); // receipt time
240
+ expect(extension).toContain('src='); // source or suser
241
+ });
242
+ });
243
+
244
+ describe('export filters by date range', () => {
245
+ it('passes date range to query', async () => {
246
+ const from = new Date('2026-01-15T00:00:00Z');
247
+ const to = new Date('2026-01-15T23:59:59Z');
248
+
249
+ await exporter.exportJSON({ from, to });
250
+
251
+ expect(mockQuery).toHaveBeenCalledWith(
252
+ expect.objectContaining({
253
+ from,
254
+ to,
255
+ })
256
+ );
257
+ });
258
+
259
+ it('filters entries by from date', async () => {
260
+ mockQuery.mockImplementation(async ({ from }) => {
261
+ return sampleEntries.filter((e) =>
262
+ !from || new Date(e.timestamp) >= from
263
+ );
264
+ });
265
+
266
+ const from = new Date('2026-01-15T10:05:00Z');
267
+ const result = await exporter.exportJSON({ from });
268
+ const parsed = JSON.parse(result);
269
+
270
+ expect(parsed.length).toBe(2);
271
+ expect(parsed[0].tool).toBe('Write');
272
+ });
273
+
274
+ it('filters entries by to date', async () => {
275
+ mockQuery.mockImplementation(async ({ to }) => {
276
+ return sampleEntries.filter((e) =>
277
+ !to || new Date(e.timestamp) <= to
278
+ );
279
+ });
280
+
281
+ const to = new Date('2026-01-15T10:05:00Z');
282
+ const result = await exporter.exportJSON({ to });
283
+ const parsed = JSON.parse(result);
284
+
285
+ expect(parsed.length).toBe(2);
286
+ expect(parsed[1].tool).toBe('Write');
287
+ });
288
+ });
289
+
290
+ describe('export supports incremental mode', () => {
291
+ it('uses lastExportPosition for incremental export', async () => {
292
+ // First export
293
+ await exporter.exportJSON({ incremental: true });
294
+
295
+ // Should have saved last position
296
+ expect(exporter.getLastExportPosition()).toBeDefined();
297
+ });
298
+
299
+ it('incremental export only returns new entries', async () => {
300
+ // Set up mock to track calls
301
+ const allEntries = [...sampleEntries];
302
+
303
+ mockQuery.mockImplementation(async ({ afterChecksum }) => {
304
+ if (afterChecksum) {
305
+ const idx = allEntries.findIndex((e) => e.checksum === afterChecksum);
306
+ return allEntries.slice(idx + 1);
307
+ }
308
+ return allEntries;
309
+ });
310
+
311
+ // First incremental export gets all
312
+ const result1 = await exporter.exportJSON({ incremental: true });
313
+ const parsed1 = JSON.parse(result1);
314
+ expect(parsed1.length).toBe(3);
315
+
316
+ // Second incremental export gets none (no new entries)
317
+ const result2 = await exporter.exportJSON({ incremental: true });
318
+ const parsed2 = JSON.parse(result2);
319
+ expect(parsed2.length).toBe(0);
320
+ });
321
+ });
322
+
323
+ describe('exportIncremental tracks last export position', () => {
324
+ it('saves last checksum after export', async () => {
325
+ await exporter.exportJSON({ incremental: true });
326
+
327
+ const position = exporter.getLastExportPosition();
328
+ expect(position).toBe('ghi789'); // Last entry's checksum
329
+ });
330
+
331
+ it('loads saved position on new exporter instance', async () => {
332
+ await exporter.exportJSON({ incremental: true });
333
+
334
+ // Create new exporter instance
335
+ const exporter2 = new AuditExporter(testDir);
336
+ const position = exporter2.getLastExportPosition();
337
+
338
+ expect(position).toBe('ghi789');
339
+ });
340
+
341
+ it('resets position when reset=true', async () => {
342
+ await exporter.exportJSON({ incremental: true });
343
+ expect(exporter.getLastExportPosition()).toBe('ghi789');
344
+
345
+ exporter.resetExportPosition();
346
+ expect(exporter.getLastExportPosition()).toBeNull();
347
+ });
348
+ });
349
+
350
+ describe('formatForSplunk includes required fields', () => {
351
+ it('formats single entry correctly', () => {
352
+ const entry = sampleEntries[0];
353
+ const formatted = exporter.formatForSplunk(entry);
354
+
355
+ expect(formatted.time).toBe(new Date(entry.timestamp).getTime() / 1000);
356
+ expect(formatted.source).toBe('tlc');
357
+ expect(formatted.sourcetype).toBe('tlc:audit');
358
+ expect(formatted.event).toMatchObject({
359
+ tool: 'Read',
360
+ classification: 'file:read',
361
+ });
362
+ });
363
+
364
+ it('includes host field when configured', () => {
365
+ const exporterWithHost = new AuditExporter(testDir, { host: 'prod-server-01' });
366
+ const formatted = exporterWithHost.formatForSplunk(sampleEntries[0]);
367
+
368
+ expect(formatted.host).toBe('prod-server-01');
369
+ });
370
+
371
+ it('includes index field when configured', () => {
372
+ const exporterWithIndex = new AuditExporter(testDir, { splunkIndex: 'main' });
373
+ const formatted = exporterWithIndex.formatForSplunk(sampleEntries[0]);
374
+
375
+ expect(formatted.index).toBe('main');
376
+ });
377
+ });
378
+
379
+ describe('formatForCEF follows CEF spec', () => {
380
+ it('formats single entry to CEF', () => {
381
+ const entry = sampleEntries[0];
382
+ const formatted = exporter.formatForCEF(entry);
383
+
384
+ expect(formatted.startsWith('CEF:')).toBe(true);
385
+ const parts = formatted.split('|');
386
+ expect(parts.length).toBe(8);
387
+ });
388
+
389
+ it('escapes pipe characters in fields', () => {
390
+ const entryWithPipe = {
391
+ ...sampleEntries[0],
392
+ tool: 'Read|Write',
393
+ };
394
+ const formatted = exporter.formatForCEF(entryWithPipe);
395
+
396
+ // Pipe should be escaped in CEF
397
+ expect(formatted).toContain('Read\\|Write');
398
+ });
399
+
400
+ it('escapes backslash and equals in extension', () => {
401
+ const entryWithSpecial = {
402
+ ...sampleEntries[0],
403
+ params: { file_path: 'C:\\test\\file=test.js' },
404
+ };
405
+ const formatted = exporter.formatForCEF(entryWithSpecial);
406
+
407
+ // Extension values should have special chars escaped
408
+ const extension = formatted.split('|')[7];
409
+ expect(extension).toMatch(/\\\\/); // escaped backslash
410
+ });
411
+
412
+ it('uses correct CEF version', () => {
413
+ const formatted = exporter.formatForCEF(sampleEntries[0]);
414
+ expect(formatted.startsWith('CEF:0')).toBe(true);
415
+ });
416
+
417
+ it('maps classification to signature ID', () => {
418
+ const formatted = exporter.formatForCEF(sampleEntries[0]);
419
+ const parts = formatted.split('|');
420
+ // Signature ID is part 4
421
+ expect(parts[4]).toBe('file:read');
422
+ });
423
+ });
424
+
425
+ describe('export to file', () => {
426
+ it('writes JSON export to file', async () => {
427
+ const filePath = path.join(testDir, 'export.json');
428
+ await exporter.exportToFile(filePath, 'json');
429
+
430
+ expect(fs.existsSync(filePath)).toBe(true);
431
+ const content = fs.readFileSync(filePath, 'utf-8');
432
+ const parsed = JSON.parse(content);
433
+ expect(parsed).toHaveLength(3);
434
+ });
435
+
436
+ it('writes CSV export to file', async () => {
437
+ const filePath = path.join(testDir, 'export.csv');
438
+ await exporter.exportToFile(filePath, 'csv');
439
+
440
+ expect(fs.existsSync(filePath)).toBe(true);
441
+ const content = fs.readFileSync(filePath, 'utf-8');
442
+ expect(content).toContain('timestamp,tool');
443
+ });
444
+
445
+ it('writes Splunk export to file', async () => {
446
+ const filePath = path.join(testDir, 'export.splunk');
447
+ await exporter.exportToFile(filePath, 'splunk');
448
+
449
+ expect(fs.existsSync(filePath)).toBe(true);
450
+ const content = fs.readFileSync(filePath, 'utf-8');
451
+ const lines = content.trim().split('\n');
452
+ expect(JSON.parse(lines[0]).sourcetype).toBe('tlc:audit');
453
+ });
454
+
455
+ it('writes CEF export to file', async () => {
456
+ const filePath = path.join(testDir, 'export.cef');
457
+ await exporter.exportToFile(filePath, 'cef');
458
+
459
+ expect(fs.existsSync(filePath)).toBe(true);
460
+ const content = fs.readFileSync(filePath, 'utf-8');
461
+ expect(content).toContain('CEF:0');
462
+ });
463
+ });
464
+ });
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Audit Logger - Main logger that combines storage, classification, and attribution
3
+ *
4
+ * Features:
5
+ * - Creates complete audit entries with tool name, params, and result
6
+ * - Automatically classifies actions using audit-classifier
7
+ * - Automatically attributes actions using audit-attribution
8
+ * - Adds timestamps in ISO 8601 format
9
+ * - Sanitizes sensitive values from parameters
10
+ * - Supports async batch writing for performance
11
+ */
12
+
13
+ import { AuditStorage } from './audit-storage.js';
14
+ import { classifyAction, detectSensitive, getSeverity } from './audit-classifier.js';
15
+ import { getAttribution, identifySource, createSessionId } from './audit-attribution.js';
16
+
17
+ /**
18
+ * Patterns for sensitive data that should be redacted
19
+ */
20
+ const SENSITIVE_PATTERNS = [
21
+ // API keys and tokens
22
+ { pattern: /sk-[a-zA-Z0-9]+/g, replacement: '[REDACTED]' },
23
+ { pattern: /ghp_[a-zA-Z0-9]+/g, replacement: '[REDACTED]' },
24
+ { pattern: /Bearer\s+[a-zA-Z0-9._-]+/gi, replacement: 'Bearer [REDACTED]' },
25
+
26
+ // Key-value patterns
27
+ { pattern: /password\s*[=:]\s*\S+/gi, replacement: 'password=[REDACTED]' },
28
+ { pattern: /api[_-]?key\s*[=:]\s*\S+/gi, replacement: 'api_key=[REDACTED]' },
29
+ { pattern: /secret\s*[=:]\s*\S+/gi, replacement: 'secret=[REDACTED]' },
30
+ { pattern: /token\s*[=:]\s*\S+/gi, replacement: 'token=[REDACTED]' },
31
+
32
+ // AWS
33
+ { pattern: /AWS_SECRET[_A-Z]*\s*[=:]\s*\S+/gi, replacement: 'AWS_SECRET=[REDACTED]' },
34
+
35
+ // Private keys
36
+ { pattern: /PRIVATE[_-]?KEY\s*[=:]\s*\S+/gi, replacement: 'PRIVATE_KEY=[REDACTED]' },
37
+ ];
38
+
39
+ /**
40
+ * Keys that should always be redacted regardless of value
41
+ */
42
+ const SENSITIVE_KEYS = [
43
+ 'password',
44
+ 'secret',
45
+ 'token',
46
+ 'api_key',
47
+ 'apikey',
48
+ 'api-key',
49
+ 'private_key',
50
+ 'privatekey',
51
+ 'access_token',
52
+ 'refresh_token',
53
+ 'auth_token',
54
+ 'authorization',
55
+ 'credential',
56
+ 'credentials',
57
+ ];
58
+
59
+ /**
60
+ * Sanitize a string value by redacting sensitive patterns
61
+ * @param {string} value - String to sanitize
62
+ * @returns {string} Sanitized string
63
+ */
64
+ function sanitizeString(value) {
65
+ if (typeof value !== 'string') {
66
+ return value;
67
+ }
68
+
69
+ let sanitized = value;
70
+ for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
71
+ sanitized = sanitized.replace(pattern, replacement);
72
+ }
73
+ return sanitized;
74
+ }
75
+
76
+ /**
77
+ * Check if a key name indicates sensitive data
78
+ * @param {string} key - Key name to check
79
+ * @returns {boolean} True if key is sensitive
80
+ */
81
+ function isSensitiveKey(key) {
82
+ const lowerKey = key.toLowerCase();
83
+ return SENSITIVE_KEYS.some((sensitiveKey) => lowerKey.includes(sensitiveKey));
84
+ }
85
+
86
+ /**
87
+ * Sanitize parameters by removing sensitive values while preserving structure
88
+ * @param {Object} params - Parameters to sanitize
89
+ * @returns {Object} Sanitized parameters
90
+ */
91
+ export function sanitizeParams(params) {
92
+ if (params === null || params === undefined) {
93
+ return params;
94
+ }
95
+
96
+ if (Array.isArray(params)) {
97
+ return params.map((item) => sanitizeParams(item));
98
+ }
99
+
100
+ if (typeof params === 'object') {
101
+ const sanitized = {};
102
+ for (const [key, value] of Object.entries(params)) {
103
+ if (value === null || value === undefined) {
104
+ sanitized[key] = value;
105
+ } else if (isSensitiveKey(key)) {
106
+ sanitized[key] = '[REDACTED]';
107
+ } else if (typeof value === 'string') {
108
+ sanitized[key] = sanitizeString(value);
109
+ } else if (typeof value === 'object') {
110
+ sanitized[key] = sanitizeParams(value);
111
+ } else {
112
+ sanitized[key] = value;
113
+ }
114
+ }
115
+ return sanitized;
116
+ }
117
+
118
+ if (typeof params === 'string') {
119
+ return sanitizeString(params);
120
+ }
121
+
122
+ return params;
123
+ }
124
+
125
+ /**
126
+ * AuditLogger class for logging actions with classification and attribution
127
+ */
128
+ export class AuditLogger {
129
+ /**
130
+ * Create an AuditLogger instance
131
+ * @param {Object} options - Configuration options
132
+ * @param {string} options.baseDir - Base directory for storage
133
+ * @param {boolean} options.batchMode - Enable batch writing mode
134
+ * @param {number} options.batchSize - Number of entries to batch before writing
135
+ */
136
+ constructor(options = {}) {
137
+ const { baseDir, batchMode = false, batchSize = 10 } = options;
138
+
139
+ this.storage = new AuditStorage(baseDir);
140
+ this.batchMode = batchMode;
141
+ this.batchSize = batchSize;
142
+ this.pendingEntries = [];
143
+ this.sessionId = createSessionId();
144
+ }
145
+
146
+ /**
147
+ * Get the number of pending entries in batch mode
148
+ * @returns {number} Number of pending entries
149
+ */
150
+ getPendingCount() {
151
+ return this.pendingEntries.length;
152
+ }
153
+
154
+ /**
155
+ * Log an action with full classification and attribution
156
+ * @param {string} tool - Tool name (e.g., 'Read', 'Write', 'Bash')
157
+ * @param {Object} params - Tool parameters
158
+ * @param {Object} result - Tool result
159
+ * @param {Object} context - Additional context
160
+ * @returns {Promise<Object>} The logged entry
161
+ */
162
+ async logAction(tool, params, result, context = {}) {
163
+ const timestamp = new Date().toISOString();
164
+
165
+ // Get classification
166
+ const action = { tool, params };
167
+ const classification = classifyAction(action);
168
+ const severity = getSeverity(action);
169
+ const sensitive = detectSensitive(action);
170
+
171
+ // Get attribution
172
+ const attribution = await getAttribution();
173
+ const sourceType = identifySource({
174
+ toolName: tool,
175
+ ...context,
176
+ });
177
+
178
+ // Build the audit entry
179
+ const entry = {
180
+ timestamp,
181
+ tool,
182
+ params: sanitizeParams(params),
183
+ result,
184
+ classification,
185
+ severity,
186
+ sensitive,
187
+ attribution,
188
+ sourceType,
189
+ sessionId: this.sessionId,
190
+ context: {
191
+ ...context,
192
+ },
193
+ };
194
+
195
+ // Handle batch mode
196
+ if (this.batchMode) {
197
+ this.pendingEntries.push(entry);
198
+
199
+ // Flush if batch size reached
200
+ if (this.pendingEntries.length >= this.batchSize) {
201
+ await this.flushBatch();
202
+ }
203
+
204
+ return entry;
205
+ }
206
+
207
+ // Write immediately
208
+ await this.storage.appendEntry(entry);
209
+ return entry;
210
+ }
211
+
212
+ /**
213
+ * Flush all pending entries to storage
214
+ * @returns {Promise<void>}
215
+ */
216
+ async flushBatch() {
217
+ if (this.pendingEntries.length === 0) {
218
+ return;
219
+ }
220
+
221
+ const entries = [...this.pendingEntries];
222
+ this.pendingEntries = [];
223
+
224
+ for (const entry of entries) {
225
+ await this.storage.appendEntry(entry);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Get the current session ID
231
+ * @returns {string} Session ID
232
+ */
233
+ getSessionId() {
234
+ return this.sessionId;
235
+ }
236
+ }