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,364 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock dependencies before importing
|
|
4
|
+
vi.mock('./audit-storage.js', () => {
|
|
5
|
+
class MockAuditStorage {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.entries = [];
|
|
8
|
+
}
|
|
9
|
+
async appendEntry(entry) {
|
|
10
|
+
this.entries.push(entry);
|
|
11
|
+
return { ...entry, checksum: 'mock-checksum' };
|
|
12
|
+
}
|
|
13
|
+
async getEntries() {
|
|
14
|
+
return this.entries;
|
|
15
|
+
}
|
|
16
|
+
async verifyIntegrity() {
|
|
17
|
+
return { valid: true };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
AuditStorage: MockAuditStorage,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vi.mock('./audit-classifier.js', () => ({
|
|
26
|
+
classifyAction: vi.fn().mockReturnValue('file:read'),
|
|
27
|
+
detectSensitive: vi.fn().mockReturnValue({ isSensitive: false, reason: null }),
|
|
28
|
+
getSeverity: vi.fn().mockReturnValue('info'),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('./audit-attribution.js', () => ({
|
|
32
|
+
getAttribution: vi.fn().mockResolvedValue({
|
|
33
|
+
user: { name: 'testuser', email: 'test@example.com' },
|
|
34
|
+
source: 'git',
|
|
35
|
+
timestamp: '2026-01-15T10:00:00.000Z',
|
|
36
|
+
}),
|
|
37
|
+
identifySource: vi.fn().mockReturnValue('agent'),
|
|
38
|
+
createSessionId: vi.fn().mockReturnValue('test-session-123'),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
import { AuditLogger, sanitizeParams } from './audit-logger.js';
|
|
42
|
+
import { classifyAction, detectSensitive, getSeverity } from './audit-classifier.js';
|
|
43
|
+
import { getAttribution, identifySource, createSessionId } from './audit-attribution.js';
|
|
44
|
+
|
|
45
|
+
describe('AuditLogger', () => {
|
|
46
|
+
let logger;
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks();
|
|
50
|
+
vi.useFakeTimers();
|
|
51
|
+
vi.setSystemTime(new Date('2026-01-15T10:00:00.000Z'));
|
|
52
|
+
|
|
53
|
+
logger = new AuditLogger();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
vi.useRealTimers();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('logAction creates complete audit entry', () => {
|
|
61
|
+
it('creates entry with tool name, params, and result', async () => {
|
|
62
|
+
const entry = await logger.logAction('Read', { file_path: '/test/file.js' }, { content: 'file content' });
|
|
63
|
+
|
|
64
|
+
expect(entry).toBeDefined();
|
|
65
|
+
expect(entry.tool).toBe('Read');
|
|
66
|
+
expect(entry.params).toBeDefined();
|
|
67
|
+
expect(entry.result).toEqual({ content: 'file content' });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('stores complete context for replay', async () => {
|
|
71
|
+
const params = { file_path: '/test/file.js', offset: 0, limit: 100 };
|
|
72
|
+
const result = { content: 'file content', lines: 100 };
|
|
73
|
+
|
|
74
|
+
const entry = await logger.logAction('Read', params, result);
|
|
75
|
+
|
|
76
|
+
expect(entry.tool).toBe('Read');
|
|
77
|
+
expect(entry.result).toEqual(result);
|
|
78
|
+
expect(entry.context).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('logAction includes classification', () => {
|
|
83
|
+
it('includes classification from classifier module', async () => {
|
|
84
|
+
classifyAction.mockReturnValueOnce('file:write');
|
|
85
|
+
getSeverity.mockReturnValueOnce('warning');
|
|
86
|
+
detectSensitive.mockReturnValueOnce({ isSensitive: false, reason: null });
|
|
87
|
+
|
|
88
|
+
const entry = await logger.logAction('Write', { file_path: '/test/file.js', content: 'new content' }, { success: true });
|
|
89
|
+
|
|
90
|
+
expect(classifyAction).toHaveBeenCalledWith({ tool: 'Write', params: expect.any(Object) });
|
|
91
|
+
expect(entry.classification).toBe('file:write');
|
|
92
|
+
expect(entry.severity).toBe('warning');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('includes sensitive flag from classifier', async () => {
|
|
96
|
+
detectSensitive.mockReturnValueOnce({ isSensitive: true, reason: 'Accessing .env file' });
|
|
97
|
+
|
|
98
|
+
const entry = await logger.logAction('Read', { file_path: '.env' }, { content: 'secrets' });
|
|
99
|
+
|
|
100
|
+
expect(detectSensitive).toHaveBeenCalled();
|
|
101
|
+
expect(entry.sensitive).toEqual({ isSensitive: true, reason: 'Accessing .env file' });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('logAction includes attribution', () => {
|
|
106
|
+
it('includes user attribution from attribution module', async () => {
|
|
107
|
+
getAttribution.mockResolvedValueOnce({
|
|
108
|
+
user: { name: 'alice', email: 'alice@example.com' },
|
|
109
|
+
source: 'git',
|
|
110
|
+
timestamp: '2026-01-15T10:00:00.000Z',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const entry = await logger.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
|
|
114
|
+
|
|
115
|
+
expect(getAttribution).toHaveBeenCalled();
|
|
116
|
+
expect(entry.attribution.user.name).toBe('alice');
|
|
117
|
+
expect(entry.attribution.source).toBe('git');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('includes source identification', async () => {
|
|
121
|
+
identifySource.mockReturnValueOnce('human');
|
|
122
|
+
|
|
123
|
+
const entry = await logger.logAction('Bash', { command: 'ls -la' }, { output: 'files' });
|
|
124
|
+
|
|
125
|
+
expect(identifySource).toHaveBeenCalled();
|
|
126
|
+
expect(entry.sourceType).toBe('human');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('includes session ID for correlating actions', async () => {
|
|
130
|
+
createSessionId.mockReturnValueOnce('session-abc-123');
|
|
131
|
+
|
|
132
|
+
const loggerWithSession = new AuditLogger();
|
|
133
|
+
const entry = await loggerWithSession.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
|
|
134
|
+
|
|
135
|
+
expect(entry.sessionId).toBeDefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('logAction includes timestamp', () => {
|
|
140
|
+
it('adds timestamp in ISO 8601 format', async () => {
|
|
141
|
+
const entry = await logger.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
|
|
142
|
+
|
|
143
|
+
expect(entry.timestamp).toBe('2026-01-15T10:00:00.000Z');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('uses current time for timestamp', async () => {
|
|
147
|
+
vi.setSystemTime(new Date('2026-02-20T15:30:45.123Z'));
|
|
148
|
+
|
|
149
|
+
const loggerNew = new AuditLogger();
|
|
150
|
+
const entry = await loggerNew.logAction('Read', { file_path: '/test/file.js' }, { content: 'data' });
|
|
151
|
+
|
|
152
|
+
expect(entry.timestamp).toBe('2026-02-20T15:30:45.123Z');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('logAction stores tool parameters (sanitized)', () => {
|
|
157
|
+
it('stores sanitized parameters', async () => {
|
|
158
|
+
const params = {
|
|
159
|
+
file_path: '/test/file.js',
|
|
160
|
+
content: 'API_KEY=sk-secret123\nPASSWORD=mypassword',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const entry = await logger.logAction('Write', params, { success: true });
|
|
164
|
+
|
|
165
|
+
// Params should be sanitized
|
|
166
|
+
expect(entry.params.content).not.toContain('sk-secret123');
|
|
167
|
+
expect(entry.params.content).not.toContain('mypassword');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('preserves non-sensitive parameters', async () => {
|
|
171
|
+
const params = {
|
|
172
|
+
file_path: '/test/file.js',
|
|
173
|
+
offset: 10,
|
|
174
|
+
limit: 100,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const entry = await logger.logAction('Read', params, { content: 'data' });
|
|
178
|
+
|
|
179
|
+
expect(entry.params.file_path).toBe('/test/file.js');
|
|
180
|
+
expect(entry.params.offset).toBe(10);
|
|
181
|
+
expect(entry.params.limit).toBe(100);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('logAction handles async batch mode', () => {
|
|
186
|
+
it('batches entries when batch mode is enabled', async () => {
|
|
187
|
+
const batchLogger = new AuditLogger({ batchMode: true, batchSize: 3 });
|
|
188
|
+
|
|
189
|
+
await batchLogger.logAction('Read', { file_path: '/file1.js' }, { content: 'a' });
|
|
190
|
+
await batchLogger.logAction('Read', { file_path: '/file2.js' }, { content: 'b' });
|
|
191
|
+
|
|
192
|
+
// Should not have written yet (batch size is 3)
|
|
193
|
+
expect(batchLogger.getPendingCount()).toBe(2);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('flushes when batch size is reached', async () => {
|
|
197
|
+
const batchLogger = new AuditLogger({ batchMode: true, batchSize: 2 });
|
|
198
|
+
|
|
199
|
+
await batchLogger.logAction('Read', { file_path: '/file1.js' }, { content: 'a' });
|
|
200
|
+
await batchLogger.logAction('Read', { file_path: '/file2.js' }, { content: 'b' });
|
|
201
|
+
|
|
202
|
+
// Should have flushed after reaching batch size
|
|
203
|
+
expect(batchLogger.getPendingCount()).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('flushBatch writes pending entries', () => {
|
|
208
|
+
it('writes all pending entries to storage', async () => {
|
|
209
|
+
const batchLogger = new AuditLogger({ batchMode: true, batchSize: 10 });
|
|
210
|
+
|
|
211
|
+
await batchLogger.logAction('Read', { file_path: '/file1.js' }, { content: 'a' });
|
|
212
|
+
await batchLogger.logAction('Read', { file_path: '/file2.js' }, { content: 'b' });
|
|
213
|
+
await batchLogger.logAction('Read', { file_path: '/file3.js' }, { content: 'c' });
|
|
214
|
+
|
|
215
|
+
expect(batchLogger.getPendingCount()).toBe(3);
|
|
216
|
+
|
|
217
|
+
await batchLogger.flushBatch();
|
|
218
|
+
|
|
219
|
+
expect(batchLogger.getPendingCount()).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('handles empty batch gracefully', async () => {
|
|
223
|
+
const batchLogger = new AuditLogger({ batchMode: true, batchSize: 10 });
|
|
224
|
+
|
|
225
|
+
// Should not throw
|
|
226
|
+
await expect(batchLogger.flushBatch()).resolves.not.toThrow();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('sanitizeParams', () => {
|
|
232
|
+
describe('sanitizeParams removes sensitive values', () => {
|
|
233
|
+
it('removes API keys', () => {
|
|
234
|
+
const params = {
|
|
235
|
+
content: 'API_KEY=sk-secret123abcdef',
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const sanitized = sanitizeParams(params);
|
|
239
|
+
|
|
240
|
+
expect(sanitized.content).not.toContain('sk-secret123abcdef');
|
|
241
|
+
expect(sanitized.content).toContain('[REDACTED]');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('removes passwords', () => {
|
|
245
|
+
const params = {
|
|
246
|
+
content: 'password=supersecret123',
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const sanitized = sanitizeParams(params);
|
|
250
|
+
|
|
251
|
+
expect(sanitized.content).not.toContain('supersecret123');
|
|
252
|
+
expect(sanitized.content).toContain('[REDACTED]');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('removes tokens', () => {
|
|
256
|
+
const params = {
|
|
257
|
+
content: 'token=ghp_1234567890abcdef',
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const sanitized = sanitizeParams(params);
|
|
261
|
+
|
|
262
|
+
expect(sanitized.content).not.toContain('ghp_1234567890abcdef');
|
|
263
|
+
expect(sanitized.content).toContain('[REDACTED]');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('removes secrets from command', () => {
|
|
267
|
+
const params = {
|
|
268
|
+
command: 'export SECRET=mysecretvalue && npm run build',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const sanitized = sanitizeParams(params);
|
|
272
|
+
|
|
273
|
+
expect(sanitized.command).not.toContain('mysecretvalue');
|
|
274
|
+
expect(sanitized.command).toContain('[REDACTED]');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('removes Bearer tokens', () => {
|
|
278
|
+
const params = {
|
|
279
|
+
content: 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const sanitized = sanitizeParams(params);
|
|
283
|
+
|
|
284
|
+
expect(sanitized.content).not.toContain('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9');
|
|
285
|
+
expect(sanitized.content).toContain('[REDACTED]');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('sanitizeParams preserves structure', () => {
|
|
290
|
+
it('preserves object structure', () => {
|
|
291
|
+
const params = {
|
|
292
|
+
file_path: '/test/file.js',
|
|
293
|
+
offset: 10,
|
|
294
|
+
limit: 100,
|
|
295
|
+
nested: {
|
|
296
|
+
value: 'safe',
|
|
297
|
+
deep: {
|
|
298
|
+
key: 'value',
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const sanitized = sanitizeParams(params);
|
|
304
|
+
|
|
305
|
+
expect(sanitized.file_path).toBe('/test/file.js');
|
|
306
|
+
expect(sanitized.offset).toBe(10);
|
|
307
|
+
expect(sanitized.limit).toBe(100);
|
|
308
|
+
expect(sanitized.nested.value).toBe('safe');
|
|
309
|
+
expect(sanitized.nested.deep.key).toBe('value');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('preserves arrays', () => {
|
|
313
|
+
const params = {
|
|
314
|
+
files: ['/file1.js', '/file2.js'],
|
|
315
|
+
numbers: [1, 2, 3],
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const sanitized = sanitizeParams(params);
|
|
319
|
+
|
|
320
|
+
expect(sanitized.files).toEqual(['/file1.js', '/file2.js']);
|
|
321
|
+
expect(sanitized.numbers).toEqual([1, 2, 3]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('handles null and undefined values', () => {
|
|
325
|
+
const params = {
|
|
326
|
+
nullValue: null,
|
|
327
|
+
undefinedValue: undefined,
|
|
328
|
+
validValue: 'test',
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const sanitized = sanitizeParams(params);
|
|
332
|
+
|
|
333
|
+
expect(sanitized.nullValue).toBeNull();
|
|
334
|
+
expect(sanitized.undefinedValue).toBeUndefined();
|
|
335
|
+
expect(sanitized.validValue).toBe('test');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('sanitizes sensitive values in nested objects', () => {
|
|
339
|
+
const params = {
|
|
340
|
+
config: {
|
|
341
|
+
api_key: 'sk-secret123',
|
|
342
|
+
endpoint: 'https://api.example.com',
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const sanitized = sanitizeParams(params);
|
|
347
|
+
|
|
348
|
+
expect(sanitized.config.api_key).toBe('[REDACTED]');
|
|
349
|
+
expect(sanitized.config.endpoint).toBe('https://api.example.com');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('sanitizes sensitive values in arrays', () => {
|
|
353
|
+
const params = {
|
|
354
|
+
items: ['password=secret1', 'normal value', 'token=abc123'],
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const sanitized = sanitizeParams(params);
|
|
358
|
+
|
|
359
|
+
expect(sanitized.items[0]).toContain('[REDACTED]');
|
|
360
|
+
expect(sanitized.items[1]).toBe('normal value');
|
|
361
|
+
expect(sanitized.items[2]).toContain('[REDACTED]');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Query Engine - Search and filter audit logs
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Filter by date range
|
|
6
|
+
* - Filter by action type (exact match or wildcard)
|
|
7
|
+
* - Filter by user (single or multiple)
|
|
8
|
+
* - Filter by severity (single, multiple, or minimum level)
|
|
9
|
+
* - Full-text search in parameters
|
|
10
|
+
* - Pagination support
|
|
11
|
+
* - Count-only queries
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// Severity levels in order from lowest to highest
|
|
15
|
+
const SEVERITY_LEVELS = ['debug', 'info', 'warning', 'error', 'critical'];
|
|
16
|
+
|
|
17
|
+
export class AuditQuery {
|
|
18
|
+
/**
|
|
19
|
+
* Create an AuditQuery instance
|
|
20
|
+
* @param {AuditStorage} storage - AuditStorage instance to query
|
|
21
|
+
*/
|
|
22
|
+
constructor(storage) {
|
|
23
|
+
this.storage = storage;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Query audit log entries with filters
|
|
28
|
+
* @param {Object} options - Query options
|
|
29
|
+
* @param {Date} options.from - Start date (inclusive)
|
|
30
|
+
* @param {Date} options.to - End date (inclusive)
|
|
31
|
+
* @param {string} options.action - Action type filter (supports wildcard with *)
|
|
32
|
+
* @param {string|string[]} options.user - User filter (single or array)
|
|
33
|
+
* @param {string|string[]} options.severity - Severity filter (single or array)
|
|
34
|
+
* @param {string} options.minSeverity - Minimum severity level
|
|
35
|
+
* @param {string} options.search - Full-text search in parameters
|
|
36
|
+
* @param {number} options.limit - Maximum entries to return
|
|
37
|
+
* @param {number} options.offset - Number of entries to skip
|
|
38
|
+
* @param {number} options.page - Page number (alternative to offset)
|
|
39
|
+
* @param {string} options.sort - Sort order: 'asc' or 'desc' (default: 'desc')
|
|
40
|
+
* @param {boolean} options.countOnly - Return only count, not entries
|
|
41
|
+
* @returns {Promise<Object>} Query result { entries, total, hasMore, page }
|
|
42
|
+
*/
|
|
43
|
+
async query(options = {}) {
|
|
44
|
+
const {
|
|
45
|
+
from,
|
|
46
|
+
to,
|
|
47
|
+
action,
|
|
48
|
+
user,
|
|
49
|
+
severity,
|
|
50
|
+
minSeverity,
|
|
51
|
+
search,
|
|
52
|
+
limit,
|
|
53
|
+
offset,
|
|
54
|
+
page,
|
|
55
|
+
sort = 'desc',
|
|
56
|
+
countOnly = false,
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
// Get all entries from storage (with basic date filtering if supported)
|
|
60
|
+
let entries = await this.storage.getEntries({ from, to });
|
|
61
|
+
|
|
62
|
+
// Apply action filter
|
|
63
|
+
if (action) {
|
|
64
|
+
entries = this.filterByAction(entries, action);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Apply user filter
|
|
68
|
+
if (user) {
|
|
69
|
+
entries = this.filterByUser(entries, user);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Apply severity filter
|
|
73
|
+
if (severity) {
|
|
74
|
+
entries = this.filterBySeverity(entries, severity);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Apply minimum severity filter
|
|
78
|
+
if (minSeverity) {
|
|
79
|
+
entries = this.filterByMinSeverity(entries, minSeverity);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Apply search filter
|
|
83
|
+
if (search) {
|
|
84
|
+
entries = this.filterBySearch(entries, search);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Sort entries
|
|
88
|
+
entries = this.sortEntries(entries, sort);
|
|
89
|
+
|
|
90
|
+
// Get total count
|
|
91
|
+
const total = entries.length;
|
|
92
|
+
|
|
93
|
+
// If count only, return early
|
|
94
|
+
if (countOnly) {
|
|
95
|
+
return { total };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply pagination
|
|
99
|
+
const { paginatedEntries, hasMore, currentPage } = this.paginate(
|
|
100
|
+
entries,
|
|
101
|
+
limit,
|
|
102
|
+
offset,
|
|
103
|
+
page
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
entries: paginatedEntries,
|
|
108
|
+
total,
|
|
109
|
+
hasMore,
|
|
110
|
+
page: currentPage,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Filter entries by action type
|
|
116
|
+
* @param {Object[]} entries - Entries to filter
|
|
117
|
+
* @param {string} action - Action filter (supports * wildcard)
|
|
118
|
+
* @returns {Object[]} Filtered entries
|
|
119
|
+
*/
|
|
120
|
+
filterByAction(entries, action) {
|
|
121
|
+
if (action.endsWith('*')) {
|
|
122
|
+
// Wildcard prefix match
|
|
123
|
+
const prefix = action.slice(0, -1);
|
|
124
|
+
return entries.filter((e) => e.action && e.action.startsWith(prefix));
|
|
125
|
+
}
|
|
126
|
+
// Exact match
|
|
127
|
+
return entries.filter((e) => e.action === action);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Filter entries by user
|
|
132
|
+
* @param {Object[]} entries - Entries to filter
|
|
133
|
+
* @param {string|string[]} user - User or array of users
|
|
134
|
+
* @returns {Object[]} Filtered entries
|
|
135
|
+
*/
|
|
136
|
+
filterByUser(entries, user) {
|
|
137
|
+
const users = Array.isArray(user) ? user : [user];
|
|
138
|
+
return entries.filter((e) => users.includes(e.user));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Filter entries by severity
|
|
143
|
+
* @param {Object[]} entries - Entries to filter
|
|
144
|
+
* @param {string|string[]} severity - Severity or array of severities
|
|
145
|
+
* @returns {Object[]} Filtered entries
|
|
146
|
+
*/
|
|
147
|
+
filterBySeverity(entries, severity) {
|
|
148
|
+
const severities = Array.isArray(severity) ? severity : [severity];
|
|
149
|
+
return entries.filter((e) => severities.includes(e.severity));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Filter entries by minimum severity level
|
|
154
|
+
* @param {Object[]} entries - Entries to filter
|
|
155
|
+
* @param {string} minSeverity - Minimum severity level
|
|
156
|
+
* @returns {Object[]} Filtered entries
|
|
157
|
+
*/
|
|
158
|
+
filterByMinSeverity(entries, minSeverity) {
|
|
159
|
+
const minIndex = SEVERITY_LEVELS.indexOf(minSeverity);
|
|
160
|
+
if (minIndex === -1) {
|
|
161
|
+
return entries; // Invalid severity, return all
|
|
162
|
+
}
|
|
163
|
+
return entries.filter((e) => {
|
|
164
|
+
const entryIndex = SEVERITY_LEVELS.indexOf(e.severity);
|
|
165
|
+
return entryIndex >= minIndex;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Filter entries by full-text search
|
|
171
|
+
* @param {Object[]} entries - Entries to filter
|
|
172
|
+
* @param {string} search - Search term
|
|
173
|
+
* @returns {Object[]} Filtered entries
|
|
174
|
+
*/
|
|
175
|
+
filterBySearch(entries, search) {
|
|
176
|
+
const searchLower = search.toLowerCase();
|
|
177
|
+
return entries.filter((e) => {
|
|
178
|
+
// Search in action field
|
|
179
|
+
if (e.action && e.action.toLowerCase().includes(searchLower)) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
// Search in user field
|
|
183
|
+
if (e.user && e.user.toLowerCase().includes(searchLower)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
// Search in parameters
|
|
187
|
+
if (e.parameters) {
|
|
188
|
+
const paramStr = JSON.stringify(e.parameters).toLowerCase();
|
|
189
|
+
if (paramStr.includes(searchLower)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Sort entries by timestamp
|
|
199
|
+
* @param {Object[]} entries - Entries to sort
|
|
200
|
+
* @param {string} order - Sort order: 'asc' or 'desc'
|
|
201
|
+
* @returns {Object[]} Sorted entries
|
|
202
|
+
*/
|
|
203
|
+
sortEntries(entries, order) {
|
|
204
|
+
const sorted = [...entries];
|
|
205
|
+
if (order === 'asc') {
|
|
206
|
+
sorted.sort((a, b) => a.timestamp - b.timestamp);
|
|
207
|
+
} else {
|
|
208
|
+
sorted.sort((a, b) => b.timestamp - a.timestamp);
|
|
209
|
+
}
|
|
210
|
+
return sorted;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Paginate entries
|
|
215
|
+
* @param {Object[]} entries - Entries to paginate
|
|
216
|
+
* @param {number} limit - Maximum entries per page
|
|
217
|
+
* @param {number} offset - Number of entries to skip
|
|
218
|
+
* @param {number} page - Page number (1-based)
|
|
219
|
+
* @returns {Object} { paginatedEntries, hasMore, currentPage }
|
|
220
|
+
*/
|
|
221
|
+
paginate(entries, limit, offset, page) {
|
|
222
|
+
if (!limit) {
|
|
223
|
+
return {
|
|
224
|
+
paginatedEntries: entries,
|
|
225
|
+
hasMore: false,
|
|
226
|
+
currentPage: 1,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let startIndex;
|
|
231
|
+
let currentPage;
|
|
232
|
+
|
|
233
|
+
if (page !== undefined) {
|
|
234
|
+
// Use page number (1-based)
|
|
235
|
+
currentPage = page;
|
|
236
|
+
startIndex = (page - 1) * limit;
|
|
237
|
+
} else if (offset !== undefined) {
|
|
238
|
+
// Use offset
|
|
239
|
+
startIndex = offset;
|
|
240
|
+
currentPage = Math.floor(offset / limit) + 1;
|
|
241
|
+
} else {
|
|
242
|
+
// Default to first page
|
|
243
|
+
startIndex = 0;
|
|
244
|
+
currentPage = 1;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const endIndex = startIndex + limit;
|
|
248
|
+
const paginatedEntries = entries.slice(startIndex, endIndex);
|
|
249
|
+
const hasMore = endIndex < entries.length;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
paginatedEntries,
|
|
253
|
+
hasMore,
|
|
254
|
+
currentPage,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|