@unchainedshop/events 5.0.0-alpha.4 → 5.0.0-alpha.5
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/lib/audit/audit-integration.d.ts +1 -1
- package/lib/audit/audit-integration.js +614 -98
- package/lib/audit/index.d.ts +18 -38
- package/lib/audit/index.js +74 -253
- package/lib/audit/ocsf-types.d.ts +9 -6
- package/lib/audit/ocsf-types.js +14 -1
- package/lib/audit/otlp-types.d.ts +62 -0
- package/lib/audit/otlp-types.js +9 -0
- package/lib/audit/otlp.d.ts +9 -0
- package/lib/audit/otlp.js +164 -0
- package/lib/audit/request-context.d.ts +8 -0
- package/lib/audit/request-context.js +8 -0
- package/lib/events-index.d.ts +2 -1
- package/lib/events-index.js +2 -1
- package/package.json +2 -2
package/lib/audit/index.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OCSF_AUTH_ACTIVITY, OCSF_ACCOUNT_ACTIVITY, OCSF_API_ACTIVITY } from './ocsf-types.ts';
|
|
2
2
|
export * from './ocsf-types.ts';
|
|
3
|
-
export interface
|
|
4
|
-
|
|
3
|
+
export interface AuditLogOptions {
|
|
4
|
+
log?: boolean;
|
|
5
5
|
collectorUrl?: string;
|
|
6
6
|
collectorHeaders?: Record<string, string>;
|
|
7
7
|
batchSize?: number;
|
|
8
8
|
flushIntervalMs?: number;
|
|
9
|
+
maxQueueSize?: number;
|
|
9
10
|
}
|
|
10
11
|
export interface AuthenticationInput {
|
|
11
12
|
activity: (typeof OCSF_AUTH_ACTIVITY)[keyof typeof OCSF_AUTH_ACTIVITY];
|
|
@@ -41,55 +42,34 @@ export interface ApiActivityInput {
|
|
|
41
42
|
httpMethod?: string;
|
|
42
43
|
path?: string;
|
|
43
44
|
responseCode?: number;
|
|
44
|
-
|
|
45
|
-
export interface AuditLogQuery {
|
|
46
|
-
classUids?: number[];
|
|
47
|
-
activityIds?: number[];
|
|
48
|
-
userId?: string;
|
|
49
|
-
success?: boolean;
|
|
50
|
-
startTime?: Date;
|
|
51
|
-
endTime?: Date;
|
|
52
|
-
limit?: number;
|
|
53
|
-
offset?: number;
|
|
54
|
-
}
|
|
55
|
-
export interface VerifyResult {
|
|
56
|
-
valid: boolean;
|
|
57
|
-
entries: number;
|
|
58
|
-
verified: number;
|
|
59
|
-
error?: string;
|
|
45
|
+
data?: Record<string, unknown>;
|
|
60
46
|
}
|
|
61
47
|
export declare class AuditLog {
|
|
62
|
-
private readonly
|
|
48
|
+
private readonly logEnabled;
|
|
49
|
+
private readonly auditLogger;
|
|
63
50
|
private readonly collectorUrl?;
|
|
64
51
|
private readonly collectorHeaders;
|
|
65
52
|
private readonly batchSize;
|
|
66
53
|
private readonly flushIntervalMs;
|
|
67
|
-
private
|
|
68
|
-
private
|
|
69
|
-
private initialized;
|
|
54
|
+
private readonly maxQueueSize;
|
|
55
|
+
private readonly serviceName;
|
|
70
56
|
private pendingEvents;
|
|
57
|
+
private droppedEvents;
|
|
58
|
+
private flushing;
|
|
71
59
|
private flushTimer?;
|
|
72
|
-
constructor(config?:
|
|
73
|
-
private getFilePath;
|
|
74
|
-
private init;
|
|
75
|
-
private computeHash;
|
|
60
|
+
constructor(config?: AuditLogOptions);
|
|
76
61
|
private createMetadata;
|
|
77
62
|
private createUser;
|
|
78
63
|
private createEndpoint;
|
|
79
|
-
private
|
|
64
|
+
private dispatch;
|
|
65
|
+
private enqueueForCollector;
|
|
66
|
+
private capPendingEvents;
|
|
80
67
|
private flushToCollector;
|
|
81
|
-
private matches;
|
|
82
68
|
logAuthentication(input: AuthenticationInput): Promise<string>;
|
|
83
69
|
logAccountChange(input: AccountChangeInput): Promise<string>;
|
|
84
70
|
logApiActivity(input: ApiActivityInput): Promise<string>;
|
|
85
|
-
find(query?: AuditLogQuery): Promise<OCSFEvent[]>;
|
|
86
|
-
count(query?: AuditLogQuery): Promise<number>;
|
|
87
|
-
getFailedLogins(params: {
|
|
88
|
-
userId?: string;
|
|
89
|
-
remoteAddress?: string;
|
|
90
|
-
since?: Date;
|
|
91
|
-
}): Promise<number>;
|
|
92
|
-
verify(): Promise<VerifyResult>;
|
|
93
71
|
close(): Promise<void>;
|
|
94
72
|
}
|
|
95
|
-
export declare function createAuditLog(config?:
|
|
73
|
+
export declare function createAuditLog(config?: AuditLogOptions): AuditLog;
|
|
74
|
+
export declare function setAuditLogInstance(instance: AuditLog): void;
|
|
75
|
+
export declare function getAuditLogInstance(): AuditLog | null;
|
package/lib/audit/index.js
CHANGED
|
@@ -1,70 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, readFile, appendFile, readdir } from 'node:fs/promises';
|
|
3
|
-
import { join } from 'node:path';
|
|
1
|
+
import { hostname } from 'node:os';
|
|
4
2
|
import { createLogger } from '@unchainedshop/logger';
|
|
5
|
-
import { OCSF_CLASS, OCSF_CATEGORY, OCSF_SEVERITY, OCSF_STATUS, OCSF_AUTH_ACTIVITY, OCSF_ACCOUNT_ACTIVITY, OCSF_API_ACTIVITY, } from "./ocsf-types.js";
|
|
3
|
+
import { OCSF_CLASS, OCSF_CATEGORY, OCSF_SEVERITY, OCSF_STATUS, OCSF_AUTH_ACTIVITY, OCSF_ACCOUNT_ACTIVITY, OCSF_API_ACTIVITY, OCSF_API_ACTIVITY_NAMES, } from "./ocsf-types.js";
|
|
4
|
+
import { exportLogs, resolveCollectorUrl, resolveCollectorHeaders } from "./otlp.js";
|
|
6
5
|
export * from "./ocsf-types.js";
|
|
7
6
|
const logger = createLogger('unchained:audit');
|
|
8
7
|
const OCSF_VERSION = '1.4.0';
|
|
9
8
|
const PRODUCT_NAME = 'Unchained Engine';
|
|
10
|
-
const PRODUCT_VERSION = '
|
|
9
|
+
const PRODUCT_VERSION = process.env.npm_package_version || '5.0';
|
|
11
10
|
const PRODUCT_VENDOR = 'Unchained';
|
|
12
|
-
const
|
|
11
|
+
const ENGINE_HOSTNAME = hostname();
|
|
13
12
|
export class AuditLog {
|
|
14
|
-
|
|
13
|
+
logEnabled;
|
|
14
|
+
auditLogger;
|
|
15
15
|
collectorUrl;
|
|
16
16
|
collectorHeaders;
|
|
17
17
|
batchSize;
|
|
18
18
|
flushIntervalMs;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
initialized = false;
|
|
19
|
+
maxQueueSize;
|
|
20
|
+
serviceName;
|
|
22
21
|
pendingEvents = [];
|
|
22
|
+
droppedEvents = 0;
|
|
23
|
+
flushing = false;
|
|
23
24
|
flushTimer;
|
|
24
25
|
constructor(config = {}) {
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
26
|
+
this.logEnabled = config.log ?? true;
|
|
27
|
+
this.auditLogger = createLogger('unchained:audit');
|
|
28
|
+
this.collectorUrl = resolveCollectorUrl(config.collectorUrl);
|
|
29
|
+
this.collectorHeaders = resolveCollectorHeaders(config.collectorHeaders);
|
|
28
30
|
this.batchSize = config.batchSize || 10;
|
|
29
31
|
this.flushIntervalMs = config.flushIntervalMs || 5000;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const date = new Date().toISOString().slice(0, 10);
|
|
33
|
-
return join(this.dir, `audit-${date}.jsonl`);
|
|
34
|
-
}
|
|
35
|
-
async init() {
|
|
36
|
-
if (this.initialized)
|
|
37
|
-
return;
|
|
38
|
-
await mkdir(this.dir, { recursive: true });
|
|
39
|
-
try {
|
|
40
|
-
const files = (await readdir(this.dir)).filter((f) => f.endsWith('.jsonl')).sort();
|
|
41
|
-
for (let i = files.length - 1; i >= 0; i--) {
|
|
42
|
-
const content = await readFile(join(this.dir, files[i]), 'utf-8');
|
|
43
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
44
|
-
if (lines.length > 0) {
|
|
45
|
-
const parsed = JSON.parse(lines[lines.length - 1]);
|
|
46
|
-
if (parsed.unmapped?.hash) {
|
|
47
|
-
this.lastEvent = parsed;
|
|
48
|
-
break;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
}
|
|
55
|
-
if (this.collectorUrl && !this.flushTimer) {
|
|
56
|
-
this.flushTimer = setInterval(() => this.flushToCollector(), this.flushIntervalMs);
|
|
57
|
-
}
|
|
58
|
-
this.initialized = true;
|
|
59
|
-
}
|
|
60
|
-
computeHash(event) {
|
|
61
|
-
const { unmapped, ...rest } = event;
|
|
62
|
-
const toHash = {
|
|
63
|
-
...rest,
|
|
64
|
-
unmapped: unmapped ? { seq: unmapped.seq, prev_hash: unmapped.prev_hash } : undefined,
|
|
65
|
-
};
|
|
66
|
-
const data = JSON.stringify(toHash, Object.keys(toHash).sort());
|
|
67
|
-
return createHash('sha256').update(data, 'utf8').digest('hex');
|
|
32
|
+
this.maxQueueSize = config.maxQueueSize || 1000;
|
|
33
|
+
this.serviceName = process.env.OTEL_SERVICE_NAME || 'unchained-engine';
|
|
68
34
|
}
|
|
69
35
|
createMetadata(uid) {
|
|
70
36
|
return {
|
|
@@ -89,82 +55,55 @@ export class AuditLog {
|
|
|
89
55
|
return undefined;
|
|
90
56
|
return { ip };
|
|
91
57
|
}
|
|
92
|
-
async
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
58
|
+
async dispatch(event) {
|
|
59
|
+
if (this.logEnabled) {
|
|
60
|
+
this.auditLogger.info(event.message || 'Audit event', { ocsf: event });
|
|
61
|
+
}
|
|
62
|
+
if (this.collectorUrl) {
|
|
63
|
+
this.enqueueForCollector(event);
|
|
64
|
+
}
|
|
65
|
+
return event.metadata.uid;
|
|
66
|
+
}
|
|
67
|
+
enqueueForCollector(event) {
|
|
68
|
+
this.pendingEvents.push(event);
|
|
69
|
+
this.capPendingEvents();
|
|
70
|
+
if (!this.flushTimer) {
|
|
71
|
+
this.flushTimer = setInterval(() => {
|
|
72
|
+
this.flushToCollector().catch((err) => logger.error(`Failed to flush audit events to collector: ${err.message}`));
|
|
73
|
+
}, this.flushIntervalMs);
|
|
74
|
+
this.flushTimer.unref?.();
|
|
75
|
+
}
|
|
76
|
+
if (this.pendingEvents.length >= this.batchSize) {
|
|
77
|
+
this.flushToCollector().catch((err) => logger.error(`Failed to flush audit events to collector: ${err.message}`));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
capPendingEvents() {
|
|
81
|
+
while (this.pendingEvents.length > this.maxQueueSize) {
|
|
82
|
+
this.pendingEvents.shift();
|
|
83
|
+
this.droppedEvents += 1;
|
|
84
|
+
if (this.droppedEvents === 1 || this.droppedEvents % 100 === 0) {
|
|
85
|
+
logger.warn(`Audit collector queue exceeded ${this.maxQueueSize} events — dropped ${this.droppedEvents} events so far`);
|
|
115
86
|
}
|
|
116
|
-
|
|
117
|
-
return eventWithChain.metadata.uid;
|
|
118
|
-
});
|
|
119
|
-
this.writeLock = result;
|
|
120
|
-
return result;
|
|
87
|
+
}
|
|
121
88
|
}
|
|
122
89
|
async flushToCollector() {
|
|
123
|
-
if (!this.collectorUrl || this.pendingEvents.length === 0)
|
|
90
|
+
if (!this.collectorUrl || this.flushing || this.pendingEvents.length === 0)
|
|
124
91
|
return;
|
|
125
|
-
|
|
92
|
+
this.flushing = true;
|
|
93
|
+
const events = this.pendingEvents;
|
|
126
94
|
this.pendingEvents = [];
|
|
127
95
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
headers: {
|
|
131
|
-
'Content-Type': 'application/json',
|
|
132
|
-
...this.collectorHeaders,
|
|
133
|
-
},
|
|
134
|
-
body: JSON.stringify({ events }),
|
|
135
|
-
});
|
|
136
|
-
if (!response.ok) {
|
|
137
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
138
|
-
}
|
|
139
|
-
logger.debug(`Flushed ${events.length} events to collector`);
|
|
96
|
+
await exportLogs(this.collectorUrl, this.collectorHeaders, events);
|
|
97
|
+
logger.debug(`Flushed ${events.length} audit events to collector`);
|
|
140
98
|
}
|
|
141
99
|
catch (err) {
|
|
142
100
|
this.pendingEvents = [...events, ...this.pendingEvents];
|
|
101
|
+
this.capPendingEvents();
|
|
143
102
|
throw err;
|
|
144
103
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (query.classUids?.length && !query.classUids.includes(event.class_uid))
|
|
148
|
-
return false;
|
|
149
|
-
if (query.activityIds?.length && !query.activityIds.includes(event.activity_id))
|
|
150
|
-
return false;
|
|
151
|
-
const userId = 'user' in event ? event.user?.uid : undefined;
|
|
152
|
-
const actorId = 'actor' in event ? event.actor?.user?.uid : undefined;
|
|
153
|
-
if (query.userId && userId !== query.userId && actorId !== query.userId)
|
|
154
|
-
return false;
|
|
155
|
-
if (query.success !== undefined) {
|
|
156
|
-
const isSuccess = event.status_id === OCSF_STATUS.SUCCESS;
|
|
157
|
-
if (query.success !== isSuccess)
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
if (query.startTime || query.endTime) {
|
|
161
|
-
const ts = new Date(event.time);
|
|
162
|
-
if (query.startTime && ts < query.startTime)
|
|
163
|
-
return false;
|
|
164
|
-
if (query.endTime && ts > query.endTime)
|
|
165
|
-
return false;
|
|
104
|
+
finally {
|
|
105
|
+
this.flushing = false;
|
|
166
106
|
}
|
|
167
|
-
return true;
|
|
168
107
|
}
|
|
169
108
|
async logAuthentication(input) {
|
|
170
109
|
const activityId = input.activity;
|
|
@@ -183,12 +122,13 @@ export class AuditLog {
|
|
|
183
122
|
message: input.message || activityMessages[activityId] || 'Authentication Event',
|
|
184
123
|
metadata: this.createMetadata(crypto.randomUUID()),
|
|
185
124
|
user: this.createUser(input.userId, input.userName),
|
|
125
|
+
service: { name: this.serviceName },
|
|
186
126
|
src_endpoint: this.createEndpoint(input.remoteAddress),
|
|
187
127
|
is_mfa: input.isMfa,
|
|
188
128
|
auth_protocol: input.authProtocol,
|
|
189
129
|
session: input.sessionId ? { uid: input.sessionId } : undefined,
|
|
190
130
|
};
|
|
191
|
-
return this.
|
|
131
|
+
return this.dispatch(event);
|
|
192
132
|
}
|
|
193
133
|
async logAccountChange(input) {
|
|
194
134
|
const activityId = input.activity;
|
|
@@ -220,7 +160,7 @@ export class AuditLog {
|
|
|
220
160
|
: undefined,
|
|
221
161
|
src_endpoint: this.createEndpoint(input.remoteAddress),
|
|
222
162
|
};
|
|
223
|
-
return this.
|
|
163
|
+
return this.dispatch(event);
|
|
224
164
|
}
|
|
225
165
|
async logApiActivity(input) {
|
|
226
166
|
const activityId = input.activity;
|
|
@@ -238,11 +178,14 @@ export class AuditLog {
|
|
|
238
178
|
};
|
|
239
179
|
const isAccessDenied = activityId === OCSF_API_ACTIVITY.ACCESS_DENIED;
|
|
240
180
|
const severity = isAccessDenied || input.success === false ? OCSF_SEVERITY.HIGH : OCSF_SEVERITY.INFORMATIONAL;
|
|
181
|
+
const isExtensionActivity = activityId >= 90 && activityId < 99;
|
|
182
|
+
const wireActivityId = isExtensionActivity ? OCSF_API_ACTIVITY.OTHER : activityId;
|
|
241
183
|
const event = {
|
|
242
184
|
category_uid: OCSF_CATEGORY.APPLICATION_ACTIVITY,
|
|
243
185
|
class_uid: OCSF_CLASS.API_ACTIVITY,
|
|
244
|
-
type_uid: OCSF_CLASS.API_ACTIVITY * 100 +
|
|
245
|
-
activity_id:
|
|
186
|
+
type_uid: OCSF_CLASS.API_ACTIVITY * 100 + wireActivityId,
|
|
187
|
+
activity_id: wireActivityId,
|
|
188
|
+
activity_name: OCSF_API_ACTIVITY_NAMES[activityId],
|
|
246
189
|
severity_id: severity,
|
|
247
190
|
status_id: input.success === false ? OCSF_STATUS.FAILURE : OCSF_STATUS.SUCCESS,
|
|
248
191
|
time: Date.now(),
|
|
@@ -256,139 +199,16 @@ export class AuditLog {
|
|
|
256
199
|
operation: input.operation,
|
|
257
200
|
response: input.responseCode ? { code: input.responseCode } : undefined,
|
|
258
201
|
},
|
|
259
|
-
src_endpoint: this.createEndpoint(input.remoteAddress),
|
|
202
|
+
src_endpoint: this.createEndpoint(input.remoteAddress) || { hostname: ENGINE_HOSTNAME },
|
|
260
203
|
http_request: input.httpMethod || input.path
|
|
261
204
|
? {
|
|
262
205
|
http_method: input.httpMethod,
|
|
263
206
|
url: input.path ? { path: input.path } : undefined,
|
|
264
207
|
}
|
|
265
208
|
: undefined,
|
|
209
|
+
...(input.data ? { unmapped: { data: input.data } } : {}),
|
|
266
210
|
};
|
|
267
|
-
return this.
|
|
268
|
-
}
|
|
269
|
-
async find(query = {}) {
|
|
270
|
-
await this.init();
|
|
271
|
-
const results = [];
|
|
272
|
-
const limit = query.limit || 100;
|
|
273
|
-
const offset = query.offset || 0;
|
|
274
|
-
let skipped = 0;
|
|
275
|
-
try {
|
|
276
|
-
const files = (await readdir(this.dir)).filter((f) => f.endsWith('.jsonl')).sort();
|
|
277
|
-
for (let i = files.length - 1; i >= 0 && results.length < limit; i--) {
|
|
278
|
-
const content = await readFile(join(this.dir, files[i]), 'utf-8');
|
|
279
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
280
|
-
for (let j = lines.length - 1; j >= 0 && results.length < limit; j--) {
|
|
281
|
-
try {
|
|
282
|
-
const event = JSON.parse(lines[j]);
|
|
283
|
-
if (!this.matches(event, query))
|
|
284
|
-
continue;
|
|
285
|
-
if (skipped < offset) {
|
|
286
|
-
skipped++;
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
results.push(event);
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
}
|
|
298
|
-
return results;
|
|
299
|
-
}
|
|
300
|
-
async count(query = {}) {
|
|
301
|
-
await this.init();
|
|
302
|
-
let count = 0;
|
|
303
|
-
try {
|
|
304
|
-
const files = (await readdir(this.dir)).filter((f) => f.endsWith('.jsonl')).sort();
|
|
305
|
-
for (const file of files) {
|
|
306
|
-
const content = await readFile(join(this.dir, file), 'utf-8');
|
|
307
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
308
|
-
for (const line of lines) {
|
|
309
|
-
try {
|
|
310
|
-
const event = JSON.parse(line);
|
|
311
|
-
if (this.matches(event, query))
|
|
312
|
-
count++;
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
}
|
|
321
|
-
return count;
|
|
322
|
-
}
|
|
323
|
-
async getFailedLogins(params) {
|
|
324
|
-
const events = await this.find({
|
|
325
|
-
classUids: [OCSF_CLASS.AUTHENTICATION],
|
|
326
|
-
activityIds: [OCSF_AUTH_ACTIVITY.LOGON],
|
|
327
|
-
userId: params.userId,
|
|
328
|
-
success: false,
|
|
329
|
-
startTime: params.since,
|
|
330
|
-
});
|
|
331
|
-
if (params.remoteAddress) {
|
|
332
|
-
return events.filter((e) => {
|
|
333
|
-
const authEvent = e;
|
|
334
|
-
return authEvent.src_endpoint?.ip === params.remoteAddress;
|
|
335
|
-
}).length;
|
|
336
|
-
}
|
|
337
|
-
return events.length;
|
|
338
|
-
}
|
|
339
|
-
async verify() {
|
|
340
|
-
await this.init();
|
|
341
|
-
let entries = 0;
|
|
342
|
-
let verified = 0;
|
|
343
|
-
let prevHash = GENESIS_HASH;
|
|
344
|
-
let prevSeq = 0;
|
|
345
|
-
try {
|
|
346
|
-
const files = (await readdir(this.dir)).filter((f) => f.endsWith('.jsonl')).sort();
|
|
347
|
-
for (const file of files) {
|
|
348
|
-
const content = await readFile(join(this.dir, file), 'utf-8');
|
|
349
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
350
|
-
for (const line of lines) {
|
|
351
|
-
entries++;
|
|
352
|
-
let event;
|
|
353
|
-
try {
|
|
354
|
-
event = JSON.parse(line);
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
return { valid: false, entries, verified, error: `Parse error at entry ${entries}` };
|
|
358
|
-
}
|
|
359
|
-
if (!event.unmapped) {
|
|
360
|
-
return { valid: false, entries, verified, error: `Missing hash chain at entry ${entries}` };
|
|
361
|
-
}
|
|
362
|
-
if (event.unmapped.seq !== prevSeq + 1) {
|
|
363
|
-
return { valid: false, entries, verified, error: `Sequence gap at ${event.unmapped.seq}` };
|
|
364
|
-
}
|
|
365
|
-
if (event.unmapped.prev_hash !== prevHash) {
|
|
366
|
-
return {
|
|
367
|
-
valid: false,
|
|
368
|
-
entries,
|
|
369
|
-
verified,
|
|
370
|
-
error: `Chain broken at seq ${event.unmapped.seq}`,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
const computedHash = this.computeHash(event);
|
|
374
|
-
if (computedHash !== event.unmapped.hash) {
|
|
375
|
-
return {
|
|
376
|
-
valid: false,
|
|
377
|
-
entries,
|
|
378
|
-
verified,
|
|
379
|
-
error: `Hash mismatch at seq ${event.unmapped.seq}`,
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
verified++;
|
|
383
|
-
prevHash = event.unmapped.hash;
|
|
384
|
-
prevSeq = event.unmapped.seq;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
catch (e) {
|
|
389
|
-
return { valid: false, entries, verified, error: `Read error: ${e}` };
|
|
390
|
-
}
|
|
391
|
-
return { valid: true, entries, verified };
|
|
211
|
+
return this.dispatch(event);
|
|
392
212
|
}
|
|
393
213
|
async close() {
|
|
394
214
|
if (this.flushTimer) {
|
|
@@ -400,17 +220,18 @@ export class AuditLog {
|
|
|
400
220
|
await this.flushToCollector();
|
|
401
221
|
}
|
|
402
222
|
catch (err) {
|
|
403
|
-
logger.error(`Failed to flush pending events on close: ${err.message}`);
|
|
223
|
+
logger.error(`Failed to flush pending audit events on close: ${err.message}`);
|
|
404
224
|
}
|
|
405
225
|
}
|
|
406
|
-
await this.writeLock;
|
|
407
|
-
this.initialized = false;
|
|
408
|
-
this.lastEvent = null;
|
|
409
226
|
}
|
|
410
227
|
}
|
|
411
|
-
export function createAuditLog(config) {
|
|
412
|
-
if (typeof config === 'string') {
|
|
413
|
-
return new AuditLog({ directory: config });
|
|
414
|
-
}
|
|
228
|
+
export function createAuditLog(config = {}) {
|
|
415
229
|
return new AuditLog(config);
|
|
416
230
|
}
|
|
231
|
+
let _auditLogInstance = null;
|
|
232
|
+
export function setAuditLogInstance(instance) {
|
|
233
|
+
_auditLogInstance = instance;
|
|
234
|
+
}
|
|
235
|
+
export function getAuditLogInstance() {
|
|
236
|
+
return _auditLogInstance;
|
|
237
|
+
}
|
|
@@ -5,7 +5,6 @@ export declare const OCSF_CATEGORY: {
|
|
|
5
5
|
export declare const OCSF_CLASS: {
|
|
6
6
|
readonly ACCOUNT_CHANGE: 3001;
|
|
7
7
|
readonly AUTHENTICATION: 3002;
|
|
8
|
-
readonly AUTHORIZE_SESSION: 3003;
|
|
9
8
|
readonly API_ACTIVITY: 6003;
|
|
10
9
|
};
|
|
11
10
|
export declare const OCSF_SEVERITY: {
|
|
@@ -62,6 +61,7 @@ export declare const OCSF_API_ACTIVITY: {
|
|
|
62
61
|
readonly ACCESS_DENIED: 95;
|
|
63
62
|
readonly OTHER: 99;
|
|
64
63
|
};
|
|
64
|
+
export declare const OCSF_API_ACTIVITY_NAMES: Record<number, string>;
|
|
65
65
|
export interface OCSFMetadata {
|
|
66
66
|
version: string;
|
|
67
67
|
product: {
|
|
@@ -99,6 +99,11 @@ export interface OCSFEndpoint {
|
|
|
99
99
|
version?: string;
|
|
100
100
|
}[];
|
|
101
101
|
}
|
|
102
|
+
export interface OCSFService {
|
|
103
|
+
name?: string;
|
|
104
|
+
uid?: string;
|
|
105
|
+
version?: string;
|
|
106
|
+
}
|
|
102
107
|
export interface OCSFApi {
|
|
103
108
|
operation?: string;
|
|
104
109
|
service?: {
|
|
@@ -117,22 +122,20 @@ export interface OCSFBaseEvent {
|
|
|
117
122
|
class_uid: number;
|
|
118
123
|
type_uid: number;
|
|
119
124
|
activity_id: number;
|
|
125
|
+
activity_name?: string;
|
|
120
126
|
severity_id: number;
|
|
121
127
|
time: number;
|
|
122
128
|
message?: string;
|
|
123
129
|
metadata: OCSFMetadata;
|
|
124
130
|
status_id?: number;
|
|
125
131
|
status_detail?: string;
|
|
126
|
-
unmapped?:
|
|
127
|
-
seq: number;
|
|
128
|
-
prev_hash: string;
|
|
129
|
-
hash: string;
|
|
130
|
-
};
|
|
132
|
+
unmapped?: Record<string, unknown>;
|
|
131
133
|
}
|
|
132
134
|
export interface OCSFAuthenticationEvent extends OCSFBaseEvent {
|
|
133
135
|
category_uid: 3;
|
|
134
136
|
class_uid: 3002;
|
|
135
137
|
user: OCSFUser;
|
|
138
|
+
service?: OCSFService;
|
|
136
139
|
src_endpoint?: OCSFEndpoint;
|
|
137
140
|
dst_endpoint?: OCSFEndpoint;
|
|
138
141
|
auth_protocol_id?: number;
|
package/lib/audit/ocsf-types.js
CHANGED
|
@@ -5,7 +5,6 @@ export const OCSF_CATEGORY = {
|
|
|
5
5
|
export const OCSF_CLASS = {
|
|
6
6
|
ACCOUNT_CHANGE: 3001,
|
|
7
7
|
AUTHENTICATION: 3002,
|
|
8
|
-
AUTHORIZE_SESSION: 3003,
|
|
9
8
|
API_ACTIVITY: 6003,
|
|
10
9
|
};
|
|
11
10
|
export const OCSF_SEVERITY = {
|
|
@@ -62,3 +61,17 @@ export const OCSF_API_ACTIVITY = {
|
|
|
62
61
|
ACCESS_DENIED: 95,
|
|
63
62
|
OTHER: 99,
|
|
64
63
|
};
|
|
64
|
+
export const OCSF_API_ACTIVITY_NAMES = {
|
|
65
|
+
[OCSF_API_ACTIVITY.UNKNOWN]: 'Unknown',
|
|
66
|
+
[OCSF_API_ACTIVITY.CREATE]: 'Create',
|
|
67
|
+
[OCSF_API_ACTIVITY.READ]: 'Read',
|
|
68
|
+
[OCSF_API_ACTIVITY.UPDATE]: 'Update',
|
|
69
|
+
[OCSF_API_ACTIVITY.DELETE]: 'Delete',
|
|
70
|
+
[OCSF_API_ACTIVITY.CHECKOUT]: 'Checkout',
|
|
71
|
+
[OCSF_API_ACTIVITY.PAYMENT]: 'Payment',
|
|
72
|
+
[OCSF_API_ACTIVITY.REFUND]: 'Refund',
|
|
73
|
+
[OCSF_API_ACTIVITY.EXPORT]: 'Export',
|
|
74
|
+
[OCSF_API_ACTIVITY.IMPORT]: 'Import',
|
|
75
|
+
[OCSF_API_ACTIVITY.ACCESS_DENIED]: 'Access Denied',
|
|
76
|
+
[OCSF_API_ACTIVITY.OTHER]: 'Other',
|
|
77
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type OTLPAnyValue = {
|
|
2
|
+
stringValue: string;
|
|
3
|
+
} | {
|
|
4
|
+
boolValue: boolean;
|
|
5
|
+
} | {
|
|
6
|
+
intValue: string;
|
|
7
|
+
} | {
|
|
8
|
+
doubleValue: number;
|
|
9
|
+
} | {
|
|
10
|
+
arrayValue: {
|
|
11
|
+
values: OTLPAnyValue[];
|
|
12
|
+
};
|
|
13
|
+
} | {
|
|
14
|
+
kvlistValue: {
|
|
15
|
+
values: OTLPKeyValue[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export interface OTLPKeyValue {
|
|
19
|
+
key: string;
|
|
20
|
+
value: OTLPAnyValue;
|
|
21
|
+
}
|
|
22
|
+
export interface OTLPResource {
|
|
23
|
+
attributes: OTLPKeyValue[];
|
|
24
|
+
}
|
|
25
|
+
export interface OTLPInstrumentationScope {
|
|
26
|
+
name: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const OTLP_SEVERITY_NUMBER: {
|
|
30
|
+
readonly UNSPECIFIED: 0;
|
|
31
|
+
readonly INFO: 9;
|
|
32
|
+
readonly INFO2: 10;
|
|
33
|
+
readonly WARN: 13;
|
|
34
|
+
readonly ERROR: 17;
|
|
35
|
+
readonly FATAL: 21;
|
|
36
|
+
readonly FATAL4: 24;
|
|
37
|
+
};
|
|
38
|
+
export interface OTLPLogRecord {
|
|
39
|
+
timeUnixNano: string;
|
|
40
|
+
observedTimeUnixNano: string;
|
|
41
|
+
severityNumber: number;
|
|
42
|
+
severityText?: string;
|
|
43
|
+
body: OTLPAnyValue;
|
|
44
|
+
attributes: OTLPKeyValue[];
|
|
45
|
+
}
|
|
46
|
+
export interface OTLPScopeLogs {
|
|
47
|
+
scope: OTLPInstrumentationScope;
|
|
48
|
+
logRecords: OTLPLogRecord[];
|
|
49
|
+
}
|
|
50
|
+
export interface OTLPResourceLogs {
|
|
51
|
+
resource: OTLPResource;
|
|
52
|
+
scopeLogs: OTLPScopeLogs[];
|
|
53
|
+
}
|
|
54
|
+
export interface OTLPExportLogsServiceRequest {
|
|
55
|
+
resourceLogs: OTLPResourceLogs[];
|
|
56
|
+
}
|
|
57
|
+
export interface OTLPExportLogsServiceResponse {
|
|
58
|
+
partialSuccess?: {
|
|
59
|
+
rejectedLogRecords?: number | string;
|
|
60
|
+
errorMessage?: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OCSFEvent } from './ocsf-types.ts';
|
|
2
|
+
import { type OTLPAnyValue, type OTLPLogRecord, type OTLPExportLogsServiceRequest } from './otlp-types.ts';
|
|
3
|
+
export declare function toAnyValue(value: unknown): OTLPAnyValue;
|
|
4
|
+
export declare function encodeOCSFLogRecord(event: OCSFEvent, observedTimeMs: number): OTLPLogRecord;
|
|
5
|
+
export declare function buildExportLogsRequest(events: OCSFEvent[], observedTimeMs?: number): OTLPExportLogsServiceRequest;
|
|
6
|
+
export declare function parseOtlpHeaders(raw?: string): Record<string, string>;
|
|
7
|
+
export declare function resolveCollectorUrl(explicit?: string): string | undefined;
|
|
8
|
+
export declare function resolveCollectorHeaders(explicit?: Record<string, string>): Record<string, string>;
|
|
9
|
+
export declare function exportLogs(collectorUrl: string, collectorHeaders: Record<string, string>, events: OCSFEvent[]): Promise<void>;
|