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.
- package/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/UsagePane.d.ts +13 -0
- package/dashboard/dist/components/UsagePane.js +51 -0
- package/dashboard/dist/components/UsagePane.test.d.ts +1 -0
- package/dashboard/dist/components/UsagePane.test.js +142 -0
- package/dashboard/dist/components/WorkspaceDocsPane.d.ts +19 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +130 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspaceDocsPane.test.js +242 -0
- package/dashboard/dist/components/WorkspacePane.d.ts +18 -0
- package/dashboard/dist/components/WorkspacePane.js +17 -0
- package/dashboard/dist/components/WorkspacePane.test.d.ts +1 -0
- package/dashboard/dist/components/WorkspacePane.test.js +84 -0
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/architecture-command.js +450 -0
- package/server/lib/architecture-command.test.js +754 -0
- package/server/lib/ast-analyzer.js +324 -0
- package/server/lib/ast-analyzer.test.js +437 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/auth-system.test.js +4 -1
- package/server/lib/boundary-detector.js +427 -0
- package/server/lib/boundary-detector.test.js +320 -0
- package/server/lib/budget-alerts.js +138 -0
- package/server/lib/budget-alerts.test.js +235 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/candidates-tracker.js +210 -0
- package/server/lib/candidates-tracker.test.js +300 -0
- package/server/lib/checkpoint-manager.js +251 -0
- package/server/lib/checkpoint-manager.test.js +474 -0
- package/server/lib/circular-detector.js +337 -0
- package/server/lib/circular-detector.test.js +353 -0
- package/server/lib/cohesion-analyzer.js +310 -0
- package/server/lib/cohesion-analyzer.test.js +447 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/contract-testing.js +625 -0
- package/server/lib/contract-testing.test.js +342 -0
- package/server/lib/conversion-planner.js +469 -0
- package/server/lib/conversion-planner.test.js +361 -0
- package/server/lib/convert-command.js +351 -0
- package/server/lib/convert-command.test.js +608 -0
- package/server/lib/coupling-calculator.js +189 -0
- package/server/lib/coupling-calculator.test.js +509 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/dependency-graph.js +367 -0
- package/server/lib/dependency-graph.test.js +516 -0
- package/server/lib/duplication-detector.js +349 -0
- package/server/lib/duplication-detector.test.js +401 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/example-service.js +616 -0
- package/server/lib/example-service.test.js +397 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/impact-scorer.js +184 -0
- package/server/lib/impact-scorer.test.js +211 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mermaid-generator.js +358 -0
- package/server/lib/mermaid-generator.test.js +301 -0
- package/server/lib/messaging-patterns.js +750 -0
- package/server/lib/messaging-patterns.test.js +213 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/microservice-template.js +386 -0
- package/server/lib/microservice-template.test.js +325 -0
- package/server/lib/new-project-microservice.js +450 -0
- package/server/lib/new-project-microservice.test.js +600 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/refactor-command.js +326 -0
- package/server/lib/refactor-command.test.js +528 -0
- package/server/lib/refactor-executor.js +254 -0
- package/server/lib/refactor-executor.test.js +305 -0
- package/server/lib/refactor-observer.js +292 -0
- package/server/lib/refactor-observer.test.js +422 -0
- package/server/lib/refactor-progress.js +193 -0
- package/server/lib/refactor-progress.test.js +251 -0
- package/server/lib/refactor-reporter.js +237 -0
- package/server/lib/refactor-reporter.test.js +247 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/semantic-analyzer.js +198 -0
- package/server/lib/semantic-analyzer.test.js +474 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-scaffold.js +486 -0
- package/server/lib/service-scaffold.test.js +373 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/shared-kernel.js +578 -0
- package/server/lib/shared-kernel.test.js +255 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/traefik-config.js +282 -0
- package/server/lib/traefik-config.test.js +312 -0
- package/server/lib/usage-command.js +218 -0
- package/server/lib/usage-command.test.js +391 -0
- package/server/lib/usage-formatter.js +192 -0
- package/server/lib/usage-formatter.test.js +267 -0
- package/server/lib/usage-history.js +122 -0
- package/server/lib/usage-history.test.js +206 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
- package/server/package-lock.json +14 -0
- 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
|
+
}
|