@serve.zone/dcrouter 7.0.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_serve/bundle.js +528 -587
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +18 -1
- package/dist_ts/logger.d.ts +2 -0
- package/dist_ts/logger.js +6 -1
- package/dist_ts/monitoring/classes.metricsmanager.d.ts +37 -0
- package/dist_ts/monitoring/classes.metricsmanager.js +101 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +2 -0
- package/dist_ts/opsserver/handlers/logs.handler.js +70 -25
- package/dist_ts/opsserver/handlers/stats.handler.js +25 -1
- package/dist_ts_interfaces/data/stats.d.ts +23 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/ops-view-logs.d.ts +7 -2
- package/dist_ts_web/elements/ops-view-logs.js +78 -102
- package/dist_ts_web/elements/ops-view-overview.d.ts +6 -0
- package/dist_ts_web/elements/ops-view-overview.js +82 -8
- package/dist_ts_web/elements/ops-view-security.js +42 -36
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +19 -1
- package/ts/logger.ts +7 -0
- package/ts/monitoring/classes.metricsmanager.ts +123 -3
- package/ts/opsserver/handlers/logs.handler.ts +77 -28
- package/ts/opsserver/handlers/stats.handler.ts +27 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/ops-view-logs.ts +72 -108
- package/ts_web/elements/ops-view-overview.ts +80 -6
- package/ts_web/elements/ops-view-security.ts +47 -36
|
@@ -2,6 +2,7 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
4
|
import { MetricsManager } from '../../monitoring/index.js';
|
|
5
|
+
import { SecurityLogger } from '../../security/classes.securitylogger.js';
|
|
5
6
|
|
|
6
7
|
export class StatsHandler {
|
|
7
8
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
@@ -203,6 +204,11 @@ export class StatsHandler {
|
|
|
203
204
|
if (sections.email) {
|
|
204
205
|
promises.push(
|
|
205
206
|
this.collectEmailStats().then(stats => {
|
|
207
|
+
// Get time-series data from MetricsManager
|
|
208
|
+
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
|
|
209
|
+
? this.opsServerRef.dcRouterRef.metricsManager.getEmailTimeSeries(24)
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
206
212
|
metrics.email = {
|
|
207
213
|
sent: stats.sentToday,
|
|
208
214
|
received: stats.receivedToday,
|
|
@@ -212,6 +218,7 @@ export class StatsHandler {
|
|
|
212
218
|
averageDeliveryTime: 0,
|
|
213
219
|
deliveryRate: stats.deliveryRate,
|
|
214
220
|
bounceRate: stats.bounceRate,
|
|
221
|
+
timeSeries,
|
|
215
222
|
};
|
|
216
223
|
})
|
|
217
224
|
);
|
|
@@ -220,6 +227,11 @@ export class StatsHandler {
|
|
|
220
227
|
if (sections.dns) {
|
|
221
228
|
promises.push(
|
|
222
229
|
this.collectDnsStats().then(stats => {
|
|
230
|
+
// Get time-series data from MetricsManager
|
|
231
|
+
const timeSeries = this.opsServerRef.dcRouterRef.metricsManager
|
|
232
|
+
? this.opsServerRef.dcRouterRef.metricsManager.getDnsTimeSeries(24)
|
|
233
|
+
: undefined;
|
|
234
|
+
|
|
223
235
|
metrics.dns = {
|
|
224
236
|
totalQueries: stats.totalQueries,
|
|
225
237
|
cacheHits: stats.cacheHits,
|
|
@@ -228,6 +240,7 @@ export class StatsHandler {
|
|
|
228
240
|
activeDomains: stats.topDomains.length,
|
|
229
241
|
averageResponseTime: 0,
|
|
230
242
|
queryTypes: stats.queryTypes,
|
|
243
|
+
timeSeries,
|
|
231
244
|
};
|
|
232
245
|
})
|
|
233
246
|
);
|
|
@@ -236,6 +249,19 @@ export class StatsHandler {
|
|
|
236
249
|
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
|
|
237
250
|
promises.push(
|
|
238
251
|
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
|
|
252
|
+
// Get recent events from the SecurityLogger singleton
|
|
253
|
+
const securityLogger = SecurityLogger.getInstance();
|
|
254
|
+
const recentEvents = securityLogger.getRecentEvents(50).map((evt) => ({
|
|
255
|
+
timestamp: evt.timestamp,
|
|
256
|
+
level: evt.level,
|
|
257
|
+
type: evt.type,
|
|
258
|
+
message: evt.message,
|
|
259
|
+
details: evt.details,
|
|
260
|
+
ipAddress: evt.ipAddress,
|
|
261
|
+
domain: evt.domain,
|
|
262
|
+
success: evt.success,
|
|
263
|
+
}));
|
|
264
|
+
|
|
239
265
|
metrics.security = {
|
|
240
266
|
blockedIPs: stats.blockedIPs,
|
|
241
267
|
reputationScores: {},
|
|
@@ -244,6 +270,7 @@ export class StatsHandler {
|
|
|
244
270
|
phishingDetected: stats.phishingDetected,
|
|
245
271
|
authenticationFailures: stats.authFailures,
|
|
246
272
|
suspiciousActivities: stats.totalThreatsBlocked,
|
|
273
|
+
recentEvents,
|
|
247
274
|
};
|
|
248
275
|
})
|
|
249
276
|
);
|
|
@@ -20,6 +20,15 @@ export class OpsViewLogs extends DeesElement {
|
|
|
20
20
|
filters: {},
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
@state()
|
|
24
|
+
accessor filterLevel: string | undefined;
|
|
25
|
+
|
|
26
|
+
@state()
|
|
27
|
+
accessor filterCategory: string | undefined;
|
|
28
|
+
|
|
29
|
+
@state()
|
|
30
|
+
accessor filterLimit: number = 100;
|
|
31
|
+
|
|
23
32
|
constructor() {
|
|
24
33
|
super();
|
|
25
34
|
const subscription = appstate.logStatePart
|
|
@@ -46,87 +55,20 @@ export class OpsViewLogs extends DeesElement {
|
|
|
46
55
|
align-items: center;
|
|
47
56
|
gap: 8px;
|
|
48
57
|
}
|
|
49
|
-
|
|
50
|
-
.logContainer {
|
|
51
|
-
background: ${cssManager.bdTheme('#f8f9fa', '#1e1e1e')};
|
|
52
|
-
border-radius: 8px;
|
|
53
|
-
padding: 16px;
|
|
54
|
-
max-height: 600px;
|
|
55
|
-
overflow-y: auto;
|
|
56
|
-
font-family: 'Consolas', 'Monaco', monospace;
|
|
57
|
-
font-size: 13px;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.logEntry {
|
|
61
|
-
margin-bottom: 8px;
|
|
62
|
-
line-height: 1.5;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.logTimestamp {
|
|
66
|
-
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
|
|
67
|
-
margin-right: 8px;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.logLevel {
|
|
71
|
-
font-weight: bold;
|
|
72
|
-
margin-right: 8px;
|
|
73
|
-
padding: 2px 6px;
|
|
74
|
-
border-radius: 3px;
|
|
75
|
-
font-size: 11px;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
.logLevel.debug {
|
|
79
|
-
color: ${cssManager.bdTheme('#6a9955', '#6a9955')};
|
|
80
|
-
background: ${cssManager.bdTheme('rgba(106, 153, 85, 0.1)', 'rgba(106, 153, 85, 0.1)')};
|
|
81
|
-
}
|
|
82
|
-
.logLevel.info {
|
|
83
|
-
color: ${cssManager.bdTheme('#569cd6', '#569cd6')};
|
|
84
|
-
background: ${cssManager.bdTheme('rgba(86, 156, 214, 0.1)', 'rgba(86, 156, 214, 0.1)')};
|
|
85
|
-
}
|
|
86
|
-
.logLevel.warn {
|
|
87
|
-
color: ${cssManager.bdTheme('#ce9178', '#ce9178')};
|
|
88
|
-
background: ${cssManager.bdTheme('rgba(206, 145, 120, 0.1)', 'rgba(206, 145, 120, 0.1)')};
|
|
89
|
-
}
|
|
90
|
-
.logLevel.error {
|
|
91
|
-
color: ${cssManager.bdTheme('#f44747', '#f44747')};
|
|
92
|
-
background: ${cssManager.bdTheme('rgba(244, 71, 71, 0.1)', 'rgba(244, 71, 71, 0.1)')};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
.logCategory {
|
|
96
|
-
color: ${cssManager.bdTheme('#c586c0', '#c586c0')};
|
|
97
|
-
margin-right: 8px;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.logMessage {
|
|
101
|
-
color: ${cssManager.bdTheme('#333', '#d4d4d4')};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.noLogs {
|
|
105
|
-
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
|
|
106
|
-
text-align: center;
|
|
107
|
-
padding: 40px;
|
|
108
|
-
}
|
|
109
58
|
`,
|
|
110
59
|
];
|
|
111
60
|
|
|
112
61
|
public render() {
|
|
113
62
|
return html`
|
|
114
63
|
<ops-sectionheading>Logs</ops-sectionheading>
|
|
115
|
-
|
|
64
|
+
|
|
116
65
|
<div class="controls">
|
|
117
66
|
<div class="filterGroup">
|
|
118
|
-
<dees-button
|
|
67
|
+
<dees-button
|
|
119
68
|
@click=${() => this.fetchLogs()}
|
|
120
69
|
>
|
|
121
70
|
Refresh Logs
|
|
122
71
|
</dees-button>
|
|
123
|
-
|
|
124
|
-
<dees-button
|
|
125
|
-
@click=${() => this.toggleStreaming()}
|
|
126
|
-
.type=${this.logState.isStreaming ? 'highlighted' : 'normal'}
|
|
127
|
-
>
|
|
128
|
-
${this.logState.isStreaming ? 'Stop Streaming' : 'Start Streaming'}
|
|
129
|
-
</dees-button>
|
|
130
72
|
</div>
|
|
131
73
|
|
|
132
74
|
<div class="filterGroup">
|
|
@@ -134,7 +76,7 @@ export class OpsViewLogs extends DeesElement {
|
|
|
134
76
|
<dees-input-dropdown
|
|
135
77
|
.options=${['all', 'debug', 'info', 'warn', 'error']}
|
|
136
78
|
.selectedOption=${'all'}
|
|
137
|
-
@selectedOption=${(e) => this.updateFilter('level', e.detail)}
|
|
79
|
+
@selectedOption=${(e: any) => this.updateFilter('level', e.detail)}
|
|
138
80
|
></dees-input-dropdown>
|
|
139
81
|
</div>
|
|
140
82
|
|
|
@@ -143,7 +85,7 @@ export class OpsViewLogs extends DeesElement {
|
|
|
143
85
|
<dees-input-dropdown
|
|
144
86
|
.options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
|
|
145
87
|
.selectedOption=${'all'}
|
|
146
|
-
@selectedOption=${(e) => this.updateFilter('category', e.detail)}
|
|
88
|
+
@selectedOption=${(e: any) => this.updateFilter('category', e.detail)}
|
|
147
89
|
></dees-input-dropdown>
|
|
148
90
|
</div>
|
|
149
91
|
|
|
@@ -152,56 +94,78 @@ export class OpsViewLogs extends DeesElement {
|
|
|
152
94
|
<dees-input-dropdown
|
|
153
95
|
.options=${['50', '100', '200', '500']}
|
|
154
96
|
.selectedOption=${'100'}
|
|
155
|
-
@selectedOption=${(e) => this.updateFilter('limit', e.detail)}
|
|
97
|
+
@selectedOption=${(e: any) => this.updateFilter('limit', e.detail)}
|
|
156
98
|
></dees-input-dropdown>
|
|
157
99
|
</div>
|
|
158
100
|
</div>
|
|
159
101
|
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<span class="logCategory">[${log.category}]</span>
|
|
167
|
-
<span class="logMessage">${log.message}</span>
|
|
168
|
-
</div>
|
|
169
|
-
`) : html`
|
|
170
|
-
<div class="noLogs">No logs to display</div>
|
|
171
|
-
`
|
|
172
|
-
}
|
|
173
|
-
</div>
|
|
102
|
+
<dees-chart-log
|
|
103
|
+
.label=${'Application Logs'}
|
|
104
|
+
.autoScroll=${true}
|
|
105
|
+
.maxEntries=${2000}
|
|
106
|
+
.showMetrics=${true}
|
|
107
|
+
></dees-chart-log>
|
|
174
108
|
`;
|
|
175
109
|
}
|
|
176
110
|
|
|
111
|
+
async connectedCallback() {
|
|
112
|
+
super.connectedCallback();
|
|
113
|
+
this.fetchLogs();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async updated(changedProperties: Map<string, any>) {
|
|
117
|
+
super.updated(changedProperties);
|
|
118
|
+
if (changedProperties.has('logState')) {
|
|
119
|
+
this.pushLogsToChart();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async pushLogsToChart() {
|
|
124
|
+
const chartLog = this.shadowRoot?.querySelector('dees-chart-log') as any;
|
|
125
|
+
if (!chartLog) return;
|
|
126
|
+
|
|
127
|
+
// Ensure the chart element has finished its own initialization
|
|
128
|
+
await chartLog.updateComplete;
|
|
129
|
+
|
|
130
|
+
chartLog.clearLogs();
|
|
131
|
+
const entries = this.getMappedLogEntries();
|
|
132
|
+
if (entries.length > 0) {
|
|
133
|
+
chartLog.updateLog(entries);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private getMappedLogEntries() {
|
|
138
|
+
return this.logState.recentLogs.map((log) => ({
|
|
139
|
+
timestamp: new Date(log.timestamp).toISOString(),
|
|
140
|
+
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
|
141
|
+
message: log.message,
|
|
142
|
+
source: log.category,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
177
146
|
private async fetchLogs() {
|
|
178
|
-
const filters = this.getActiveFilters();
|
|
179
147
|
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
|
180
|
-
limit:
|
|
181
|
-
level:
|
|
182
|
-
category:
|
|
148
|
+
limit: this.filterLimit,
|
|
149
|
+
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
|
150
|
+
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
|
183
151
|
});
|
|
184
152
|
}
|
|
185
153
|
|
|
186
154
|
private updateFilter(type: string, value: string) {
|
|
187
|
-
|
|
188
|
-
|
|
155
|
+
const resolved = value === 'all' ? undefined : value;
|
|
156
|
+
|
|
157
|
+
switch (type) {
|
|
158
|
+
case 'level':
|
|
159
|
+
this.filterLevel = resolved;
|
|
160
|
+
break;
|
|
161
|
+
case 'category':
|
|
162
|
+
this.filterCategory = resolved;
|
|
163
|
+
break;
|
|
164
|
+
case 'limit':
|
|
165
|
+
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
|
|
166
|
+
break;
|
|
189
167
|
}
|
|
190
|
-
|
|
191
|
-
// Update filters then fetch logs
|
|
192
|
-
this.fetchLogs();
|
|
193
|
-
}
|
|
194
168
|
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
level: this.logState.filters.level?.[0],
|
|
198
|
-
category: this.logState.filters.category?.[0],
|
|
199
|
-
limit: 100,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private toggleStreaming() {
|
|
204
|
-
// TODO: Implement log streaming with VirtualStream
|
|
205
|
-
console.log('Streaming toggle not yet implemented');
|
|
169
|
+
this.fetchLogs();
|
|
206
170
|
}
|
|
207
|
-
}
|
|
171
|
+
}
|
|
@@ -26,14 +26,36 @@ export class OpsViewOverview extends DeesElement {
|
|
|
26
26
|
error: null,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
@state()
|
|
30
|
+
accessor logState: appstate.ILogState = {
|
|
31
|
+
recentLogs: [],
|
|
32
|
+
isStreaming: false,
|
|
33
|
+
filters: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
29
36
|
constructor() {
|
|
30
37
|
super();
|
|
31
|
-
const
|
|
38
|
+
const statsSub = appstate.statsStatePart
|
|
32
39
|
.select((stateArg) => stateArg)
|
|
33
40
|
.subscribe((statsState) => {
|
|
34
41
|
this.statsState = statsState;
|
|
35
42
|
});
|
|
36
|
-
this.rxSubscriptions.push(
|
|
43
|
+
this.rxSubscriptions.push(statsSub);
|
|
44
|
+
|
|
45
|
+
const logSub = appstate.logStatePart
|
|
46
|
+
.select((stateArg) => stateArg)
|
|
47
|
+
.subscribe((logState) => {
|
|
48
|
+
this.logState = logState;
|
|
49
|
+
});
|
|
50
|
+
this.rxSubscriptions.push(logSub);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async connectedCallback() {
|
|
54
|
+
super.connectedCallback();
|
|
55
|
+
// Ensure logs are fetched for the overview charts
|
|
56
|
+
if (this.logState.recentLogs.length === 0) {
|
|
57
|
+
appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
|
|
58
|
+
}
|
|
37
59
|
}
|
|
38
60
|
|
|
39
61
|
public static styles = [
|
|
@@ -96,10 +118,24 @@ export class OpsViewOverview extends DeesElement {
|
|
|
96
118
|
${this.renderDnsStats()}
|
|
97
119
|
|
|
98
120
|
<div class="chartGrid">
|
|
99
|
-
<dees-chart-area
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
<dees-chart-area
|
|
122
|
+
.label=${'Email Traffic (24h)'}
|
|
123
|
+
.series=${this.getEmailTrafficSeries()}
|
|
124
|
+
.yAxisFormatter=${(val: number) => `${val}`}
|
|
125
|
+
></dees-chart-area>
|
|
126
|
+
<dees-chart-area
|
|
127
|
+
.label=${'DNS Queries (24h)'}
|
|
128
|
+
.series=${this.getDnsQuerySeries()}
|
|
129
|
+
.yAxisFormatter=${(val: number) => `${val}`}
|
|
130
|
+
></dees-chart-area>
|
|
131
|
+
<dees-chart-log
|
|
132
|
+
.label=${'Recent Events'}
|
|
133
|
+
.logEntries=${this.getRecentEventEntries()}
|
|
134
|
+
></dees-chart-log>
|
|
135
|
+
<dees-chart-log
|
|
136
|
+
.label=${'Security Alerts'}
|
|
137
|
+
.logEntries=${this.getSecurityAlertEntries()}
|
|
138
|
+
></dees-chart-log>
|
|
103
139
|
</div>
|
|
104
140
|
`}
|
|
105
141
|
`;
|
|
@@ -337,4 +373,42 @@ export class OpsViewOverview extends DeesElement {
|
|
|
337
373
|
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
|
338
374
|
`;
|
|
339
375
|
}
|
|
376
|
+
|
|
377
|
+
// --- Chart data helpers ---
|
|
378
|
+
|
|
379
|
+
private getRecentEventEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
|
380
|
+
return this.logState.recentLogs.map((log) => ({
|
|
381
|
+
timestamp: new Date(log.timestamp).toISOString(),
|
|
382
|
+
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
|
383
|
+
message: log.message,
|
|
384
|
+
source: log.category,
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private getSecurityAlertEntries(): Array<{ timestamp: string; level: 'debug' | 'info' | 'warn' | 'error' | 'success'; message: string; source?: string }> {
|
|
389
|
+
const events: any[] = this.statsState.securityMetrics?.recentEvents || [];
|
|
390
|
+
return events.map((evt: any) => ({
|
|
391
|
+
timestamp: new Date(evt.timestamp).toISOString(),
|
|
392
|
+
level: evt.level === 'critical' || evt.level === 'error' ? 'error' as const : evt.level === 'warn' ? 'warn' as const : 'info' as const,
|
|
393
|
+
message: evt.message,
|
|
394
|
+
source: evt.type,
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private getEmailTrafficSeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
|
399
|
+
const ts = this.statsState.emailStats?.timeSeries;
|
|
400
|
+
if (!ts) return [];
|
|
401
|
+
return [
|
|
402
|
+
{ name: 'Sent', color: '#22c55e', data: (ts.sent || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
403
|
+
{ name: 'Received', color: '#3b82f6', data: (ts.received || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private getDnsQuerySeries(): Array<{ name: string; color: string; data: Array<{ x: number; y: number }> }> {
|
|
408
|
+
const ts = this.statsState.dnsStats?.timeSeries;
|
|
409
|
+
if (!ts) return [];
|
|
410
|
+
return [
|
|
411
|
+
{ name: 'Queries', color: '#8b5cf6', data: (ts.queries || []).map((p: any) => ({ x: p.timestamp, y: p.value })) },
|
|
412
|
+
];
|
|
413
|
+
}
|
|
340
414
|
}
|
|
@@ -249,7 +249,14 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
249
249
|
private renderOverview(metrics: any) {
|
|
250
250
|
const threatLevel = this.calculateThreatLevel(metrics);
|
|
251
251
|
const threatScore = this.getThreatScore(metrics);
|
|
252
|
-
|
|
252
|
+
|
|
253
|
+
// Derive active sessions from recent successful auth events (last hour)
|
|
254
|
+
const allEvents: any[] = metrics.recentEvents || [];
|
|
255
|
+
const oneHourAgo = Date.now() - 3600000;
|
|
256
|
+
const recentAuthSuccesses = allEvents.filter(
|
|
257
|
+
(evt: any) => evt.type === 'authentication' && evt.success === true && evt.timestamp >= oneHourAgo
|
|
258
|
+
).length;
|
|
259
|
+
|
|
253
260
|
const tiles: IStatsTile[] = [
|
|
254
261
|
{
|
|
255
262
|
id: 'threatLevel',
|
|
@@ -271,7 +278,7 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
271
278
|
{
|
|
272
279
|
id: 'blockedThreats',
|
|
273
280
|
title: 'Blocked Threats',
|
|
274
|
-
value: metrics.blockedIPs
|
|
281
|
+
value: (metrics.blockedIPs?.length || 0) + metrics.spamDetected,
|
|
275
282
|
type: 'number',
|
|
276
283
|
icon: 'lucide:ShieldCheck',
|
|
277
284
|
color: '#ef4444',
|
|
@@ -280,11 +287,11 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
280
287
|
{
|
|
281
288
|
id: 'activeSessions',
|
|
282
289
|
title: 'Active Sessions',
|
|
283
|
-
value:
|
|
290
|
+
value: recentAuthSuccesses,
|
|
284
291
|
type: 'number',
|
|
285
292
|
icon: 'lucide:Users',
|
|
286
293
|
color: '#22c55e',
|
|
287
|
-
description: '
|
|
294
|
+
description: 'Authenticated in last hour',
|
|
288
295
|
},
|
|
289
296
|
{
|
|
290
297
|
id: 'authFailures',
|
|
@@ -349,6 +356,11 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
349
356
|
}
|
|
350
357
|
|
|
351
358
|
private renderAuthentication(metrics: any) {
|
|
359
|
+
// Derive auth events from recentEvents
|
|
360
|
+
const allEvents: any[] = metrics.recentEvents || [];
|
|
361
|
+
const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
|
|
362
|
+
const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
|
|
363
|
+
|
|
352
364
|
const tiles: IStatsTile[] = [
|
|
353
365
|
{
|
|
354
366
|
id: 'authFailures',
|
|
@@ -362,7 +374,7 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
362
374
|
{
|
|
363
375
|
id: 'successfulLogins',
|
|
364
376
|
title: 'Successful Logins',
|
|
365
|
-
value:
|
|
377
|
+
value: successfulLogins,
|
|
366
378
|
type: 'number',
|
|
367
379
|
icon: 'lucide:Lock',
|
|
368
380
|
color: '#22c55e',
|
|
@@ -370,6 +382,15 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
370
382
|
},
|
|
371
383
|
];
|
|
372
384
|
|
|
385
|
+
// Map auth events to login history table data
|
|
386
|
+
const loginHistory = authEvents.map((evt: any) => ({
|
|
387
|
+
timestamp: evt.timestamp,
|
|
388
|
+
username: evt.details?.username || 'unknown',
|
|
389
|
+
ipAddress: evt.ipAddress || 'unknown',
|
|
390
|
+
success: evt.success ?? false,
|
|
391
|
+
reason: evt.success ? '' : evt.message || 'Authentication failed',
|
|
392
|
+
}));
|
|
393
|
+
|
|
373
394
|
return html`
|
|
374
395
|
<dees-statsgrid
|
|
375
396
|
.tiles=${tiles}
|
|
@@ -380,7 +401,7 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
380
401
|
<dees-table
|
|
381
402
|
.heading1=${'Login History'}
|
|
382
403
|
.heading2=${'Recent authentication attempts'}
|
|
383
|
-
.data=${
|
|
404
|
+
.data=${loginHistory}
|
|
384
405
|
.displayFunction=${(item) => ({
|
|
385
406
|
'Time': new Date(item.timestamp).toLocaleString(),
|
|
386
407
|
'Username': item.username,
|
|
@@ -483,48 +504,38 @@ export class OpsViewSecurity extends DeesElement {
|
|
|
483
504
|
private getThreatScore(metrics: any): number {
|
|
484
505
|
// Simple scoring algorithm
|
|
485
506
|
let score = 100;
|
|
486
|
-
|
|
487
|
-
score -=
|
|
488
|
-
score -= metrics.
|
|
489
|
-
score -= metrics.
|
|
490
|
-
score -= metrics.
|
|
491
|
-
score -= metrics.
|
|
507
|
+
const blockedCount = Array.isArray(metrics.blockedIPs) ? metrics.blockedIPs.length : (metrics.blockedIPs || 0);
|
|
508
|
+
score -= blockedCount * 2;
|
|
509
|
+
score -= (metrics.authenticationFailures || 0) * 1;
|
|
510
|
+
score -= (metrics.spamDetected || 0) * 0.5;
|
|
511
|
+
score -= (metrics.malwareDetected || 0) * 3;
|
|
512
|
+
score -= (metrics.phishingDetected || 0) * 3;
|
|
513
|
+
score -= (metrics.suspiciousActivities || 0) * 2;
|
|
492
514
|
return Math.max(0, Math.min(100, Math.round(score)));
|
|
493
515
|
}
|
|
494
516
|
|
|
495
517
|
private getSecurityEvents(metrics: any): any[] {
|
|
496
|
-
|
|
497
|
-
return
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
timestamp: Date.now() - 1000 * 60 * 15,
|
|
506
|
-
event: 'SPF check failed',
|
|
507
|
-
severity: 'medium',
|
|
508
|
-
details: 'Domain: example.com',
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
timestamp: Date.now() - 1000 * 60 * 30,
|
|
512
|
-
event: 'IP blocked due to spam',
|
|
513
|
-
severity: 'high',
|
|
514
|
-
details: 'IP: 10.0.0.1',
|
|
515
|
-
},
|
|
516
|
-
];
|
|
518
|
+
const events: any[] = metrics.recentEvents || [];
|
|
519
|
+
return events.map((evt: any) => ({
|
|
520
|
+
timestamp: evt.timestamp,
|
|
521
|
+
event: evt.message,
|
|
522
|
+
severity: evt.level === 'critical' ? 'critical' : evt.level === 'error' ? 'high' : evt.level === 'warn' ? 'warning' : 'info',
|
|
523
|
+
details: evt.ipAddress ? `IP: ${evt.ipAddress}` : evt.domain ? `Domain: ${evt.domain}` : evt.type,
|
|
524
|
+
}));
|
|
517
525
|
}
|
|
518
526
|
|
|
519
527
|
private async clearBlockedIPs() {
|
|
520
|
-
|
|
528
|
+
// SmartProxy manages IP blocking — not yet exposed via API
|
|
529
|
+
alert('Clearing blocked IPs is not yet supported from the UI.');
|
|
521
530
|
}
|
|
522
531
|
|
|
523
532
|
private async unblockIP(ip: string) {
|
|
524
|
-
|
|
533
|
+
// SmartProxy manages IP blocking — not yet exposed via API
|
|
534
|
+
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
|
|
525
535
|
}
|
|
526
536
|
|
|
527
537
|
private async saveEmailSecuritySettings() {
|
|
528
|
-
|
|
538
|
+
// Config is read-only from the UI for now
|
|
539
|
+
alert('Email security settings are read-only. Update the dcrouter configuration file to change these settings.');
|
|
529
540
|
}
|
|
530
541
|
}
|