@serve.zone/dcrouter 18.1.0 → 18.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +8 -8
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +4 -1
- package/dist_ts/classes.dcrouter.js +30 -6
- package/dist_ts/db/documents/classes.authentication-event.doc.d.ts +25 -0
- package/dist_ts/db/documents/classes.authentication-event.doc.js +273 -0
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.d.ts +25 -0
- package/dist_ts/db/documents/classes.email-traffic-bucket.doc.js +200 -0
- package/dist_ts/db/documents/index.d.ts +2 -0
- package/dist_ts/db/documents/index.js +3 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +1 -0
- package/dist_ts/email/classes.accepted-email-spool.js +9 -1
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +19 -0
- package/dist_ts/monitoring/classes.metricsmanager.js +240 -44
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/admin.handler.js +61 -14
- package/dist_ts/opsserver/handlers/security.handler.js +21 -3
- package/dist_ts/opsserver/handlers/stats.handler.js +27 -10
- package/dist_ts/security/classes.authentication-event-manager.d.ts +35 -0
- package/dist_ts/security/classes.authentication-event-manager.js +185 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts_interfaces/data/stats.d.ts +16 -0
- package/dist_ts_migrations/index.js +74 -15
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/overview/ops-view-overview.js +3 -2
- package/dist_ts_web/elements/security/ops-view-security-authentication.js +20 -13
- package/dist_ts_web/elements/security/ops-view-security-overview.js +8 -11
- package/package.json +1 -1
- package/readme.md +8 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +31 -5
- package/ts/db/documents/classes.authentication-event.doc.ts +248 -0
- package/ts/db/documents/classes.email-traffic-bucket.doc.ts +161 -0
- package/ts/db/documents/index.ts +2 -0
- package/ts/email/classes.accepted-email-spool.ts +8 -0
- package/ts/monitoring/classes.metricsmanager.ts +287 -50
- package/ts/opsserver/handlers/admin.handler.ts +74 -15
- package/ts/opsserver/handlers/security.handler.ts +23 -2
- package/ts/opsserver/handlers/stats.handler.ts +29 -9
- package/ts/security/classes.authentication-event-manager.ts +221 -0
- package/ts/security/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/overview/ops-view-overview.ts +2 -1
- package/ts_web/elements/security/ops-view-security-authentication.ts +20 -13
- package/ts_web/elements/security/ops-view-security-overview.ts +7 -12
|
@@ -66,10 +66,10 @@ export class StatsHandler {
|
|
|
66
66
|
stats: {
|
|
67
67
|
sent: stats.sentToday,
|
|
68
68
|
received: stats.receivedToday,
|
|
69
|
-
bounced:
|
|
69
|
+
bounced: stats.bouncedToday,
|
|
70
70
|
queued: stats.queueSize,
|
|
71
|
-
failed:
|
|
72
|
-
averageDeliveryTime:
|
|
71
|
+
failed: stats.failedToday,
|
|
72
|
+
averageDeliveryTime: stats.averageDeliveryTime,
|
|
73
73
|
deliveryRate: stats.deliveryRate,
|
|
74
74
|
bounceRate: stats.bounceRate,
|
|
75
75
|
},
|
|
@@ -219,10 +219,10 @@ export class StatsHandler {
|
|
|
219
219
|
metrics.email = {
|
|
220
220
|
sent: stats.sentToday,
|
|
221
221
|
received: stats.receivedToday,
|
|
222
|
-
bounced:
|
|
222
|
+
bounced: stats.bouncedToday,
|
|
223
223
|
queued: stats.queueSize,
|
|
224
|
-
failed:
|
|
225
|
-
averageDeliveryTime:
|
|
224
|
+
failed: stats.failedToday,
|
|
225
|
+
averageDeliveryTime: stats.averageDeliveryTime,
|
|
226
226
|
deliveryRate: stats.deliveryRate,
|
|
227
227
|
bounceRate: stats.bounceRate,
|
|
228
228
|
timeSeries,
|
|
@@ -256,7 +256,15 @@ export class StatsHandler {
|
|
|
256
256
|
|
|
257
257
|
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
|
|
258
258
|
promises.push(
|
|
259
|
-
|
|
259
|
+
Promise.all([
|
|
260
|
+
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats(),
|
|
261
|
+
this.opsServerRef.dcRouterRef.authenticationEventManager
|
|
262
|
+
? this.opsServerRef.dcRouterRef.authenticationEventManager.getWindowStats()
|
|
263
|
+
: Promise.resolve({
|
|
264
|
+
windowStart: Date.now() - 86_400_000,
|
|
265
|
+
successes: 0, failures: 0, events: [],
|
|
266
|
+
}),
|
|
267
|
+
]).then(([stats, authentication]) => {
|
|
260
268
|
// Get recent events from the SecurityLogger singleton
|
|
261
269
|
const securityLogger = SecurityLogger.getInstance();
|
|
262
270
|
const recentEvents = securityLogger.getRecentEvents(50).map((evt) => ({
|
|
@@ -276,7 +284,10 @@ export class StatsHandler {
|
|
|
276
284
|
spamDetected: stats.spamDetected,
|
|
277
285
|
malwareDetected: stats.malwareDetected,
|
|
278
286
|
phishingDetected: stats.phishingDetected,
|
|
279
|
-
authenticationFailures:
|
|
287
|
+
authenticationFailures: authentication.failures,
|
|
288
|
+
authenticationSuccesses: authentication.successes,
|
|
289
|
+
authenticationWindowStart: authentication.windowStart,
|
|
290
|
+
authenticationEvents: authentication.events,
|
|
280
291
|
suspiciousActivities: stats.totalThreatsBlocked,
|
|
281
292
|
recentEvents,
|
|
282
293
|
};
|
|
@@ -463,9 +474,12 @@ export class StatsHandler {
|
|
|
463
474
|
sentToday: number;
|
|
464
475
|
receivedToday: number;
|
|
465
476
|
bounceRate: number;
|
|
477
|
+
failedToday: number;
|
|
478
|
+
bouncedToday: number;
|
|
466
479
|
deliveryRate: number;
|
|
467
480
|
queueSize: number;
|
|
468
481
|
domainBreakdown?: { [domain: string]: interfaces.data.IEmailStats };
|
|
482
|
+
averageDeliveryTime: number;
|
|
469
483
|
}> {
|
|
470
484
|
// Get metrics from MetricsManager if available
|
|
471
485
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
|
@@ -474,8 +488,11 @@ export class StatsHandler {
|
|
|
474
488
|
sentToday: emailStats.sentToday,
|
|
475
489
|
receivedToday: emailStats.receivedToday,
|
|
476
490
|
bounceRate: emailStats.bounceRate,
|
|
491
|
+
failedToday: emailStats.failedToday,
|
|
492
|
+
bouncedToday: emailStats.bouncedToday,
|
|
477
493
|
deliveryRate: emailStats.deliveryRate,
|
|
478
494
|
queueSize: emailStats.queueSize,
|
|
495
|
+
averageDeliveryTime: emailStats.averageDeliveryTime,
|
|
479
496
|
};
|
|
480
497
|
}
|
|
481
498
|
|
|
@@ -484,8 +501,11 @@ export class StatsHandler {
|
|
|
484
501
|
sentToday: 0,
|
|
485
502
|
receivedToday: 0,
|
|
486
503
|
bounceRate: 0,
|
|
487
|
-
deliveryRate:
|
|
504
|
+
deliveryRate: 1,
|
|
505
|
+
failedToday: 0,
|
|
506
|
+
bouncedToday: 0,
|
|
488
507
|
queueSize: 0,
|
|
508
|
+
averageDeliveryTime: 0,
|
|
489
509
|
};
|
|
490
510
|
}
|
|
491
511
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import type { DcRouter } from '../classes.dcrouter.js';
|
|
4
|
+
import { AuthenticationEventDoc } from '../db/documents/classes.authentication-event.doc.js';
|
|
5
|
+
import type {
|
|
6
|
+
IAuthenticationEvent,
|
|
7
|
+
TAuthenticationFailureReason,
|
|
8
|
+
TAuthenticationSource,
|
|
9
|
+
TResolvedAuthenticationSource,
|
|
10
|
+
} from '../../ts_interfaces/data/stats.js';
|
|
11
|
+
import {
|
|
12
|
+
SecurityEventType,
|
|
13
|
+
SecurityLogLevel,
|
|
14
|
+
SecurityLogger,
|
|
15
|
+
} from './classes.securitylogger.js';
|
|
16
|
+
|
|
17
|
+
const MAX_IN_MEMORY_EVENTS = 10_000;
|
|
18
|
+
const MAX_PERSIST_BATCH_SIZE = 500;
|
|
19
|
+
|
|
20
|
+
export interface IAuthenticationWindowStats {
|
|
21
|
+
windowStart: number;
|
|
22
|
+
successes: number;
|
|
23
|
+
failures: number;
|
|
24
|
+
events: IAuthenticationEvent[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class AuthenticationEventManager {
|
|
28
|
+
private pendingEvents = new Map<string, IAuthenticationEvent>();
|
|
29
|
+
private recentEvents = new Map<string, IAuthenticationEvent>();
|
|
30
|
+
private flushInFlight?: Promise<void>;
|
|
31
|
+
private pruneInFlight?: Promise<number>;
|
|
32
|
+
private windowStatsInFlight = new Map<string, Promise<IAuthenticationWindowStats>>();
|
|
33
|
+
|
|
34
|
+
constructor(private dcRouterRef: DcRouter) {}
|
|
35
|
+
|
|
36
|
+
private canPersist(): boolean {
|
|
37
|
+
return this.dcRouterRef.options.dbConfig?.enabled !== false
|
|
38
|
+
&& Boolean(this.dcRouterRef.dcRouterDb?.isReady());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private rememberEvent(eventArg: IAuthenticationEvent): void {
|
|
42
|
+
this.pendingEvents.set(eventArg.id, eventArg);
|
|
43
|
+
this.recentEvents.set(eventArg.id, eventArg);
|
|
44
|
+
while (this.pendingEvents.size > MAX_IN_MEMORY_EVENTS) {
|
|
45
|
+
const oldestId = this.pendingEvents.keys().next().value as string | undefined;
|
|
46
|
+
if (!oldestId) break;
|
|
47
|
+
this.pendingEvents.delete(oldestId);
|
|
48
|
+
logger.log('error', 'Authentication event persistence buffer reached its limit; oldest event dropped');
|
|
49
|
+
}
|
|
50
|
+
while (this.recentEvents.size > MAX_IN_MEMORY_EVENTS) {
|
|
51
|
+
const oldestId = this.recentEvents.keys().next().value as string | undefined;
|
|
52
|
+
if (!oldestId) break;
|
|
53
|
+
this.recentEvents.delete(oldestId);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async flushBufferedUnlocked(): Promise<void> {
|
|
58
|
+
if (!this.canPersist() || this.pendingEvents.size === 0) return;
|
|
59
|
+
const events = [...this.pendingEvents.values()];
|
|
60
|
+
for (let offset = 0; offset < events.length; offset += MAX_PERSIST_BATCH_SIZE) {
|
|
61
|
+
const batch = events.slice(offset, offset + MAX_PERSIST_BATCH_SIZE);
|
|
62
|
+
await AuthenticationEventDoc.upsertMany(batch);
|
|
63
|
+
for (const event of batch) {
|
|
64
|
+
if (this.pendingEvents.get(event.id) === event) {
|
|
65
|
+
this.pendingEvents.delete(event.id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public async recordAttempt(optionsArg: {
|
|
72
|
+
username: string;
|
|
73
|
+
userId?: string;
|
|
74
|
+
success: boolean;
|
|
75
|
+
requestedAuthSource: TAuthenticationSource;
|
|
76
|
+
resolvedAuthSource?: TResolvedAuthenticationSource;
|
|
77
|
+
failureReason?: TAuthenticationFailureReason;
|
|
78
|
+
}): Promise<IAuthenticationEvent> {
|
|
79
|
+
const event: IAuthenticationEvent = {
|
|
80
|
+
id: `auth-${plugins.uuid.v4()}`,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
username: String(optionsArg.username || '').trim().toLowerCase() || 'unknown',
|
|
83
|
+
success: optionsArg.success,
|
|
84
|
+
requestedAuthSource: optionsArg.requestedAuthSource,
|
|
85
|
+
...(optionsArg.userId ? { userId: optionsArg.userId } : {}),
|
|
86
|
+
...(optionsArg.resolvedAuthSource
|
|
87
|
+
? { resolvedAuthSource: optionsArg.resolvedAuthSource }
|
|
88
|
+
: {}),
|
|
89
|
+
...(optionsArg.failureReason ? { failureReason: optionsArg.failureReason } : {}),
|
|
90
|
+
};
|
|
91
|
+
// Buffer before any asynchronous persistence so authentication cannot be
|
|
92
|
+
// held behind a slow database and the bounded drop policy remains effective.
|
|
93
|
+
this.rememberEvent(event);
|
|
94
|
+
try {
|
|
95
|
+
SecurityLogger.getInstance().logEvent({
|
|
96
|
+
level: event.success ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
|
|
97
|
+
type: SecurityEventType.AUTHENTICATION,
|
|
98
|
+
message: event.success ? 'Authentication succeeded' : 'Authentication failed',
|
|
99
|
+
userId: event.userId,
|
|
100
|
+
success: event.success,
|
|
101
|
+
details: { authenticationEvent: event },
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
logger.log('warn', `Unable to mirror authentication event to the security log: ${(error as Error).message}`);
|
|
105
|
+
}
|
|
106
|
+
return event;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public flushBuffered(): Promise<void> {
|
|
110
|
+
if (this.flushInFlight) return this.flushInFlight;
|
|
111
|
+
const run = this.flushBufferedUnlocked();
|
|
112
|
+
this.flushInFlight = run;
|
|
113
|
+
run.then(
|
|
114
|
+
() => { if (this.flushInFlight === run) this.flushInFlight = undefined; },
|
|
115
|
+
() => { if (this.flushInFlight === run) this.flushInFlight = undefined; },
|
|
116
|
+
);
|
|
117
|
+
return run;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
public pruneBefore(cutoffArg: number): Promise<number> {
|
|
121
|
+
if (this.pruneInFlight) return this.pruneInFlight;
|
|
122
|
+
const run = this.canPersist()
|
|
123
|
+
? AuthenticationEventDoc.pruneBefore(cutoffArg)
|
|
124
|
+
: Promise.resolve(0);
|
|
125
|
+
this.pruneInFlight = run;
|
|
126
|
+
run.then(
|
|
127
|
+
() => { if (this.pruneInFlight === run) this.pruneInFlight = undefined; },
|
|
128
|
+
() => { if (this.pruneInFlight === run) this.pruneInFlight = undefined; },
|
|
129
|
+
);
|
|
130
|
+
return run;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private getInMemoryWindow(cutoffArg: number, limitArg: number): IAuthenticationWindowStats {
|
|
134
|
+
const events = [...this.recentEvents.values()]
|
|
135
|
+
.filter((event) => event.timestamp >= cutoffArg)
|
|
136
|
+
.sort((a, b) => b.timestamp - a.timestamp || b.id.localeCompare(a.id));
|
|
137
|
+
return {
|
|
138
|
+
windowStart: cutoffArg,
|
|
139
|
+
successes: events.filter((event) => event.success).length,
|
|
140
|
+
failures: events.filter((event) => !event.success).length,
|
|
141
|
+
events: events.slice(0, limitArg),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private async getWindowStatsUnlocked(
|
|
146
|
+
windowMsArg = 24 * 60 * 60 * 1000,
|
|
147
|
+
limitArg = 100,
|
|
148
|
+
): Promise<IAuthenticationWindowStats> {
|
|
149
|
+
const cutoff = Date.now() - windowMsArg;
|
|
150
|
+
if (!this.canPersist()) {
|
|
151
|
+
return this.getInMemoryWindow(cutoff, limitArg);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await this.flushBuffered();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.log('warn', `Unable to flush authentication event buffer: ${(error as Error).message}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const durable = await AuthenticationEventDoc.getWindowSummary(cutoff, limitArg);
|
|
162
|
+
const buffered = [...this.pendingEvents.values()]
|
|
163
|
+
.filter((event) => event.timestamp >= cutoff);
|
|
164
|
+
const existingIds = new Set<string>();
|
|
165
|
+
const bufferedIds = buffered.map((event) => event.id);
|
|
166
|
+
for (let offset = 0; offset < bufferedIds.length; offset += MAX_PERSIST_BATCH_SIZE) {
|
|
167
|
+
const existingBatch = await AuthenticationEventDoc.findExistingIds(
|
|
168
|
+
bufferedIds.slice(offset, offset + MAX_PERSIST_BATCH_SIZE),
|
|
169
|
+
);
|
|
170
|
+
existingBatch.forEach((id) => existingIds.add(id));
|
|
171
|
+
}
|
|
172
|
+
const unpersisted = buffered.filter((event) => !existingIds.has(event.id));
|
|
173
|
+
const mergedById = new Map<string, IAuthenticationEvent>();
|
|
174
|
+
for (const event of durable.events) mergedById.set(event.id, event);
|
|
175
|
+
for (const event of unpersisted) {
|
|
176
|
+
if (!mergedById.has(event.id)) mergedById.set(event.id, event);
|
|
177
|
+
}
|
|
178
|
+
const mergedEvents = [...mergedById.values()]
|
|
179
|
+
.sort((a, b) => b.timestamp - a.timestamp || b.id.localeCompare(a.id))
|
|
180
|
+
.slice(0, limitArg);
|
|
181
|
+
return {
|
|
182
|
+
windowStart: cutoff,
|
|
183
|
+
successes: durable.successes + unpersisted.filter((event) => event.success).length,
|
|
184
|
+
failures: durable.failures + unpersisted.filter((event) => !event.success).length,
|
|
185
|
+
events: mergedEvents,
|
|
186
|
+
};
|
|
187
|
+
} catch (error) {
|
|
188
|
+
logger.log('warn', `Unable to query durable authentication events: ${(error as Error).message}`);
|
|
189
|
+
return this.getInMemoryWindow(cutoff, limitArg);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public getWindowStats(
|
|
194
|
+
windowMsArg = 24 * 60 * 60 * 1000,
|
|
195
|
+
limitArg = 100,
|
|
196
|
+
): Promise<IAuthenticationWindowStats> {
|
|
197
|
+
if (!Number.isSafeInteger(windowMsArg) || windowMsArg <= 0) {
|
|
198
|
+
return Promise.reject(new Error('Authentication window must be a positive integer'));
|
|
199
|
+
}
|
|
200
|
+
if (!Number.isSafeInteger(limitArg) || limitArg <= 0 || limitArg > 500) {
|
|
201
|
+
return Promise.reject(new Error('Authentication event limit must be from 1 to 500'));
|
|
202
|
+
}
|
|
203
|
+
const key = `${windowMsArg}:${limitArg}`;
|
|
204
|
+
const existing = this.windowStatsInFlight.get(key);
|
|
205
|
+
if (existing) return existing;
|
|
206
|
+
const run = this.getWindowStatsUnlocked(windowMsArg, limitArg);
|
|
207
|
+
this.windowStatsInFlight.set(key, run);
|
|
208
|
+
run.then(
|
|
209
|
+
() => { if (this.windowStatsInFlight.get(key) === run) this.windowStatsInFlight.delete(key); },
|
|
210
|
+
() => { if (this.windowStatsInFlight.get(key) === run) this.windowStatsInFlight.delete(key); },
|
|
211
|
+
);
|
|
212
|
+
return run;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Test-only reset for the DcRouter-owned in-process buffers. */
|
|
216
|
+
public async resetForTests(): Promise<void> {
|
|
217
|
+
await this.flushInFlight?.catch(() => undefined);
|
|
218
|
+
this.pendingEvents.clear();
|
|
219
|
+
this.recentEvents.clear();
|
|
220
|
+
}
|
|
221
|
+
}
|
package/ts/security/index.ts
CHANGED
|
@@ -119,7 +119,7 @@ export class OpsViewOverview extends DeesElement {
|
|
|
119
119
|
<dees-heading level="hr">Activity Charts</dees-heading>
|
|
120
120
|
<div class="chartGrid">
|
|
121
121
|
<dees-chart-area
|
|
122
|
-
.label=${'Email Traffic
|
|
122
|
+
.label=${'Email Traffic — Last 24 Hours'}
|
|
123
123
|
.series=${this.getEmailTrafficSeries()}
|
|
124
124
|
.realtimeMode=${true}
|
|
125
125
|
.rollingWindow=${86400000}
|
|
@@ -506,6 +506,7 @@ export class OpsViewOverview extends DeesElement {
|
|
|
506
506
|
return [
|
|
507
507
|
{ name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
508
508
|
{ name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
509
|
+
{ name: 'Failed', color: '#ef4444', data: (ts.failed || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
509
510
|
];
|
|
510
511
|
}
|
|
511
512
|
|
|
@@ -60,10 +60,8 @@ export class OpsViewSecurityAuthentication extends DeesElement {
|
|
|
60
60
|
`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
|
|
66
|
-
const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
|
|
63
|
+
const authEvents = metrics.authenticationEvents || [];
|
|
64
|
+
const successfulLogins = metrics.authenticationSuccesses || 0;
|
|
67
65
|
|
|
68
66
|
const tiles: IStatsTile[] = [
|
|
69
67
|
{
|
|
@@ -73,7 +71,7 @@ export class OpsViewSecurityAuthentication extends DeesElement {
|
|
|
73
71
|
type: 'number',
|
|
74
72
|
icon: 'lucide:LockOpen',
|
|
75
73
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
|
76
|
-
description: 'Failed authentication attempts
|
|
74
|
+
description: 'Failed authentication attempts in the last 24 hours',
|
|
77
75
|
},
|
|
78
76
|
{
|
|
79
77
|
id: 'successfulLogins',
|
|
@@ -82,17 +80,26 @@ export class OpsViewSecurityAuthentication extends DeesElement {
|
|
|
82
80
|
type: 'number',
|
|
83
81
|
icon: 'lucide:Lock',
|
|
84
82
|
color: '#22c55e',
|
|
85
|
-
description: 'Successful logins
|
|
83
|
+
description: 'Successful logins in the last 24 hours',
|
|
86
84
|
},
|
|
87
85
|
];
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
const failureLabels: Record<string, string> = {
|
|
88
|
+
invalidCredentials: 'Invalid credentials',
|
|
89
|
+
serviceUnavailable: 'Authentication service unavailable',
|
|
90
|
+
identityIssuanceFailed: 'Session identity could not be issued',
|
|
91
|
+
internalError: 'Internal authentication error',
|
|
92
|
+
};
|
|
93
|
+
const loginHistory = authEvents.map((evt) => ({
|
|
91
94
|
timestamp: evt.timestamp,
|
|
92
|
-
username: evt.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
username: evt.username,
|
|
96
|
+
source: evt.resolvedAuthSource
|
|
97
|
+
? evt.requestedAuthSource === 'auto'
|
|
98
|
+
? `Auto → ${evt.resolvedAuthSource}`
|
|
99
|
+
: evt.resolvedAuthSource
|
|
100
|
+
: evt.requestedAuthSource,
|
|
101
|
+
success: evt.success,
|
|
102
|
+
reason: evt.success ? '' : failureLabels[evt.failureReason || ''] || 'Authentication failed',
|
|
96
103
|
}));
|
|
97
104
|
|
|
98
105
|
return html`
|
|
@@ -111,7 +118,7 @@ export class OpsViewSecurityAuthentication extends DeesElement {
|
|
|
111
118
|
.displayFunction=${(item) => ({
|
|
112
119
|
'Time': new Date(item.timestamp).toLocaleString(),
|
|
113
120
|
'Username': item.username,
|
|
114
|
-
'
|
|
121
|
+
'Source': item.source,
|
|
115
122
|
'Status': item.success ? 'Success' : 'Failed',
|
|
116
123
|
'Reason': item.reason || '-',
|
|
117
124
|
})}
|
|
@@ -63,12 +63,7 @@ export class OpsViewSecurityOverview extends DeesElement {
|
|
|
63
63
|
const threatLevel = this.calculateThreatLevel(metrics);
|
|
64
64
|
const threatScore = this.getThreatScore(metrics);
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
const allEvents: any[] = metrics.recentEvents || [];
|
|
68
|
-
const oneHourAgo = Date.now() - 3600000;
|
|
69
|
-
const recentAuthSuccesses = allEvents.filter(
|
|
70
|
-
(evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
|
|
71
|
-
).length;
|
|
66
|
+
const successfulLogins = metrics.authenticationSuccesses || 0;
|
|
72
67
|
|
|
73
68
|
const tiles: IStatsTile[] = [
|
|
74
69
|
{
|
|
@@ -95,16 +90,16 @@ export class OpsViewSecurityOverview extends DeesElement {
|
|
|
95
90
|
type: 'number',
|
|
96
91
|
icon: 'lucide:ShieldCheck',
|
|
97
92
|
color: '#ef4444',
|
|
98
|
-
description: '
|
|
93
|
+
description: 'Threats observed in the last 24 hours',
|
|
99
94
|
},
|
|
100
95
|
{
|
|
101
|
-
id: '
|
|
102
|
-
title: '
|
|
103
|
-
value:
|
|
96
|
+
id: 'successfulLogins',
|
|
97
|
+
title: 'Successful Logins',
|
|
98
|
+
value: successfulLogins,
|
|
104
99
|
type: 'number',
|
|
105
100
|
icon: 'lucide:Users',
|
|
106
101
|
color: '#22c55e',
|
|
107
|
-
description: '
|
|
102
|
+
description: 'Successful logins in the last 24 hours',
|
|
108
103
|
},
|
|
109
104
|
{
|
|
110
105
|
id: 'authFailures',
|
|
@@ -113,7 +108,7 @@ export class OpsViewSecurityOverview extends DeesElement {
|
|
|
113
108
|
type: 'number',
|
|
114
109
|
icon: 'lucide:LockOpen',
|
|
115
110
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
|
116
|
-
description: 'Failed login attempts
|
|
111
|
+
description: 'Failed login attempts in the last 24 hours',
|
|
117
112
|
},
|
|
118
113
|
];
|
|
119
114
|
|