@trentapps/manager-protocol 1.1.3 → 1.2.1
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/README.md +164 -17
- package/dist/analyzers/CSSAnalyzer.d.ts +180 -8
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/CSSAnalyzer.js +561 -105
- package/dist/analyzers/CSSAnalyzer.js.map +1 -1
- package/dist/config/dashboard.d.ts +55 -0
- package/dist/config/dashboard.d.ts.map +1 -0
- package/dist/config/dashboard.js +103 -0
- package/dist/config/dashboard.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/dashboard/httpDashboard.d.ts +100 -0
- package/dist/dashboard/httpDashboard.d.ts.map +1 -0
- package/dist/dashboard/httpDashboard.js +1276 -0
- package/dist/dashboard/httpDashboard.js.map +1 -0
- package/dist/dashboard/index.d.ts +6 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +370 -2
- package/dist/engine/AuditLogger.d.ts.map +1 -1
- package/dist/engine/AuditLogger.js +1064 -24
- package/dist/engine/AuditLogger.js.map +1 -1
- package/dist/engine/GitHubClient.d.ts +183 -0
- package/dist/engine/GitHubClient.d.ts.map +1 -0
- package/dist/engine/GitHubClient.js +411 -0
- package/dist/engine/GitHubClient.js.map +1 -0
- package/dist/engine/RateLimiter.d.ts +5 -3
- package/dist/engine/RateLimiter.d.ts.map +1 -1
- package/dist/engine/RateLimiter.js +49 -72
- package/dist/engine/RateLimiter.js.map +1 -1
- package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
- package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
- package/dist/engine/RuleDependencyAnalyzer.js +475 -0
- package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
- package/dist/engine/RulesEngine.d.ts +102 -3
- package/dist/engine/RulesEngine.d.ts.map +1 -1
- package/dist/engine/RulesEngine.js +326 -21
- package/dist/engine/RulesEngine.js.map +1 -1
- package/dist/engine/TaskManager.d.ts +10 -14
- package/dist/engine/TaskManager.d.ts.map +1 -1
- package/dist/engine/TaskManager.js +169 -197
- package/dist/engine/TaskManager.js.map +1 -1
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +5 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/rules/azure.d.ts.map +1 -1
- package/dist/rules/azure.js +12 -14
- package/dist/rules/azure.js.map +1 -1
- package/dist/rules/compliance.d.ts.map +1 -1
- package/dist/rules/compliance.js +23 -41
- package/dist/rules/compliance.js.map +1 -1
- package/dist/rules/condition-optimizer.d.ts +151 -0
- package/dist/rules/condition-optimizer.d.ts.map +1 -0
- package/dist/rules/condition-optimizer.js +479 -0
- package/dist/rules/condition-optimizer.js.map +1 -0
- package/dist/rules/css.d.ts.map +1 -1
- package/dist/rules/css.js +538 -0
- package/dist/rules/css.js.map +1 -1
- package/dist/rules/field-standards.d.ts +1172 -0
- package/dist/rules/field-standards.d.ts.map +1 -0
- package/dist/rules/field-standards.js +908 -0
- package/dist/rules/field-standards.js.map +1 -0
- package/dist/rules/flask.d.ts.map +1 -1
- package/dist/rules/flask.js +18 -31
- package/dist/rules/flask.js.map +1 -1
- package/dist/rules/index.d.ts +220 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +155 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/ml-ai.d.ts.map +1 -1
- package/dist/rules/ml-ai.js +11 -13
- package/dist/rules/ml-ai.js.map +1 -1
- package/dist/rules/patterns.d.ts +568 -0
- package/dist/rules/patterns.d.ts.map +1 -0
- package/dist/rules/patterns.js +1359 -0
- package/dist/rules/patterns.js.map +1 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +580 -19
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/shared-patterns.d.ts +268 -0
- package/dist/rules/shared-patterns.d.ts.map +1 -0
- package/dist/rules/shared-patterns.js +556 -0
- package/dist/rules/shared-patterns.js.map +1 -0
- package/dist/rules/storage.d.ts +8 -2
- package/dist/rules/storage.d.ts.map +1 -1
- package/dist/rules/storage.js +541 -3
- package/dist/rules/storage.js.map +1 -1
- package/dist/rules/stripe.d.ts.map +1 -1
- package/dist/rules/stripe.js +19 -26
- package/dist/rules/stripe.js.map +1 -1
- package/dist/rules/websocket.d.ts.map +1 -1
- package/dist/rules/websocket.js +32 -40
- package/dist/rules/websocket.js.map +1 -1
- package/dist/supervisor/AgentSupervisor.d.ts +52 -0
- package/dist/supervisor/AgentSupervisor.d.ts.map +1 -1
- package/dist/supervisor/AgentSupervisor.js +120 -1
- package/dist/supervisor/AgentSupervisor.js.map +1 -1
- package/dist/supervisor/ManagedServerRegistry.d.ts +139 -2
- package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -1
- package/dist/supervisor/ManagedServerRegistry.js +590 -6
- package/dist/supervisor/ManagedServerRegistry.js.map +1 -1
- package/dist/supervisor/ProjectTracker.d.ts +2 -1
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
- package/dist/supervisor/ProjectTracker.js +5 -9
- package/dist/supervisor/ProjectTracker.js.map +1 -1
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/rule-tester.d.ts +217 -0
- package/dist/testing/rule-tester.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.d.ts +57 -0
- package/dist/testing/rule-tester.examples.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.js +375 -0
- package/dist/testing/rule-tester.examples.js.map +1 -0
- package/dist/testing/rule-tester.js +381 -0
- package/dist/testing/rule-tester.js.map +1 -0
- package/dist/testing/rule-validator.d.ts +141 -0
- package/dist/testing/rule-validator.d.ts.map +1 -0
- package/dist/testing/rule-validator.js +640 -0
- package/dist/testing/rule-validator.js.map +1 -0
- package/dist/types/index.d.ts +265 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +57 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/rate-limiting.d.ts +268 -0
- package/dist/utils/rate-limiting.d.ts.map +1 -0
- package/dist/utils/rate-limiting.js +403 -0
- package/dist/utils/rate-limiting.js.map +1 -0
- package/dist/utils/shared.d.ts +306 -0
- package/dist/utils/shared.d.ts.map +1 -0
- package/dist/utils/shared.js +464 -0
- package/dist/utils/shared.js.map +1 -0
- package/package.json +3 -2
|
@@ -2,12 +2,248 @@
|
|
|
2
2
|
* Enterprise Agent Supervisor - Audit Logger
|
|
3
3
|
*
|
|
4
4
|
* Comprehensive audit logging for compliance, security, and operational visibility.
|
|
5
|
+
*
|
|
6
|
+
* Storage Strategy:
|
|
7
|
+
* - Uses write-through caching: events are only added to memory after successful DB write
|
|
8
|
+
* - Failed writes are queued for retry
|
|
9
|
+
* - Periodic sync checks ensure memory/DB consistency
|
|
10
|
+
*
|
|
11
|
+
* Query System (Task #49):
|
|
12
|
+
* - QueryBuilder provides fluent API for complex queries
|
|
13
|
+
* - Supports cursor-based pagination for large result sets
|
|
14
|
+
* - Aggregation queries for analytics and dashboards
|
|
15
|
+
* - Full-text search in metadata and details JSON fields
|
|
5
16
|
*/
|
|
6
17
|
import { v4 as uuidv4 } from 'uuid';
|
|
7
18
|
import { mkdirSync } from 'fs';
|
|
8
19
|
import path from 'path';
|
|
9
20
|
import Database from 'better-sqlite3';
|
|
10
21
|
import { withRetry, WebhookDeliveryError, formatError } from '../utils/errors.js';
|
|
22
|
+
import { calculateGroupedCounts } from '../utils/shared.js';
|
|
23
|
+
/**
|
|
24
|
+
* QueryBuilder - Fluent API for building audit event queries
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const results = await logger.query()
|
|
29
|
+
* .where('eventType', 'action_executed')
|
|
30
|
+
* .where('outcome', 'success')
|
|
31
|
+
* .since(new Date('2024-01-01'))
|
|
32
|
+
* .until(new Date('2024-12-31'))
|
|
33
|
+
* .orderBy('timestamp', 'desc')
|
|
34
|
+
* .limit(100)
|
|
35
|
+
* .execute();
|
|
36
|
+
*
|
|
37
|
+
* // With pagination
|
|
38
|
+
* const page1 = await logger.query()
|
|
39
|
+
* .eventType('action_evaluated')
|
|
40
|
+
* .limit(50)
|
|
41
|
+
* .executePaginated();
|
|
42
|
+
*
|
|
43
|
+
* // Get next page
|
|
44
|
+
* const page2 = await logger.query()
|
|
45
|
+
* .eventType('action_evaluated')
|
|
46
|
+
* .cursor(page1.pagination.nextCursor!)
|
|
47
|
+
* .limit(50)
|
|
48
|
+
* .executePaginated();
|
|
49
|
+
*
|
|
50
|
+
* // Aggregation
|
|
51
|
+
* const stats = await logger.query()
|
|
52
|
+
* .since(new Date('2024-01-01'))
|
|
53
|
+
* .aggregate('day');
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class QueryBuilder {
|
|
57
|
+
filters = {};
|
|
58
|
+
paginationOpts = {};
|
|
59
|
+
orderByField = 'timestamp';
|
|
60
|
+
orderDirection = 'desc';
|
|
61
|
+
logger;
|
|
62
|
+
constructor(logger) {
|
|
63
|
+
this.logger = logger;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Add a filter condition
|
|
67
|
+
*/
|
|
68
|
+
where(field, value) {
|
|
69
|
+
this.filters[field] = value;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Filter by event type(s)
|
|
74
|
+
*/
|
|
75
|
+
eventType(type) {
|
|
76
|
+
this.filters.eventType = type;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Filter by agent ID(s)
|
|
81
|
+
*/
|
|
82
|
+
agentId(id) {
|
|
83
|
+
this.filters.agentId = id;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Filter by session ID(s)
|
|
88
|
+
*/
|
|
89
|
+
sessionId(id) {
|
|
90
|
+
this.filters.sessionId = id;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Filter by user ID(s)
|
|
95
|
+
*/
|
|
96
|
+
userId(id) {
|
|
97
|
+
this.filters.userId = id;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Filter by outcome(s)
|
|
102
|
+
*/
|
|
103
|
+
outcome(outcome) {
|
|
104
|
+
this.filters.outcome = outcome;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Filter by risk level(s)
|
|
109
|
+
*/
|
|
110
|
+
riskLevel(level) {
|
|
111
|
+
this.filters.riskLevel = level;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Filter events after this date (inclusive)
|
|
116
|
+
*/
|
|
117
|
+
since(date) {
|
|
118
|
+
this.filters.since = date instanceof Date ? date.toISOString() : date;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Filter events before this date (inclusive)
|
|
123
|
+
*/
|
|
124
|
+
until(date) {
|
|
125
|
+
this.filters.until = date instanceof Date ? date.toISOString() : date;
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Filter by correlation ID
|
|
130
|
+
*/
|
|
131
|
+
correlatedWith(correlationId) {
|
|
132
|
+
this.filters.correlationId = correlationId;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Filter by action (partial match, case-insensitive)
|
|
137
|
+
*/
|
|
138
|
+
action(actionPattern) {
|
|
139
|
+
this.filters.action = actionPattern;
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Full-text search in metadata JSON
|
|
144
|
+
*/
|
|
145
|
+
searchMetadata(searchTerm) {
|
|
146
|
+
this.filters.metadataSearch = searchTerm;
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Full-text search in details JSON
|
|
151
|
+
*/
|
|
152
|
+
searchDetails(searchTerm) {
|
|
153
|
+
this.filters.detailsSearch = searchTerm;
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Set ordering
|
|
158
|
+
*/
|
|
159
|
+
orderBy(field, direction = 'desc') {
|
|
160
|
+
this.orderByField = field;
|
|
161
|
+
this.orderDirection = direction;
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Set result limit
|
|
166
|
+
*/
|
|
167
|
+
limit(count) {
|
|
168
|
+
this.paginationOpts.limit = count;
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Set offset for offset-based pagination
|
|
173
|
+
*/
|
|
174
|
+
offset(count) {
|
|
175
|
+
this.paginationOpts.offset = count;
|
|
176
|
+
return this;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Set cursor for cursor-based pagination
|
|
180
|
+
*/
|
|
181
|
+
cursor(cursorString) {
|
|
182
|
+
this.paginationOpts.cursor = cursorString;
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Execute the query and return results
|
|
187
|
+
*/
|
|
188
|
+
async execute() {
|
|
189
|
+
return this.logger.executeQuery(this.filters, this.paginationOpts, this.orderByField, this.orderDirection);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Execute query with pagination metadata
|
|
193
|
+
*/
|
|
194
|
+
async executePaginated() {
|
|
195
|
+
return this.logger.executeQueryPaginated(this.filters, this.paginationOpts, this.orderByField, this.orderDirection);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get aggregated statistics for the filtered events
|
|
199
|
+
*/
|
|
200
|
+
async aggregate(interval) {
|
|
201
|
+
return this.logger.aggregateQuery(this.filters, interval);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Get count of matching events
|
|
205
|
+
*/
|
|
206
|
+
async count() {
|
|
207
|
+
return this.logger.countQuery(this.filters);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Get the first matching event
|
|
211
|
+
*/
|
|
212
|
+
async first() {
|
|
213
|
+
const results = await this.limit(1).execute();
|
|
214
|
+
return results[0];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Check if any events match
|
|
218
|
+
*/
|
|
219
|
+
async exists() {
|
|
220
|
+
const count = await this.count();
|
|
221
|
+
return count > 0;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Get filters for inspection/debugging
|
|
225
|
+
*/
|
|
226
|
+
getFilters() {
|
|
227
|
+
return { ...this.filters };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Get pagination options for inspection/debugging
|
|
231
|
+
*/
|
|
232
|
+
getPaginationOptions() {
|
|
233
|
+
return { ...this.paginationOpts };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Clone the query builder for branching queries
|
|
237
|
+
*/
|
|
238
|
+
clone() {
|
|
239
|
+
const cloned = new QueryBuilder(this.logger);
|
|
240
|
+
cloned.filters = { ...this.filters };
|
|
241
|
+
cloned.paginationOpts = { ...this.paginationOpts };
|
|
242
|
+
cloned.orderByField = this.orderByField;
|
|
243
|
+
cloned.orderDirection = this.orderDirection;
|
|
244
|
+
return cloned;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
11
247
|
export class AuditLogger {
|
|
12
248
|
events = [];
|
|
13
249
|
maxEvents;
|
|
@@ -21,6 +257,15 @@ export class AuditLogger {
|
|
|
21
257
|
db;
|
|
22
258
|
initialized = false;
|
|
23
259
|
initPromise;
|
|
260
|
+
// Retry queue for failed database writes
|
|
261
|
+
failedWrites = [];
|
|
262
|
+
retryIntervalMs;
|
|
263
|
+
maxRetryAttempts;
|
|
264
|
+
retryTimer;
|
|
265
|
+
// Sync mechanism
|
|
266
|
+
syncIntervalMs;
|
|
267
|
+
syncTimer;
|
|
268
|
+
lastSyncTime;
|
|
24
269
|
constructor(options = {}) {
|
|
25
270
|
this.maxEvents = options.maxEvents || 10000;
|
|
26
271
|
this.enableConsoleLog = options.enableConsoleLog || false;
|
|
@@ -30,6 +275,9 @@ export class AuditLogger {
|
|
|
30
275
|
this.onEvent = options.onEvent;
|
|
31
276
|
this.onWebhookError = options.onWebhookError;
|
|
32
277
|
this.dbPath = options.dbPath || options.logFile; // Support legacy logFile option
|
|
278
|
+
this.retryIntervalMs = options.retryIntervalMs ?? 5000;
|
|
279
|
+
this.maxRetryAttempts = options.maxRetryAttempts ?? 5;
|
|
280
|
+
this.syncIntervalMs = options.syncIntervalMs ?? 60000; // Default: check every minute
|
|
33
281
|
}
|
|
34
282
|
/**
|
|
35
283
|
* Initialize and setup SQLite database if configured
|
|
@@ -59,9 +307,278 @@ export class AuditLogger {
|
|
|
59
307
|
async doInitialize() {
|
|
60
308
|
if (this.dbPath) {
|
|
61
309
|
await this.initDatabase();
|
|
310
|
+
// Start retry timer for failed writes
|
|
311
|
+
this.startRetryTimer();
|
|
312
|
+
// Start sync timer if enabled
|
|
313
|
+
if (this.syncIntervalMs > 0) {
|
|
314
|
+
this.startSyncTimer();
|
|
315
|
+
}
|
|
62
316
|
}
|
|
63
317
|
this.initialized = true;
|
|
64
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Start the retry timer for failed database writes
|
|
321
|
+
*/
|
|
322
|
+
startRetryTimer() {
|
|
323
|
+
if (this.retryTimer)
|
|
324
|
+
return;
|
|
325
|
+
this.retryTimer = setInterval(() => {
|
|
326
|
+
this.processRetryQueue();
|
|
327
|
+
}, this.retryIntervalMs);
|
|
328
|
+
// Don't prevent Node from exiting
|
|
329
|
+
if (this.retryTimer.unref) {
|
|
330
|
+
this.retryTimer.unref();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Start the periodic sync timer
|
|
335
|
+
*/
|
|
336
|
+
startSyncTimer() {
|
|
337
|
+
if (this.syncTimer)
|
|
338
|
+
return;
|
|
339
|
+
this.syncTimer = setInterval(() => {
|
|
340
|
+
this.checkSync().catch(err => {
|
|
341
|
+
console.error('[AuditLogger] Sync check failed:', err);
|
|
342
|
+
});
|
|
343
|
+
}, this.syncIntervalMs);
|
|
344
|
+
// Don't prevent Node from exiting
|
|
345
|
+
if (this.syncTimer.unref) {
|
|
346
|
+
this.syncTimer.unref();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Stop all timers (for cleanup/shutdown)
|
|
351
|
+
*/
|
|
352
|
+
stopTimers() {
|
|
353
|
+
if (this.retryTimer) {
|
|
354
|
+
clearInterval(this.retryTimer);
|
|
355
|
+
this.retryTimer = undefined;
|
|
356
|
+
}
|
|
357
|
+
if (this.syncTimer) {
|
|
358
|
+
clearInterval(this.syncTimer);
|
|
359
|
+
this.syncTimer = undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Process the retry queue for failed database writes
|
|
364
|
+
*/
|
|
365
|
+
processRetryQueue() {
|
|
366
|
+
if (this.failedWrites.length === 0 || !this.db)
|
|
367
|
+
return;
|
|
368
|
+
const toRetry = [...this.failedWrites];
|
|
369
|
+
this.failedWrites = [];
|
|
370
|
+
for (const item of toRetry) {
|
|
371
|
+
const success = this.saveToDatabase(item.event);
|
|
372
|
+
if (success) {
|
|
373
|
+
// Successfully written - add to memory cache
|
|
374
|
+
this.addToMemoryCache(item.event);
|
|
375
|
+
console.log(`[AuditLogger] Retry succeeded for event ${item.event.eventId} after ${item.attempts + 1} attempts`);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// Still failing
|
|
379
|
+
item.attempts++;
|
|
380
|
+
item.lastError = 'Database write failed';
|
|
381
|
+
if (item.attempts < this.maxRetryAttempts) {
|
|
382
|
+
this.failedWrites.push(item);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
console.error(`[AuditLogger] Giving up on event ${item.event.eventId} after ${item.attempts} attempts. ` +
|
|
386
|
+
`First attempt: ${item.firstAttempt}, Last error: ${item.lastError}`);
|
|
387
|
+
// Still add to memory cache so event isn't completely lost
|
|
388
|
+
// but mark it as potentially inconsistent
|
|
389
|
+
this.addToMemoryCache(item.event);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Check consistency between memory cache and database
|
|
396
|
+
* Returns sync status information
|
|
397
|
+
*/
|
|
398
|
+
async checkSync() {
|
|
399
|
+
const result = {
|
|
400
|
+
inSync: true,
|
|
401
|
+
memoryCount: this.events.length,
|
|
402
|
+
dbCount: 0,
|
|
403
|
+
missingInDb: [],
|
|
404
|
+
missingInMemory: [],
|
|
405
|
+
pendingRetries: this.failedWrites.length
|
|
406
|
+
};
|
|
407
|
+
if (!this.db) {
|
|
408
|
+
// No database, memory is source of truth
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
// Get count from database
|
|
413
|
+
const countStmt = this.db.prepare('SELECT COUNT(*) as count FROM audit_events');
|
|
414
|
+
const countResult = countStmt.get();
|
|
415
|
+
result.dbCount = countResult.count;
|
|
416
|
+
// Check recent events for consistency (last 100)
|
|
417
|
+
const recentMemory = this.events.slice(-100);
|
|
418
|
+
const recentMemoryIds = new Set(recentMemory.map(e => e.eventId));
|
|
419
|
+
const recentDbStmt = this.db.prepare(`
|
|
420
|
+
SELECT eventId FROM audit_events
|
|
421
|
+
ORDER BY timestamp DESC
|
|
422
|
+
LIMIT 100
|
|
423
|
+
`);
|
|
424
|
+
const recentDbRows = recentDbStmt.all();
|
|
425
|
+
const recentDbIds = new Set(recentDbRows.map(r => r.eventId));
|
|
426
|
+
// Find events in memory but not in DB
|
|
427
|
+
for (const event of recentMemory) {
|
|
428
|
+
if (!recentDbIds.has(event.eventId)) {
|
|
429
|
+
result.missingInDb.push(event.eventId);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// Find events in DB but not in memory
|
|
433
|
+
for (const row of recentDbRows) {
|
|
434
|
+
if (!recentMemoryIds.has(row.eventId)) {
|
|
435
|
+
result.missingInMemory.push(row.eventId);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
result.inSync = result.missingInDb.length === 0 &&
|
|
439
|
+
result.missingInMemory.length === 0 &&
|
|
440
|
+
result.pendingRetries === 0;
|
|
441
|
+
this.lastSyncTime = new Date().toISOString();
|
|
442
|
+
if (!result.inSync) {
|
|
443
|
+
console.warn('[AuditLogger] Sync check found inconsistencies:', {
|
|
444
|
+
missingInDb: result.missingInDb.length,
|
|
445
|
+
missingInMemory: result.missingInMemory.length,
|
|
446
|
+
pendingRetries: result.pendingRetries
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
console.error('[AuditLogger] Sync check error:', error);
|
|
453
|
+
result.inSync = false;
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Force synchronization between memory and database
|
|
459
|
+
* Reconciles any differences by:
|
|
460
|
+
* 1. Writing memory-only events to database
|
|
461
|
+
* 2. Loading database-only events to memory
|
|
462
|
+
* 3. Processing any pending retries
|
|
463
|
+
*/
|
|
464
|
+
async sync() {
|
|
465
|
+
const result = {
|
|
466
|
+
eventsSyncedToDb: 0,
|
|
467
|
+
eventsSyncedToMemory: 0,
|
|
468
|
+
retriesProcessed: 0,
|
|
469
|
+
errors: []
|
|
470
|
+
};
|
|
471
|
+
if (!this.db) {
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
// First, process any pending retries
|
|
476
|
+
const pendingCount = this.failedWrites.length;
|
|
477
|
+
this.processRetryQueue();
|
|
478
|
+
result.retriesProcessed = pendingCount - this.failedWrites.length;
|
|
479
|
+
// Check current sync status
|
|
480
|
+
const status = await this.checkSync();
|
|
481
|
+
// Write memory-only events to database
|
|
482
|
+
for (const eventId of status.missingInDb) {
|
|
483
|
+
const event = this.events.find(e => e.eventId === eventId);
|
|
484
|
+
if (event) {
|
|
485
|
+
const success = this.saveToDatabase(event);
|
|
486
|
+
if (success) {
|
|
487
|
+
result.eventsSyncedToDb++;
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
result.errors.push(`Failed to sync event ${eventId} to database`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Load database-only events to memory
|
|
495
|
+
if (status.missingInMemory.length > 0) {
|
|
496
|
+
const placeholders = status.missingInMemory.map(() => '?').join(',');
|
|
497
|
+
const stmt = this.db.prepare(`
|
|
498
|
+
SELECT * FROM audit_events
|
|
499
|
+
WHERE eventId IN (${placeholders})
|
|
500
|
+
`);
|
|
501
|
+
const rows = stmt.all(...status.missingInMemory);
|
|
502
|
+
for (const row of rows) {
|
|
503
|
+
const event = {
|
|
504
|
+
eventId: row.eventId,
|
|
505
|
+
eventType: row.eventType,
|
|
506
|
+
action: row.action,
|
|
507
|
+
timestamp: row.timestamp,
|
|
508
|
+
outcome: row.outcome,
|
|
509
|
+
agentId: row.agentId,
|
|
510
|
+
sessionId: row.sessionId,
|
|
511
|
+
userId: row.userId,
|
|
512
|
+
riskLevel: row.riskLevel,
|
|
513
|
+
details: row.details ? JSON.parse(row.details) : undefined,
|
|
514
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
515
|
+
correlationId: row.correlationId,
|
|
516
|
+
parentEventId: row.parentEventId
|
|
517
|
+
};
|
|
518
|
+
// Insert in correct position based on timestamp
|
|
519
|
+
this.insertEventSorted(event);
|
|
520
|
+
result.eventsSyncedToMemory++;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
console.log('[AuditLogger] Sync completed:', result);
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
528
|
+
result.errors.push(`Sync error: ${errorMsg}`);
|
|
529
|
+
console.error('[AuditLogger] Sync failed:', error);
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Insert event into memory cache in sorted order by timestamp
|
|
535
|
+
*/
|
|
536
|
+
insertEventSorted(event) {
|
|
537
|
+
// Find insertion point
|
|
538
|
+
let insertIdx = this.events.length;
|
|
539
|
+
for (let i = this.events.length - 1; i >= 0; i--) {
|
|
540
|
+
if (this.events[i].timestamp <= event.timestamp) {
|
|
541
|
+
insertIdx = i + 1;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
if (i === 0) {
|
|
545
|
+
insertIdx = 0;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
this.events.splice(insertIdx, 0, event);
|
|
549
|
+
// Trim if exceeds max
|
|
550
|
+
if (this.events.length > this.maxEvents) {
|
|
551
|
+
this.events = this.events.slice(-this.maxEvents);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Add event to memory cache (internal helper)
|
|
556
|
+
*/
|
|
557
|
+
addToMemoryCache(event) {
|
|
558
|
+
// Check if already in cache (to avoid duplicates from retries)
|
|
559
|
+
if (this.events.some(e => e.eventId === event.eventId)) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this.events.push(event);
|
|
563
|
+
// Trim if exceeds max
|
|
564
|
+
if (this.events.length > this.maxEvents) {
|
|
565
|
+
this.events = this.events.slice(-this.maxEvents);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Get retry queue status
|
|
570
|
+
*/
|
|
571
|
+
getRetryQueueStatus() {
|
|
572
|
+
return {
|
|
573
|
+
pendingCount: this.failedWrites.length,
|
|
574
|
+
oldestAttempt: this.failedWrites.length > 0 ? this.failedWrites[0].firstAttempt : null,
|
|
575
|
+
events: this.failedWrites.map(fw => ({
|
|
576
|
+
eventId: fw.event.eventId,
|
|
577
|
+
attempts: fw.attempts,
|
|
578
|
+
lastError: fw.lastError
|
|
579
|
+
}))
|
|
580
|
+
};
|
|
581
|
+
}
|
|
65
582
|
/**
|
|
66
583
|
* Initialize SQLite database
|
|
67
584
|
*/
|
|
@@ -115,6 +632,11 @@ export class AuditLogger {
|
|
|
115
632
|
}
|
|
116
633
|
/**
|
|
117
634
|
* Log an audit event
|
|
635
|
+
*
|
|
636
|
+
* Storage Strategy (Write-Through Caching):
|
|
637
|
+
* - If database is available: write to DB first, then add to memory on success
|
|
638
|
+
* - If DB write fails: queue for retry, do NOT add to memory (prevents drift)
|
|
639
|
+
* - If no database: add directly to memory (memory-only mode)
|
|
118
640
|
*/
|
|
119
641
|
async log(params) {
|
|
120
642
|
const event = {
|
|
@@ -132,13 +654,29 @@ export class AuditLogger {
|
|
|
132
654
|
correlationId: params.correlationId,
|
|
133
655
|
parentEventId: params.parentEventId
|
|
134
656
|
};
|
|
135
|
-
//
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
657
|
+
// Write-through caching: DB first, then memory
|
|
658
|
+
if (this.db) {
|
|
659
|
+
const dbWriteSuccess = this.saveToDatabase(event);
|
|
660
|
+
if (dbWriteSuccess) {
|
|
661
|
+
// DB write succeeded - safe to add to memory
|
|
662
|
+
this.addToMemoryCache(event);
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
// DB write failed - queue for retry, don't add to memory yet
|
|
666
|
+
this.failedWrites.push({
|
|
667
|
+
event,
|
|
668
|
+
attempts: 1,
|
|
669
|
+
lastError: 'Initial database write failed',
|
|
670
|
+
firstAttempt: new Date().toISOString()
|
|
671
|
+
});
|
|
672
|
+
console.warn(`[AuditLogger] Event ${event.eventId} queued for retry after initial DB write failure`);
|
|
673
|
+
}
|
|
140
674
|
}
|
|
141
|
-
|
|
675
|
+
else {
|
|
676
|
+
// No database - memory only mode
|
|
677
|
+
this.addToMemoryCache(event);
|
|
678
|
+
}
|
|
679
|
+
// Console logging (always happens regardless of DB status)
|
|
142
680
|
if (this.enableConsoleLog) {
|
|
143
681
|
this.logToConsole(event);
|
|
144
682
|
}
|
|
@@ -150,10 +688,6 @@ export class AuditLogger {
|
|
|
150
688
|
if (this.onEvent) {
|
|
151
689
|
await this.onEvent(event);
|
|
152
690
|
}
|
|
153
|
-
// Database logging
|
|
154
|
-
if (this.db) {
|
|
155
|
-
this.saveToDatabase(event);
|
|
156
|
-
}
|
|
157
691
|
return event;
|
|
158
692
|
}
|
|
159
693
|
/**
|
|
@@ -237,10 +771,11 @@ export class AuditLogger {
|
|
|
237
771
|
}
|
|
238
772
|
/**
|
|
239
773
|
* Save event to database
|
|
774
|
+
* @returns true if save was successful, false otherwise
|
|
240
775
|
*/
|
|
241
776
|
saveToDatabase(event) {
|
|
242
777
|
if (!this.db)
|
|
243
|
-
return;
|
|
778
|
+
return false;
|
|
244
779
|
try {
|
|
245
780
|
const stmt = this.db.prepare(`
|
|
246
781
|
INSERT INTO audit_events (
|
|
@@ -250,9 +785,11 @@ export class AuditLogger {
|
|
|
250
785
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
251
786
|
`);
|
|
252
787
|
stmt.run(event.eventId, event.eventType, event.action, event.timestamp, event.outcome, event.agentId || null, event.sessionId || null, event.userId || null, event.riskLevel || null, event.details ? JSON.stringify(event.details) : null, event.metadata ? JSON.stringify(event.metadata) : null, event.correlationId || null, event.parentEventId || null);
|
|
788
|
+
return true;
|
|
253
789
|
}
|
|
254
790
|
catch (error) {
|
|
255
791
|
console.error('[AuditLogger] Failed to save to database:', error);
|
|
792
|
+
return false;
|
|
256
793
|
}
|
|
257
794
|
}
|
|
258
795
|
/**
|
|
@@ -289,8 +826,484 @@ export class AuditLogger {
|
|
|
289
826
|
return [];
|
|
290
827
|
}
|
|
291
828
|
}
|
|
829
|
+
// ============================================================================
|
|
830
|
+
// QUERY BUILDER METHODS (Task #49)
|
|
831
|
+
// ============================================================================
|
|
832
|
+
/**
|
|
833
|
+
* Create a new QueryBuilder for fluent query construction
|
|
834
|
+
*
|
|
835
|
+
* Usage:
|
|
836
|
+
* ```typescript
|
|
837
|
+
* const events = await logger.query()
|
|
838
|
+
* .eventType('action_executed')
|
|
839
|
+
* .since(new Date('2024-01-01'))
|
|
840
|
+
* .limit(100)
|
|
841
|
+
* .execute();
|
|
842
|
+
* ```
|
|
843
|
+
*/
|
|
844
|
+
query() {
|
|
845
|
+
return new QueryBuilder(this);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Execute a query with filters (called by QueryBuilder)
|
|
849
|
+
*/
|
|
850
|
+
executeQuery(filters, pagination, orderByField, orderDirection) {
|
|
851
|
+
// Use database if available for better performance
|
|
852
|
+
if (this.db) {
|
|
853
|
+
return this.executeDbQuery(filters, pagination, orderByField, orderDirection);
|
|
854
|
+
}
|
|
855
|
+
// Fall back to memory-based filtering
|
|
856
|
+
return this.executeMemoryQuery(filters, pagination, orderByField, orderDirection);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Execute query with pagination metadata (called by QueryBuilder)
|
|
860
|
+
*/
|
|
861
|
+
async executeQueryPaginated(filters, pagination, orderByField, orderDirection) {
|
|
862
|
+
const totalCount = this.countQuery(filters);
|
|
863
|
+
const limit = pagination.limit || 100;
|
|
864
|
+
// If cursor is provided, decode it
|
|
865
|
+
let decodedCursor;
|
|
866
|
+
if (pagination.cursor) {
|
|
867
|
+
try {
|
|
868
|
+
decodedCursor = JSON.parse(Buffer.from(pagination.cursor, 'base64').toString('utf-8'));
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
// Invalid cursor, ignore it
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Adjust filters for cursor-based pagination
|
|
875
|
+
const adjustedFilters = { ...filters };
|
|
876
|
+
if (decodedCursor) {
|
|
877
|
+
if (decodedCursor.direction === 'forward') {
|
|
878
|
+
if (orderDirection === 'desc') {
|
|
879
|
+
adjustedFilters.until = decodedCursor.timestamp;
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
adjustedFilters.since = decodedCursor.timestamp;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const data = this.executeQuery(adjustedFilters, { ...pagination, limit: limit + 1 }, orderByField, orderDirection);
|
|
887
|
+
// Check if there are more results
|
|
888
|
+
const hasMore = data.length > limit;
|
|
889
|
+
if (hasMore) {
|
|
890
|
+
data.pop(); // Remove the extra item
|
|
891
|
+
}
|
|
892
|
+
// Generate cursors
|
|
893
|
+
let nextCursor;
|
|
894
|
+
let previousCursor;
|
|
895
|
+
if (data.length > 0) {
|
|
896
|
+
const lastEvent = data[data.length - 1];
|
|
897
|
+
if (hasMore) {
|
|
898
|
+
const cursor = {
|
|
899
|
+
timestamp: lastEvent.timestamp,
|
|
900
|
+
eventId: lastEvent.eventId,
|
|
901
|
+
direction: 'forward'
|
|
902
|
+
};
|
|
903
|
+
nextCursor = Buffer.from(JSON.stringify(cursor)).toString('base64');
|
|
904
|
+
}
|
|
905
|
+
if (decodedCursor) {
|
|
906
|
+
const firstEvent = data[0];
|
|
907
|
+
const cursor = {
|
|
908
|
+
timestamp: firstEvent.timestamp,
|
|
909
|
+
eventId: firstEvent.eventId,
|
|
910
|
+
direction: 'backward'
|
|
911
|
+
};
|
|
912
|
+
previousCursor = Buffer.from(JSON.stringify(cursor)).toString('base64');
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
data,
|
|
917
|
+
pagination: {
|
|
918
|
+
totalCount,
|
|
919
|
+
hasMore,
|
|
920
|
+
nextCursor,
|
|
921
|
+
previousCursor,
|
|
922
|
+
limit,
|
|
923
|
+
offset: pagination.offset
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Get count of events matching filters (called by QueryBuilder)
|
|
929
|
+
*/
|
|
930
|
+
countQuery(filters) {
|
|
931
|
+
if (this.db) {
|
|
932
|
+
const { whereClause, params } = this.buildWhereClause(filters);
|
|
933
|
+
const sql = `SELECT COUNT(*) as count FROM audit_events ${whereClause}`;
|
|
934
|
+
try {
|
|
935
|
+
const stmt = this.db.prepare(sql);
|
|
936
|
+
const result = stmt.get(...params);
|
|
937
|
+
return result.count;
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
console.error('[AuditLogger] Count query failed:', error);
|
|
941
|
+
return 0;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// Memory-based count
|
|
945
|
+
return this.filterMemoryEvents(filters).length;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Execute aggregation query (called by QueryBuilder)
|
|
949
|
+
*/
|
|
950
|
+
aggregateQuery(filters, interval) {
|
|
951
|
+
const events = this.db
|
|
952
|
+
? this.executeDbQuery(filters, {}, 'timestamp', 'asc')
|
|
953
|
+
: this.filterMemoryEvents(filters);
|
|
954
|
+
const result = {
|
|
955
|
+
byEventType: {},
|
|
956
|
+
byOutcome: {},
|
|
957
|
+
byRiskLevel: {},
|
|
958
|
+
byAgent: {},
|
|
959
|
+
byUser: {},
|
|
960
|
+
timeSeries: [],
|
|
961
|
+
total: events.length
|
|
962
|
+
};
|
|
963
|
+
// Build aggregations
|
|
964
|
+
const timeSeriesMap = new Map();
|
|
965
|
+
for (const event of events) {
|
|
966
|
+
// By event type
|
|
967
|
+
result.byEventType[event.eventType] = (result.byEventType[event.eventType] || 0) + 1;
|
|
968
|
+
// By outcome
|
|
969
|
+
result.byOutcome[event.outcome] = (result.byOutcome[event.outcome] || 0) + 1;
|
|
970
|
+
// By risk level
|
|
971
|
+
if (event.riskLevel) {
|
|
972
|
+
result.byRiskLevel[event.riskLevel] = (result.byRiskLevel[event.riskLevel] || 0) + 1;
|
|
973
|
+
}
|
|
974
|
+
// By agent
|
|
975
|
+
if (event.agentId) {
|
|
976
|
+
result.byAgent[event.agentId] = (result.byAgent[event.agentId] || 0) + 1;
|
|
977
|
+
}
|
|
978
|
+
// By user
|
|
979
|
+
if (event.userId) {
|
|
980
|
+
result.byUser[event.userId] = (result.byUser[event.userId] || 0) + 1;
|
|
981
|
+
}
|
|
982
|
+
// Time series
|
|
983
|
+
if (interval) {
|
|
984
|
+
const bucket = this.truncateTimestamp(event.timestamp, interval);
|
|
985
|
+
let entry = timeSeriesMap.get(bucket);
|
|
986
|
+
if (!entry) {
|
|
987
|
+
entry = { count: 0, byOutcome: {} };
|
|
988
|
+
timeSeriesMap.set(bucket, entry);
|
|
989
|
+
}
|
|
990
|
+
entry.count++;
|
|
991
|
+
entry.byOutcome[event.outcome] = (entry.byOutcome[event.outcome] || 0) + 1;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
// Convert time series map to array
|
|
995
|
+
if (interval) {
|
|
996
|
+
result.timeSeries = Array.from(timeSeriesMap.entries())
|
|
997
|
+
.map(([timestamp, data]) => ({
|
|
998
|
+
timestamp,
|
|
999
|
+
count: data.count,
|
|
1000
|
+
byOutcome: data.byOutcome
|
|
1001
|
+
}))
|
|
1002
|
+
.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
1003
|
+
}
|
|
1004
|
+
return result;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Truncate timestamp to the specified interval for time series grouping
|
|
1008
|
+
*/
|
|
1009
|
+
truncateTimestamp(timestamp, interval) {
|
|
1010
|
+
const date = new Date(timestamp);
|
|
1011
|
+
switch (interval) {
|
|
1012
|
+
case 'minute':
|
|
1013
|
+
date.setSeconds(0, 0);
|
|
1014
|
+
break;
|
|
1015
|
+
case 'hour':
|
|
1016
|
+
date.setMinutes(0, 0, 0);
|
|
1017
|
+
break;
|
|
1018
|
+
case 'day':
|
|
1019
|
+
date.setHours(0, 0, 0, 0);
|
|
1020
|
+
break;
|
|
1021
|
+
case 'week': {
|
|
1022
|
+
const day = date.getDay();
|
|
1023
|
+
date.setDate(date.getDate() - day);
|
|
1024
|
+
date.setHours(0, 0, 0, 0);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
case 'month':
|
|
1028
|
+
date.setDate(1);
|
|
1029
|
+
date.setHours(0, 0, 0, 0);
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
return date.toISOString();
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Execute query against database
|
|
1036
|
+
*/
|
|
1037
|
+
executeDbQuery(filters, pagination, orderByField, orderDirection) {
|
|
1038
|
+
if (!this.db)
|
|
1039
|
+
return [];
|
|
1040
|
+
const { whereClause, params } = this.buildWhereClause(filters);
|
|
1041
|
+
// Validate order field to prevent SQL injection
|
|
1042
|
+
const allowedOrderFields = ['timestamp', 'eventType', 'action', 'outcome', 'riskLevel', 'agentId', 'userId'];
|
|
1043
|
+
const safeOrderField = allowedOrderFields.includes(orderByField) ? orderByField : 'timestamp';
|
|
1044
|
+
const safeDirection = orderDirection === 'asc' ? 'ASC' : 'DESC';
|
|
1045
|
+
let sql = `SELECT * FROM audit_events ${whereClause} ORDER BY ${safeOrderField} ${safeDirection}`;
|
|
1046
|
+
// Add pagination
|
|
1047
|
+
if (pagination.limit) {
|
|
1048
|
+
sql += ` LIMIT ?`;
|
|
1049
|
+
params.push(pagination.limit);
|
|
1050
|
+
}
|
|
1051
|
+
if (pagination.offset) {
|
|
1052
|
+
sql += ` OFFSET ?`;
|
|
1053
|
+
params.push(pagination.offset);
|
|
1054
|
+
}
|
|
1055
|
+
try {
|
|
1056
|
+
const stmt = this.db.prepare(sql);
|
|
1057
|
+
const rows = stmt.all(...params);
|
|
1058
|
+
return rows.map(row => this.rowToEvent(row));
|
|
1059
|
+
}
|
|
1060
|
+
catch (error) {
|
|
1061
|
+
console.error('[AuditLogger] Database query failed:', error);
|
|
1062
|
+
return [];
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Build WHERE clause from filters with parameterized queries (SQL injection safe)
|
|
1067
|
+
*/
|
|
1068
|
+
buildWhereClause(filters) {
|
|
1069
|
+
const conditions = [];
|
|
1070
|
+
const params = [];
|
|
1071
|
+
// Event type filter
|
|
1072
|
+
if (filters.eventType) {
|
|
1073
|
+
if (Array.isArray(filters.eventType)) {
|
|
1074
|
+
const placeholders = filters.eventType.map(() => '?').join(', ');
|
|
1075
|
+
conditions.push(`eventType IN (${placeholders})`);
|
|
1076
|
+
params.push(...filters.eventType);
|
|
1077
|
+
}
|
|
1078
|
+
else {
|
|
1079
|
+
conditions.push('eventType = ?');
|
|
1080
|
+
params.push(filters.eventType);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Agent ID filter
|
|
1084
|
+
if (filters.agentId) {
|
|
1085
|
+
if (Array.isArray(filters.agentId)) {
|
|
1086
|
+
const placeholders = filters.agentId.map(() => '?').join(', ');
|
|
1087
|
+
conditions.push(`agentId IN (${placeholders})`);
|
|
1088
|
+
params.push(...filters.agentId);
|
|
1089
|
+
}
|
|
1090
|
+
else {
|
|
1091
|
+
conditions.push('agentId = ?');
|
|
1092
|
+
params.push(filters.agentId);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Session ID filter
|
|
1096
|
+
if (filters.sessionId) {
|
|
1097
|
+
if (Array.isArray(filters.sessionId)) {
|
|
1098
|
+
const placeholders = filters.sessionId.map(() => '?').join(', ');
|
|
1099
|
+
conditions.push(`sessionId IN (${placeholders})`);
|
|
1100
|
+
params.push(...filters.sessionId);
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
conditions.push('sessionId = ?');
|
|
1104
|
+
params.push(filters.sessionId);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
// User ID filter
|
|
1108
|
+
if (filters.userId) {
|
|
1109
|
+
if (Array.isArray(filters.userId)) {
|
|
1110
|
+
const placeholders = filters.userId.map(() => '?').join(', ');
|
|
1111
|
+
conditions.push(`userId IN (${placeholders})`);
|
|
1112
|
+
params.push(...filters.userId);
|
|
1113
|
+
}
|
|
1114
|
+
else {
|
|
1115
|
+
conditions.push('userId = ?');
|
|
1116
|
+
params.push(filters.userId);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
// Outcome filter
|
|
1120
|
+
if (filters.outcome) {
|
|
1121
|
+
if (Array.isArray(filters.outcome)) {
|
|
1122
|
+
const placeholders = filters.outcome.map(() => '?').join(', ');
|
|
1123
|
+
conditions.push(`outcome IN (${placeholders})`);
|
|
1124
|
+
params.push(...filters.outcome);
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
conditions.push('outcome = ?');
|
|
1128
|
+
params.push(filters.outcome);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// Risk level filter
|
|
1132
|
+
if (filters.riskLevel) {
|
|
1133
|
+
if (Array.isArray(filters.riskLevel)) {
|
|
1134
|
+
const placeholders = filters.riskLevel.map(() => '?').join(', ');
|
|
1135
|
+
conditions.push(`riskLevel IN (${placeholders})`);
|
|
1136
|
+
params.push(...filters.riskLevel);
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
conditions.push('riskLevel = ?');
|
|
1140
|
+
params.push(filters.riskLevel);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
// Date range filters
|
|
1144
|
+
if (filters.since) {
|
|
1145
|
+
const sinceStr = filters.since instanceof Date ? filters.since.toISOString() : filters.since;
|
|
1146
|
+
conditions.push('timestamp >= ?');
|
|
1147
|
+
params.push(sinceStr);
|
|
1148
|
+
}
|
|
1149
|
+
if (filters.until) {
|
|
1150
|
+
const untilStr = filters.until instanceof Date ? filters.until.toISOString() : filters.until;
|
|
1151
|
+
conditions.push('timestamp <= ?');
|
|
1152
|
+
params.push(untilStr);
|
|
1153
|
+
}
|
|
1154
|
+
// Correlation ID filter
|
|
1155
|
+
if (filters.correlationId) {
|
|
1156
|
+
conditions.push('correlationId = ?');
|
|
1157
|
+
params.push(filters.correlationId);
|
|
1158
|
+
}
|
|
1159
|
+
// Action partial match (case-insensitive)
|
|
1160
|
+
if (filters.action) {
|
|
1161
|
+
conditions.push('LOWER(action) LIKE ?');
|
|
1162
|
+
params.push(`%${filters.action.toLowerCase()}%`);
|
|
1163
|
+
}
|
|
1164
|
+
// Full-text search in metadata
|
|
1165
|
+
if (filters.metadataSearch) {
|
|
1166
|
+
conditions.push('LOWER(metadata) LIKE ?');
|
|
1167
|
+
params.push(`%${filters.metadataSearch.toLowerCase()}%`);
|
|
1168
|
+
}
|
|
1169
|
+
// Full-text search in details
|
|
1170
|
+
if (filters.detailsSearch) {
|
|
1171
|
+
conditions.push('LOWER(details) LIKE ?');
|
|
1172
|
+
params.push(`%${filters.detailsSearch.toLowerCase()}%`);
|
|
1173
|
+
}
|
|
1174
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
1175
|
+
return { whereClause, params };
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Convert database row to AuditEvent
|
|
1179
|
+
*/
|
|
1180
|
+
rowToEvent(row) {
|
|
1181
|
+
return {
|
|
1182
|
+
eventId: row.eventId,
|
|
1183
|
+
eventType: row.eventType,
|
|
1184
|
+
action: row.action,
|
|
1185
|
+
timestamp: row.timestamp,
|
|
1186
|
+
outcome: row.outcome,
|
|
1187
|
+
agentId: row.agentId || undefined,
|
|
1188
|
+
sessionId: row.sessionId || undefined,
|
|
1189
|
+
userId: row.userId || undefined,
|
|
1190
|
+
riskLevel: row.riskLevel || undefined,
|
|
1191
|
+
details: row.details ? JSON.parse(row.details) : undefined,
|
|
1192
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
1193
|
+
correlationId: row.correlationId || undefined,
|
|
1194
|
+
parentEventId: row.parentEventId || undefined
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
292
1197
|
/**
|
|
293
|
-
*
|
|
1198
|
+
* Execute query against memory cache
|
|
1199
|
+
*/
|
|
1200
|
+
executeMemoryQuery(filters, pagination, orderByField, orderDirection) {
|
|
1201
|
+
let results = this.filterMemoryEvents(filters);
|
|
1202
|
+
// Sort
|
|
1203
|
+
results.sort((a, b) => {
|
|
1204
|
+
const aVal = a[orderByField] || '';
|
|
1205
|
+
const bVal = b[orderByField] || '';
|
|
1206
|
+
const comparison = String(aVal).localeCompare(String(bVal));
|
|
1207
|
+
return orderDirection === 'asc' ? comparison : -comparison;
|
|
1208
|
+
});
|
|
1209
|
+
// Pagination
|
|
1210
|
+
if (pagination.offset) {
|
|
1211
|
+
results = results.slice(pagination.offset);
|
|
1212
|
+
}
|
|
1213
|
+
if (pagination.limit) {
|
|
1214
|
+
results = results.slice(0, pagination.limit);
|
|
1215
|
+
}
|
|
1216
|
+
return results;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Filter events from memory cache
|
|
1220
|
+
*/
|
|
1221
|
+
filterMemoryEvents(filters) {
|
|
1222
|
+
return this.events.filter(event => {
|
|
1223
|
+
// Event type filter
|
|
1224
|
+
if (filters.eventType) {
|
|
1225
|
+
const types = Array.isArray(filters.eventType) ? filters.eventType : [filters.eventType];
|
|
1226
|
+
if (!types.includes(event.eventType))
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
// Agent ID filter
|
|
1230
|
+
if (filters.agentId) {
|
|
1231
|
+
const ids = Array.isArray(filters.agentId) ? filters.agentId : [filters.agentId];
|
|
1232
|
+
if (!event.agentId || !ids.includes(event.agentId))
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
// Session ID filter
|
|
1236
|
+
if (filters.sessionId) {
|
|
1237
|
+
const ids = Array.isArray(filters.sessionId) ? filters.sessionId : [filters.sessionId];
|
|
1238
|
+
if (!event.sessionId || !ids.includes(event.sessionId))
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
// User ID filter
|
|
1242
|
+
if (filters.userId) {
|
|
1243
|
+
const ids = Array.isArray(filters.userId) ? filters.userId : [filters.userId];
|
|
1244
|
+
if (!event.userId || !ids.includes(event.userId))
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
// Outcome filter
|
|
1248
|
+
if (filters.outcome) {
|
|
1249
|
+
const outcomes = Array.isArray(filters.outcome) ? filters.outcome : [filters.outcome];
|
|
1250
|
+
if (!outcomes.includes(event.outcome))
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
// Risk level filter
|
|
1254
|
+
if (filters.riskLevel) {
|
|
1255
|
+
const levels = Array.isArray(filters.riskLevel) ? filters.riskLevel : [filters.riskLevel];
|
|
1256
|
+
if (!event.riskLevel || !levels.includes(event.riskLevel))
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
// Date range filters
|
|
1260
|
+
if (filters.since) {
|
|
1261
|
+
const sinceStr = filters.since instanceof Date ? filters.since.toISOString() : filters.since;
|
|
1262
|
+
if (event.timestamp < sinceStr)
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
if (filters.until) {
|
|
1266
|
+
const untilStr = filters.until instanceof Date ? filters.until.toISOString() : filters.until;
|
|
1267
|
+
if (event.timestamp > untilStr)
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
// Correlation ID filter
|
|
1271
|
+
if (filters.correlationId) {
|
|
1272
|
+
if (event.correlationId !== filters.correlationId)
|
|
1273
|
+
return false;
|
|
1274
|
+
}
|
|
1275
|
+
// Action partial match
|
|
1276
|
+
if (filters.action) {
|
|
1277
|
+
if (!event.action.toLowerCase().includes(filters.action.toLowerCase()))
|
|
1278
|
+
return false;
|
|
1279
|
+
}
|
|
1280
|
+
// Full-text search in metadata
|
|
1281
|
+
if (filters.metadataSearch && event.metadata) {
|
|
1282
|
+
const metadataStr = JSON.stringify(event.metadata).toLowerCase();
|
|
1283
|
+
if (!metadataStr.includes(filters.metadataSearch.toLowerCase()))
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
else if (filters.metadataSearch && !event.metadata) {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
// Full-text search in details
|
|
1290
|
+
if (filters.detailsSearch && event.details) {
|
|
1291
|
+
const detailsStr = JSON.stringify(event.details).toLowerCase();
|
|
1292
|
+
if (!detailsStr.includes(filters.detailsSearch.toLowerCase()))
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
else if (filters.detailsSearch && !event.details) {
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
1298
|
+
return true;
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
// ============================================================================
|
|
1302
|
+
// LEGACY QUERY METHODS (preserved for backward compatibility)
|
|
1303
|
+
// ============================================================================
|
|
1304
|
+
/**
|
|
1305
|
+
* Query events (legacy method - consider using query() instead)
|
|
1306
|
+
* @deprecated Use query() for more powerful filtering options
|
|
294
1307
|
*/
|
|
295
1308
|
getEvents(filter) {
|
|
296
1309
|
// If database is available, query from it
|
|
@@ -350,22 +1363,19 @@ export class AuditLogger {
|
|
|
350
1363
|
}
|
|
351
1364
|
/**
|
|
352
1365
|
* Get summary statistics
|
|
1366
|
+
* Uses shared calculateGroupedCounts utility for consistency
|
|
353
1367
|
*/
|
|
354
1368
|
getStats(since) {
|
|
355
1369
|
let events = this.events;
|
|
356
1370
|
if (since) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const byOutcome = {};
|
|
361
|
-
const byRiskLevel = {};
|
|
362
|
-
for (const event of events) {
|
|
363
|
-
byType[event.eventType] = (byType[event.eventType] || 0) + 1;
|
|
364
|
-
byOutcome[event.outcome] = (byOutcome[event.outcome] || 0) + 1;
|
|
365
|
-
if (event.riskLevel) {
|
|
366
|
-
byRiskLevel[event.riskLevel] = (byRiskLevel[event.riskLevel] || 0) + 1;
|
|
367
|
-
}
|
|
1371
|
+
// Use time window for filtering if a time-based since is provided
|
|
1372
|
+
const sinceTime = new Date(since).getTime();
|
|
1373
|
+
events = events.filter(e => new Date(e.timestamp).getTime() >= sinceTime);
|
|
368
1374
|
}
|
|
1375
|
+
// Use shared utility for grouped counts
|
|
1376
|
+
const byType = calculateGroupedCounts(events, e => e.eventType);
|
|
1377
|
+
const byOutcome = calculateGroupedCounts(events, e => e.outcome);
|
|
1378
|
+
const byRiskLevel = calculateGroupedCounts(events, e => e.riskLevel);
|
|
369
1379
|
return {
|
|
370
1380
|
total: events.length,
|
|
371
1381
|
byType,
|
|
@@ -381,10 +1391,40 @@ export class AuditLogger {
|
|
|
381
1391
|
return JSON.stringify(events, null, 2);
|
|
382
1392
|
}
|
|
383
1393
|
/**
|
|
384
|
-
* Clear all events
|
|
1394
|
+
* Clear all events from memory cache
|
|
1395
|
+
* Note: This does NOT clear the database - use clearAll() for that
|
|
385
1396
|
*/
|
|
386
1397
|
clear() {
|
|
387
1398
|
this.events = [];
|
|
1399
|
+
this.failedWrites = [];
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Clear all events from both memory and database
|
|
1403
|
+
*/
|
|
1404
|
+
clearAll() {
|
|
1405
|
+
this.events = [];
|
|
1406
|
+
this.failedWrites = [];
|
|
1407
|
+
if (this.db) {
|
|
1408
|
+
try {
|
|
1409
|
+
this.db.exec('DELETE FROM audit_events');
|
|
1410
|
+
console.log('[AuditLogger] Cleared all events from database');
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
console.error('[AuditLogger] Failed to clear database:', error);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Get extended statistics including sync status
|
|
1419
|
+
*/
|
|
1420
|
+
getExtendedStats() {
|
|
1421
|
+
const basicStats = this.getStats();
|
|
1422
|
+
return {
|
|
1423
|
+
...basicStats,
|
|
1424
|
+
memoryCount: this.events.length,
|
|
1425
|
+
pendingRetries: this.failedWrites.length,
|
|
1426
|
+
lastSyncTime: this.lastSyncTime || null
|
|
1427
|
+
};
|
|
388
1428
|
}
|
|
389
1429
|
/**
|
|
390
1430
|
* Log to console with formatting
|