@trentapps/manager-protocol 1.3.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/LICENSE +21 -0
- package/README.md +639 -0
- package/dist/analyzers/ArchitectureDetector.d.ts +44 -0
- package/dist/analyzers/ArchitectureDetector.d.ts.map +1 -0
- package/dist/analyzers/ArchitectureDetector.js +218 -0
- package/dist/analyzers/ArchitectureDetector.js.map +1 -0
- package/dist/analyzers/CSSAnalyzer.d.ts +284 -0
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/CSSAnalyzer.js +1180 -0
- package/dist/analyzers/CSSAnalyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +5 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +5 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +174 -0
- package/dist/cli.js.map +1 -0
- package/dist/design-system/index.d.ts +6 -0
- package/dist/design-system/index.d.ts.map +1 -0
- package/dist/design-system/index.js +6 -0
- package/dist/design-system/index.js.map +1 -0
- package/dist/design-system/tokens.d.ts +106 -0
- package/dist/design-system/tokens.d.ts.map +1 -0
- package/dist/design-system/tokens.js +554 -0
- package/dist/design-system/tokens.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +506 -0
- package/dist/engine/AuditLogger.d.ts.map +1 -0
- package/dist/engine/AuditLogger.js +1491 -0
- package/dist/engine/AuditLogger.js.map +1 -0
- package/dist/engine/GitHubApprovalManager.d.ts +123 -0
- package/dist/engine/GitHubApprovalManager.d.ts.map +1 -0
- package/dist/engine/GitHubApprovalManager.js +347 -0
- package/dist/engine/GitHubApprovalManager.js.map +1 -0
- 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 +81 -0
- package/dist/engine/RateLimiter.d.ts.map +1 -0
- package/dist/engine/RateLimiter.js +215 -0
- package/dist/engine/RateLimiter.js.map +1 -0
- 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 +176 -0
- package/dist/engine/RulesEngine.d.ts.map +1 -0
- package/dist/engine/RulesEngine.js +705 -0
- package/dist/engine/RulesEngine.js.map +1 -0
- package/dist/engine/TaskManager.d.ts +174 -0
- package/dist/engine/TaskManager.d.ts.map +1 -0
- package/dist/engine/TaskManager.js +663 -0
- package/dist/engine/TaskManager.js.map +1 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +13 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/architecture.d.ts +9 -0
- package/dist/rules/architecture.d.ts.map +1 -0
- package/dist/rules/architecture.js +322 -0
- package/dist/rules/architecture.js.map +1 -0
- package/dist/rules/azure.d.ts +7 -0
- package/dist/rules/azure.d.ts.map +1 -0
- package/dist/rules/azure.js +136 -0
- package/dist/rules/azure.js.map +1 -0
- package/dist/rules/compliance.d.ts +9 -0
- package/dist/rules/compliance.d.ts.map +1 -0
- package/dist/rules/compliance.js +286 -0
- package/dist/rules/compliance.js.map +1 -0
- 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 +10 -0
- package/dist/rules/css.d.ts.map +1 -0
- package/dist/rules/css.js +1777 -0
- package/dist/rules/css.js.map +1 -0
- 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 +7 -0
- package/dist/rules/flask.d.ts.map +1 -0
- package/dist/rules/flask.js +142 -0
- package/dist/rules/flask.js.map +1 -0
- package/dist/rules/index.d.ts +827 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +556 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/ml-ai.d.ts +7 -0
- package/dist/rules/ml-ai.d.ts.map +1 -0
- package/dist/rules/ml-ai.js +148 -0
- package/dist/rules/ml-ai.js.map +1 -0
- package/dist/rules/operational.d.ts +9 -0
- package/dist/rules/operational.d.ts.map +1 -0
- package/dist/rules/operational.js +318 -0
- package/dist/rules/operational.js.map +1 -0
- 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 +9 -0
- package/dist/rules/security.d.ts.map +1 -0
- package/dist/rules/security.js +848 -0
- package/dist/rules/security.js.map +1 -0
- 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 +13 -0
- package/dist/rules/storage.d.ts.map +1 -0
- package/dist/rules/storage.js +672 -0
- package/dist/rules/storage.js.map +1 -0
- package/dist/rules/stripe.d.ts +7 -0
- package/dist/rules/stripe.d.ts.map +1 -0
- package/dist/rules/stripe.js +133 -0
- package/dist/rules/stripe.js.map +1 -0
- package/dist/rules/testing.d.ts +7 -0
- package/dist/rules/testing.d.ts.map +1 -0
- package/dist/rules/testing.js +135 -0
- package/dist/rules/testing.js.map +1 -0
- package/dist/rules/ux.d.ts +9 -0
- package/dist/rules/ux.d.ts.map +1 -0
- package/dist/rules/ux.js +280 -0
- package/dist/rules/ux.js.map +1 -0
- package/dist/rules/websocket.d.ts +7 -0
- package/dist/rules/websocket.d.ts.map +1 -0
- package/dist/rules/websocket.js +128 -0
- package/dist/rules/websocket.js.map +1 -0
- package/dist/server.d.ts +43 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1967 -0
- package/dist/server.js.map +1 -0
- package/dist/supervisor/AgentSupervisor.d.ts +195 -0
- package/dist/supervisor/AgentSupervisor.d.ts.map +1 -0
- package/dist/supervisor/AgentSupervisor.js +569 -0
- package/dist/supervisor/AgentSupervisor.js.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts +185 -0
- package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -0
- package/dist/supervisor/ManagedServerRegistry.js +729 -0
- package/dist/supervisor/ManagedServerRegistry.js.map +1 -0
- package/dist/supervisor/ProjectTracker.d.ts +210 -0
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -0
- package/dist/supervisor/ProjectTracker.js +709 -0
- package/dist/supervisor/ProjectTracker.js.map +1 -0
- package/dist/supervisor/index.d.ts +6 -0
- package/dist/supervisor/index.d.ts.map +1 -0
- package/dist/supervisor/index.js +6 -0
- package/dist/supervisor/index.js.map +1 -0
- 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 +1282 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +386 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/errors.d.ts +86 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +171 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- 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/dist/utils/shell.d.ts +22 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +29 -0
- package/dist/utils/shell.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,1491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enterprise Agent Supervisor - Audit Logger
|
|
3
|
+
*
|
|
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
|
|
16
|
+
*/
|
|
17
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
18
|
+
import { mkdirSync } from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import Database from 'better-sqlite3';
|
|
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
|
+
}
|
|
247
|
+
export class AuditLogger {
|
|
248
|
+
events = [];
|
|
249
|
+
maxEvents;
|
|
250
|
+
enableConsoleLog;
|
|
251
|
+
webhookUrl;
|
|
252
|
+
webhookRetries;
|
|
253
|
+
webhookTimeoutMs;
|
|
254
|
+
onEvent;
|
|
255
|
+
onWebhookError;
|
|
256
|
+
dbPath;
|
|
257
|
+
db;
|
|
258
|
+
initialized = false;
|
|
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;
|
|
269
|
+
constructor(options = {}) {
|
|
270
|
+
this.maxEvents = options.maxEvents || 10000;
|
|
271
|
+
this.enableConsoleLog = options.enableConsoleLog || false;
|
|
272
|
+
this.webhookUrl = options.webhookUrl;
|
|
273
|
+
this.webhookRetries = options.webhookRetries ?? 3;
|
|
274
|
+
this.webhookTimeoutMs = options.webhookTimeoutMs ?? 5000;
|
|
275
|
+
this.onEvent = options.onEvent;
|
|
276
|
+
this.onWebhookError = options.onWebhookError;
|
|
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
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Initialize and setup SQLite database if configured
|
|
284
|
+
* Uses Promise-based locking to prevent race conditions in concurrent scenarios
|
|
285
|
+
*/
|
|
286
|
+
async initialize() {
|
|
287
|
+
// If already initialized, return immediately
|
|
288
|
+
if (this.initialized)
|
|
289
|
+
return;
|
|
290
|
+
// If initialization is in progress, wait for it to complete
|
|
291
|
+
if (this.initPromise) {
|
|
292
|
+
return this.initPromise;
|
|
293
|
+
}
|
|
294
|
+
// Start initialization and store the promise
|
|
295
|
+
this.initPromise = this.doInitialize();
|
|
296
|
+
try {
|
|
297
|
+
await this.initPromise;
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
// Clear the promise reference once done (success or failure)
|
|
301
|
+
this.initPromise = undefined;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Internal initialization implementation
|
|
306
|
+
*/
|
|
307
|
+
async doInitialize() {
|
|
308
|
+
if (this.dbPath) {
|
|
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
|
+
}
|
|
316
|
+
}
|
|
317
|
+
this.initialized = true;
|
|
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
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Initialize SQLite database
|
|
584
|
+
*/
|
|
585
|
+
async initDatabase() {
|
|
586
|
+
if (!this.dbPath)
|
|
587
|
+
return;
|
|
588
|
+
try {
|
|
589
|
+
// Ensure directory exists with secure permissions (owner-only: 0o700)
|
|
590
|
+
const dir = path.dirname(this.dbPath);
|
|
591
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
592
|
+
// Open database
|
|
593
|
+
this.db = new Database(this.dbPath);
|
|
594
|
+
// Create table if not exists
|
|
595
|
+
this.db.exec(`
|
|
596
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
597
|
+
eventId TEXT PRIMARY KEY,
|
|
598
|
+
eventType TEXT NOT NULL,
|
|
599
|
+
action TEXT NOT NULL,
|
|
600
|
+
timestamp TEXT NOT NULL,
|
|
601
|
+
outcome TEXT NOT NULL,
|
|
602
|
+
agentId TEXT,
|
|
603
|
+
sessionId TEXT,
|
|
604
|
+
userId TEXT,
|
|
605
|
+
riskLevel TEXT,
|
|
606
|
+
details TEXT,
|
|
607
|
+
metadata TEXT,
|
|
608
|
+
correlationId TEXT,
|
|
609
|
+
parentEventId TEXT
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
CREATE INDEX IF NOT EXISTS idx_timestamp ON audit_events(timestamp);
|
|
613
|
+
CREATE INDEX IF NOT EXISTS idx_eventType ON audit_events(eventType);
|
|
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);
|
|
618
|
+
`);
|
|
619
|
+
console.log('[AuditLogger] SQLite database initialized at', this.dbPath);
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
console.error('[AuditLogger] Failed to initialize database:', error);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Reload events from database
|
|
627
|
+
*/
|
|
628
|
+
async reload() {
|
|
629
|
+
if (this.db) {
|
|
630
|
+
this.events = this.loadFromDatabase(this.maxEvents);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
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)
|
|
640
|
+
*/
|
|
641
|
+
async log(params) {
|
|
642
|
+
const event = {
|
|
643
|
+
eventId: uuidv4(),
|
|
644
|
+
eventType: params.eventType,
|
|
645
|
+
action: params.action,
|
|
646
|
+
timestamp: new Date().toISOString(),
|
|
647
|
+
outcome: params.outcome,
|
|
648
|
+
agentId: params.agentId,
|
|
649
|
+
sessionId: params.sessionId,
|
|
650
|
+
userId: params.userId,
|
|
651
|
+
riskLevel: params.riskLevel,
|
|
652
|
+
details: params.details,
|
|
653
|
+
metadata: params.metadata,
|
|
654
|
+
correlationId: params.correlationId,
|
|
655
|
+
parentEventId: params.parentEventId
|
|
656
|
+
};
|
|
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
|
+
}
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
// No database - memory only mode
|
|
677
|
+
this.addToMemoryCache(event);
|
|
678
|
+
}
|
|
679
|
+
// Console logging (always happens regardless of DB status)
|
|
680
|
+
if (this.enableConsoleLog) {
|
|
681
|
+
this.logToConsole(event);
|
|
682
|
+
}
|
|
683
|
+
// Webhook notification
|
|
684
|
+
if (this.webhookUrl) {
|
|
685
|
+
await this.sendWebhook(event);
|
|
686
|
+
}
|
|
687
|
+
// Custom callback
|
|
688
|
+
if (this.onEvent) {
|
|
689
|
+
await this.onEvent(event);
|
|
690
|
+
}
|
|
691
|
+
return event;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Quick log helpers
|
|
695
|
+
*/
|
|
696
|
+
async logActionEvaluated(action, outcome, details) {
|
|
697
|
+
return this.log({
|
|
698
|
+
eventType: 'action_evaluated',
|
|
699
|
+
action,
|
|
700
|
+
outcome,
|
|
701
|
+
details
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
async logActionApproved(action, details) {
|
|
705
|
+
return this.log({
|
|
706
|
+
eventType: 'action_approved',
|
|
707
|
+
action,
|
|
708
|
+
outcome: 'success',
|
|
709
|
+
details
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async logActionDenied(action, reason, riskLevel) {
|
|
713
|
+
return this.log({
|
|
714
|
+
eventType: 'action_denied',
|
|
715
|
+
action,
|
|
716
|
+
outcome: 'failure',
|
|
717
|
+
riskLevel,
|
|
718
|
+
details: { reason }
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
async logApprovalRequested(action, requestId, reason) {
|
|
722
|
+
return this.log({
|
|
723
|
+
eventType: 'approval_requested',
|
|
724
|
+
action,
|
|
725
|
+
outcome: 'pending',
|
|
726
|
+
details: { requestId, reason }
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
async logRateLimitHit(action, limitId, agentId) {
|
|
730
|
+
return this.log({
|
|
731
|
+
eventType: 'rate_limit_hit',
|
|
732
|
+
action,
|
|
733
|
+
outcome: 'failure',
|
|
734
|
+
agentId,
|
|
735
|
+
details: { limitId }
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async logSecurityAlert(action, alertType, severity, details) {
|
|
739
|
+
return this.log({
|
|
740
|
+
eventType: 'security_alert',
|
|
741
|
+
action,
|
|
742
|
+
outcome: 'failure',
|
|
743
|
+
riskLevel: severity,
|
|
744
|
+
details: { alertType, ...details }
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
async logComplianceViolation(action, violation, framework) {
|
|
748
|
+
return this.log({
|
|
749
|
+
eventType: 'compliance_violation',
|
|
750
|
+
action,
|
|
751
|
+
outcome: 'failure',
|
|
752
|
+
riskLevel: 'high',
|
|
753
|
+
details: { violation, framework }
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
async logRuleTriggered(action, ruleId, ruleName, outcome) {
|
|
757
|
+
return this.log({
|
|
758
|
+
eventType: 'rule_triggered',
|
|
759
|
+
action,
|
|
760
|
+
outcome,
|
|
761
|
+
details: { ruleId, ruleName }
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
async logConfigChanged(action, changeType, details) {
|
|
765
|
+
return this.log({
|
|
766
|
+
eventType: 'config_changed',
|
|
767
|
+
action,
|
|
768
|
+
outcome: 'success',
|
|
769
|
+
details: { changeType, ...details }
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Save event to database
|
|
774
|
+
* @returns true if save was successful, false otherwise
|
|
775
|
+
*/
|
|
776
|
+
saveToDatabase(event) {
|
|
777
|
+
if (!this.db)
|
|
778
|
+
return false;
|
|
779
|
+
try {
|
|
780
|
+
const stmt = this.db.prepare(`
|
|
781
|
+
INSERT INTO audit_events (
|
|
782
|
+
eventId, eventType, action, timestamp, outcome,
|
|
783
|
+
agentId, sessionId, userId, riskLevel,
|
|
784
|
+
details, metadata, correlationId, parentEventId
|
|
785
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
786
|
+
`);
|
|
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;
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
console.error('[AuditLogger] Failed to save to database:', error);
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Load events from database
|
|
797
|
+
*/
|
|
798
|
+
loadFromDatabase(limit) {
|
|
799
|
+
if (!this.db)
|
|
800
|
+
return [];
|
|
801
|
+
try {
|
|
802
|
+
const stmt = this.db.prepare(`
|
|
803
|
+
SELECT * FROM audit_events
|
|
804
|
+
ORDER BY timestamp DESC
|
|
805
|
+
LIMIT ?
|
|
806
|
+
`);
|
|
807
|
+
const rows = stmt.all(limit);
|
|
808
|
+
return rows.map(row => ({
|
|
809
|
+
eventId: row.eventId,
|
|
810
|
+
eventType: row.eventType,
|
|
811
|
+
action: row.action,
|
|
812
|
+
timestamp: row.timestamp,
|
|
813
|
+
outcome: row.outcome,
|
|
814
|
+
agentId: row.agentId,
|
|
815
|
+
sessionId: row.sessionId,
|
|
816
|
+
userId: row.userId,
|
|
817
|
+
riskLevel: row.riskLevel,
|
|
818
|
+
details: row.details ? JSON.parse(row.details) : undefined,
|
|
819
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
|
|
820
|
+
correlationId: row.correlationId,
|
|
821
|
+
parentEventId: row.parentEventId
|
|
822
|
+
})).reverse(); // Reverse to get chronological order
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
console.error('[AuditLogger] Failed to load from database:', error);
|
|
826
|
+
return [];
|
|
827
|
+
}
|
|
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
|
+
}
|
|
1197
|
+
/**
|
|
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
|
|
1307
|
+
*/
|
|
1308
|
+
getEvents(filter) {
|
|
1309
|
+
// If database is available, query from it
|
|
1310
|
+
if (this.db && filter?.limit) {
|
|
1311
|
+
return this.loadFromDatabase(filter.limit);
|
|
1312
|
+
}
|
|
1313
|
+
// Otherwise use in-memory events
|
|
1314
|
+
let result = [...this.events];
|
|
1315
|
+
if (filter) {
|
|
1316
|
+
if (filter.eventType) {
|
|
1317
|
+
result = result.filter(e => e.eventType === filter.eventType);
|
|
1318
|
+
}
|
|
1319
|
+
if (filter.agentId) {
|
|
1320
|
+
result = result.filter(e => e.agentId === filter.agentId);
|
|
1321
|
+
}
|
|
1322
|
+
if (filter.sessionId) {
|
|
1323
|
+
result = result.filter(e => e.sessionId === filter.sessionId);
|
|
1324
|
+
}
|
|
1325
|
+
if (filter.userId) {
|
|
1326
|
+
result = result.filter(e => e.userId === filter.userId);
|
|
1327
|
+
}
|
|
1328
|
+
if (filter.outcome) {
|
|
1329
|
+
result = result.filter(e => e.outcome === filter.outcome);
|
|
1330
|
+
}
|
|
1331
|
+
if (filter.riskLevel) {
|
|
1332
|
+
result = result.filter(e => e.riskLevel === filter.riskLevel);
|
|
1333
|
+
}
|
|
1334
|
+
if (filter.since) {
|
|
1335
|
+
result = result.filter(e => e.timestamp >= filter.since);
|
|
1336
|
+
}
|
|
1337
|
+
if (filter.until) {
|
|
1338
|
+
result = result.filter(e => e.timestamp <= filter.until);
|
|
1339
|
+
}
|
|
1340
|
+
if (filter.limit) {
|
|
1341
|
+
result = result.slice(-filter.limit);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
return result;
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Get event by ID
|
|
1348
|
+
*/
|
|
1349
|
+
getEvent(eventId) {
|
|
1350
|
+
return this.events.find(e => e.eventId === eventId);
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Get events by correlation ID
|
|
1354
|
+
*/
|
|
1355
|
+
getCorrelatedEvents(correlationId) {
|
|
1356
|
+
return this.events.filter(e => e.correlationId === correlationId);
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Get event count
|
|
1360
|
+
*/
|
|
1361
|
+
getEventCount() {
|
|
1362
|
+
return this.events.length;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Get summary statistics
|
|
1366
|
+
* Uses shared calculateGroupedCounts utility for consistency
|
|
1367
|
+
*/
|
|
1368
|
+
getStats(since) {
|
|
1369
|
+
let events = this.events;
|
|
1370
|
+
if (since) {
|
|
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);
|
|
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);
|
|
1379
|
+
return {
|
|
1380
|
+
total: events.length,
|
|
1381
|
+
byType,
|
|
1382
|
+
byOutcome,
|
|
1383
|
+
byRiskLevel
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Export events as JSON
|
|
1388
|
+
*/
|
|
1389
|
+
exportEvents(filter) {
|
|
1390
|
+
const events = this.getEvents(filter);
|
|
1391
|
+
return JSON.stringify(events, null, 2);
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Clear all events from memory cache
|
|
1395
|
+
* Note: This does NOT clear the database - use clearAll() for that
|
|
1396
|
+
*/
|
|
1397
|
+
clear() {
|
|
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
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Log to console with formatting
|
|
1431
|
+
*/
|
|
1432
|
+
logToConsole(event) {
|
|
1433
|
+
const levelColors = {
|
|
1434
|
+
critical: '\x1b[31m', // Red
|
|
1435
|
+
high: '\x1b[33m', // Yellow
|
|
1436
|
+
medium: '\x1b[36m', // Cyan
|
|
1437
|
+
low: '\x1b[32m', // Green
|
|
1438
|
+
minimal: '\x1b[37m' // White
|
|
1439
|
+
};
|
|
1440
|
+
const reset = '\x1b[0m';
|
|
1441
|
+
const color = event.riskLevel ? levelColors[event.riskLevel] : '\x1b[37m';
|
|
1442
|
+
console.log(`${color}[${event.timestamp}] ${event.eventType.toUpperCase()} | ` +
|
|
1443
|
+
`${event.action} | ${event.outcome}${reset}`, event.details ? JSON.stringify(event.details) : '');
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Send event to webhook with retry logic
|
|
1447
|
+
*/
|
|
1448
|
+
async sendWebhook(event) {
|
|
1449
|
+
if (!this.webhookUrl) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
const url = this.webhookUrl;
|
|
1453
|
+
const timeoutMs = this.webhookTimeoutMs;
|
|
1454
|
+
try {
|
|
1455
|
+
await withRetry(async () => {
|
|
1456
|
+
const controller = new AbortController();
|
|
1457
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1458
|
+
try {
|
|
1459
|
+
const response = await fetch(url, {
|
|
1460
|
+
method: 'POST',
|
|
1461
|
+
headers: {
|
|
1462
|
+
'Content-Type': 'application/json',
|
|
1463
|
+
'X-Event-Id': event.eventId,
|
|
1464
|
+
'X-Event-Type': event.eventType
|
|
1465
|
+
},
|
|
1466
|
+
body: JSON.stringify(event),
|
|
1467
|
+
signal: controller.signal
|
|
1468
|
+
});
|
|
1469
|
+
if (!response.ok) {
|
|
1470
|
+
throw new WebhookDeliveryError(url, `HTTP ${response.status}: ${response.statusText}`, response.status);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
finally {
|
|
1474
|
+
clearTimeout(timeoutId);
|
|
1475
|
+
}
|
|
1476
|
+
}, { maxRetries: this.webhookRetries });
|
|
1477
|
+
}
|
|
1478
|
+
catch (error) {
|
|
1479
|
+
const webhookError = error instanceof Error ? error : new Error(String(error));
|
|
1480
|
+
// Call error callback if provided
|
|
1481
|
+
if (this.onWebhookError) {
|
|
1482
|
+
this.onWebhookError(webhookError, event);
|
|
1483
|
+
}
|
|
1484
|
+
// Log to console with formatted error
|
|
1485
|
+
console.error(`Failed to send audit event to webhook: ${formatError(error)}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
// Export singleton instance
|
|
1490
|
+
export const auditLogger = new AuditLogger();
|
|
1491
|
+
//# sourceMappingURL=AuditLogger.js.map
|