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 ## 2026-05-20 - 13.32.1
### Fixes ### Fixes
+1 -1
View File
@@ -53,7 +53,7 @@
"@push.rocks/smartmetrics": "^3.0.3", "@push.rocks/smartmetrics": "^3.0.3",
"@push.rocks/smartmigration": "1.4.1", "@push.rocks/smartmigration": "1.4.1",
"@push.rocks/smartmta": "^5.3.3", "@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/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.4", "@push.rocks/smartpromise": "^4.2.4",
"@push.rocks/smartproxy": "^27.10.3", "@push.rocks/smartproxy": "^27.10.3",
+8 -8
View File
@@ -75,8 +75,8 @@ importers:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.3.3 version: 5.3.3
'@push.rocks/smartnetwork': '@push.rocks/smartnetwork':
specifier: ^4.7.1 specifier: ^4.7.2
version: 4.7.1 version: 4.7.2
'@push.rocks/smartpath': '@push.rocks/smartpath':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
@@ -1395,8 +1395,8 @@ packages:
'@push.rocks/smartmustache@3.0.2': '@push.rocks/smartmustache@3.0.2':
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==} resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
'@push.rocks/smartnetwork@4.7.1': '@push.rocks/smartnetwork@4.7.2':
resolution: {integrity: sha512-x9SolGn8lU3oh+fKL26dR5dIhsus5f0p/Xiaut2pK5Wamgwrvt5y5To8F+pzF1pQr6yA0XwWZ0Dgoppp2E+ziQ==} resolution: {integrity: sha512-OwT8kwQeEO+E3RuCyCfgQEBz+FyydUVaTBivZzzVchdJCUDgoDkXSnRkbIuGoHd1BfRFkUg9DQlSzt0uDfsIbw==}
'@push.rocks/smartnftables@1.2.0': '@push.rocks/smartnftables@1.2.0':
resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==} resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==}
@@ -5306,7 +5306,7 @@ snapshots:
'@push.rocks/smartjson': 6.0.1 '@push.rocks/smartjson': 6.0.1
'@push.rocks/smartlog': 3.2.2 '@push.rocks/smartlog': 3.2.2
'@push.rocks/smartmongo': 7.0.0(socks@2.8.8) '@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/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartrequest': 5.0.3 '@push.rocks/smartrequest': 5.0.3
@@ -6115,7 +6115,7 @@ snapshots:
'@push.rocks/smartdelay': 3.1.0 '@push.rocks/smartdelay': 3.1.0
'@push.rocks/smartdns': 7.9.2 '@push.rocks/smartdns': 7.9.2
'@push.rocks/smartlog': 3.2.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/smartstring': 4.1.1
'@push.rocks/smarttime': 4.2.3 '@push.rocks/smarttime': 4.2.3
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
@@ -6591,7 +6591,7 @@ snapshots:
dependencies: dependencies:
handlebars: 4.7.9 handlebars: 4.7.9
'@push.rocks/smartnetwork@4.7.1': '@push.rocks/smartnetwork@4.7.2':
dependencies: dependencies:
'@push.rocks/smartdns': 7.9.2 '@push.rocks/smartdns': 7.9.2
'@push.rocks/smartrust': 1.4.0 '@push.rocks/smartrust': 1.4.0
@@ -6654,7 +6654,7 @@ snapshots:
'@push.rocks/smartdelay': 3.1.0 '@push.rocks/smartdelay': 3.1.0
'@push.rocks/smartfs': 1.5.1 '@push.rocks/smartfs': 1.5.1
'@push.rocks/smartjimp': 1.2.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/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.4 '@push.rocks/smartpromise': 4.2.4
'@push.rocks/smartpuppeteer': 2.0.6(typescript@6.0.3) '@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>; backendMetrics?: Map<string, any>;
protocolCache?: any[]; protocolCache?: any[];
requestsTotal?: number; 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 { return {
connections: { connections: {
active: () => 0, active: () => 0,
total: () => 0, total: () => 0,
byRoute: () => args.connectionsByRoute, byRoute: () => args.connectionsByRoute,
byIP: () => new Map<string, number>(), byIP: () => connectionsByIP,
topIPs: () => [], 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, domainRequestsByIP: () => args.domainRequestsByIP,
topDomainRequests: () => [], topDomainRequests: () => [],
frontendProtocols: () => emptyProtocolDistribution, frontendProtocols: () => emptyProtocolDistribution,
@@ -42,7 +49,7 @@ function createProxyMetrics(args: {
custom: () => ({ in: 0, out: 0 }), custom: () => ({ in: 0, out: 0 }),
history: () => [], history: () => [],
byRoute: () => args.throughputByRoute, byRoute: () => args.throughputByRoute,
byIP: () => new Map<string, { in: number; out: number }>(), byIP: () => throughputByIP,
}, },
requests: { requests: {
perSecond: () => 0, 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(); 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(); 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 () => { tap.test('SecurityPolicyManager compiles start-end CIDR rules for edge firewall snapshots', async () => {
await testDbPromise; await testDbPromise;
await clearTestState(); await clearTestState();
@@ -120,6 +137,60 @@ tap.test('SecurityPolicyManager returns an explicit empty edge firewall snapshot
expect(firewall).toEqual({ blockedIps: [] }); 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 () => { tap.test('cleanup security policy test db', async () => {
const dbHandle = await testDbPromise; const dbHandle = await testDbPromise;
await clearTestState(); await clearTestState();
+4 -1
View File
@@ -725,7 +725,10 @@ export class MetricsManager {
.slice(0, 10) .slice(0, 10)
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut })); .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 // Build domain activity using per-IP domain request counts from Rust engine
const connectionsByRoute = proxyMetrics.connections.byRoute(); const connectionsByRoute = proxyMetrics.connections.byRoute();
+8 -1
View File
@@ -180,7 +180,14 @@ export class SecurityHandler {
async (dataArg) => { async (dataArg) => {
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' }); await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'security:read' });
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager; 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[]; 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 { export class SecurityPolicyManager {
private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({ private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
cacheTtl: 24 * 60 * 60 * 1000, cacheTtl: 24 * 60 * 60 * 1000,
ipIntelligenceTimeout: 5_000,
}); });
private readonly intelligenceRefreshMs: number; 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>; private readonly onPolicyChanged?: () => void | Promise<void>;
constructor(options: ISecurityPolicyManagerOptions = {}) { constructor(options: ISecurityPolicyManagerOptions = {}) {
@@ -37,6 +49,9 @@ export class SecurityPolicyManager {
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
this.isStopping = true;
this.observationQueue.length = 0;
this.queuedObservations.clear();
await this.smartNetwork.stop(); await this.smartNetwork.stop();
} }
@@ -45,13 +60,55 @@ export class SecurityPolicyManager {
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip))); 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> { public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
const ip = this.normalizeIp(ipAddress); const ip = this.normalizeIp(ipAddress);
if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) { if (!ip || !this.isPublicIp(ip)) {
return; 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 { try {
const now = Date.now(); const now = Date.now();
let doc = await IpIntelligenceDoc.findByIp(ip); let doc = await IpIntelligenceDoc.findByIp(ip);
@@ -81,8 +138,6 @@ export class SecurityPolicyManager {
} }
} catch (err) { } catch (err) {
logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`); 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)); return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
} }
public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> { public async listIpIntelligence(options: { ipAddresses?: string[]; limit?: number } = {}): Promise<IIpIntelligenceRecord[]> {
return (await IpIntelligenceDoc.findAll()).map((doc) => this.intelligenceFromDoc(doc)); 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> { public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
@@ -104,6 +173,45 @@ export class SecurityPolicyManager {
return doc ? this.intelligenceFromDoc(doc) : null; 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[]> { public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({ return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
id: doc.id, id: doc.id,
@@ -89,6 +89,8 @@ export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces.
request: { request: {
identity?: authInterfaces.IIdentity; identity?: authInterfaces.IIdentity;
apiToken?: string; apiToken?: string;
ipAddresses?: string[];
limit?: number;
}; };
response: { response: {
records: IIpIntelligenceRecord[]; records: IIpIntelligenceRecord[];
+82 -48
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 // Fetch Network Stats Action
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise<INetworkState> => { export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise<INetworkState> => {
const context = getActionContext(); const context = getActionContext();
@@ -594,18 +640,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
interfaces.requests.IReq_GetNetworkStats interfaces.requests.IReq_GetNetworkStats
>('/typedrequest', 'getNetworkStats'); >('/typedrequest', 'getNetworkStats');
const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< const networkStatsResponse = await networkStatsRequest.fire({
interfaces.requests.IReq_ListIpIntelligence identity: context.identity,
>('/typedrequest', 'listIpIntelligence'); });
const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
networkStatsRequest.fire({
identity: context.identity,
}),
ipIntelligenceRequest.fire({
identity: context.identity,
}),
]);
// Use the connections data for the connection list // Use the connections data for the connection list
// and network stats for throughput and IP analytics // 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 { return {
connections, connections,
connectionsByIP, connectionsByIP,
@@ -647,7 +690,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
topIPs: networkStatsResponse.topIPs || [], topIPs: networkStatsResponse.topIPs || [],
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [], topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
throughputByIP: networkStatsResponse.throughputByIP || [], throughputByIP: networkStatsResponse.throughputByIP || [],
ipIntelligence: ipIntelligenceResponse.records || [], ipIntelligence: currentState.ipIntelligence,
domainActivity: networkStatsResponse.domainActivity || [], domainActivity: networkStatsResponse.domainActivity || [],
throughputHistory: networkStatsResponse.throughputHistory || [], throughputHistory: networkStatsResponse.throughputHistory || [],
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0, requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
@@ -683,9 +726,6 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_ListSecurityBlockRules interfaces.requests.IReq_ListSecurityBlockRules
>('/typedrequest', '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< const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetCompiledSecurityPolicy interfaces.requests.IReq_GetCompiledSecurityPolicy
>('/typedrequest', 'getCompiledSecurityPolicy'); >('/typedrequest', 'getCompiledSecurityPolicy');
@@ -693,16 +733,17 @@ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
interfaces.requests.IReq_ListSecurityPolicyAudit interfaces.requests.IReq_ListSecurityPolicyAudit
>('/typedrequest', 'listSecurityPolicyAudit'); >('/typedrequest', 'listSecurityPolicyAudit');
const [rulesResponse, intelligenceResponse, compiledPolicyResponse, auditResponse] = await Promise.all([ const [rulesResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
rulesRequest.fire({ identity: context.identity }), rulesRequest.fire({ identity: context.identity }),
intelligenceRequest.fire({ identity: context.identity }),
compiledPolicyRequest.fire({ identity: context.identity }), compiledPolicyRequest.fire({ identity: context.identity }),
auditRequest.fire({ identity: context.identity, limit: 100 }), auditRequest.fire({ identity: context.identity, limit: 100 }),
]); ]);
refreshSecurityIpIntelligence(context.identity);
return { return {
rules: rulesResponse.rules || [], rules: rulesResponse.rules || [],
ipIntelligence: intelligenceResponse.records || [], ipIntelligence: currentState.ipIntelligence,
compiledPolicy: compiledPolicyResponse.policy, compiledPolicy: compiledPolicyResponse.policy,
auditEvents: auditResponse.events || [], auditEvents: auditResponse.events || [],
isLoading: false, isLoading: false,
@@ -835,7 +876,15 @@ export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<
if (!response.success) { if (!response.success) {
return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' }; 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) { } catch (error: unknown) {
return { return {
...currentState, ...currentState,
@@ -3112,53 +3161,38 @@ async function dispatchCombinedRefreshActionInner() {
error: null, error: null,
}); });
try { refreshNetworkIpIntelligence(context.identity, [
const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< ...network.connectionDetails.map((conn) => conn.remoteAddress),
interfaces.requests.IReq_ListIpIntelligence ...network.topEndpoints.map((endpoint) => endpoint.endpoint),
>('/typedrequest', 'listIpIntelligence'); ...(network.topEndpointsByBandwidth || []).map((endpoint) => endpoint.endpoint),
const intelligenceResponse = await intelligenceRequest.fire({ identity: context.identity }); ]);
networkStatePart.setState({
...networkStatePart.getState()!,
ipIntelligence: intelligenceResponse.records || [],
});
} catch (error) {
console.error('IP intelligence refresh failed:', error);
}
} }
if (currentView === 'security') { if (currentView === 'security') {
try { runBackgroundRefresh('securityPolicy', 'Security policy refresh failed:', async () => {
await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null); await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
} catch (error) { });
console.error('Security policy refresh failed:', error);
}
} }
// Refresh certificate data if on Domains > Certificates subview // Refresh certificate data if on Domains > Certificates subview
if (currentView === 'domains' && currentSubview === 'certificates') { if (currentView === 'domains' && currentSubview === 'certificates') {
try { runBackgroundRefresh('certificates', 'Certificate refresh failed:', async () => {
await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null); await certificateStatePart.dispatchAction(fetchCertificateOverviewAction, null);
} catch (error) { });
console.error('Certificate refresh failed:', error);
}
} }
// Refresh remote ingress data if on the Network → Remote Ingress subview // Refresh remote ingress data if on the Network → Remote Ingress subview
if (currentView === 'network' && currentSubview === 'remoteingress') { if (currentView === 'network' && currentSubview === 'remoteingress') {
try { runBackgroundRefresh('remoteIngress', 'Remote ingress refresh failed:', async () => {
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null); await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
} catch (error) { });
console.error('Remote ingress refresh failed:', error);
}
} }
// Refresh VPN data if on the Network → VPN subview // Refresh VPN data if on the Network → VPN subview
if (currentView === 'network' && currentSubview === 'vpn') { if (currentView === 'network' && currentSubview === 'vpn') {
try { runBackgroundRefresh('vpn', 'VPN refresh failed:', async () => {
await vpnStatePart.dispatchAction(fetchVpnAction, null); await vpnStatePart.dispatchAction(fetchVpnAction, null);
} catch (error) { });
console.error('VPN refresh failed:', error);
}
} }
} catch (error) { } catch (error) {
console.error('Combined refresh failed:', error); console.error('Combined refresh failed:', error);