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,352 @@
|
|
|
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 { AuditQuery } from './audit-query.js';
|
|
6
|
+
import { AuditStorage } from './audit-storage.js';
|
|
7
|
+
|
|
8
|
+
describe('AuditQuery', () => {
|
|
9
|
+
let testDir;
|
|
10
|
+
let auditStorage;
|
|
11
|
+
let auditQuery;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-audit-query-test-'));
|
|
15
|
+
auditStorage = new AuditStorage(testDir);
|
|
16
|
+
auditQuery = new AuditQuery(auditStorage);
|
|
17
|
+
|
|
18
|
+
// Seed test data
|
|
19
|
+
await auditStorage.appendEntry({
|
|
20
|
+
action: 'user.login',
|
|
21
|
+
user: 'alice',
|
|
22
|
+
severity: 'info',
|
|
23
|
+
timestamp: new Date('2026-01-15T10:00:00Z').getTime(),
|
|
24
|
+
parameters: { ip: '192.168.1.1', browser: 'Chrome' },
|
|
25
|
+
});
|
|
26
|
+
await auditStorage.appendEntry({
|
|
27
|
+
action: 'user.logout',
|
|
28
|
+
user: 'alice',
|
|
29
|
+
severity: 'info',
|
|
30
|
+
timestamp: new Date('2026-01-15T11:00:00Z').getTime(),
|
|
31
|
+
parameters: { duration: '1h' },
|
|
32
|
+
});
|
|
33
|
+
await auditStorage.appendEntry({
|
|
34
|
+
action: 'config.update',
|
|
35
|
+
user: 'bob',
|
|
36
|
+
severity: 'warning',
|
|
37
|
+
timestamp: new Date('2026-01-16T09:00:00Z').getTime(),
|
|
38
|
+
parameters: { setting: 'timeout', oldValue: 30, newValue: 60 },
|
|
39
|
+
});
|
|
40
|
+
await auditStorage.appendEntry({
|
|
41
|
+
action: 'security.alert',
|
|
42
|
+
user: 'system',
|
|
43
|
+
severity: 'error',
|
|
44
|
+
timestamp: new Date('2026-01-16T14:00:00Z').getTime(),
|
|
45
|
+
parameters: { reason: 'brute force detected', ip: '10.0.0.1' },
|
|
46
|
+
});
|
|
47
|
+
await auditStorage.appendEntry({
|
|
48
|
+
action: 'user.login',
|
|
49
|
+
user: 'charlie',
|
|
50
|
+
severity: 'info',
|
|
51
|
+
timestamp: new Date('2026-01-17T08:00:00Z').getTime(),
|
|
52
|
+
parameters: { ip: '192.168.1.50', browser: 'Firefox' },
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
58
|
+
vi.useRealTimers();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('query filters by date range', () => {
|
|
62
|
+
it('filters entries within date range', async () => {
|
|
63
|
+
const result = await auditQuery.query({
|
|
64
|
+
from: new Date('2026-01-16T00:00:00Z'),
|
|
65
|
+
to: new Date('2026-01-16T23:59:59Z'),
|
|
66
|
+
sort: 'asc',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.entries).toHaveLength(2);
|
|
70
|
+
expect(result.entries[0].user).toBe('bob');
|
|
71
|
+
expect(result.entries[1].user).toBe('system');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('filters by from date only', async () => {
|
|
75
|
+
const result = await auditQuery.query({
|
|
76
|
+
from: new Date('2026-01-16T00:00:00Z'),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result.entries).toHaveLength(3);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('filters by to date only', async () => {
|
|
83
|
+
const result = await auditQuery.query({
|
|
84
|
+
to: new Date('2026-01-15T23:59:59Z'),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.entries).toHaveLength(2);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('query filters by action type', () => {
|
|
92
|
+
it('filters by exact action', async () => {
|
|
93
|
+
const result = await auditQuery.query({
|
|
94
|
+
action: 'user.login',
|
|
95
|
+
sort: 'asc',
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
expect(result.entries).toHaveLength(2);
|
|
99
|
+
expect(result.entries[0].user).toBe('alice');
|
|
100
|
+
expect(result.entries[1].user).toBe('charlie');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('filters by action prefix', async () => {
|
|
104
|
+
const result = await auditQuery.query({
|
|
105
|
+
action: 'user.*',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result.entries).toHaveLength(3);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns empty for non-existent action', async () => {
|
|
112
|
+
const result = await auditQuery.query({
|
|
113
|
+
action: 'nonexistent.action',
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(result.entries).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('query filters by user', () => {
|
|
121
|
+
it('filters by single user', async () => {
|
|
122
|
+
const result = await auditQuery.query({
|
|
123
|
+
user: 'alice',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(result.entries).toHaveLength(2);
|
|
127
|
+
expect(result.entries.every((e) => e.user === 'alice')).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('filters by multiple users', async () => {
|
|
131
|
+
const result = await auditQuery.query({
|
|
132
|
+
user: ['alice', 'bob'],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(result.entries).toHaveLength(3);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('query filters by severity', () => {
|
|
140
|
+
it('filters by single severity', async () => {
|
|
141
|
+
const result = await auditQuery.query({
|
|
142
|
+
severity: 'warning',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.entries).toHaveLength(1);
|
|
146
|
+
expect(result.entries[0].action).toBe('config.update');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('filters by multiple severities', async () => {
|
|
150
|
+
const result = await auditQuery.query({
|
|
151
|
+
severity: ['warning', 'error'],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(result.entries).toHaveLength(2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('filters by severity level (minimum)', async () => {
|
|
158
|
+
const result = await auditQuery.query({
|
|
159
|
+
minSeverity: 'warning',
|
|
160
|
+
sort: 'asc',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(result.entries).toHaveLength(2);
|
|
164
|
+
expect(result.entries[0].severity).toBe('warning');
|
|
165
|
+
expect(result.entries[1].severity).toBe('error');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('query supports multiple filters combined', () => {
|
|
170
|
+
it('combines date range and action filter', async () => {
|
|
171
|
+
const result = await auditQuery.query({
|
|
172
|
+
from: new Date('2026-01-15T00:00:00Z'),
|
|
173
|
+
to: new Date('2026-01-15T23:59:59Z'),
|
|
174
|
+
action: 'user.login',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(result.entries).toHaveLength(1);
|
|
178
|
+
expect(result.entries[0].user).toBe('alice');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('combines user, severity, and action filters', async () => {
|
|
182
|
+
const result = await auditQuery.query({
|
|
183
|
+
user: 'alice',
|
|
184
|
+
severity: 'info',
|
|
185
|
+
action: 'user.*',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(result.entries).toHaveLength(2);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('combines all filters', async () => {
|
|
192
|
+
const result = await auditQuery.query({
|
|
193
|
+
from: new Date('2026-01-15T00:00:00Z'),
|
|
194
|
+
to: new Date('2026-01-16T23:59:59Z'),
|
|
195
|
+
user: ['bob', 'system'],
|
|
196
|
+
severity: ['warning', 'error'],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result.entries).toHaveLength(2);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('query returns paginated results', () => {
|
|
204
|
+
it('returns first page with limit', async () => {
|
|
205
|
+
const result = await auditQuery.query({
|
|
206
|
+
limit: 2,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
expect(result.entries).toHaveLength(2);
|
|
210
|
+
expect(result.total).toBe(5);
|
|
211
|
+
expect(result.hasMore).toBe(true);
|
|
212
|
+
expect(result.page).toBe(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('returns second page with offset', async () => {
|
|
216
|
+
const result = await auditQuery.query({
|
|
217
|
+
limit: 2,
|
|
218
|
+
offset: 2,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(result.entries).toHaveLength(2);
|
|
222
|
+
expect(result.page).toBe(2);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('returns last page correctly', async () => {
|
|
226
|
+
const result = await auditQuery.query({
|
|
227
|
+
limit: 2,
|
|
228
|
+
offset: 4,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result.entries).toHaveLength(1);
|
|
232
|
+
expect(result.hasMore).toBe(false);
|
|
233
|
+
expect(result.page).toBe(3);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('supports page parameter instead of offset', async () => {
|
|
237
|
+
const result = await auditQuery.query({
|
|
238
|
+
limit: 2,
|
|
239
|
+
page: 2,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(result.entries).toHaveLength(2);
|
|
243
|
+
expect(result.page).toBe(2);
|
|
244
|
+
expect(result.entries[0].user).toBe('bob');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('query searches parameter content', () => {
|
|
249
|
+
it('searches in parameter values', async () => {
|
|
250
|
+
const result = await auditQuery.query({
|
|
251
|
+
search: 'Chrome',
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
expect(result.entries).toHaveLength(1);
|
|
255
|
+
expect(result.entries[0].user).toBe('alice');
|
|
256
|
+
expect(result.entries[0].action).toBe('user.login');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('searches in nested parameter values', async () => {
|
|
260
|
+
const result = await auditQuery.query({
|
|
261
|
+
search: 'brute force',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result.entries).toHaveLength(1);
|
|
265
|
+
expect(result.entries[0].action).toBe('security.alert');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('searches case-insensitively', async () => {
|
|
269
|
+
const result = await auditQuery.query({
|
|
270
|
+
search: 'firefox',
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(result.entries).toHaveLength(1);
|
|
274
|
+
expect(result.entries[0].user).toBe('charlie');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('searches across action and user fields too', async () => {
|
|
278
|
+
const result = await auditQuery.query({
|
|
279
|
+
search: 'config',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(result.entries).toHaveLength(1);
|
|
283
|
+
expect(result.entries[0].user).toBe('bob');
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('query returns count without results', () => {
|
|
288
|
+
it('returns only count when countOnly is true', async () => {
|
|
289
|
+
const result = await auditQuery.query({
|
|
290
|
+
countOnly: true,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
expect(result.total).toBe(5);
|
|
294
|
+
expect(result.entries).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('returns filtered count', async () => {
|
|
298
|
+
const result = await auditQuery.query({
|
|
299
|
+
user: 'alice',
|
|
300
|
+
countOnly: true,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
expect(result.total).toBe(2);
|
|
304
|
+
expect(result.entries).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('query handles empty results', () => {
|
|
309
|
+
it('returns empty array for no matches', async () => {
|
|
310
|
+
const result = await auditQuery.query({
|
|
311
|
+
user: 'nonexistent',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(result.entries).toHaveLength(0);
|
|
315
|
+
expect(result.total).toBe(0);
|
|
316
|
+
expect(result.hasMore).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('handles empty storage gracefully', async () => {
|
|
320
|
+
const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-audit-empty-'));
|
|
321
|
+
const emptyStorage = new AuditStorage(emptyDir);
|
|
322
|
+
const emptyQuery = new AuditQuery(emptyStorage);
|
|
323
|
+
|
|
324
|
+
const result = await emptyQuery.query({});
|
|
325
|
+
|
|
326
|
+
expect(result.entries).toHaveLength(0);
|
|
327
|
+
expect(result.total).toBe(0);
|
|
328
|
+
|
|
329
|
+
fs.rmSync(emptyDir, { recursive: true, force: true });
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('query sorting', () => {
|
|
334
|
+
it('sorts by timestamp descending by default', async () => {
|
|
335
|
+
const result = await auditQuery.query({});
|
|
336
|
+
|
|
337
|
+
// Most recent first
|
|
338
|
+
expect(result.entries[0].user).toBe('charlie');
|
|
339
|
+
expect(result.entries[4].user).toBe('alice');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('sorts by timestamp ascending when specified', async () => {
|
|
343
|
+
const result = await auditQuery.query({
|
|
344
|
+
sort: 'asc',
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Oldest first
|
|
348
|
+
expect(result.entries[0].user).toBe('alice');
|
|
349
|
+
expect(result.entries[0].action).toBe('user.login');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Log Storage - Append-only audit log storage with tamper-evident checksums
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Append-only log files (no overwrites)
|
|
6
|
+
* - Each entry has SHA-256 checksum
|
|
7
|
+
* - Checksum chains to previous entry (blockchain-style)
|
|
8
|
+
* - Daily log rotation with configurable retention
|
|
9
|
+
* - Stores in .tlc/audit/ directory
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
|
|
16
|
+
export const AUDIT_PATH = '.tlc/audit';
|
|
17
|
+
|
|
18
|
+
export class AuditStorage {
|
|
19
|
+
constructor(baseDir = process.cwd()) {
|
|
20
|
+
this.baseDir = baseDir;
|
|
21
|
+
this.auditDir = path.join(baseDir, AUDIT_PATH);
|
|
22
|
+
this.lastChecksum = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure audit directory exists
|
|
27
|
+
*/
|
|
28
|
+
ensureDirectory() {
|
|
29
|
+
if (!fs.existsSync(this.auditDir)) {
|
|
30
|
+
fs.mkdirSync(this.auditDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get current date string for log file naming
|
|
36
|
+
* @returns {string} YYYY-MM-DD format
|
|
37
|
+
*/
|
|
38
|
+
getDateString() {
|
|
39
|
+
return new Date().toISOString().split('T')[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get current log file path
|
|
44
|
+
* @returns {string} Path to current day's log file
|
|
45
|
+
*/
|
|
46
|
+
getCurrentLogFile() {
|
|
47
|
+
const dateStr = this.getDateString();
|
|
48
|
+
return path.join(this.auditDir, `audit-${dateStr}.jsonl`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Calculate SHA-256 checksum for entry data
|
|
53
|
+
* @param {Object} entryData - Entry data without checksum
|
|
54
|
+
* @returns {string} Hex-encoded SHA-256 hash
|
|
55
|
+
*/
|
|
56
|
+
calculateChecksum(entryData) {
|
|
57
|
+
return crypto
|
|
58
|
+
.createHash('sha256')
|
|
59
|
+
.update(JSON.stringify(entryData))
|
|
60
|
+
.digest('hex');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the last entry's checksum from the current log file
|
|
65
|
+
* @returns {string|null} Last checksum or null if no entries
|
|
66
|
+
*/
|
|
67
|
+
getLastChecksum() {
|
|
68
|
+
const logFile = this.getCurrentLogFile();
|
|
69
|
+
if (!fs.existsSync(logFile)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const content = fs.readFileSync(logFile, 'utf-8').trim();
|
|
74
|
+
if (!content) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lines = content.split('\n');
|
|
79
|
+
const lastLine = lines[lines.length - 1];
|
|
80
|
+
try {
|
|
81
|
+
const entry = JSON.parse(lastLine);
|
|
82
|
+
return entry.checksum || null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Append an audit entry with checksum
|
|
90
|
+
* @param {Object} entry - Entry data to append
|
|
91
|
+
* @returns {Promise<Object>} Appended entry with checksum
|
|
92
|
+
*/
|
|
93
|
+
async appendEntry(entry) {
|
|
94
|
+
this.ensureDirectory();
|
|
95
|
+
|
|
96
|
+
const logFile = this.getCurrentLogFile();
|
|
97
|
+
const previousChecksum = this.getLastChecksum();
|
|
98
|
+
|
|
99
|
+
// Build entry with metadata
|
|
100
|
+
const entryData = {
|
|
101
|
+
...entry,
|
|
102
|
+
previousChecksum,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Calculate checksum
|
|
106
|
+
const checksum = this.calculateChecksum(entryData);
|
|
107
|
+
const finalEntry = {
|
|
108
|
+
...entryData,
|
|
109
|
+
checksum,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Append to log file (append-only)
|
|
113
|
+
fs.appendFileSync(logFile, JSON.stringify(finalEntry) + '\n', 'utf-8');
|
|
114
|
+
|
|
115
|
+
this.lastChecksum = checksum;
|
|
116
|
+
return finalEntry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get all log files in the audit directory
|
|
121
|
+
* @returns {string[]} Array of log file paths sorted by date
|
|
122
|
+
*/
|
|
123
|
+
getLogFiles() {
|
|
124
|
+
if (!fs.existsSync(this.auditDir)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return fs
|
|
129
|
+
.readdirSync(this.auditDir)
|
|
130
|
+
.filter((f) => f.startsWith('audit-') && f.endsWith('.jsonl'))
|
|
131
|
+
.sort()
|
|
132
|
+
.map((f) => path.join(this.auditDir, f));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read entries from a single log file
|
|
137
|
+
* @param {string} logFile - Path to log file
|
|
138
|
+
* @returns {Object[]} Array of entries
|
|
139
|
+
*/
|
|
140
|
+
readLogFile(logFile) {
|
|
141
|
+
if (!fs.existsSync(logFile)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const content = fs.readFileSync(logFile, 'utf-8').trim();
|
|
146
|
+
if (!content) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return content.split('\n').map((line) => {
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(line);
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}).filter(Boolean);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get all entries, optionally filtered by date range
|
|
161
|
+
* @param {Object} options - Filter options
|
|
162
|
+
* @param {Date} options.from - Start date (inclusive)
|
|
163
|
+
* @param {Date} options.to - End date (inclusive)
|
|
164
|
+
* @returns {Promise<Object[]>} Array of entries sorted by timestamp
|
|
165
|
+
*/
|
|
166
|
+
async getEntries(options = {}) {
|
|
167
|
+
const { from, to } = options;
|
|
168
|
+
const logFiles = this.getLogFiles();
|
|
169
|
+
|
|
170
|
+
let allEntries = [];
|
|
171
|
+
for (const logFile of logFiles) {
|
|
172
|
+
const entries = this.readLogFile(logFile);
|
|
173
|
+
allEntries = allEntries.concat(entries);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Filter by date range if specified
|
|
177
|
+
if (from || to) {
|
|
178
|
+
allEntries = allEntries.filter((entry) => {
|
|
179
|
+
const ts = entry.timestamp;
|
|
180
|
+
if (from && ts < from.getTime()) return false;
|
|
181
|
+
if (to && ts > to.getTime()) return false;
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Sort by timestamp ascending
|
|
187
|
+
return allEntries.sort((a, b) => a.timestamp - b.timestamp);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Verify integrity of the audit log
|
|
192
|
+
* @returns {Promise<Object>} Verification result { valid: boolean, error?: string, entryCount: number }
|
|
193
|
+
*/
|
|
194
|
+
async verifyIntegrity() {
|
|
195
|
+
const logFiles = this.getLogFiles();
|
|
196
|
+
|
|
197
|
+
let entryCount = 0;
|
|
198
|
+
let previousChecksum = null;
|
|
199
|
+
|
|
200
|
+
for (const logFile of logFiles) {
|
|
201
|
+
const entries = this.readLogFile(logFile);
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < entries.length; i++) {
|
|
204
|
+
const entry = entries[i];
|
|
205
|
+
entryCount++;
|
|
206
|
+
|
|
207
|
+
// Verify entry's own checksum
|
|
208
|
+
const { checksum, ...entryData } = entry;
|
|
209
|
+
const calculatedChecksum = this.calculateChecksum(entryData);
|
|
210
|
+
|
|
211
|
+
if (checksum !== calculatedChecksum) {
|
|
212
|
+
return {
|
|
213
|
+
valid: false,
|
|
214
|
+
error: `Entry ${entryCount} has invalid checksum (expected ${calculatedChecksum}, got ${checksum})`,
|
|
215
|
+
entryCount,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Verify chain (previousChecksum matches previous entry's checksum)
|
|
220
|
+
if (entry.previousChecksum !== previousChecksum) {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
error: `Entry ${entryCount} has broken chain (expected previousChecksum ${previousChecksum}, got ${entry.previousChecksum})`,
|
|
224
|
+
entryCount,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
previousChecksum = checksum;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
valid: true,
|
|
234
|
+
entryCount,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Rotate logs, removing files older than retention period
|
|
240
|
+
* @param {Object} options - Rotation options
|
|
241
|
+
* @param {number} options.retentionDays - Number of days to retain (default: 30)
|
|
242
|
+
*/
|
|
243
|
+
async rotateLog(options = {}) {
|
|
244
|
+
const { retentionDays = 30 } = options;
|
|
245
|
+
|
|
246
|
+
if (!fs.existsSync(this.auditDir)) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const now = new Date();
|
|
251
|
+
const cutoffDate = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000);
|
|
252
|
+
const cutoffStr = cutoffDate.toISOString().split('T')[0];
|
|
253
|
+
|
|
254
|
+
const files = fs.readdirSync(this.auditDir).filter(
|
|
255
|
+
(f) => f.startsWith('audit-') && f.endsWith('.jsonl')
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
for (const file of files) {
|
|
259
|
+
// Extract date from filename: audit-YYYY-MM-DD.jsonl
|
|
260
|
+
const match = file.match(/audit-(\d{4}-\d{2}-\d{2})\.jsonl/);
|
|
261
|
+
if (match) {
|
|
262
|
+
const fileDate = match[1];
|
|
263
|
+
if (fileDate < cutoffStr) {
|
|
264
|
+
fs.unlinkSync(path.join(this.auditDir, file));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|