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,359 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getAttribution,
|
|
4
|
+
identifySource,
|
|
5
|
+
createSessionId,
|
|
6
|
+
correlateSession,
|
|
7
|
+
getGitUser,
|
|
8
|
+
getSystemUser,
|
|
9
|
+
getParentProcessContext,
|
|
10
|
+
} from './audit-attribution.js';
|
|
11
|
+
|
|
12
|
+
describe('audit-attribution', () => {
|
|
13
|
+
let originalEnv;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
originalEnv = { ...process.env };
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.env = originalEnv;
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('getGitUser', () => {
|
|
25
|
+
it('returns git user info when available', async () => {
|
|
26
|
+
const mockExecSync = vi.fn()
|
|
27
|
+
.mockReturnValueOnce(Buffer.from('Test User\n'))
|
|
28
|
+
.mockReturnValueOnce(Buffer.from('test@example.com\n'));
|
|
29
|
+
|
|
30
|
+
const result = await getGitUser({ execSync: mockExecSync });
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual({
|
|
33
|
+
name: 'Test User',
|
|
34
|
+
email: 'test@example.com',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns null when git config fails', async () => {
|
|
39
|
+
const mockExecSync = vi.fn().mockImplementation(() => {
|
|
40
|
+
throw new Error('git not configured');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = await getGitUser({ execSync: mockExecSync });
|
|
44
|
+
|
|
45
|
+
expect(result).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getSystemUser', () => {
|
|
50
|
+
it('returns username from os.userInfo', () => {
|
|
51
|
+
const mockUserInfo = vi.fn().mockReturnValue({ username: 'systemuser' });
|
|
52
|
+
|
|
53
|
+
const result = getSystemUser({ userInfo: mockUserInfo });
|
|
54
|
+
|
|
55
|
+
expect(result).toBe('systemuser');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('falls back to USER env var', () => {
|
|
59
|
+
const mockUserInfo = vi.fn().mockImplementation(() => {
|
|
60
|
+
throw new Error('userInfo not available');
|
|
61
|
+
});
|
|
62
|
+
const env = { USER: 'envuser' };
|
|
63
|
+
|
|
64
|
+
const result = getSystemUser({ userInfo: mockUserInfo, env });
|
|
65
|
+
|
|
66
|
+
expect(result).toBe('envuser');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('falls back to USERNAME env var on Windows', () => {
|
|
70
|
+
const mockUserInfo = vi.fn().mockImplementation(() => {
|
|
71
|
+
throw new Error('userInfo not available');
|
|
72
|
+
});
|
|
73
|
+
const env = { USERNAME: 'winuser' };
|
|
74
|
+
|
|
75
|
+
const result = getSystemUser({ userInfo: mockUserInfo, env });
|
|
76
|
+
|
|
77
|
+
expect(result).toBe('winuser');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns unknown when all methods fail', () => {
|
|
81
|
+
const mockUserInfo = vi.fn().mockImplementation(() => {
|
|
82
|
+
throw new Error('userInfo not available');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const result = getSystemUser({ userInfo: mockUserInfo, env: {} });
|
|
86
|
+
|
|
87
|
+
expect(result).toBe('unknown');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('getAttribution', () => {
|
|
92
|
+
it('returns git user info when available', async () => {
|
|
93
|
+
const mockExecSync = vi.fn()
|
|
94
|
+
.mockReturnValueOnce(Buffer.from('Git User\n'))
|
|
95
|
+
.mockReturnValueOnce(Buffer.from('git@example.com\n'));
|
|
96
|
+
|
|
97
|
+
const result = await getAttribution({
|
|
98
|
+
execSync: mockExecSync,
|
|
99
|
+
env: {},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(result.user.name).toBe('Git User');
|
|
103
|
+
expect(result.user.email).toBe('git@example.com');
|
|
104
|
+
expect(result.source).toBe('git');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('uses TLC_USER if set', async () => {
|
|
108
|
+
const mockExecSync = vi.fn()
|
|
109
|
+
.mockReturnValueOnce(Buffer.from('Git User\n'))
|
|
110
|
+
.mockReturnValueOnce(Buffer.from('git@example.com\n'));
|
|
111
|
+
const env = { TLC_USER: 'tlc-custom-user' };
|
|
112
|
+
|
|
113
|
+
const result = await getAttribution({
|
|
114
|
+
execSync: mockExecSync,
|
|
115
|
+
env,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(result.user.name).toBe('tlc-custom-user');
|
|
119
|
+
expect(result.source).toBe('env');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('falls back to system user when git unavailable', async () => {
|
|
123
|
+
const mockExecSync = vi.fn().mockImplementation(() => {
|
|
124
|
+
throw new Error('git not configured');
|
|
125
|
+
});
|
|
126
|
+
const mockUserInfo = vi.fn().mockReturnValue({ username: 'sysuser' });
|
|
127
|
+
|
|
128
|
+
const result = await getAttribution({
|
|
129
|
+
execSync: mockExecSync,
|
|
130
|
+
userInfo: mockUserInfo,
|
|
131
|
+
env: {},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.user.name).toBe('sysuser');
|
|
135
|
+
expect(result.source).toBe('system');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('includes timestamp', async () => {
|
|
139
|
+
const mockExecSync = vi.fn()
|
|
140
|
+
.mockReturnValueOnce(Buffer.from('User\n'))
|
|
141
|
+
.mockReturnValueOnce(Buffer.from('user@example.com\n'));
|
|
142
|
+
|
|
143
|
+
const result = await getAttribution({
|
|
144
|
+
execSync: mockExecSync,
|
|
145
|
+
env: {},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result.timestamp).toBeDefined();
|
|
149
|
+
expect(typeof result.timestamp).toBe('string');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('identifySource', () => {
|
|
154
|
+
it('returns agent for Task tool calls', () => {
|
|
155
|
+
const context = {
|
|
156
|
+
toolName: 'Task',
|
|
157
|
+
parentProcess: 'claude',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const result = identifySource(context);
|
|
161
|
+
|
|
162
|
+
expect(result).toBe('agent');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns agent for Skill tool calls', () => {
|
|
166
|
+
const context = {
|
|
167
|
+
toolName: 'Skill',
|
|
168
|
+
parentProcess: 'claude',
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = identifySource(context);
|
|
172
|
+
|
|
173
|
+
expect(result).toBe('agent');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('returns human for direct commands', () => {
|
|
177
|
+
const context = {
|
|
178
|
+
toolName: null,
|
|
179
|
+
parentProcess: 'bash',
|
|
180
|
+
tty: true,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const result = identifySource(context);
|
|
184
|
+
|
|
185
|
+
expect(result).toBe('human');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns human for terminal interactions', () => {
|
|
189
|
+
const context = {
|
|
190
|
+
parentProcess: 'zsh',
|
|
191
|
+
tty: true,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const result = identifySource(context);
|
|
195
|
+
|
|
196
|
+
expect(result).toBe('human');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns hook for hook-triggered actions', () => {
|
|
200
|
+
const context = {
|
|
201
|
+
env: { TLC_HOOK: 'pre-commit' },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const result = identifySource(context);
|
|
205
|
+
|
|
206
|
+
expect(result).toBe('hook');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns hook when triggered by git hooks', () => {
|
|
210
|
+
const context = {
|
|
211
|
+
parentProcess: 'git',
|
|
212
|
+
env: { GIT_EXEC_PATH: '/usr/lib/git-core' },
|
|
213
|
+
argv: ['pre-commit'],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const result = identifySource(context);
|
|
217
|
+
|
|
218
|
+
expect(result).toBe('hook');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('returns agent when CLAUDE_CODE is set', () => {
|
|
222
|
+
const context = {
|
|
223
|
+
env: { CLAUDE_CODE: '1' },
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = identifySource(context);
|
|
227
|
+
|
|
228
|
+
expect(result).toBe('agent');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('returns unknown for unidentifiable source', () => {
|
|
232
|
+
const context = {};
|
|
233
|
+
|
|
234
|
+
const result = identifySource(context);
|
|
235
|
+
|
|
236
|
+
expect(result).toBe('unknown');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('createSessionId', () => {
|
|
241
|
+
it('generates unique ID', () => {
|
|
242
|
+
const id1 = createSessionId();
|
|
243
|
+
const id2 = createSessionId();
|
|
244
|
+
|
|
245
|
+
expect(id1).not.toBe(id2);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('returns string', () => {
|
|
249
|
+
const id = createSessionId();
|
|
250
|
+
|
|
251
|
+
expect(typeof id).toBe('string');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('has expected format (uuid-like)', () => {
|
|
255
|
+
const id = createSessionId();
|
|
256
|
+
|
|
257
|
+
// Should be a valid UUID-like format or similar unique string
|
|
258
|
+
expect(id.length).toBeGreaterThan(0);
|
|
259
|
+
expect(id).toMatch(/^[a-z0-9-]+$/i);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('includes timestamp component for ordering', () => {
|
|
263
|
+
const before = Date.now();
|
|
264
|
+
const id = createSessionId();
|
|
265
|
+
const after = Date.now();
|
|
266
|
+
|
|
267
|
+
// The ID should encode time information (first part is timestamp)
|
|
268
|
+
const parts = id.split('-');
|
|
269
|
+
expect(parts.length).toBeGreaterThanOrEqual(1);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('correlateSession', () => {
|
|
274
|
+
it('groups related actions by session ID', () => {
|
|
275
|
+
const sessionId = createSessionId();
|
|
276
|
+
const actions = [
|
|
277
|
+
{ id: 'a1', sessionId, timestamp: new Date('2024-01-01T10:00:00Z').toISOString() },
|
|
278
|
+
{ id: 'a2', sessionId, timestamp: new Date('2024-01-01T10:01:00Z').toISOString() },
|
|
279
|
+
{ id: 'a3', sessionId: 'other', timestamp: new Date('2024-01-01T10:02:00Z').toISOString() },
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
const result = correlateSession(sessionId, actions);
|
|
283
|
+
|
|
284
|
+
expect(result).toHaveLength(2);
|
|
285
|
+
expect(result.map(a => a.id)).toEqual(['a1', 'a2']);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('returns empty array when no matching session', () => {
|
|
289
|
+
const actions = [
|
|
290
|
+
{ id: 'a1', sessionId: 'session-1' },
|
|
291
|
+
{ id: 'a2', sessionId: 'session-2' },
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
const result = correlateSession('nonexistent', actions);
|
|
295
|
+
|
|
296
|
+
expect(result).toEqual([]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('sorts actions by timestamp', () => {
|
|
300
|
+
const sessionId = 'test-session';
|
|
301
|
+
const actions = [
|
|
302
|
+
{ id: 'a2', sessionId, timestamp: new Date('2024-01-01T10:05:00Z').toISOString() },
|
|
303
|
+
{ id: 'a1', sessionId, timestamp: new Date('2024-01-01T10:00:00Z').toISOString() },
|
|
304
|
+
{ id: 'a3', sessionId, timestamp: new Date('2024-01-01T10:10:00Z').toISOString() },
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
const result = correlateSession(sessionId, actions);
|
|
308
|
+
|
|
309
|
+
expect(result.map(a => a.id)).toEqual(['a1', 'a2', 'a3']);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('handles actions without timestamps', () => {
|
|
313
|
+
const sessionId = 'test-session';
|
|
314
|
+
const actions = [
|
|
315
|
+
{ id: 'a1', sessionId },
|
|
316
|
+
{ id: 'a2', sessionId },
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
const result = correlateSession(sessionId, actions);
|
|
320
|
+
|
|
321
|
+
expect(result).toHaveLength(2);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('getParentProcessContext', () => {
|
|
326
|
+
it('returns process info', () => {
|
|
327
|
+
const mockProcess = {
|
|
328
|
+
ppid: 1234,
|
|
329
|
+
env: { TERM: 'xterm' },
|
|
330
|
+
argv: ['node', 'script.js'],
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const result = getParentProcessContext({ process: mockProcess });
|
|
334
|
+
|
|
335
|
+
expect(result.ppid).toBe(1234);
|
|
336
|
+
expect(result.argv).toEqual(['node', 'script.js']);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('includes terminal info when available', () => {
|
|
340
|
+
const mockProcess = {
|
|
341
|
+
ppid: 1234,
|
|
342
|
+
stdout: { isTTY: true },
|
|
343
|
+
env: { TERM: 'xterm-256color' },
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const result = getParentProcessContext({ process: mockProcess });
|
|
347
|
+
|
|
348
|
+
expect(result.tty).toBe(true);
|
|
349
|
+
expect(result.term).toBe('xterm-256color');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('handles missing process info gracefully', () => {
|
|
353
|
+
const result = getParentProcessContext({ process: {} });
|
|
354
|
+
|
|
355
|
+
expect(result).toBeDefined();
|
|
356
|
+
expect(result.ppid).toBeUndefined();
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Action Classifier Module
|
|
3
|
+
* Classifies agent actions into categories for filtering and compliance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sensitive file patterns that should trigger alerts
|
|
8
|
+
*/
|
|
9
|
+
const SENSITIVE_FILE_PATTERNS = [
|
|
10
|
+
/\.env($|\.)/i,
|
|
11
|
+
/credentials\.json$/i,
|
|
12
|
+
/\.secrets\//i,
|
|
13
|
+
/secrets\.json$/i,
|
|
14
|
+
/\.aws\/credentials$/i,
|
|
15
|
+
/\.ssh\/(id_rsa|id_ed25519|id_dsa)$/i,
|
|
16
|
+
/\.pem$/i,
|
|
17
|
+
/private\.key$/i,
|
|
18
|
+
/\.p12$/i,
|
|
19
|
+
/\.pfx$/i,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sensitive content patterns that should trigger alerts
|
|
24
|
+
*/
|
|
25
|
+
const SENSITIVE_CONTENT_PATTERNS = [
|
|
26
|
+
/password\s*[=:]/i,
|
|
27
|
+
/api[_-]?key\s*[=:]/i,
|
|
28
|
+
/secret\s*[=:]/i,
|
|
29
|
+
/token\s*[=:]/i,
|
|
30
|
+
/Bearer\s+[a-zA-Z0-9_-]+/i,
|
|
31
|
+
/sk-[a-zA-Z0-9]+/i, // OpenAI keys
|
|
32
|
+
/ghp_[a-zA-Z0-9]+/i, // GitHub tokens
|
|
33
|
+
/AWS_SECRET/i,
|
|
34
|
+
/PRIVATE_KEY/i,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Destructive command patterns
|
|
39
|
+
*/
|
|
40
|
+
const DESTRUCTIVE_COMMAND_PATTERNS = [
|
|
41
|
+
/rm\s+(-rf|-r\s+-f|-f\s+-r)\s+/i,
|
|
42
|
+
/git\s+push\s+--force/i,
|
|
43
|
+
/git\s+push\s+-f\b/i,
|
|
44
|
+
/git\s+reset\s+--hard/i,
|
|
45
|
+
/git\s+clean\s+-f/i,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Classify an agent action into a category
|
|
50
|
+
* @param {Object} action - Action object with tool and params
|
|
51
|
+
* @returns {string} Classification category
|
|
52
|
+
*/
|
|
53
|
+
function classifyAction(action) {
|
|
54
|
+
const { tool, params } = action;
|
|
55
|
+
|
|
56
|
+
// File operations
|
|
57
|
+
if (tool === 'Read') {
|
|
58
|
+
return 'file:read';
|
|
59
|
+
}
|
|
60
|
+
if (tool === 'Write') {
|
|
61
|
+
return 'file:write';
|
|
62
|
+
}
|
|
63
|
+
if (tool === 'Edit') {
|
|
64
|
+
return 'file:edit';
|
|
65
|
+
}
|
|
66
|
+
if (tool === 'Glob') {
|
|
67
|
+
return 'file:glob';
|
|
68
|
+
}
|
|
69
|
+
if (tool === 'Grep') {
|
|
70
|
+
return 'file:grep';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Network operations
|
|
74
|
+
if (tool === 'WebFetch') {
|
|
75
|
+
return 'network:fetch';
|
|
76
|
+
}
|
|
77
|
+
if (tool === 'WebSearch') {
|
|
78
|
+
return 'network:search';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Shell operations
|
|
82
|
+
if (tool === 'Bash') {
|
|
83
|
+
const command = params?.command || '';
|
|
84
|
+
|
|
85
|
+
// Git commands
|
|
86
|
+
if (/^\s*git\s+/i.test(command)) {
|
|
87
|
+
return 'shell:git';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// NPM commands
|
|
91
|
+
if (/^\s*npm\s+/i.test(command)) {
|
|
92
|
+
return 'shell:npm';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return 'shell:execute';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return 'unknown';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Detect if an action involves sensitive data
|
|
103
|
+
* @param {Object} action - Action object with tool and params
|
|
104
|
+
* @returns {Object} Detection result with isSensitive flag and reason
|
|
105
|
+
*/
|
|
106
|
+
function detectSensitive(action) {
|
|
107
|
+
const { tool, params } = action;
|
|
108
|
+
|
|
109
|
+
// Check file paths
|
|
110
|
+
const filePath = params?.file_path || '';
|
|
111
|
+
for (const pattern of SENSITIVE_FILE_PATTERNS) {
|
|
112
|
+
if (pattern.test(filePath)) {
|
|
113
|
+
let reason = 'Accessing sensitive file';
|
|
114
|
+
if (/\.env/i.test(filePath)) {
|
|
115
|
+
reason = 'Accessing .env file';
|
|
116
|
+
} else if (/credential/i.test(filePath)) {
|
|
117
|
+
reason = 'Accessing credential file';
|
|
118
|
+
} else if (/\.ssh.*id_/i.test(filePath)) {
|
|
119
|
+
reason = 'Accessing private key file';
|
|
120
|
+
} else if (/\.aws/i.test(filePath)) {
|
|
121
|
+
reason = 'Accessing AWS credentials';
|
|
122
|
+
} else if (/\.secrets/i.test(filePath)) {
|
|
123
|
+
reason = 'Accessing secrets directory';
|
|
124
|
+
}
|
|
125
|
+
return { isSensitive: true, reason };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check command content
|
|
130
|
+
const command = params?.command || '';
|
|
131
|
+
for (const pattern of SENSITIVE_CONTENT_PATTERNS) {
|
|
132
|
+
if (pattern.test(command)) {
|
|
133
|
+
return { isSensitive: true, reason: 'Command contains sensitive data' };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check file content for write operations
|
|
138
|
+
const content = params?.content || '';
|
|
139
|
+
for (const pattern of SENSITIVE_CONTENT_PATTERNS) {
|
|
140
|
+
if (pattern.test(content)) {
|
|
141
|
+
return { isSensitive: true, reason: 'File content contains sensitive data' };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { isSensitive: false, reason: null };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get severity level for an action
|
|
150
|
+
* @param {Object} action - Action object with tool and params
|
|
151
|
+
* @returns {string} Severity level: 'info', 'warning', or 'critical'
|
|
152
|
+
*/
|
|
153
|
+
function getSeverity(action) {
|
|
154
|
+
const { tool, params } = action;
|
|
155
|
+
|
|
156
|
+
// First check if it's a sensitive operation
|
|
157
|
+
const sensitiveCheck = detectSensitive(action);
|
|
158
|
+
if (sensitiveCheck.isSensitive) {
|
|
159
|
+
return 'critical';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check for destructive commands
|
|
163
|
+
const command = params?.command || '';
|
|
164
|
+
for (const pattern of DESTRUCTIVE_COMMAND_PATTERNS) {
|
|
165
|
+
if (pattern.test(command)) {
|
|
166
|
+
return 'critical';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// File operations
|
|
171
|
+
if (tool === 'Write' || tool === 'Edit') {
|
|
172
|
+
return 'warning';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (tool === 'Read' || tool === 'Glob' || tool === 'Grep') {
|
|
176
|
+
return 'info';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Network operations
|
|
180
|
+
if (tool === 'WebFetch' || tool === 'WebSearch') {
|
|
181
|
+
return 'info';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Shell operations
|
|
185
|
+
if (tool === 'Bash') {
|
|
186
|
+
// Git read commands are info level
|
|
187
|
+
if (/^\s*git\s+(status|log|diff|branch|show)\b/i.test(command)) {
|
|
188
|
+
return 'info';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// General shell execution is warning
|
|
192
|
+
return 'warning';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return 'info';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
classifyAction,
|
|
200
|
+
detectSensitive,
|
|
201
|
+
getSeverity,
|
|
202
|
+
};
|