@serve.zone/dcrouter 13.32.0 → 13.33.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 +588 -588
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
- package/dist_ts/config/classes.route-config-manager.js +28 -3
- package/dist_ts/config/classes.target-profile-manager.d.ts +1 -0
- package/dist_ts/config/classes.target-profile-manager.js +11 -8
- package/dist_ts/monitoring/classes.metricsmanager.js +5 -2
- package/dist_ts/opsserver/handlers/admin.handler.d.ts +2 -1
- package/dist_ts/opsserver/handlers/admin.handler.js +38 -23
- package/dist_ts/opsserver/handlers/security.handler.js +9 -2
- package/dist_ts/security/classes.security-policy-manager.d.ts +15 -1
- package/dist_ts/security/classes.security-policy-manager.js +108 -9
- package/dist_ts_interfaces/requests/security-policy.d.ts +2 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.js +75 -47
- package/package.json +4 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.route-config-manager.ts +35 -2
- package/ts/config/classes.target-profile-manager.ts +10 -7
- package/ts/monitoring/classes.metricsmanager.ts +4 -1
- package/ts/opsserver/handlers/admin.handler.ts +45 -22
- package/ts/opsserver/handlers/security.handler.ts +8 -1
- package/ts/security/classes.security-policy-manager.ts +115 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +82 -48
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/dcrouter",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "13.
|
|
4
|
+
"version": "13.33.0",
|
|
5
5
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@push.rocks/qenv": "^6.1.4",
|
|
35
35
|
"@push.rocks/smartacme": "^9.5.0",
|
|
36
36
|
"@push.rocks/smartdata": "^7.1.7",
|
|
37
|
-
"@push.rocks/smartdb": "^2.10.
|
|
37
|
+
"@push.rocks/smartdb": "^2.10.1",
|
|
38
38
|
"@push.rocks/smartdns": "^7.9.2",
|
|
39
39
|
"@push.rocks/smartfs": "^1.5.1",
|
|
40
40
|
"@push.rocks/smartguard": "^3.1.0",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"@push.rocks/smartmetrics": "^3.0.3",
|
|
44
44
|
"@push.rocks/smartmigration": "1.4.1",
|
|
45
45
|
"@push.rocks/smartmta": "^5.3.3",
|
|
46
|
-
"@push.rocks/smartnetwork": "^4.7.
|
|
46
|
+
"@push.rocks/smartnetwork": "^4.7.2",
|
|
47
47
|
"@push.rocks/smartpath": "^6.0.0",
|
|
48
48
|
"@push.rocks/smartpromise": "^4.2.4",
|
|
49
|
-
"@push.rocks/smartproxy": "^27.10.
|
|
49
|
+
"@push.rocks/smartproxy": "^27.10.3",
|
|
50
50
|
"@push.rocks/smartradius": "^1.1.2",
|
|
51
51
|
"@push.rocks/smartrequest": "^5.0.3",
|
|
52
52
|
"@push.rocks/smartrx": "^3.0.10",
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -608,9 +608,23 @@ export class RouteConfigManager {
|
|
|
608
608
|
routeId?: string,
|
|
609
609
|
): plugins.smartproxy.IRouteConfig {
|
|
610
610
|
const dcRoute = route as IDcRouterRouteConfig;
|
|
611
|
-
if (!dcRoute.vpnOnly) return route;
|
|
612
|
-
|
|
613
611
|
const vpnEntries = this.getVpnClientIpsForRoute?.(dcRoute, routeId) || [];
|
|
612
|
+
|
|
613
|
+
if (!dcRoute.vpnOnly) {
|
|
614
|
+
const existingAllowList = route.security?.ipAllowList;
|
|
615
|
+
if (!Array.isArray(existingAllowList) || existingAllowList.length === 0 || vpnEntries.length === 0) {
|
|
616
|
+
return route;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
...route,
|
|
621
|
+
security: {
|
|
622
|
+
...route.security,
|
|
623
|
+
ipAllowList: this.mergeIpAllowEntries(existingAllowList as TIpAllowEntry[], vpnEntries),
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
614
628
|
const existingBlockList = route.security?.ipBlockList || [];
|
|
615
629
|
const ipBlockList = vpnEntries.length
|
|
616
630
|
? existingBlockList
|
|
@@ -625,4 +639,23 @@ export class RouteConfigManager {
|
|
|
625
639
|
},
|
|
626
640
|
};
|
|
627
641
|
}
|
|
642
|
+
|
|
643
|
+
private mergeIpAllowEntries(
|
|
644
|
+
existingEntries: TIpAllowEntry[],
|
|
645
|
+
vpnEntries: TIpAllowEntry[],
|
|
646
|
+
): TIpAllowEntry[] {
|
|
647
|
+
const merged: TIpAllowEntry[] = [];
|
|
648
|
+
const seen = new Set<string>();
|
|
649
|
+
|
|
650
|
+
for (const entry of [...existingEntries, ...vpnEntries]) {
|
|
651
|
+
const key = typeof entry === 'string'
|
|
652
|
+
? `ip:${entry}`
|
|
653
|
+
: `domain:${entry.ip}:${[...entry.domains].sort().join(',')}`;
|
|
654
|
+
if (seen.has(key)) continue;
|
|
655
|
+
seen.add(key);
|
|
656
|
+
merged.push(entry);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return merged;
|
|
660
|
+
}
|
|
628
661
|
}
|
|
@@ -217,7 +217,7 @@ export class TargetProfileManager {
|
|
|
217
217
|
allRoutes: Map<string, IRoute> = new Map(),
|
|
218
218
|
): Array<string | { ip: string; domains: string[] }> {
|
|
219
219
|
const entries: Array<string | { ip: string; domains: string[] }> = [];
|
|
220
|
-
const routeDomains
|
|
220
|
+
const routeDomains = this.getRouteDomains(route);
|
|
221
221
|
const routeNameIndex = this.buildRouteNameIndex(allRoutes);
|
|
222
222
|
|
|
223
223
|
for (const client of clients) {
|
|
@@ -298,11 +298,8 @@ export class TargetProfileManager {
|
|
|
298
298
|
profile,
|
|
299
299
|
routeNameIndex,
|
|
300
300
|
)) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
for (const d of routeDomains) {
|
|
304
|
-
domains.add(d);
|
|
305
|
-
}
|
|
301
|
+
for (const d of this.getRouteDomains(route.route as IDcRouterRouteConfig)) {
|
|
302
|
+
domains.add(d);
|
|
306
303
|
}
|
|
307
304
|
}
|
|
308
305
|
}
|
|
@@ -327,7 +324,7 @@ export class TargetProfileManager {
|
|
|
327
324
|
profile: ITargetProfile,
|
|
328
325
|
routeNameIndex: Map<string, string[]>,
|
|
329
326
|
): boolean {
|
|
330
|
-
const routeDomains
|
|
327
|
+
const routeDomains = this.getRouteDomains(route);
|
|
331
328
|
const result = this.routeMatchesProfileDetailed(
|
|
332
329
|
route,
|
|
333
330
|
routeId,
|
|
@@ -425,6 +422,12 @@ export class TargetProfileManager {
|
|
|
425
422
|
return false;
|
|
426
423
|
}
|
|
427
424
|
|
|
425
|
+
private getRouteDomains(route: IDcRouterRouteConfig): string[] {
|
|
426
|
+
const domains = (route.match as any)?.domains;
|
|
427
|
+
if (!domains) return [];
|
|
428
|
+
return Array.isArray(domains) ? domains : [domains];
|
|
429
|
+
}
|
|
430
|
+
|
|
428
431
|
private normalizeRouteRefs(routeRefs?: string[]): string[] | undefined {
|
|
429
432
|
const allRoutes = this.getAllRoutes?.() || new Map<string, IRoute>();
|
|
430
433
|
return this.normalizeRouteRefsAgainstRoutes(routeRefs, allRoutes, 'strict');
|
|
@@ -725,7 +725,10 @@ export class MetricsManager {
|
|
|
725
725
|
.slice(0, 10)
|
|
726
726
|
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
|
|
727
727
|
|
|
728
|
-
|
|
728
|
+
this.dcRouter.securityPolicyManager?.queueObservedIps([
|
|
729
|
+
...topIPs.map((item) => item.ip),
|
|
730
|
+
...topIPsByBandwidth.map((item) => item.ip),
|
|
731
|
+
]);
|
|
729
732
|
|
|
730
733
|
// Build domain activity using per-IP domain request counts from Rust engine
|
|
731
734
|
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
|
@@ -24,7 +24,8 @@ export class AdminHandler {
|
|
|
24
24
|
// JWT instance
|
|
25
25
|
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
|
|
26
26
|
|
|
27
|
-
// Ephemeral bootstrap users.
|
|
27
|
+
// Ephemeral bootstrap users. DB-backed instances may use these only until the
|
|
28
|
+
// database is ready and the first persistent admin account has been created.
|
|
28
29
|
private users = new Map<string, {
|
|
29
30
|
id: string;
|
|
30
31
|
username: string;
|
|
@@ -87,9 +88,12 @@ export class AdminHandler {
|
|
|
87
88
|
* Used by UsersHandler to serve the admin-only listUsers endpoint.
|
|
88
89
|
*/
|
|
89
90
|
public async listUsers(): Promise<interfaces.requests.IAdminUserProjection[]> {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const accountState = await this.getPersistentAccountState();
|
|
92
|
+
if (accountState.dbEnabled && !accountState.dbReady) {
|
|
93
|
+
throw new plugins.typedrequest.TypedResponseError('database is not ready');
|
|
94
|
+
}
|
|
95
|
+
if (accountState.hasPersistentAdmin) {
|
|
96
|
+
const accounts = await accountState.store!.listAccounts();
|
|
93
97
|
return accounts.map((accountArg) => this.accountToUser(accountArg));
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -101,16 +105,14 @@ export class AdminHandler {
|
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
public async getBootstrapStatus(): Promise<interfaces.requests.IReq_GetAdminBootstrapStatus['response']> {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const dbReady = !!store;
|
|
107
|
-
const hasPersistentAdmin = dbReady ? await store.hasActiveAdminAccount() : false;
|
|
108
|
+
const accountState = await this.getPersistentAccountState();
|
|
109
|
+
const bootstrapAvailable = !accountState.dbEnabled || (accountState.dbReady && !accountState.hasPersistentAdmin);
|
|
108
110
|
return {
|
|
109
|
-
dbEnabled,
|
|
110
|
-
dbReady,
|
|
111
|
-
hasPersistentAdmin,
|
|
112
|
-
needsBootstrap: dbEnabled && dbReady && !hasPersistentAdmin,
|
|
113
|
-
ephemeralAdminAvailable:
|
|
111
|
+
dbEnabled: accountState.dbEnabled,
|
|
112
|
+
dbReady: accountState.dbReady,
|
|
113
|
+
hasPersistentAdmin: accountState.hasPersistentAdmin,
|
|
114
|
+
needsBootstrap: accountState.dbEnabled && accountState.dbReady && !accountState.hasPersistentAdmin,
|
|
115
|
+
ephemeralAdminAvailable: bootstrapAvailable,
|
|
114
116
|
idpGlobalConfigured: this.isIdpGlobalConfigured(),
|
|
115
117
|
};
|
|
116
118
|
}
|
|
@@ -408,10 +410,14 @@ export class AdminHandler {
|
|
|
408
410
|
password: string;
|
|
409
411
|
authSource?: interfaces.requests.TAdminLoginAuthSource;
|
|
410
412
|
}): Promise<TAdminUser | null> {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
+
const accountState = await this.getPersistentAccountState();
|
|
414
|
+
if (accountState.dbEnabled && !accountState.dbReady) {
|
|
415
|
+
throw new plugins.typedrequest.TypedResponseError('database is not ready');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (accountState.hasPersistentAdmin) {
|
|
413
419
|
const authService = new plugins.idpSdkServer.AccountAuthService({
|
|
414
|
-
store: store!,
|
|
420
|
+
store: accountState.store!,
|
|
415
421
|
idpClient: this.getIdpClient() as plugins.idpSdkServer.IdpGlobalServerClient | undefined,
|
|
416
422
|
});
|
|
417
423
|
const result = await authService.authenticate({
|
|
@@ -431,8 +437,13 @@ export class AdminHandler {
|
|
|
431
437
|
}
|
|
432
438
|
|
|
433
439
|
private async resolveUser(userIdArg: string): Promise<TAdminUser | null> {
|
|
434
|
-
|
|
435
|
-
|
|
440
|
+
const accountState = await this.getPersistentAccountState();
|
|
441
|
+
if (accountState.dbEnabled && !accountState.dbReady) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (accountState.hasPersistentAdmin) {
|
|
446
|
+
const account = await accountState.store!.getAccountById(userIdArg);
|
|
436
447
|
if (!account || account.status !== 'active') {
|
|
437
448
|
return null;
|
|
438
449
|
}
|
|
@@ -442,13 +453,25 @@ export class AdminHandler {
|
|
|
442
453
|
return this.users.get(userIdArg) || null;
|
|
443
454
|
}
|
|
444
455
|
|
|
445
|
-
private async
|
|
446
|
-
|
|
447
|
-
|
|
456
|
+
private async getPersistentAccountState(): Promise<{
|
|
457
|
+
dbEnabled: boolean;
|
|
458
|
+
dbReady: boolean;
|
|
459
|
+
store: plugins.idpSdkServer.SmartdataAccountStore | null;
|
|
460
|
+
hasPersistentAdmin: boolean;
|
|
461
|
+
}> {
|
|
462
|
+
const dbEnabled = this.isPersistenceEnabled();
|
|
463
|
+
const store = dbEnabled ? this.getAccountStore() : null;
|
|
464
|
+
const dbReady = !!store;
|
|
465
|
+
const hasPersistentAdmin = store ? await store.hasActiveAdminAccount() : false;
|
|
466
|
+
return { dbEnabled, dbReady, store, hasPersistentAdmin };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private isPersistenceEnabled(): boolean {
|
|
470
|
+
return this.opsServerRef.dcRouterRef.options.dbConfig?.enabled !== false;
|
|
448
471
|
}
|
|
449
472
|
|
|
450
473
|
private getAccountStore(): plugins.idpSdkServer.SmartdataAccountStore | null {
|
|
451
|
-
if (this.
|
|
474
|
+
if (!this.isPersistenceEnabled()) {
|
|
452
475
|
return null;
|
|
453
476
|
}
|
|
454
477
|
const dcRouterDb = this.opsServerRef.dcRouterRef.dcRouterDb;
|
|
@@ -180,7 +180,14 @@ export class SecurityHandler {
|
|
|
180
180
|
async (dataArg) => {
|
|
181
181
|
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
|
|
182
182
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
|
183
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
records: manager
|
|
185
|
+
? await manager.listIpIntelligence({
|
|
186
|
+
ipAddresses: dataArg.ipAddresses,
|
|
187
|
+
limit: dataArg.limit,
|
|
188
|
+
})
|
|
189
|
+
: [],
|
|
190
|
+
};
|
|
184
191
|
},
|
|
185
192
|
),
|
|
186
193
|
);
|
|
@@ -19,12 +19,24 @@ export interface IRemoteIngressFirewallSnapshot {
|
|
|
19
19
|
blockedIps: string[];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const OBSERVED_IP_QUEUE_LIMIT = 512;
|
|
23
|
+
const OBSERVED_IP_BATCH_LIMIT = 20;
|
|
24
|
+
const OBSERVED_IP_QUEUE_CONCURRENCY = 2;
|
|
25
|
+
const OBSERVED_IP_REQUEUE_THROTTLE_MS = 60_000;
|
|
26
|
+
|
|
22
27
|
export class SecurityPolicyManager {
|
|
23
28
|
private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
|
|
24
29
|
cacheTtl: 24 * 60 * 60 * 1000,
|
|
30
|
+
ipIntelligenceTimeout: 5_000,
|
|
25
31
|
});
|
|
26
32
|
private readonly intelligenceRefreshMs: number;
|
|
27
|
-
private readonly inFlightObservations = new
|
|
33
|
+
private readonly inFlightObservations = new Map<string, Promise<void>>();
|
|
34
|
+
private readonly queuedObservations = new Set<string>();
|
|
35
|
+
private readonly observationQueue: string[] = [];
|
|
36
|
+
private readonly lastQueuedAt = new Map<string, number>();
|
|
37
|
+
private activeQueuedObservations = 0;
|
|
38
|
+
private queueDrainScheduled = false;
|
|
39
|
+
private isStopping = false;
|
|
28
40
|
private readonly onPolicyChanged?: () => void | Promise<void>;
|
|
29
41
|
|
|
30
42
|
constructor(options: ISecurityPolicyManagerOptions = {}) {
|
|
@@ -37,6 +49,9 @@ export class SecurityPolicyManager {
|
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
public async stop(): Promise<void> {
|
|
52
|
+
this.isStopping = true;
|
|
53
|
+
this.observationQueue.length = 0;
|
|
54
|
+
this.queuedObservations.clear();
|
|
40
55
|
await this.smartNetwork.stop();
|
|
41
56
|
}
|
|
42
57
|
|
|
@@ -45,13 +60,55 @@ export class SecurityPolicyManager {
|
|
|
45
60
|
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
|
|
46
61
|
}
|
|
47
62
|
|
|
63
|
+
public queueObservedIps(ips: string[]): void {
|
|
64
|
+
if (this.isStopping) return;
|
|
65
|
+
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
|
|
68
|
+
|
|
69
|
+
for (const ip of uniqueIps.slice(0, OBSERVED_IP_BATCH_LIMIT)) {
|
|
70
|
+
if (!this.isPublicIp(ip)) continue;
|
|
71
|
+
if (this.inFlightObservations.has(ip) || this.queuedObservations.has(ip)) continue;
|
|
72
|
+
|
|
73
|
+
const lastQueuedAt = this.lastQueuedAt.get(ip);
|
|
74
|
+
if (lastQueuedAt && now - lastQueuedAt < OBSERVED_IP_REQUEUE_THROTTLE_MS) continue;
|
|
75
|
+
|
|
76
|
+
if (this.observationQueue.length >= OBSERVED_IP_QUEUE_LIMIT) {
|
|
77
|
+
const droppedIp = this.observationQueue.shift();
|
|
78
|
+
if (droppedIp) this.queuedObservations.delete(droppedIp);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.observationQueue.push(ip);
|
|
82
|
+
this.queuedObservations.add(ip);
|
|
83
|
+
this.lastQueuedAt.set(ip, now);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.pruneQueuedIpMemory(now);
|
|
87
|
+
this.scheduleQueueDrain();
|
|
88
|
+
}
|
|
89
|
+
|
|
48
90
|
public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
|
|
49
91
|
const ip = this.normalizeIp(ipAddress);
|
|
50
|
-
if (!ip || !this.isPublicIp(ip)
|
|
92
|
+
if (!ip || !this.isPublicIp(ip)) {
|
|
51
93
|
return;
|
|
52
94
|
}
|
|
53
95
|
|
|
54
|
-
this.inFlightObservations.
|
|
96
|
+
const existingObservation = this.inFlightObservations.get(ip);
|
|
97
|
+
if (existingObservation) {
|
|
98
|
+
await existingObservation;
|
|
99
|
+
if (!options.force) return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const observationPromise = this.performObserveIp(ip, options).finally(() => {
|
|
103
|
+
if (this.inFlightObservations.get(ip) === observationPromise) {
|
|
104
|
+
this.inFlightObservations.delete(ip);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
this.inFlightObservations.set(ip, observationPromise);
|
|
108
|
+
await observationPromise;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async performObserveIp(ip: string, options: { force?: boolean } = {}): Promise<void> {
|
|
55
112
|
try {
|
|
56
113
|
const now = Date.now();
|
|
57
114
|
let doc = await IpIntelligenceDoc.findByIp(ip);
|
|
@@ -81,8 +138,6 @@ export class SecurityPolicyManager {
|
|
|
81
138
|
}
|
|
82
139
|
} catch (err) {
|
|
83
140
|
logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
|
|
84
|
-
} finally {
|
|
85
|
-
this.inFlightObservations.delete(ip);
|
|
86
141
|
}
|
|
87
142
|
}
|
|
88
143
|
|
|
@@ -90,8 +145,22 @@ export class SecurityPolicyManager {
|
|
|
90
145
|
return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
|
|
91
146
|
}
|
|
92
147
|
|
|
93
|
-
public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
|
|
94
|
-
|
|
148
|
+
public async listIpIntelligence(options: { ipAddresses?: string[]; limit?: number } = {}): Promise<IIpIntelligenceRecord[]> {
|
|
149
|
+
const limit = Number.isInteger(options.limit) && options.limit! > 0
|
|
150
|
+
? Math.min(options.limit!, 500)
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
let docs: IpIntelligenceDoc[];
|
|
154
|
+
if (options.ipAddresses?.length) {
|
|
155
|
+
const ips = [...new Set(options.ipAddresses.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
|
|
156
|
+
const results = await Promise.all(ips.map((ip) => IpIntelligenceDoc.findByIp(ip)));
|
|
157
|
+
docs = results.filter(Boolean) as IpIntelligenceDoc[];
|
|
158
|
+
} else {
|
|
159
|
+
docs = await IpIntelligenceDoc.findAll();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const sortedDocs = docs.sort((a, b) => (b.lastSeenAt || 0) - (a.lastSeenAt || 0));
|
|
163
|
+
return (limit ? sortedDocs.slice(0, limit) : sortedDocs).map((doc) => this.intelligenceFromDoc(doc));
|
|
95
164
|
}
|
|
96
165
|
|
|
97
166
|
public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
|
|
@@ -104,6 +173,45 @@ export class SecurityPolicyManager {
|
|
|
104
173
|
return doc ? this.intelligenceFromDoc(doc) : null;
|
|
105
174
|
}
|
|
106
175
|
|
|
176
|
+
private scheduleQueueDrain(): void {
|
|
177
|
+
if (this.queueDrainScheduled || this.isStopping) return;
|
|
178
|
+
this.queueDrainScheduled = true;
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
this.queueDrainScheduled = false;
|
|
181
|
+
this.drainObservationQueue();
|
|
182
|
+
}, 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private drainObservationQueue(): void {
|
|
186
|
+
if (this.isStopping) return;
|
|
187
|
+
|
|
188
|
+
while (
|
|
189
|
+
this.activeQueuedObservations < OBSERVED_IP_QUEUE_CONCURRENCY &&
|
|
190
|
+
this.observationQueue.length > 0
|
|
191
|
+
) {
|
|
192
|
+
const ip = this.observationQueue.shift()!;
|
|
193
|
+
this.queuedObservations.delete(ip);
|
|
194
|
+
this.activeQueuedObservations++;
|
|
195
|
+
void this.observeIp(ip)
|
|
196
|
+
.catch(() => undefined)
|
|
197
|
+
.finally(() => {
|
|
198
|
+
this.activeQueuedObservations--;
|
|
199
|
+
if (this.observationQueue.length > 0) {
|
|
200
|
+
this.scheduleQueueDrain();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private pruneQueuedIpMemory(now: number): void {
|
|
207
|
+
if (this.lastQueuedAt.size <= OBSERVED_IP_QUEUE_LIMIT * 2) return;
|
|
208
|
+
for (const [ip, lastQueuedAt] of this.lastQueuedAt) {
|
|
209
|
+
if (now - lastQueuedAt > OBSERVED_IP_REQUEUE_THROTTLE_MS * 2) {
|
|
210
|
+
this.lastQueuedAt.delete(ip);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
107
215
|
public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
|
|
108
216
|
return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
|
|
109
217
|
id: doc.id,
|
package/ts_web/appstate.ts
CHANGED
|
@@ -582,6 +582,52 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|
|
582
582
|
};
|
|
583
583
|
});
|
|
584
584
|
|
|
585
|
+
const backgroundRefreshesInFlight = new Set<string>();
|
|
586
|
+
|
|
587
|
+
function runBackgroundRefresh(key: string, errorMessage: string, task: () => Promise<void>): void {
|
|
588
|
+
if (backgroundRefreshesInFlight.has(key)) return;
|
|
589
|
+
backgroundRefreshesInFlight.add(key);
|
|
590
|
+
void task()
|
|
591
|
+
.catch((error) => console.error(errorMessage, error))
|
|
592
|
+
.finally(() => backgroundRefreshesInFlight.delete(key));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function refreshNetworkIpIntelligence(identity: interfaces.data.IIdentity, ipAddresses: string[]): void {
|
|
596
|
+
const ips = [...new Set(ipAddresses.filter(Boolean))].slice(0, 100);
|
|
597
|
+
if (ips.length === 0) return;
|
|
598
|
+
|
|
599
|
+
runBackgroundRefresh('networkIpIntelligence', 'IP intelligence refresh failed:', async () => {
|
|
600
|
+
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
601
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
602
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
603
|
+
const intelligenceResponse = await intelligenceRequest.fire({
|
|
604
|
+
identity,
|
|
605
|
+
ipAddresses: ips,
|
|
606
|
+
limit: Math.max(100, ips.length),
|
|
607
|
+
});
|
|
608
|
+
networkStatePart.setState({
|
|
609
|
+
...networkStatePart.getState()!,
|
|
610
|
+
ipIntelligence: intelligenceResponse.records || [],
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function refreshSecurityIpIntelligence(identity: interfaces.data.IIdentity): void {
|
|
616
|
+
runBackgroundRefresh('securityIpIntelligence', 'Security IP intelligence refresh failed:', async () => {
|
|
617
|
+
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
618
|
+
interfaces.requests.IReq_ListIpIntelligence
|
|
619
|
+
>('/typedrequest', 'listIpIntelligence');
|
|
620
|
+
const intelligenceResponse = await intelligenceRequest.fire({
|
|
621
|
+
identity,
|
|
622
|
+
limit: 500,
|
|
623
|
+
});
|
|
624
|
+
securityPolicyStatePart.setState({
|
|
625
|
+
...securityPolicyStatePart.getState()!,
|
|
626
|
+
ipIntelligence: intelligenceResponse.records || [],
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
585
631
|
// Fetch Network Stats Action
|
|
586
632
|
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise<INetworkState> => {
|
|
587
633
|
const context = getActionContext();
|
|
@@ -594,18 +640,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
594
640
|
interfaces.requests.IReq_GetNetworkStats
|
|
595
641
|
>('/typedrequest', 'getNetworkStats');
|
|
596
642
|
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
|
|
602
|
-
networkStatsRequest.fire({
|
|
603
|
-
identity: context.identity,
|
|
604
|
-
}),
|
|
605
|
-
ipIntelligenceRequest.fire({
|
|
606
|
-
identity: context.identity,
|
|
607
|
-
}),
|
|
608
|
-
]);
|
|
643
|
+
const networkStatsResponse = await networkStatsRequest.fire({
|
|
644
|
+
identity: context.identity,
|
|
645
|
+
});
|
|
609
646
|
|
|
610
647
|
// Use the connections data for the connection list
|
|
611
648
|
// and network stats for throughput and IP analytics
|
|
@@ -637,6 +674,12 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
637
674
|
};
|
|
638
675
|
});
|
|
639
676
|
|
|
677
|
+
refreshNetworkIpIntelligence(context.identity, [
|
|
678
|
+
...Object.keys(connectionsByIP),
|
|
679
|
+
...(networkStatsResponse.topIPs || []).map((item) => item.ip),
|
|
680
|
+
...(networkStatsResponse.topIPsByBandwidth || []).map((item) => item.ip),
|
|
681
|
+
]);
|
|
682
|
+
|
|
640
683
|
return {
|
|
641
684
|
connections,
|
|
642
685
|
connectionsByIP,
|
|
@@ -647,7 +690,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|
|
647
690
|
topIPs: networkStatsResponse.topIPs || [],
|
|
648
691
|
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
|
|
649
692
|
throughputByIP: networkStatsResponse.throughputByIP || [],
|
|
650
|
-
ipIntelligence:
|
|
693
|
+
ipIntelligence: currentState.ipIntelligence,
|
|
651
694
|
domainActivity: networkStatsResponse.domainActivity || [],
|
|
652
695
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
|
653
696
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
|
@@ -683,9 +726,6 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
|
|
683
726
|
const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
684
727
|
interfaces.requests.IReq_ListSecurityBlockRules
|
|
685
728
|
>('/typedrequest', 'listSecurityBlockRules');
|
|
686
|
-
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
687
|
-
interfaces.requests.IReq_ListIpIntelligence
|
|
688
|
-
>('/typedrequest', 'listIpIntelligence');
|
|
689
729
|
const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
690
730
|
interfaces.requests.IReq_GetCompiledSecurityPolicy
|
|
691
731
|
>('/typedrequest', 'getCompiledSecurityPolicy');
|
|
@@ -693,16 +733,17 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
|
|
693
733
|
interfaces.requests.IReq_ListSecurityPolicyAudit
|
|
694
734
|
>('/typedrequest', 'listSecurityPolicyAudit');
|
|
695
735
|
|
|
696
|
-
const [rulesResponse,
|
|
736
|
+
const [rulesResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
|
|
697
737
|
rulesRequest.fire({ identity: context.identity }),
|
|
698
|
-
intelligenceRequest.fire({ identity: context.identity }),
|
|
699
738
|
compiledPolicyRequest.fire({ identity: context.identity }),
|
|
700
739
|
auditRequest.fire({ identity: context.identity, limit: 100 }),
|
|
701
740
|
]);
|
|
702
741
|
|
|
742
|
+
refreshSecurityIpIntelligence(context.identity);
|
|
743
|
+
|
|
703
744
|
return {
|
|
704
745
|
rules: rulesResponse.rules || [],
|
|
705
|
-
ipIntelligence:
|
|
746
|
+
ipIntelligence: currentState.ipIntelligence,
|
|
706
747
|
compiledPolicy: compiledPolicyResponse.policy,
|
|
707
748
|
auditEvents: auditResponse.events || [],
|
|
708
749
|
isLoading: false,
|
|
@@ -835,7 +876,15 @@ export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<
|
|
|
835
876
|
if (!response.success) {
|
|
836
877
|
return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
|
|
837
878
|
}
|
|
838
|
-
|
|
879
|
+
const refreshedState = await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
|
880
|
+
if (!response.record) return refreshedState;
|
|
881
|
+
return {
|
|
882
|
+
...refreshedState,
|
|
883
|
+
ipIntelligence: [
|
|
884
|
+
response.record,
|
|
885
|
+
...refreshedState.ipIntelligence.filter((record) => record.ipAddress !== response.record!.ipAddress),
|
|
886
|
+
],
|
|
887
|
+
};
|
|
839
888
|
} catch (error: unknown) {
|
|
840
889
|
return {
|
|
841
890
|
...currentState,
|
|
@@ -3112,53 +3161,38 @@ async function dispatchCombinedRefreshActionInner() {
|
|
|
3112
3161
|
error: null,
|
|
3113
3162
|
});
|
|
3114
3163
|
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
networkStatePart.setState({
|
|
3121
|
-
...networkStatePart.getState()!,
|
|
3122
|
-
ipIntelligence: intelligenceResponse.records || [],
|
|
3123
|
-
});
|
|
3124
|
-
} catch (error) {
|
|
3125
|
-
console.error('IP intelligence refresh failed:', error);
|
|
3126
|
-
}
|
|
3164
|
+
refreshNetworkIpIntelligence(context.identity, [
|
|
3165
|
+
...network.connectionDetails.map((conn) => conn.remoteAddress),
|
|
3166
|
+
...network.topEndpoints.map((endpoint) => endpoint.endpoint),
|
|
3167
|
+
...(network.topEndpointsByBandwidth || []).map((endpoint) => endpoint.endpoint),
|
|
3168
|
+
]);
|
|
3127
3169
|
}
|
|
3128
3170
|
|
|
3129
3171
|
if (currentView === 'security') {
|
|
3130
|
-
|
|
3172
|
+
runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => {
|
|
3131
3173
|
await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
|
|
3132
|
-
}
|
|
3133
|
-
console.error('Security policy refresh failed:', error);
|
|
3134
|
-
}
|
|
3174
|
+
});
|
|
3135
3175
|
}
|
|
3136
3176
|
|
|
3137
3177
|
// Refresh certificate data if on Domains > Certificates subview
|
|
3138
3178
|
if (currentView === 'domains' && currentSubview === 'certificates') {
|
|
3139
|
-
|
|
3179
|
+
runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => {
|
|
3140
3180
|
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
|
|
3141
|
-
}
|
|
3142
|
-
console.error('Certificate refresh failed:', error);
|
|
3143
|
-
}
|
|
3181
|
+
});
|
|
3144
3182
|
}
|
|
3145
3183
|
|
|
3146
3184
|
// Refresh remote ingress data if on the Network → Remote Ingress subview
|
|
3147
3185
|
if (currentView === 'network' && currentSubview === 'remoteingress') {
|
|
3148
|
-
|
|
3186
|
+
runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => {
|
|
3149
3187
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
|
3150
|
-
}
|
|
3151
|
-
console.error('Remote ingress refresh failed:', error);
|
|
3152
|
-
}
|
|
3188
|
+
});
|
|
3153
3189
|
}
|
|
3154
3190
|
|
|
3155
3191
|
// Refresh VPN data if on the Network → VPN subview
|
|
3156
3192
|
if (currentView === 'network' && currentSubview === 'vpn') {
|
|
3157
|
-
|
|
3193
|
+
runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => {
|
|
3158
3194
|
await vpnStatePart.dispatchAction(fetchVpnAction, null);
|
|
3159
|
-
}
|
|
3160
|
-
console.error('VPN refresh failed:', error);
|
|
3161
|
-
}
|
|
3195
|
+
});
|
|
3162
3196
|
}
|
|
3163
3197
|
} catch (error) {
|
|
3164
3198
|
console.error('Combined refresh failed:', error);
|