@nahisaho/katashiro-security 0.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.
@@ -0,0 +1,372 @@
1
+ /**
2
+ * ActionLogger テスト
3
+ *
4
+ * @requirement REQ-012-05
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach } from 'vitest';
8
+ import {
9
+ ActionLogger,
10
+ InMemoryLogStorage,
11
+ Action,
12
+ SecurityAnalysis,
13
+ Observation,
14
+ } from '../src';
15
+
16
+ describe('ActionLogger', () => {
17
+ let logger: ActionLogger;
18
+
19
+ beforeEach(() => {
20
+ logger = new ActionLogger();
21
+ });
22
+
23
+ describe('REQ-012-05: アクションログ記録', () => {
24
+ it('アクションをログに記録できる', async () => {
25
+ const action: Action = {
26
+ type: 'file_read',
27
+ name: 'read file',
28
+ target: '/tmp/test.txt',
29
+ context: { userId: 'user-1' },
30
+ };
31
+
32
+ const analysis: SecurityAnalysis = {
33
+ riskLevel: 'low',
34
+ reasons: ['File read is safe'],
35
+ requiresConfirmation: false,
36
+ allowed: true,
37
+ matchedRules: ['file_read_low_risk'],
38
+ };
39
+
40
+ const logId = await logger.logAction(action, analysis);
41
+
42
+ expect(logId).toBeTruthy();
43
+ expect(logId).toMatch(/^log-/);
44
+ });
45
+
46
+ it('タイムスタンプが記録される', async () => {
47
+ const action: Action = {
48
+ type: 'file_read',
49
+ name: 'read file',
50
+ target: '/tmp/test.txt',
51
+ };
52
+
53
+ const analysis: SecurityAnalysis = {
54
+ riskLevel: 'low',
55
+ reasons: [],
56
+ requiresConfirmation: false,
57
+ allowed: true,
58
+ matchedRules: [],
59
+ };
60
+
61
+ await logger.logAction(action, analysis);
62
+
63
+ const logs = await logger.getRecentLogs(1);
64
+ expect(logs.length).toBe(1);
65
+ expect(logs[0].timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
66
+ });
67
+
68
+ it('実行結果を記録できる', async () => {
69
+ const action: Action = {
70
+ type: 'file_read',
71
+ name: 'read file',
72
+ target: '/tmp/test.txt',
73
+ };
74
+
75
+ const analysis: SecurityAnalysis = {
76
+ riskLevel: 'low',
77
+ reasons: [],
78
+ requiresConfirmation: false,
79
+ allowed: true,
80
+ matchedRules: [],
81
+ };
82
+
83
+ const observation: Observation = {
84
+ success: true,
85
+ data: 'file content',
86
+ duration: 50,
87
+ };
88
+
89
+ await logger.logAction(action, analysis, observation);
90
+
91
+ const logs = await logger.getRecentLogs(1);
92
+ expect(logs[0].observation).toBeDefined();
93
+ expect(logs[0].observation?.success).toBe(true);
94
+ expect(logs[0].observation?.duration).toBe(50);
95
+ });
96
+
97
+ it('ユーザー確認を記録できる', async () => {
98
+ const action: Action = {
99
+ type: 'file_delete',
100
+ name: 'delete file',
101
+ target: '/tmp/test.txt',
102
+ };
103
+
104
+ const analysis: SecurityAnalysis = {
105
+ riskLevel: 'high',
106
+ reasons: ['File deletion is high risk'],
107
+ requiresConfirmation: true,
108
+ allowed: true,
109
+ matchedRules: ['file_delete_high_risk'],
110
+ };
111
+
112
+ const observation: Observation = {
113
+ success: true,
114
+ duration: 10,
115
+ };
116
+
117
+ const userConfirmation = {
118
+ confirmed: true,
119
+ confirmedAt: new Date().toISOString(),
120
+ confirmedBy: 'user-1',
121
+ comment: 'Approved',
122
+ };
123
+
124
+ await logger.logAction(action, analysis, observation, userConfirmation);
125
+
126
+ const logs = await logger.getRecentLogs(1);
127
+ expect(logs[0].userConfirmation).toBeDefined();
128
+ expect(logs[0].userConfirmation?.confirmed).toBe(true);
129
+ expect(logs[0].userConfirmation?.confirmedBy).toBe('user-1');
130
+ });
131
+ });
132
+
133
+ describe('ログ検索', () => {
134
+ beforeEach(async () => {
135
+ // テストデータを準備
136
+ const actions: { action: Action; analysis: SecurityAnalysis; observation?: Observation }[] = [
137
+ {
138
+ action: {
139
+ type: 'file_read',
140
+ name: 'read file 1',
141
+ target: '/tmp/test1.txt',
142
+ context: { userId: 'user-1' },
143
+ },
144
+ analysis: {
145
+ riskLevel: 'low',
146
+ reasons: [],
147
+ requiresConfirmation: false,
148
+ allowed: true,
149
+ matchedRules: [],
150
+ },
151
+ observation: { success: true, duration: 10 },
152
+ },
153
+ {
154
+ action: {
155
+ type: 'file_delete',
156
+ name: 'delete file',
157
+ target: '/tmp/test2.txt',
158
+ context: { userId: 'user-2' },
159
+ },
160
+ analysis: {
161
+ riskLevel: 'high',
162
+ reasons: ['File deletion'],
163
+ requiresConfirmation: true,
164
+ allowed: true,
165
+ matchedRules: ['file_delete_high_risk'],
166
+ },
167
+ observation: { success: false, error: 'Permission denied', duration: 5 },
168
+ },
169
+ {
170
+ action: {
171
+ type: 'network_request',
172
+ name: 'fetch data',
173
+ target: 'https://api.example.com',
174
+ context: { userId: 'user-1' },
175
+ },
176
+ analysis: {
177
+ riskLevel: 'medium',
178
+ reasons: [],
179
+ requiresConfirmation: false,
180
+ allowed: true,
181
+ matchedRules: [],
182
+ },
183
+ observation: { success: true, duration: 100 },
184
+ },
185
+ ];
186
+
187
+ for (const { action, analysis, observation } of actions) {
188
+ await logger.logAction(action, analysis, observation);
189
+ }
190
+ });
191
+
192
+ it('アクションタイプでフィルターできる', async () => {
193
+ const logs = await logger.queryLogs({ actionTypes: ['file_delete'] });
194
+
195
+ expect(logs.length).toBe(1);
196
+ expect(logs[0].action.type).toBe('file_delete');
197
+ });
198
+
199
+ it('リスクレベルでフィルターできる', async () => {
200
+ const logs = await logger.queryLogs({ minRiskLevel: 'high' });
201
+
202
+ expect(logs.length).toBe(1);
203
+ expect(logs[0].analysis.riskLevel).toBe('high');
204
+ });
205
+
206
+ it('成功/失敗でフィルターできる', async () => {
207
+ const successLogs = await logger.queryLogs({ success: true });
208
+ expect(successLogs.length).toBe(2);
209
+
210
+ const failureLogs = await logger.queryLogs({ success: false });
211
+ expect(failureLogs.length).toBe(1);
212
+ });
213
+
214
+ it('ユーザーIDでフィルターできる', async () => {
215
+ const logs = await logger.queryLogs({ userId: 'user-1' });
216
+
217
+ expect(logs.length).toBe(2);
218
+ expect(logs.every((l) => l.action.context?.userId === 'user-1')).toBe(true);
219
+ });
220
+
221
+ it('キーワードで検索できる', async () => {
222
+ const logs = await logger.queryLogs({ keyword: 'Permission denied' });
223
+
224
+ expect(logs.length).toBe(1);
225
+ expect(logs[0].observation?.error).toContain('Permission denied');
226
+ });
227
+
228
+ it('件数制限ができる', async () => {
229
+ const logs = await logger.queryLogs({ limit: 2 });
230
+
231
+ expect(logs.length).toBe(2);
232
+ });
233
+ });
234
+
235
+ describe('便利メソッド', () => {
236
+ it('getRecentLogs - 最近のログを取得', async () => {
237
+ await logger.logAction(
238
+ { type: 'file_read', name: 'read' },
239
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] }
240
+ );
241
+
242
+ const logs = await logger.getRecentLogs(5);
243
+ expect(logs.length).toBe(1);
244
+ });
245
+
246
+ it('getHighRiskLogs - 高リスクログを取得', async () => {
247
+ await logger.logAction(
248
+ { type: 'file_delete', name: 'delete' },
249
+ { riskLevel: 'high', reasons: [], requiresConfirmation: true, allowed: true, matchedRules: [] }
250
+ );
251
+
252
+ const logs = await logger.getHighRiskLogs(5);
253
+ expect(logs.length).toBe(1);
254
+ expect(logs[0].analysis.riskLevel).toBe('high');
255
+ });
256
+
257
+ it('getUserLogs - ユーザーのログを取得', async () => {
258
+ await logger.logAction(
259
+ { type: 'file_read', name: 'read', context: { userId: 'test-user' } },
260
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] }
261
+ );
262
+
263
+ const logs = await logger.getUserLogs('test-user');
264
+ expect(logs.length).toBe(1);
265
+ });
266
+
267
+ it('getLogCount - ログ件数を取得', async () => {
268
+ await logger.logAction(
269
+ { type: 'file_read', name: 'read' },
270
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] }
271
+ );
272
+
273
+ const count = await logger.getLogCount();
274
+ expect(count).toBe(1);
275
+ });
276
+
277
+ it('clearLogs - ログをクリア', async () => {
278
+ await logger.logAction(
279
+ { type: 'file_read', name: 'read' },
280
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] }
281
+ );
282
+
283
+ await logger.clearLogs();
284
+
285
+ const count = await logger.getLogCount();
286
+ expect(count).toBe(0);
287
+ });
288
+ });
289
+
290
+ describe('サマリー生成', () => {
291
+ beforeEach(async () => {
292
+ // テストデータ
293
+ await logger.logAction(
294
+ { type: 'file_read', name: 'read 1' },
295
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] },
296
+ { success: true, duration: 10 }
297
+ );
298
+ await logger.logAction(
299
+ { type: 'file_read', name: 'read 2' },
300
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] },
301
+ { success: true, duration: 20 }
302
+ );
303
+ await logger.logAction(
304
+ { type: 'file_delete', name: 'delete' },
305
+ { riskLevel: 'high', reasons: [], requiresConfirmation: true, allowed: true, matchedRules: [] },
306
+ { success: false, error: 'Failed', duration: 5 }
307
+ );
308
+ await logger.logAction(
309
+ { type: 'file_write', name: 'blocked write' },
310
+ { riskLevel: 'critical', reasons: [], requiresConfirmation: false, allowed: false, blockReason: 'Blocked', matchedRules: [] }
311
+ );
312
+ });
313
+
314
+ it('サマリーを生成できる', async () => {
315
+ const summary = await logger.generateSummary();
316
+
317
+ expect(summary.totalActions).toBe(4);
318
+ expect(summary.byRiskLevel.low).toBe(2);
319
+ expect(summary.byRiskLevel.high).toBe(1);
320
+ expect(summary.byRiskLevel.critical).toBe(1);
321
+ expect(summary.byActionType.file_read).toBe(2);
322
+ expect(summary.byActionType.file_delete).toBe(1);
323
+ expect(summary.successRate).toBeCloseTo(2 / 3);
324
+ expect(summary.blockedCount).toBe(1);
325
+ });
326
+ });
327
+
328
+ describe('InMemoryLogStorage', () => {
329
+ it('最大件数を超えたら古いログが削除される', async () => {
330
+ const storage = new InMemoryLogStorage(3);
331
+ const customLogger = new ActionLogger({ storage });
332
+
333
+ for (let i = 0; i < 5; i++) {
334
+ await customLogger.logAction(
335
+ { type: 'file_read', name: `read ${i}` },
336
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] }
337
+ );
338
+ }
339
+
340
+ const logs = await customLogger.getRecentLogs(10);
341
+ expect(logs.length).toBe(3);
342
+ });
343
+
344
+ it('カスタムID生成関数を使用できる', async () => {
345
+ let counter = 0;
346
+ const customLogger = new ActionLogger({
347
+ generateId: () => `custom-${++counter}`,
348
+ });
349
+
350
+ const logId = await customLogger.logAction(
351
+ { type: 'file_read', name: 'read' },
352
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] }
353
+ );
354
+
355
+ expect(logId).toBe('custom-1');
356
+ });
357
+
358
+ it('最小リスクレベル未満はログされない', async () => {
359
+ const customLogger = new ActionLogger({
360
+ minLogLevel: 'medium',
361
+ });
362
+
363
+ await customLogger.logAction(
364
+ { type: 'file_read', name: 'read' },
365
+ { riskLevel: 'low', reasons: [], requiresConfirmation: false, allowed: true, matchedRules: [] }
366
+ );
367
+
368
+ const count = await customLogger.getLogCount();
369
+ expect(count).toBe(0);
370
+ });
371
+ });
372
+ });