feat(security): add queued IP intelligence observation and filtered retrieval for network and security views

This commit is contained in:
2026-05-21 01:56:17 +00:00
parent ca5c57a329
commit 98913c1977
10 changed files with 342 additions and 69 deletions
+8
View File
@@ -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
+1 -1
View File
@@ -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",
+8 -8
View File
@@ -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)
+43 -3
View File
@@ -22,14 +22,21 @@ function createProxyMetrics(args: {
backendMetrics?: Map<string, any>;
protocolCache?: any[];
requestsTotal?: number;
connectionsByIP?: Map<string, number>;
throughputByIP?: Map<string, { in: number; out: number }>;
}) {
const connectionsByIP = args.connectionsByIP || new Map<string, number>();
const throughputByIP = args.throughputByIP || new Map<string, { in: number; out: number }>();
return {
connections: {
active: () => 0,
total: () => 0,
byRoute: () => args.connectionsByRoute,
byIP: () => new Map<string, number>(),
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<string, { in: number; out: number }>(),
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();
+71
View File
@@ -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<void>((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();
+4 -1
View File
@@ -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();
+8 -1
View File
@@ -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,
})
: [],
};
},
),
);
+115 -7
View File
@@ -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<string>();
private readonly inFlightObservations = new Map<string, Promise<void>>();
private readonly queuedObservations = new Set<string>();
private readonly observationQueue: string[] = [];
private readonly lastQueuedAt = new Map<string, number>();
private activeQueuedObservations = 0;
private queueDrainScheduled = false;
private isStopping = false;
private readonly onPolicyChanged?: () => void | Promise<void>;
constructor(options: ISecurityPolicyManagerOptions = {}) {
@@ -37,6 +49,9 @@ export class SecurityPolicyManager {
}
public async stop(): Promise<void> {
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<void> {
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<void> {
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<IIpIntelligenceRecord[]> {
return (await IpIntelligenceDoc.findAll()).map((doc) => this.intelligenceFromDoc(doc));
public async listIpIntelligence(options: { ipAddresses?: string[]; limit?: number } = {}): Promise<IIpIntelligenceRecord[]> {
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<IIpIntelligenceRecord | null> {
@@ -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<ISecurityPolicyAuditEvent[]> {
return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
id: doc.id,
@@ -89,6 +89,8 @@ export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces.
request: {
identity?: authInterfaces.IIdentity;
apiToken?: string;
ipAddresses?: string[];
limit?: number;
};
response: {
records: IIpIntelligenceRecord[];
+81 -47
View File
@@ -582,6 +582,52 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
};
});
const backgroundRefreshesInFlight = new Set<string>();
function runBackgroundRefresh(key: string, errorMessage: string, task: () => Promise<void>): 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<INetworkState> => {
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({
const networkStatsResponse = await networkStatsRequest.fire({
identity: context.identity,
}),
ipIntelligenceRequest.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);