@trentapps/manager-protocol 1.1.2 → 1.2.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/README.md +29 -1
- package/dist/analyzers/CSSAnalyzer.d.ts +188 -8
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/CSSAnalyzer.js +794 -192
- package/dist/analyzers/CSSAnalyzer.js.map +1 -1
- package/dist/cli.js +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 +1067 -24
- package/dist/engine/AuditLogger.js.map +1 -1
- package/dist/engine/GitHubApprovalManager.d.ts +13 -0
- package/dist/engine/GitHubApprovalManager.d.ts.map +1 -1
- package/dist/engine/GitHubApprovalManager.js +72 -46
- package/dist/engine/GitHubApprovalManager.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 +53 -70
- 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 +11 -10
- package/dist/engine/TaskManager.d.ts.map +1 -1
- package/dist/engine/TaskManager.js +180 -195
- 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/server.d.ts.map +1 -1
- package/dist/server.js +96 -17
- package/dist/server.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 +24 -2
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
- package/dist/supervisor/ProjectTracker.js +151 -59
- 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 +2 -1
|
@@ -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
|
*/
|
|
@@ -95,6 +612,9 @@ export class AuditLogger {
|
|
|
95
612
|
CREATE INDEX IF NOT EXISTS idx_timestamp ON audit_events(timestamp);
|
|
96
613
|
CREATE INDEX IF NOT EXISTS idx_eventType ON audit_events(eventType);
|
|
97
614
|
CREATE INDEX IF NOT EXISTS idx_outcome ON audit_events(outcome);
|
|
615
|
+
CREATE INDEX IF NOT EXISTS idx_correlationId ON audit_events(correlationId);
|
|
616
|
+
CREATE INDEX IF NOT EXISTS idx_agentId ON audit_events(agentId);
|
|
617
|
+
CREATE INDEX IF NOT EXISTS idx_sessionId ON audit_events(sessionId);
|
|
98
618
|
`);
|
|
99
619
|
console.log('[AuditLogger] SQLite database initialized at', this.dbPath);
|
|
100
620
|
}
|
|
@@ -112,6 +632,11 @@ export class AuditLogger {
|
|
|
112
632
|
}
|
|
113
633
|
/**
|
|
114
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)
|
|
115
640
|
*/
|
|
116
641
|
async log(params) {
|
|
117
642
|
const event = {
|
|
@@ -129,13 +654,29 @@ export class AuditLogger {
|
|
|
129
654
|
correlationId: params.correlationId,
|
|
130
655
|
parentEventId: params.parentEventId
|
|
131
656
|
};
|
|
132
|
-
//
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
674
|
}
|
|
138
|
-
|
|
675
|
+
else {
|
|
676
|
+
// No database - memory only mode
|
|
677
|
+
this.addToMemoryCache(event);
|
|
678
|
+
}
|
|
679
|
+
// Console logging (always happens regardless of DB status)
|
|
139
680
|
if (this.enableConsoleLog) {
|
|
140
681
|
this.logToConsole(event);
|
|
141
682
|
}
|
|
@@ -147,10 +688,6 @@ export class AuditLogger {
|
|
|
147
688
|
if (this.onEvent) {
|
|
148
689
|
await this.onEvent(event);
|
|
149
690
|
}
|
|
150
|
-
// Database logging
|
|
151
|
-
if (this.db) {
|
|
152
|
-
this.saveToDatabase(event);
|
|
153
|
-
}
|
|
154
691
|
return event;
|
|
155
692
|
}
|
|
156
693
|
/**
|
|
@@ -234,10 +771,11 @@ export class AuditLogger {
|
|
|
234
771
|
}
|
|
235
772
|
/**
|
|
236
773
|
* Save event to database
|
|
774
|
+
* @returns true if save was successful, false otherwise
|
|
237
775
|
*/
|
|
238
776
|
saveToDatabase(event) {
|
|
239
777
|
if (!this.db)
|
|
240
|
-
return;
|
|
778
|
+
return false;
|
|
241
779
|
try {
|
|
242
780
|
const stmt = this.db.prepare(`
|
|
243
781
|
INSERT INTO audit_events (
|
|
@@ -247,9 +785,11 @@ export class AuditLogger {
|
|
|
247
785
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
248
786
|
`);
|
|
249
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;
|
|
250
789
|
}
|
|
251
790
|
catch (error) {
|
|
252
791
|
console.error('[AuditLogger] Failed to save to database:', error);
|
|
792
|
+
return false;
|
|
253
793
|
}
|
|
254
794
|
}
|
|
255
795
|
/**
|
|
@@ -286,8 +826,484 @@ export class AuditLogger {
|
|
|
286
826
|
return [];
|
|
287
827
|
}
|
|
288
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
|
+
}
|
|
289
1197
|
/**
|
|
290
|
-
*
|
|
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
|
|
291
1307
|
*/
|
|
292
1308
|
getEvents(filter) {
|
|
293
1309
|
// If database is available, query from it
|
|
@@ -347,22 +1363,19 @@ export class AuditLogger {
|
|
|
347
1363
|
}
|
|
348
1364
|
/**
|
|
349
1365
|
* Get summary statistics
|
|
1366
|
+
* Uses shared calculateGroupedCounts utility for consistency
|
|
350
1367
|
*/
|
|
351
1368
|
getStats(since) {
|
|
352
1369
|
let events = this.events;
|
|
353
1370
|
if (since) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const byOutcome = {};
|
|
358
|
-
const byRiskLevel = {};
|
|
359
|
-
for (const event of events) {
|
|
360
|
-
byType[event.eventType] = (byType[event.eventType] || 0) + 1;
|
|
361
|
-
byOutcome[event.outcome] = (byOutcome[event.outcome] || 0) + 1;
|
|
362
|
-
if (event.riskLevel) {
|
|
363
|
-
byRiskLevel[event.riskLevel] = (byRiskLevel[event.riskLevel] || 0) + 1;
|
|
364
|
-
}
|
|
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);
|
|
365
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);
|
|
366
1379
|
return {
|
|
367
1380
|
total: events.length,
|
|
368
1381
|
byType,
|
|
@@ -378,10 +1391,40 @@ export class AuditLogger {
|
|
|
378
1391
|
return JSON.stringify(events, null, 2);
|
|
379
1392
|
}
|
|
380
1393
|
/**
|
|
381
|
-
* Clear all events
|
|
1394
|
+
* Clear all events from memory cache
|
|
1395
|
+
* Note: This does NOT clear the database - use clearAll() for that
|
|
382
1396
|
*/
|
|
383
1397
|
clear() {
|
|
384
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
|
+
};
|
|
385
1428
|
}
|
|
386
1429
|
/**
|
|
387
1430
|
* Log to console with formatting
|