diff --git a/changelog.md b/changelog.md index a19acef..5fffbdf 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,14 @@ +### Features + +- add queued IP intelligence observation and filtered retrieval for network and security views (security) + - Queue observed public IPs from network metrics with throttled background enrichment instead of awaiting lookups during stats collection. + - Allow listing IP intelligence records by specific IP addresses and limit through the security handler and request interface. + - Update web app state to refresh IP intelligence asynchronously in the background and preserve current UI state during refreshes. + - Improve security policy manager observation handling so forced refresh waits for in-flight lookups before fetching updated intelligence. + ## 2026-05-20 - 13.32.1 ### Fixes diff --git a/package.json b/package.json index d9a5882..19b80b1 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@push.rocks/smartmetrics": "^3.0.3", "@push.rocks/smartmigration": "1.4.1", "@push.rocks/smartmta": "^5.3.3", - "@push.rocks/smartnetwork": "^4.7.1", + "@push.rocks/smartnetwork": "^4.7.2", "@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpromise": "^4.2.4", "@push.rocks/smartproxy": "^27.10.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d53e132..48956c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,8 +75,8 @@ importers: specifier: ^5.3.3 version: 5.3.3 '@push.rocks/smartnetwork': - specifier: ^4.7.1 - version: 4.7.1 + specifier: ^4.7.2 + version: 4.7.2 '@push.rocks/smartpath': specifier: ^6.0.0 version: 6.0.0 @@ -1395,8 +1395,8 @@ packages: '@push.rocks/smartmustache@3.0.2': resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==} - '@push.rocks/smartnetwork@4.7.1': - resolution: {integrity: sha512-x9SolGn8lU3oh+fKL26dR5dIhsus5f0p/Xiaut2pK5Wamgwrvt5y5To8F+pzF1pQr6yA0XwWZ0Dgoppp2E+ziQ==} + '@push.rocks/smartnetwork@4.7.2': + resolution: {integrity: sha512-OwT8kwQeEO+E3RuCyCfgQEBz+FyydUVaTBivZzzVchdJCUDgoDkXSnRkbIuGoHd1BfRFkUg9DQlSzt0uDfsIbw==} '@push.rocks/smartnftables@1.2.0': resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==} @@ -5306,7 +5306,7 @@ snapshots: '@push.rocks/smartjson': 6.0.1 '@push.rocks/smartlog': 3.2.2 '@push.rocks/smartmongo': 7.0.0(socks@2.8.8) - '@push.rocks/smartnetwork': 4.7.1 + '@push.rocks/smartnetwork': 4.7.2 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartrequest': 5.0.3 @@ -6115,7 +6115,7 @@ snapshots: '@push.rocks/smartdelay': 3.1.0 '@push.rocks/smartdns': 7.9.2 '@push.rocks/smartlog': 3.2.2 - '@push.rocks/smartnetwork': 4.7.1 + '@push.rocks/smartnetwork': 4.7.2 '@push.rocks/smartstring': 4.1.1 '@push.rocks/smarttime': 4.2.3 '@push.rocks/smartunique': 3.0.9 @@ -6591,7 +6591,7 @@ snapshots: dependencies: handlebars: 4.7.9 - '@push.rocks/smartnetwork@4.7.1': + '@push.rocks/smartnetwork@4.7.2': dependencies: '@push.rocks/smartdns': 7.9.2 '@push.rocks/smartrust': 1.4.0 @@ -6654,7 +6654,7 @@ snapshots: '@push.rocks/smartdelay': 3.1.0 '@push.rocks/smartfs': 1.5.1 '@push.rocks/smartjimp': 1.2.1 - '@push.rocks/smartnetwork': 4.7.1 + '@push.rocks/smartnetwork': 4.7.2 '@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartpuppeteer': 2.0.6(typescript@6.0.3) diff --git a/test/test.metricsmanager.route-keys.node.ts b/test/test.metricsmanager.route-keys.node.ts index 796a7ad..a857a4d 100644 --- a/test/test.metricsmanager.route-keys.node.ts +++ b/test/test.metricsmanager.route-keys.node.ts @@ -22,14 +22,21 @@ function createProxyMetrics(args: { backendMetrics?: Map; protocolCache?: any[]; requestsTotal?: number; + connectionsByIP?: Map; + throughputByIP?: Map; }) { + const connectionsByIP = args.connectionsByIP || new Map(); + const throughputByIP = args.throughputByIP || new Map(); return { connections: { active: () => 0, total: () => 0, byRoute: () => args.connectionsByRoute, - byIP: () => new Map(), - topIPs: () => [], + byIP: () => connectionsByIP, + topIPs: (limit = 10) => Array.from(connectionsByIP.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, limit) + .map(([ip, count]) => ({ ip, count })), domainRequestsByIP: () => args.domainRequestsByIP, topDomainRequests: () => [], frontendProtocols: () => emptyProtocolDistribution, @@ -42,7 +49,7 @@ function createProxyMetrics(args: { custom: () => ({ in: 0, out: 0 }), history: () => [], byRoute: () => args.throughputByRoute, - byIP: () => new Map(), + byIP: () => throughputByIP, }, requests: { perSecond: () => 0, @@ -239,4 +246,37 @@ tap.test('MetricsManager does not duplicate backend active counts onto protocol expect(cacheRows.every((item) => item.activeConnections === 0)).toBeTrue(); }); +tap.test('MetricsManager queues IP intelligence without awaiting enrichment', async () => { + const proxyMetrics = createProxyMetrics({ + connectionsByRoute: new Map(), + throughputByRoute: new Map(), + domainRequestsByIP: new Map(), + connectionsByIP: new Map([ + ['8.8.8.8', 4], + ['1.1.1.1', 2], + ]), + throughputByIP: new Map([ + ['8.8.8.8', { in: 500, out: 250 }], + ['1.1.1.1', { in: 1500, out: 1000 }], + ]), + }); + + const queuedIps: string[][] = []; + const manager = new MetricsManager({ + smartProxy: { + getMetrics: () => proxyMetrics, + routeManager: { getRoutes: () => [] }, + }, + securityPolicyManager: { + queueObservedIps: (ips: string[]) => queuedIps.push(ips), + }, + } as any); + + await manager.getNetworkStats(); + + expect(queuedIps).toHaveLength(1); + expect(queuedIps[0]).toContain('8.8.8.8'); + expect(queuedIps[0]).toContain('1.1.1.1'); +}); + export default tap.start(); diff --git a/test/test.security-policy-manager.node.ts b/test/test.security-policy-manager.node.ts index 962333b..ea94e0b 100644 --- a/test/test.security-policy-manager.node.ts +++ b/test/test.security-policy-manager.node.ts @@ -40,6 +40,23 @@ const clearTestState = async () => { } }; +const createIntelligenceResult = (asn: number) => ({ + asn, + asnOrg: `ASN ${asn}`, + registrantOrg: null, + registrantCountry: null, + networkRange: null, + networkCidrs: null, + abuseContact: null, + country: null, + countryCode: 'US', + city: null, + latitude: null, + longitude: null, + accuracyRadius: null, + timezone: null, +}); + tap.test('SecurityPolicyManager compiles start-end CIDR rules for edge firewall snapshots', async () => { await testDbPromise; await clearTestState(); @@ -120,6 +137,60 @@ tap.test('SecurityPolicyManager returns an explicit empty edge firewall snapshot expect(firewall).toEqual({ blockedIps: [] }); }); +tap.test('SecurityPolicyManager filters listed IP intelligence records', async () => { + await testDbPromise; + await clearTestState(); + const manager = new SecurityPolicyManager(); + + for (const [ipAddress, asn] of [['8.8.8.8', 15169], ['1.1.1.1', 13335]] as const) { + const intelligenceDoc = new IpIntelligenceDoc(); + intelligenceDoc.ipAddress = ipAddress; + intelligenceDoc.asn = asn; + intelligenceDoc.asnOrg = `ASN ${asn}`; + intelligenceDoc.firstSeenAt = Date.now(); + intelligenceDoc.lastSeenAt = Date.now(); + intelligenceDoc.updatedAt = Date.now(); + intelligenceDoc.seenCount = 1; + await intelligenceDoc.save(); + } + + const records = await manager.listIpIntelligence({ ipAddresses: ['1.1.1.1'] }); + + expect(records).toHaveLength(1); + expect(records[0].ipAddress).toEqual('1.1.1.1'); +}); + +tap.test('SecurityPolicyManager force refresh waits for an in-flight background observation', async () => { + await testDbPromise; + await clearTestState(); + const manager = new SecurityPolicyManager({ intelligenceRefreshMs: 0 }); + + let releaseFirstLookup!: () => void; + let lookupCount = 0; + (manager as any).smartNetwork = { + getIpIntelligence: async () => { + lookupCount++; + if (lookupCount === 1) { + await new Promise((resolve) => { releaseFirstLookup = resolve; }); + return createIntelligenceResult(64500); + } + return createIntelligenceResult(64501); + }, + stop: async () => {}, + }; + + const backgroundObservation = manager.observeIp('8.8.8.8'); + await new Promise((resolve) => setTimeout(resolve, 10)); + const forcedRefresh = manager.refreshIpIntelligence('8.8.8.8'); + releaseFirstLookup(); + + const record = await forcedRefresh; + await backgroundObservation; + + expect(lookupCount).toEqual(2); + expect(record?.asn).toEqual(64501); +}); + tap.test('cleanup security policy test db', async () => { const dbHandle = await testDbPromise; await clearTestState(); diff --git a/ts/monitoring/classes.metricsmanager.ts b/ts/monitoring/classes.metricsmanager.ts index eadb8d8..b070ff2 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -725,7 +725,10 @@ export class MetricsManager { .slice(0, 10) .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut })); - void this.dcRouter.securityPolicyManager?.observeIps([...allIPData.keys()]); + this.dcRouter.securityPolicyManager?.queueObservedIps([ + ...topIPs.map((item) => item.ip), + ...topIPsByBandwidth.map((item) => item.ip), + ]); // Build domain activity using per-IP domain request counts from Rust engine const connectionsByRoute = proxyMetrics.connections.byRoute(); diff --git a/ts/opsserver/handlers/security.handler.ts b/ts/opsserver/handlers/security.handler.ts index d664f3b..5757ee2 100644 --- a/ts/opsserver/handlers/security.handler.ts +++ b/ts/opsserver/handlers/security.handler.ts @@ -180,7 +180,14 @@ export class SecurityHandler { async (dataArg) => { await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; - return { records: manager ? await manager.listIpIntelligence() : [] }; + return { + records: manager + ? await manager.listIpIntelligence({ + ipAddresses: dataArg.ipAddresses, + limit: dataArg.limit, + }) + : [], + }; }, ), ); diff --git a/ts/security/classes.security-policy-manager.ts b/ts/security/classes.security-policy-manager.ts index 7ce97fd..5a18ea3 100644 --- a/ts/security/classes.security-policy-manager.ts +++ b/ts/security/classes.security-policy-manager.ts @@ -19,12 +19,24 @@ export interface IRemoteIngressFirewallSnapshot { blockedIps: string[]; } +const OBSERVED_IP_QUEUE_LIMIT = 512; +const OBSERVED_IP_BATCH_LIMIT = 20; +const OBSERVED_IP_QUEUE_CONCURRENCY = 2; +const OBSERVED_IP_REQUEUE_THROTTLE_MS = 60_000; + export class SecurityPolicyManager { private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({ cacheTtl: 24 * 60 * 60 * 1000, + ipIntelligenceTimeout: 5_000, }); private readonly intelligenceRefreshMs: number; - private readonly inFlightObservations = new Set(); + private readonly inFlightObservations = new Map>(); + private readonly queuedObservations = new Set(); + private readonly observationQueue: string[] = []; + private readonly lastQueuedAt = new Map(); + private activeQueuedObservations = 0; + private queueDrainScheduled = false; + private isStopping = false; private readonly onPolicyChanged?: () => void | Promise; constructor(options: ISecurityPolicyManagerOptions = {}) { @@ -37,6 +49,9 @@ export class SecurityPolicyManager { } public async stop(): Promise { + this.isStopping = true; + this.observationQueue.length = 0; + this.queuedObservations.clear(); await this.smartNetwork.stop(); } @@ -45,13 +60,55 @@ export class SecurityPolicyManager { await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip))); } + public queueObservedIps(ips: string[]): void { + if (this.isStopping) return; + + const now = Date.now(); + const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])]; + + for (const ip of uniqueIps.slice(0, OBSERVED_IP_BATCH_LIMIT)) { + if (!this.isPublicIp(ip)) continue; + if (this.inFlightObservations.has(ip) || this.queuedObservations.has(ip)) continue; + + const lastQueuedAt = this.lastQueuedAt.get(ip); + if (lastQueuedAt && now - lastQueuedAt < OBSERVED_IP_REQUEUE_THROTTLE_MS) continue; + + if (this.observationQueue.length >= OBSERVED_IP_QUEUE_LIMIT) { + const droppedIp = this.observationQueue.shift(); + if (droppedIp) this.queuedObservations.delete(droppedIp); + } + + this.observationQueue.push(ip); + this.queuedObservations.add(ip); + this.lastQueuedAt.set(ip, now); + } + + this.pruneQueuedIpMemory(now); + this.scheduleQueueDrain(); + } + public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise { const ip = this.normalizeIp(ipAddress); - if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) { + if (!ip || !this.isPublicIp(ip)) { return; } - this.inFlightObservations.add(ip); + const existingObservation = this.inFlightObservations.get(ip); + if (existingObservation) { + await existingObservation; + if (!options.force) return; + } + + const observationPromise = this.performObserveIp(ip, options).finally(() => { + if (this.inFlightObservations.get(ip) === observationPromise) { + this.inFlightObservations.delete(ip); + } + }); + this.inFlightObservations.set(ip, observationPromise); + await observationPromise; + } + + private async performObserveIp(ip: string, options: { force?: boolean } = {}): Promise { try { const now = Date.now(); let doc = await IpIntelligenceDoc.findByIp(ip); @@ -81,8 +138,6 @@ export class SecurityPolicyManager { } } catch (err) { logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`); - } finally { - this.inFlightObservations.delete(ip); } } @@ -90,8 +145,22 @@ export class SecurityPolicyManager { return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc)); } - public async listIpIntelligence(): Promise { - return (await IpIntelligenceDoc.findAll()).map((doc) => this.intelligenceFromDoc(doc)); + public async listIpIntelligence(options: { ipAddresses?: string[]; limit?: number } = {}): Promise { + const limit = Number.isInteger(options.limit) && options.limit! > 0 + ? Math.min(options.limit!, 500) + : undefined; + + let docs: IpIntelligenceDoc[]; + if (options.ipAddresses?.length) { + const ips = [...new Set(options.ipAddresses.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])]; + const results = await Promise.all(ips.map((ip) => IpIntelligenceDoc.findByIp(ip))); + docs = results.filter(Boolean) as IpIntelligenceDoc[]; + } else { + docs = await IpIntelligenceDoc.findAll(); + } + + const sortedDocs = docs.sort((a, b) => (b.lastSeenAt || 0) - (a.lastSeenAt || 0)); + return (limit ? sortedDocs.slice(0, limit) : sortedDocs).map((doc) => this.intelligenceFromDoc(doc)); } public async refreshIpIntelligence(ipAddress: string): Promise { @@ -104,6 +173,45 @@ export class SecurityPolicyManager { return doc ? this.intelligenceFromDoc(doc) : null; } + private scheduleQueueDrain(): void { + if (this.queueDrainScheduled || this.isStopping) return; + this.queueDrainScheduled = true; + setTimeout(() => { + this.queueDrainScheduled = false; + this.drainObservationQueue(); + }, 0); + } + + private drainObservationQueue(): void { + if (this.isStopping) return; + + while ( + this.activeQueuedObservations < OBSERVED_IP_QUEUE_CONCURRENCY && + this.observationQueue.length > 0 + ) { + const ip = this.observationQueue.shift()!; + this.queuedObservations.delete(ip); + this.activeQueuedObservations++; + void this.observeIp(ip) + .catch(() => undefined) + .finally(() => { + this.activeQueuedObservations--; + if (this.observationQueue.length > 0) { + this.scheduleQueueDrain(); + } + }); + } + } + + private pruneQueuedIpMemory(now: number): void { + if (this.lastQueuedAt.size <= OBSERVED_IP_QUEUE_LIMIT * 2) return; + for (const [ip, lastQueuedAt] of this.lastQueuedAt) { + if (now - lastQueuedAt > OBSERVED_IP_REQUEUE_THROTTLE_MS * 2) { + this.lastQueuedAt.delete(ip); + } + } + } + public async listAuditEvents(limit = 100): Promise { return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({ id: doc.id, diff --git a/ts_interfaces/requests/security-policy.ts b/ts_interfaces/requests/security-policy.ts index 320946d..0ca67fe 100644 --- a/ts_interfaces/requests/security-policy.ts +++ b/ts_interfaces/requests/security-policy.ts @@ -89,6 +89,8 @@ export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces. request: { identity?: authInterfaces.IIdentity; apiToken?: string; + ipAddresses?: string[]; + limit?: number; }; response: { records: IIpIntelligenceRecord[]; diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 270760d..cb42ccb 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -582,6 +582,52 @@ export const setActiveViewAction = uiStatePart.createAction(async (state }; }); +const backgroundRefreshesInFlight = new Set(); + +function runBackgroundRefresh(key: string, errorMessage: string, task: () => Promise): void { + if (backgroundRefreshesInFlight.has(key)) return; + backgroundRefreshesInFlight.add(key); + void task() + .catch((error) => console.error(errorMessage, error)) + .finally(() => backgroundRefreshesInFlight.delete(key)); +} + +function refreshNetworkIpIntelligence(identity: interfaces.data.IIdentity, ipAddresses: string[]): void { + const ips = [...new Set(ipAddresses.filter(Boolean))].slice(0, 100); + if (ips.length === 0) return; + + runBackgroundRefresh('networkIpIntelligence', 'IP intelligence refresh failed:', async () => { + const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< + interfaces.requests.IReq_ListIpIntelligence + >('/typedrequest', 'listIpIntelligence'); + const intelligenceResponse = await intelligenceRequest.fire({ + identity, + ipAddresses: ips, + limit: Math.max(100, ips.length), + }); + networkStatePart.setState({ + ...networkStatePart.getState()!, + ipIntelligence: intelligenceResponse.records || [], + }); + }); +} + +function refreshSecurityIpIntelligence(identity: interfaces.data.IIdentity): void { + runBackgroundRefresh('securityIpIntelligence', 'Security IP intelligence refresh failed:', async () => { + const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< + interfaces.requests.IReq_ListIpIntelligence + >('/typedrequest', 'listIpIntelligence'); + const intelligenceResponse = await intelligenceRequest.fire({ + identity, + limit: 500, + }); + securityPolicyStatePart.setState({ + ...securityPolicyStatePart.getState()!, + ipIntelligence: intelligenceResponse.records || [], + }); + }); +} + // Fetch Network Stats Action export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise => { const context = getActionContext(); @@ -594,18 +640,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat interfaces.requests.IReq_GetNetworkStats >('/typedrequest', 'getNetworkStats'); - const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_ListIpIntelligence - >('/typedrequest', 'listIpIntelligence'); - - const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([ - networkStatsRequest.fire({ - identity: context.identity, - }), - ipIntelligenceRequest.fire({ - identity: context.identity, - }), - ]); + const networkStatsResponse = await networkStatsRequest.fire({ + identity: context.identity, + }); // Use the connections data for the connection list // and network stats for throughput and IP analytics @@ -637,6 +674,12 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat }; }); + refreshNetworkIpIntelligence(context.identity, [ + ...Object.keys(connectionsByIP), + ...(networkStatsResponse.topIPs || []).map((item) => item.ip), + ...(networkStatsResponse.topIPsByBandwidth || []).map((item) => item.ip), + ]); + return { connections, connectionsByIP, @@ -647,7 +690,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat topIPs: networkStatsResponse.topIPs || [], topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [], throughputByIP: networkStatsResponse.throughputByIP || [], - ipIntelligence: ipIntelligenceResponse.records || [], + ipIntelligence: currentState.ipIntelligence, domainActivity: networkStatsResponse.domainActivity || [], throughputHistory: networkStatsResponse.throughputHistory || [], requestsPerSecond: networkStatsResponse.requestsPerSecond || 0, @@ -683,9 +726,6 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction( const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_ListSecurityBlockRules >('/typedrequest', 'listSecurityBlockRules'); - const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_ListIpIntelligence - >('/typedrequest', 'listIpIntelligence'); const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetCompiledSecurityPolicy >('/typedrequest', 'getCompiledSecurityPolicy'); @@ -693,16 +733,17 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction( interfaces.requests.IReq_ListSecurityPolicyAudit >('/typedrequest', 'listSecurityPolicyAudit'); - const [rulesResponse, intelligenceResponse, compiledPolicyResponse, auditResponse] = await Promise.all([ + const [rulesResponse, compiledPolicyResponse, auditResponse] = await Promise.all([ rulesRequest.fire({ identity: context.identity }), - intelligenceRequest.fire({ identity: context.identity }), compiledPolicyRequest.fire({ identity: context.identity }), auditRequest.fire({ identity: context.identity, limit: 100 }), ]); + refreshSecurityIpIntelligence(context.identity); + return { rules: rulesResponse.rules || [], - ipIntelligence: intelligenceResponse.records || [], + ipIntelligence: currentState.ipIntelligence, compiledPolicy: compiledPolicyResponse.policy, auditEvents: auditResponse.events || [], isLoading: false, @@ -835,7 +876,15 @@ export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction< if (!response.success) { return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' }; } - return await actionContext!.dispatch(fetchSecurityPolicyAction, null); + const refreshedState = await actionContext!.dispatch(fetchSecurityPolicyAction, null); + if (!response.record) return refreshedState; + return { + ...refreshedState, + ipIntelligence: [ + response.record, + ...refreshedState.ipIntelligence.filter((record) => record.ipAddress !== response.record!.ipAddress), + ], + }; } catch (error: unknown) { return { ...currentState, @@ -3112,53 +3161,38 @@ async function dispatchCombinedRefreshActionInner() { error: null, }); - try { - const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< - interfaces.requests.IReq_ListIpIntelligence - >('/typedrequest', 'listIpIntelligence'); - const intelligenceResponse = await intelligenceRequest.fire({ identity: context.identity }); - networkStatePart.setState({ - ...networkStatePart.getState()!, - ipIntelligence: intelligenceResponse.records || [], - }); - } catch (error) { - console.error('IP intelligence refresh failed:', error); - } + refreshNetworkIpIntelligence(context.identity, [ + ...network.connectionDetails.map((conn) => conn.remoteAddress), + ...network.topEndpoints.map((endpoint) => endpoint.endpoint), + ...(network.topEndpointsByBandwidth || []).map((endpoint) => endpoint.endpoint), + ]); } if (currentView === 'security') { - try { + runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => { await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null); - } catch (error) { - console.error('Security policy refresh failed:', error); - } + }); } // Refresh certificate data if on Domains > Certificates subview if (currentView === 'domains' && currentSubview === 'certificates') { - try { + runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => { await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null); - } catch (error) { - console.error('Certificate refresh failed:', error); - } + }); } // Refresh remote ingress data if on the Network → Remote Ingress subview if (currentView === 'network' && currentSubview === 'remoteingress') { - try { + runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => { await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null); - } catch (error) { - console.error('Remote ingress refresh failed:', error); - } + }); } // Refresh VPN data if on the Network → VPN subview if (currentView === 'network' && currentSubview === 'vpn') { - try { + runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => { await vpnStatePart.dispatchAction(fetchVpnAction, null); - } catch (error) { - console.error('VPN refresh failed:', error); - } + }); } } catch (error) { console.error('Combined refresh failed:', error);