Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89ab918826 | |||
| e5c3578163 | |||
| 1567606c49 | |||
| af31982d58 | |||
| a322308623 | |||
| ec5374900c | |||
| 49ce265d7e | |||
| 63729697c5 | |||
| ce93b726ef |
@@ -1,5 +1,34 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-26 - 13.24.0 - feat(security)
|
||||||
|
add security policy management and IP intelligence operations to the ops UI
|
||||||
|
|
||||||
|
- adds typed request endpoints to fetch compiled security policy, list audit events, and force-refresh IP intelligence
|
||||||
|
- introduces dedicated security policy state and actions for loading, creating, updating, deleting, and refreshing security data
|
||||||
|
- enhances the network activity view with IP intelligence columns, detail dialogs, and block-rule actions
|
||||||
|
- expands the security blocked view into a full management interface for rules, compiled policy, IP intelligence, and audit history
|
||||||
|
|
||||||
|
## 2026-04-26 - 13.23.0 - feat(security)
|
||||||
|
add managed security policies with IP intelligence and remote ingress firewall propagation
|
||||||
|
|
||||||
|
- introduces a SecurityPolicyManager that observes public IPs, stores IP intelligence, compiles block policies, and audits policy changes
|
||||||
|
- adds database documents and shared interfaces for security block rules, IP intelligence records, and security policy audit events
|
||||||
|
- exposes ops/admin request handlers to list IP intelligence and create, update, or delete security block rules
|
||||||
|
- applies merged security policies to SmartProxy and propagates firewall snapshots to remote ingress edges and tunnel synchronization
|
||||||
|
|
||||||
|
## 2026-04-26 - 13.22.0 - feat(remoteingress)
|
||||||
|
add remote ingress performance configuration and expose tunnel transport metrics
|
||||||
|
|
||||||
|
- upgrade @serve.zone/remoteingress to support performance tuning and richer tunnel status data
|
||||||
|
- pass remote ingress performance settings through router startup and config APIs
|
||||||
|
- serialize allowed-edge sync operations and await route update hooks to avoid tunnel sync races
|
||||||
|
- expose UDP listen ports and transport, flow control, queue, and traffic metrics in remote ingress APIs and ops UI
|
||||||
|
|
||||||
|
## 2026-04-26 - 13.21.1 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^27.8.1
|
||||||
|
|
||||||
|
- Updates @push.rocks/smartproxy from ^27.8.0 to ^27.8.1 in package.json.
|
||||||
|
|
||||||
## 2026-04-25 - 13.21.0 - feat(monitoring)
|
## 2026-04-25 - 13.21.0 - feat(monitoring)
|
||||||
improve network activity metrics with live domain request rates and backend identifiers
|
improve network activity metrics with live domain request rates and backend identifiers
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "13.21.0",
|
"version": "13.24.0",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.6.0",
|
"@push.rocks/smartnetwork": "^4.6.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^27.8.0",
|
"@push.rocks/smartproxy": "^27.9.0",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"@push.rocks/taskbuffer": "^8.0.2",
|
"@push.rocks/taskbuffer": "^8.0.2",
|
||||||
"@serve.zone/catalog": "^2.12.4",
|
"@serve.zone/catalog": "^2.12.4",
|
||||||
"@serve.zone/interfaces": "^5.4.3",
|
"@serve.zone/interfaces": "^5.4.3",
|
||||||
"@serve.zone/remoteingress": "^4.15.3",
|
"@serve.zone/remoteingress": "^4.17.1",
|
||||||
"@tsclass/tsclass": "^9.5.0",
|
"@tsclass/tsclass": "^9.5.0",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"lru-cache": "^11.3.5",
|
"lru-cache": "^11.3.5",
|
||||||
|
|||||||
Generated
+19
-11
@@ -81,8 +81,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^27.8.0
|
specifier: ^27.9.0
|
||||||
version: 27.8.0
|
version: 27.9.0
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -111,8 +111,8 @@ importers:
|
|||||||
specifier: ^5.4.3
|
specifier: ^5.4.3
|
||||||
version: 5.4.3
|
version: 5.4.3
|
||||||
'@serve.zone/remoteingress':
|
'@serve.zone/remoteingress':
|
||||||
specifier: ^4.15.3
|
specifier: ^4.17.1
|
||||||
version: 4.15.3
|
version: 4.17.1
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.5.0
|
specifier: ^9.5.0
|
||||||
version: 9.5.0
|
version: 9.5.0
|
||||||
@@ -1263,6 +1263,9 @@ packages:
|
|||||||
'@push.rocks/smartnftables@1.1.0':
|
'@push.rocks/smartnftables@1.1.0':
|
||||||
resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==}
|
resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==}
|
||||||
|
|
||||||
|
'@push.rocks/smartnftables@1.2.0':
|
||||||
|
resolution: {integrity: sha512-VTRHnxHrJj9VOq2MaCOqxiA4JLGRnzEaZ7kXxA7v3ljX+Y2wWK9VYpwKKBEbjgjoTpQyOf+I0gEG9wkR/jtUvQ==}
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
|
||||||
|
|
||||||
@@ -1284,8 +1287,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.8.0':
|
'@push.rocks/smartproxy@27.9.0':
|
||||||
resolution: {integrity: sha512-/+rfSAz9hRopuRRBwaI/VVtFTLNemnh9RIf0YAPRhrLCL4WGJXkjnpX4Zi6W1AAPDU2wlz7Zm0F6TO+nXLqP5w==}
|
resolution: {integrity: sha512-lzOxueA89pBf4ZcTzF+VkjXQ0es8z8C20PW6FA0HcIzcCpnh4NjLwnXyD8NnTpCf+HKh/EAgD77Kt9Dn+sssUQ==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -1594,8 +1597,8 @@ packages:
|
|||||||
'@serve.zone/interfaces@5.4.3':
|
'@serve.zone/interfaces@5.4.3':
|
||||||
resolution: {integrity: sha512-9ijFhHoC7GYyyAUJbBoDYmcoCmIXTFPiD6fI3x68SWiC0xA+2LG0nOe14D32c1QN9X/3i2Ac5/1sUibfjHsIGg==}
|
resolution: {integrity: sha512-9ijFhHoC7GYyyAUJbBoDYmcoCmIXTFPiD6fI3x68SWiC0xA+2LG0nOe14D32c1QN9X/3i2Ac5/1sUibfjHsIGg==}
|
||||||
|
|
||||||
'@serve.zone/remoteingress@4.15.3':
|
'@serve.zone/remoteingress@4.17.1':
|
||||||
resolution: {integrity: sha512-kg/bmR+qcFRFuigTDr5Fao72cb7m/mSkI5APm7KZDKSUYTFuytNoj6KCIE0ICkc3Nh34y8oDwFJsS6oFo64AyQ==}
|
resolution: {integrity: sha512-k3n+AF1rNybiKPlHHyhwCVEF0/T7eZD46kNn7JlEJPCxfUy09mjkpwDQ2CzaUkppqNgFOAYXgAKqjDqpJ27RvA==}
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0':
|
'@sindresorhus/is@5.6.0':
|
||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
@@ -6443,6 +6446,11 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
|
'@push.rocks/smartnftables@1.2.0':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartlog': 3.2.2
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
'@push.rocks/smartnpm@2.0.6':
|
'@push.rocks/smartnpm@2.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
@@ -6512,7 +6520,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.8.0':
|
'@push.rocks/smartproxy@27.9.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
@@ -6933,10 +6941,10 @@ snapshots:
|
|||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@tsclass/tsclass': 9.5.0
|
'@tsclass/tsclass': 9.5.0
|
||||||
|
|
||||||
'@serve.zone/remoteingress@4.15.3':
|
'@serve.zone/remoteingress@4.17.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
'@push.rocks/smartnftables': 1.1.0
|
'@push.rocks/smartnftables': 1.2.0
|
||||||
'@push.rocks/smartrust': 1.3.2
|
'@push.rocks/smartrust': 1.3.2
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0': {}
|
'@sindresorhus/is@5.6.0': {}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.21.0',
|
version: '13.24.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
+106
-6
@@ -27,12 +27,13 @@ import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
|||||||
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
|
import { VpnManager, type IVpnManagerConfig } from './vpn/index.js';
|
||||||
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
|
import { RouteConfigManager, ApiTokenManager, ReferenceResolver, DbSeeder, TargetProfileManager } from './config/index.js';
|
||||||
import type { TIpAllowEntry } from './config/classes.route-config-manager.js';
|
import type { TIpAllowEntry } from './config/classes.route-config-manager.js';
|
||||||
import { SecurityLogger, ContentScanner, IPReputationChecker } from './security/index.js';
|
import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyManager } from './security/index.js';
|
||||||
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
|
||||||
import { DnsManager } from './dns/manager.dns.js';
|
import { DnsManager } from './dns/manager.dns.js';
|
||||||
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
import { AcmeConfigManager } from './acme/manager.acme-config.js';
|
||||||
import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
|
import { EmailDomainManager, SmartMtaStorageManager, buildEmailDnsRecords } from './email/index.js';
|
||||||
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
import type { IRoute } from '../ts_interfaces/data/route-management.js';
|
||||||
|
import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||||
@@ -178,6 +179,8 @@ export interface IDcRouterOptions {
|
|||||||
certPath?: string;
|
certPath?: string;
|
||||||
keyPath?: string;
|
keyPath?: string;
|
||||||
};
|
};
|
||||||
|
/** Performance profile and limits for remote ingress hub/edge tunnels. */
|
||||||
|
performance?: import('../ts_interfaces/data/remoteingress.js').IRemoteIngressPerformanceConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -282,6 +285,7 @@ export class DcRouter {
|
|||||||
// ACME configuration (DB-backed singleton, replaces tls.contactEmail)
|
// ACME configuration (DB-backed singleton, replaces tls.contactEmail)
|
||||||
public acmeConfigManager?: AcmeConfigManager;
|
public acmeConfigManager?: AcmeConfigManager;
|
||||||
public emailDomainManager?: EmailDomainManager;
|
public emailDomainManager?: EmailDomainManager;
|
||||||
|
public securityPolicyManager?: SecurityPolicyManager;
|
||||||
|
|
||||||
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
|
// Auto-discovered public IP (populated by generateAuthoritativeRecords)
|
||||||
public detectedPublicIp: string | null = null;
|
public detectedPublicIp: string | null = null;
|
||||||
@@ -469,12 +473,36 @@ export class DcRouter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecurityPolicyManager: optional, depends on DcRouterDb — owns IP intelligence
|
||||||
|
// and compiles the global block policy for SmartProxy and remote ingress edges.
|
||||||
|
if (this.options.dbConfig?.enabled !== false) {
|
||||||
|
this.serviceManager.addService(
|
||||||
|
new plugins.taskbuffer.Service('SecurityPolicyManager')
|
||||||
|
.optional()
|
||||||
|
.dependsOn('DcRouterDb')
|
||||||
|
.withStart(async () => {
|
||||||
|
this.securityPolicyManager = new SecurityPolicyManager({
|
||||||
|
onPolicyChanged: () => this.applySecurityPolicy(),
|
||||||
|
});
|
||||||
|
await this.securityPolicyManager.start();
|
||||||
|
})
|
||||||
|
.withStop(async () => {
|
||||||
|
if (this.securityPolicyManager) {
|
||||||
|
await this.securityPolicyManager.stop();
|
||||||
|
this.securityPolicyManager = undefined;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.withRetry({ maxRetries: 1, baseDelayMs: 500 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
|
// SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
|
||||||
const smartProxyDeps: string[] = [];
|
const smartProxyDeps: string[] = [];
|
||||||
if (this.options.dbConfig?.enabled !== false) {
|
if (this.options.dbConfig?.enabled !== false) {
|
||||||
smartProxyDeps.push('DcRouterDb');
|
smartProxyDeps.push('DcRouterDb');
|
||||||
smartProxyDeps.push('DnsManager');
|
smartProxyDeps.push('DnsManager');
|
||||||
smartProxyDeps.push('AcmeConfigManager');
|
smartProxyDeps.push('AcmeConfigManager');
|
||||||
|
smartProxyDeps.push('SecurityPolicyManager');
|
||||||
}
|
}
|
||||||
this.serviceManager.addService(
|
this.serviceManager.addService(
|
||||||
new plugins.taskbuffer.Service('SmartProxy')
|
new plugins.taskbuffer.Service('SmartProxy')
|
||||||
@@ -570,12 +598,16 @@ export class DcRouter {
|
|||||||
this.referenceResolver,
|
this.referenceResolver,
|
||||||
// Sync routes to RemoteIngressManager whenever routes change,
|
// Sync routes to RemoteIngressManager whenever routes change,
|
||||||
// then push updated derived ports to the Rust hub binary
|
// then push updated derived ports to the Rust hub binary
|
||||||
(routes) => {
|
async (routes) => {
|
||||||
if (this.remoteIngressManager) {
|
if (this.remoteIngressManager) {
|
||||||
this.remoteIngressManager.setRoutes(routes as any[]);
|
this.remoteIngressManager.setRoutes(routes as any[]);
|
||||||
}
|
}
|
||||||
if (this.tunnelManager) {
|
if (this.tunnelManager) {
|
||||||
this.tunnelManager.syncAllowedEdges();
|
try {
|
||||||
|
await this.tunnelManager.syncAllowedEdges();
|
||||||
|
} catch (err: unknown) {
|
||||||
|
logger.log('error', `Failed to sync Remote Ingress allowed edges: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@@ -965,6 +997,12 @@ export class DcRouter {
|
|||||||
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
logger.log('info', 'HTTP/3: Augmented qualifying HTTPS routes with QUIC/H3 configuration');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compiledSecurityPolicy = await this.securityPolicyManager?.compileSmartProxyPolicy();
|
||||||
|
const mergedSecurityPolicy = this.mergeSecurityPolicies(
|
||||||
|
(this.options.smartProxyConfig as any)?.securityPolicy,
|
||||||
|
compiledSecurityPolicy,
|
||||||
|
);
|
||||||
|
|
||||||
// If we have routes or need a basic SmartProxy instance, create it
|
// If we have routes or need a basic SmartProxy instance, create it
|
||||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||||
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
logger.log('info', 'Setting up SmartProxy with combined configuration');
|
||||||
@@ -996,6 +1034,7 @@ export class DcRouter {
|
|||||||
// --- always set by dcrouter (after spread) ---
|
// --- always set by dcrouter (after spread) ---
|
||||||
routes,
|
routes,
|
||||||
acme: acmeConfig,
|
acme: acmeConfig,
|
||||||
|
...(mergedSecurityPolicy ? { securityPolicy: mergedSecurityPolicy } as any : {}),
|
||||||
certStore: {
|
certStore: {
|
||||||
loadAll: async () => {
|
loadAll: async () => {
|
||||||
const docs = await ProxyCertDoc.findAll();
|
const docs = await ProxyCertDoc.findAll();
|
||||||
@@ -1120,7 +1159,12 @@ export class DcRouter {
|
|||||||
// to SmartProxy with PROXY protocol v1 headers to preserve client IPs.
|
// to SmartProxy with PROXY protocol v1 headers to preserve client IPs.
|
||||||
if (this.options.remoteIngressConfig?.enabled) {
|
if (this.options.remoteIngressConfig?.enabled) {
|
||||||
smartProxyConfig.acceptProxyProtocol = true;
|
smartProxyConfig.acceptProxyProtocol = true;
|
||||||
smartProxyConfig.proxyIPs = ['127.0.0.1'];
|
if (!smartProxyConfig.proxyIPs) {
|
||||||
|
smartProxyConfig.proxyIPs = [];
|
||||||
|
}
|
||||||
|
if (!smartProxyConfig.proxyIPs.includes('127.0.0.1')) {
|
||||||
|
smartProxyConfig.proxyIPs.push('127.0.0.1');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
|
// VPN uses socket mode with PP v2 — SmartProxy must accept proxy protocol from localhost
|
||||||
@@ -1233,8 +1277,60 @@ export class DcRouter {
|
|||||||
logger.log('info', `SmartProxy started with ${routes.length} routes`);
|
logger.log('info', `SmartProxy started with ${routes.length} routes`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async applySecurityPolicy(): Promise<void> {
|
||||||
|
if (!this.securityPolicyManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const compiledSmartProxyPolicy = await this.securityPolicyManager.compileSmartProxyPolicy();
|
||||||
|
const mergedSecurityPolicy = this.mergeSecurityPolicies(
|
||||||
|
(this.options.smartProxyConfig as any)?.securityPolicy,
|
||||||
|
compiledSmartProxyPolicy,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.smartProxy && mergedSecurityPolicy) {
|
||||||
|
const smartProxyWithPolicyApi = this.smartProxy as any;
|
||||||
|
if (typeof smartProxyWithPolicyApi.updateSecurityPolicy === 'function') {
|
||||||
|
await smartProxyWithPolicyApi.updateSecurityPolicy(mergedSecurityPolicy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firewallConfig = await this.securityPolicyManager.compileRemoteIngressFirewall();
|
||||||
|
if (this.remoteIngressManager) {
|
||||||
|
(this.remoteIngressManager as any).setFirewallConfig?.(firewallConfig);
|
||||||
|
}
|
||||||
|
if (this.tunnelManager) {
|
||||||
|
await this.tunnelManager.syncAllowedEdges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mergeSecurityPolicies(
|
||||||
|
...policies: Array<Partial<ISecurityCompiledPolicy> | undefined>
|
||||||
|
): ISecurityCompiledPolicy | undefined {
|
||||||
|
const blockedIps = new Set<string>();
|
||||||
|
const blockedCidrs = new Set<string>();
|
||||||
|
|
||||||
|
for (const policy of policies) {
|
||||||
|
for (const ip of policy?.blockedIps || []) {
|
||||||
|
if (ip) blockedIps.add(ip);
|
||||||
|
}
|
||||||
|
for (const cidr of policy?.blockedCidrs || []) {
|
||||||
|
if (cidr) blockedCidrs.add(cidr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockedIps.size === 0 && blockedCidrs.size === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockedIps: [...blockedIps].sort(),
|
||||||
|
blockedCidrs: [...blockedCidrs].sort(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate SmartProxy routes for email configuration
|
* Generate SmartProxy routes for email configuration
|
||||||
@@ -2221,6 +2317,9 @@ export class DcRouter {
|
|||||||
// Initialize the edge registration manager
|
// Initialize the edge registration manager
|
||||||
this.remoteIngressManager = new RemoteIngressManager();
|
this.remoteIngressManager = new RemoteIngressManager();
|
||||||
await this.remoteIngressManager.initialize();
|
await this.remoteIngressManager.initialize();
|
||||||
|
this.remoteIngressManager.setFirewallConfig(
|
||||||
|
await this.securityPolicyManager?.compileRemoteIngressFirewall(),
|
||||||
|
);
|
||||||
|
|
||||||
// Pass current bootstrap routes so the manager can derive edge ports initially.
|
// Pass current bootstrap routes so the manager can derive edge ports initially.
|
||||||
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
|
// Once RouteConfigManager applies the full DB set, the onRoutesApplied callback
|
||||||
@@ -2270,6 +2369,7 @@ export class DcRouter {
|
|||||||
tunnelPort: riCfg.tunnelPort ?? 8443,
|
tunnelPort: riCfg.tunnelPort ?? 8443,
|
||||||
targetHost: '127.0.0.1',
|
targetHost: '127.0.0.1',
|
||||||
tls: tlsConfig,
|
tls: tlsConfig,
|
||||||
|
performance: riCfg.performance,
|
||||||
});
|
});
|
||||||
await this.tunnelManager.start();
|
await this.tunnelManager.start();
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export class RouteConfigManager {
|
|||||||
private getHttp3Config?: () => IHttp3Config | undefined,
|
private getHttp3Config?: () => IHttp3Config | undefined,
|
||||||
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
|
private getVpnClientIpsForRoute?: (route: IDcRouterRouteConfig, routeId?: string) => TIpAllowEntry[],
|
||||||
private referenceResolver?: ReferenceResolver,
|
private referenceResolver?: ReferenceResolver,
|
||||||
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void,
|
private onRoutesApplied?: (routes: plugins.smartproxy.IRouteConfig[]) => void | Promise<void>,
|
||||||
private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
|
private getRuntimeRoutes?: () => plugins.smartproxy.IRouteConfig[],
|
||||||
private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
|
private hydrateStoredRoute?: (storedRoute: IRoute) => plugins.smartproxy.IRouteConfig | undefined,
|
||||||
) {}
|
) {}
|
||||||
@@ -540,7 +540,7 @@ export class RouteConfigManager {
|
|||||||
|
|
||||||
// Notify listeners (e.g. RemoteIngressManager) of the route set
|
// Notify listeners (e.g. RemoteIngressManager) of the route set
|
||||||
if (this.onRoutesApplied) {
|
if (this.onRoutesApplied) {
|
||||||
this.onRoutesApplied(enabledRoutes);
|
await this.onRoutesApplied(enabledRoutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.routes.size} total)`);
|
logger.log('info', `Applied ${enabledRoutes.length} routes to SmartProxy (${this.routes.size} total)`);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||||
|
import type { IIpIntelligenceRecord } from '../../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||||
|
|
||||||
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
|
export class IpIntelligenceDoc extends plugins.smartdata.SmartDataDbDoc<IpIntelligenceDoc, IpIntelligenceDoc> implements IIpIntelligenceRecord {
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public ipAddress!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public asn: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public asnOrg: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public registrantOrg: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public registrantCountry: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public networkRange: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public abuseContact: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public country: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public countryCode: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public city: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public latitude: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public longitude: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public accuracyRadius: number | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public timezone: string | null = null;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public firstSeenAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public lastSeenAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public updatedAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public seenCount: number = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findByIp(ipAddress: string): Promise<IpIntelligenceDoc | null> {
|
||||||
|
return await IpIntelligenceDoc.getInstance({ ipAddress });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findAll(): Promise<IpIntelligenceDoc[]> {
|
||||||
|
return await IpIntelligenceDoc.getInstances({});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||||
|
import type { ISecurityBlockRule, TSecurityBlockRuleMatchMode, TSecurityBlockRuleType } from '../../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||||
|
|
||||||
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
|
export class SecurityBlockRuleDoc extends plugins.smartdata.SmartDataDbDoc<SecurityBlockRuleDoc, SecurityBlockRuleDoc> implements ISecurityBlockRule {
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public type!: TSecurityBlockRuleType;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public value!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public enabled: boolean = true;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public reason?: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public updatedAt: number = Date.now();
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdBy: string = 'system';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findById(id: string): Promise<SecurityBlockRuleDoc | null> {
|
||||||
|
return await SecurityBlockRuleDoc.getInstance({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findAll(): Promise<SecurityBlockRuleDoc[]> {
|
||||||
|
return await SecurityBlockRuleDoc.getInstances({});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findEnabled(): Promise<SecurityBlockRuleDoc[]> {
|
||||||
|
return await SecurityBlockRuleDoc.getInstances({ enabled: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
||||||
|
import type { ISecurityPolicyAuditEvent } from '../../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
||||||
|
|
||||||
|
@plugins.smartdata.Collection(() => getDb())
|
||||||
|
export class SecurityPolicyAuditDoc extends plugins.smartdata.SmartDataDbDoc<SecurityPolicyAuditDoc, SecurityPolicyAuditDoc> implements ISecurityPolicyAuditEvent {
|
||||||
|
@plugins.smartdata.unI()
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public id!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public action!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public actor!: string;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public details!: Record<string, unknown>;
|
||||||
|
|
||||||
|
@plugins.smartdata.svDb()
|
||||||
|
public createdAt: number = Date.now();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async findRecent(limit = 100): Promise<SecurityPolicyAuditDoc[]> {
|
||||||
|
const docs = await SecurityPolicyAuditDoc.getInstances({});
|
||||||
|
return docs.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
// Cached/TTL document classes
|
// Cached/TTL document classes
|
||||||
export * from './classes.cached.email.js';
|
export * from './classes.cached.email.js';
|
||||||
export * from './classes.cached.ip.reputation.js';
|
export * from './classes.cached.ip.reputation.js';
|
||||||
|
export * from './classes.ip-intelligence.doc.js';
|
||||||
|
export * from './classes.security-block-rule.doc.js';
|
||||||
|
export * from './classes.security-policy-audit.doc.js';
|
||||||
|
|
||||||
// Config document classes
|
// Config document classes
|
||||||
export * from './classes.route.doc.js';
|
export * from './classes.route.doc.js';
|
||||||
|
|||||||
@@ -725,6 +725,8 @@ 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()]);
|
||||||
|
|
||||||
// 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();
|
||||||
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export class ConfigHandler {
|
|||||||
hubDomain: riCfg?.hubDomain || null,
|
hubDomain: riCfg?.hubDomain || null,
|
||||||
tlsMode,
|
tlsMode,
|
||||||
connectedEdgeIps,
|
connectedEdgeIps,
|
||||||
|
performance: riCfg?.performance,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class RemoteIngressHandler {
|
|||||||
...e,
|
...e,
|
||||||
secret: '********', // Never expose secrets via API
|
secret: '********', // Never expose secrets via API
|
||||||
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
effectiveListenPorts: manager.getEffectiveListenPorts(e),
|
||||||
|
effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(e),
|
||||||
manualPorts: breakdown.manual,
|
manualPorts: breakdown.manual,
|
||||||
derivedPorts: breakdown.derived,
|
derivedPorts: breakdown.derived,
|
||||||
};
|
};
|
||||||
@@ -133,6 +134,7 @@ export class RemoteIngressHandler {
|
|||||||
...edge,
|
...edge,
|
||||||
secret: '********',
|
secret: '********',
|
||||||
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
|
effectiveListenPorts: manager.getEffectiveListenPorts(edge),
|
||||||
|
effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(edge),
|
||||||
manualPorts: breakdown.manual,
|
manualPorts: breakdown.manual,
|
||||||
derivedPorts: breakdown.derived,
|
derivedPorts: breakdown.derived,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -157,6 +157,113 @@ export class SecurityHandler {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityBlockRules>(
|
||||||
|
'listSecurityBlockRules',
|
||||||
|
async () => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return { rules: manager ? await manager.listBlockRules() : [] };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListIpIntelligence>(
|
||||||
|
'listIpIntelligence',
|
||||||
|
async () => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return { records: manager ? await manager.listIpIntelligence() : [] };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCompiledSecurityPolicy>(
|
||||||
|
'getCompiledSecurityPolicy',
|
||||||
|
async () => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return {
|
||||||
|
policy: manager
|
||||||
|
? await manager.compilePolicy()
|
||||||
|
: { blockedIps: [], blockedCidrs: [] },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
router.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityPolicyAudit>(
|
||||||
|
'listSecurityPolicyAudit',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
return { events: manager ? await manager.listAuditEvents(dataArg.limit || 100) : [] };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const adminRouter = this.opsServerRef.adminRouter;
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecurityBlockRule>(
|
||||||
|
'createSecurityBlockRule',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const rule = await manager.createBlockRule({
|
||||||
|
type: dataArg.type,
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
}, dataArg.identity.userId);
|
||||||
|
return { success: true, rule };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecurityBlockRule>(
|
||||||
|
'updateSecurityBlockRule',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const rule = await manager.updateBlockRule(dataArg.id, {
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
}, dataArg.identity.userId);
|
||||||
|
return rule ? { success: true, rule } : { success: false, message: 'Rule not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecurityBlockRule>(
|
||||||
|
'deleteSecurityBlockRule',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const success = await manager.deleteBlockRule(dataArg.id, dataArg.identity.userId);
|
||||||
|
return { success, message: success ? undefined : 'Rule not found' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
adminRouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RefreshIpIntelligence>(
|
||||||
|
'refreshIpIntelligence',
|
||||||
|
async (dataArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
|
||||||
|
if (!manager) return { success: false, message: 'Security policy manager not initialized' };
|
||||||
|
const record = await manager.refreshIpIntelligence(dataArg.ipAddress);
|
||||||
|
return record
|
||||||
|
? { success: true, record }
|
||||||
|
: { success: false, message: 'IP address is invalid or not public' };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectSecurityMetrics(): Promise<{
|
private async collectSecurityMetrics(): Promise<{
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import * as plugins from '../plugins.js';
|
|||||||
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
||||||
import { RemoteIngressEdgeDoc } from '../db/index.js';
|
import { RemoteIngressEdgeDoc } from '../db/index.js';
|
||||||
|
|
||||||
|
interface IRemoteIngressFirewallConfig {
|
||||||
|
blockedIps?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +35,7 @@ function extractPorts(portRange: number | Array<number | { from: number; to: num
|
|||||||
export class RemoteIngressManager {
|
export class RemoteIngressManager {
|
||||||
private edges: Map<string, IRemoteIngress> = new Map();
|
private edges: Map<string, IRemoteIngress> = new Map();
|
||||||
private routes: IDcRouterRouteConfig[] = [];
|
private routes: IDcRouterRouteConfig[] = [];
|
||||||
|
private firewallConfig?: IRemoteIngressFirewallConfig;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
@@ -69,6 +74,13 @@ export class RemoteIngressManager {
|
|||||||
this.routes = routes;
|
this.routes = routes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the full desired firewall snapshot pushed to all edges.
|
||||||
|
*/
|
||||||
|
public setFirewallConfig(firewallConfig?: IRemoteIngressFirewallConfig): void {
|
||||||
|
this.firewallConfig = firewallConfig;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
|
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
|
||||||
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
|
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
|
||||||
@@ -305,8 +317,8 @@ export class RemoteIngressManager {
|
|||||||
* Get the list of allowed edges (enabled only) for the Rust hub.
|
* Get the list of allowed edges (enabled only) for the Rust hub.
|
||||||
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
|
* Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
|
||||||
*/
|
*/
|
||||||
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> {
|
public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> {
|
||||||
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> = [];
|
const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> = [];
|
||||||
for (const edge of this.edges.values()) {
|
for (const edge of this.edges.values()) {
|
||||||
if (edge.enabled) {
|
if (edge.enabled) {
|
||||||
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
|
||||||
@@ -315,6 +327,7 @@ export class RemoteIngressManager {
|
|||||||
secret: edge.secret,
|
secret: edge.secret,
|
||||||
listenPorts: this.getEffectiveListenPorts(edge),
|
listenPorts: this.getEffectiveListenPorts(edge),
|
||||||
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
|
||||||
|
...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ITunnelManagerConfig {
|
|||||||
certPem?: string;
|
certPem?: string;
|
||||||
keyPem?: string;
|
keyPem?: string;
|
||||||
};
|
};
|
||||||
|
performance?: import('../../ts_interfaces/data/remoteingress.js').IRemoteIngressPerformanceConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +21,7 @@ export class TunnelManager {
|
|||||||
private config: ITunnelManagerConfig;
|
private config: ITunnelManagerConfig;
|
||||||
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
||||||
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
|
private reconcileInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private syncChain: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@@ -66,7 +68,8 @@ export class TunnelManager {
|
|||||||
tunnelPort: this.config.tunnelPort ?? 8443,
|
tunnelPort: this.config.tunnelPort ?? 8443,
|
||||||
targetHost: this.config.targetHost ?? '127.0.0.1',
|
targetHost: this.config.targetHost ?? '127.0.0.1',
|
||||||
tls: this.config.tls,
|
tls: this.config.tls,
|
||||||
});
|
...(this.config.performance ? { performance: this.config.performance } : {}),
|
||||||
|
} as any);
|
||||||
|
|
||||||
// Send allowed edges to the hub
|
// Send allowed edges to the hub
|
||||||
await this.syncAllowedEdges();
|
await this.syncAllowedEdges();
|
||||||
@@ -107,20 +110,23 @@ export class TunnelManager {
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
existing.activeTunnels = rustEdge.activeStreams;
|
existing.activeTunnels = rustEdge.activeStreams;
|
||||||
existing.lastHeartbeat = Date.now();
|
existing.lastHeartbeat = Date.now();
|
||||||
|
this.applyRustStatus(existing, rustEdge);
|
||||||
// Update peer address if available from Rust hub
|
// Update peer address if available from Rust hub
|
||||||
if (rustEdge.peerAddr) {
|
if (rustEdge.peerAddr) {
|
||||||
existing.publicIp = rustEdge.peerAddr;
|
existing.publicIp = rustEdge.peerAddr;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Missed edgeConnected event — add entry
|
// Missed edgeConnected event — add entry
|
||||||
this.edgeStatuses.set(rustEdge.edgeId, {
|
const status: IRemoteIngressStatus = {
|
||||||
edgeId: rustEdge.edgeId,
|
edgeId: rustEdge.edgeId,
|
||||||
connected: true,
|
connected: true,
|
||||||
publicIp: rustEdge.peerAddr || null,
|
publicIp: rustEdge.peerAddr || null,
|
||||||
activeTunnels: rustEdge.activeStreams,
|
activeTunnels: rustEdge.activeStreams,
|
||||||
lastHeartbeat: Date.now(),
|
lastHeartbeat: Date.now(),
|
||||||
connectedAt: rustEdge.connectedAt * 1000,
|
connectedAt: rustEdge.connectedAt * 1000,
|
||||||
});
|
};
|
||||||
|
this.applyRustStatus(status, rustEdge);
|
||||||
|
this.edgeStatuses.set(rustEdge.edgeId, status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +143,22 @@ export class TunnelManager {
|
|||||||
* Call this after creating/deleting/updating edges.
|
* Call this after creating/deleting/updating edges.
|
||||||
*/
|
*/
|
||||||
public async syncAllowedEdges(): Promise<void> {
|
public async syncAllowedEdges(): Promise<void> {
|
||||||
const edges = this.manager.getAllowedEdges();
|
const run = this.syncChain.catch(() => {}).then(async () => {
|
||||||
await this.hub.updateAllowedEdges(edges);
|
const edges = this.manager.getAllowedEdges();
|
||||||
|
await this.hub.updateAllowedEdges(edges as any);
|
||||||
|
});
|
||||||
|
this.syncChain = run;
|
||||||
|
await run;
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyRustStatus(status: IRemoteIngressStatus, rustEdge: any): void {
|
||||||
|
status.transportMode = rustEdge.transportMode;
|
||||||
|
status.fallbackUsed = rustEdge.fallbackUsed;
|
||||||
|
status.performance = rustEdge.performance;
|
||||||
|
status.flowControl = rustEdge.flowControl;
|
||||||
|
status.queues = rustEdge.queues;
|
||||||
|
status.traffic = rustEdge.traffic;
|
||||||
|
status.udp = rustEdge.udp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
|
import { IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../db/index.js';
|
||||||
|
import type {
|
||||||
|
IIpIntelligenceRecord,
|
||||||
|
ISecurityBlockRule,
|
||||||
|
ISecurityCompiledPolicy,
|
||||||
|
ISecurityPolicyAuditEvent,
|
||||||
|
TSecurityBlockRuleMatchMode,
|
||||||
|
TSecurityBlockRuleType,
|
||||||
|
} from '../../ts_interfaces/data/security-policy.js';
|
||||||
|
|
||||||
|
export interface ISecurityPolicyManagerOptions {
|
||||||
|
intelligenceRefreshMs?: number;
|
||||||
|
onPolicyChanged?: () => void | Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoteIngressFirewallSnapshot {
|
||||||
|
blockedIps?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecurityPolicyManager {
|
||||||
|
private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
|
||||||
|
cacheTtl: 24 * 60 * 60 * 1000,
|
||||||
|
});
|
||||||
|
private readonly intelligenceRefreshMs: number;
|
||||||
|
private readonly inFlightObservations = new Set<string>();
|
||||||
|
private readonly onPolicyChanged?: () => void | Promise<void>;
|
||||||
|
|
||||||
|
constructor(options: ISecurityPolicyManagerOptions = {}) {
|
||||||
|
this.intelligenceRefreshMs = options.intelligenceRefreshMs ?? 24 * 60 * 60 * 1000;
|
||||||
|
this.onPolicyChanged = options.onPolicyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
logger.log('info', 'SecurityPolicyManager started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
await this.smartNetwork.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async observeIps(ips: string[]): Promise<void> {
|
||||||
|
const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
|
||||||
|
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
|
||||||
|
const ip = this.normalizeIp(ipAddress);
|
||||||
|
if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inFlightObservations.add(ip);
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
let doc = await IpIntelligenceDoc.findByIp(ip);
|
||||||
|
if (doc && !options.force && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
||||||
|
if (now - doc.lastSeenAt > 60_000) {
|
||||||
|
doc.lastSeenAt = now;
|
||||||
|
doc.seenCount = (doc.seenCount || 0) + 1;
|
||||||
|
await doc.save();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intelligence = await this.smartNetwork.getIpIntelligence(ip);
|
||||||
|
if (!doc) {
|
||||||
|
doc = new IpIntelligenceDoc();
|
||||||
|
doc.ipAddress = ip;
|
||||||
|
doc.firstSeenAt = now;
|
||||||
|
}
|
||||||
|
Object.assign(doc, intelligence);
|
||||||
|
doc.lastSeenAt = now;
|
||||||
|
doc.updatedAt = now;
|
||||||
|
doc.seenCount = (doc.seenCount || 0) + 1;
|
||||||
|
await doc.save();
|
||||||
|
|
||||||
|
if (await this.matchesAnyReactiveRule(doc)) {
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
this.inFlightObservations.delete(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listBlockRules(): Promise<ISecurityBlockRule[]> {
|
||||||
|
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 refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
|
||||||
|
const ip = this.normalizeIp(ipAddress);
|
||||||
|
if (!ip || !this.isPublicIp(ip)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await this.observeIp(ip, { force: true });
|
||||||
|
const doc = await IpIntelligenceDoc.findByIp(ip);
|
||||||
|
return doc ? this.intelligenceFromDoc(doc) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
|
||||||
|
return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
|
||||||
|
id: doc.id,
|
||||||
|
action: doc.action,
|
||||||
|
actor: doc.actor,
|
||||||
|
details: doc.details,
|
||||||
|
createdAt: doc.createdAt,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private intelligenceFromDoc(doc: IpIntelligenceDoc): IIpIntelligenceRecord {
|
||||||
|
return {
|
||||||
|
ipAddress: doc.ipAddress,
|
||||||
|
asn: doc.asn,
|
||||||
|
asnOrg: doc.asnOrg,
|
||||||
|
registrantOrg: doc.registrantOrg,
|
||||||
|
registrantCountry: doc.registrantCountry,
|
||||||
|
networkRange: doc.networkRange,
|
||||||
|
abuseContact: doc.abuseContact,
|
||||||
|
country: doc.country,
|
||||||
|
countryCode: doc.countryCode,
|
||||||
|
city: doc.city,
|
||||||
|
latitude: doc.latitude,
|
||||||
|
longitude: doc.longitude,
|
||||||
|
accuracyRadius: doc.accuracyRadius,
|
||||||
|
timezone: doc.timezone,
|
||||||
|
firstSeenAt: doc.firstSeenAt,
|
||||||
|
lastSeenAt: doc.lastSeenAt,
|
||||||
|
updatedAt: doc.updatedAt,
|
||||||
|
seenCount: doc.seenCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createBlockRule(input: {
|
||||||
|
type: TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}, actor = 'system'): Promise<ISecurityBlockRule> {
|
||||||
|
const now = Date.now();
|
||||||
|
const doc = new SecurityBlockRuleDoc();
|
||||||
|
doc.id = plugins.uuid.v4();
|
||||||
|
doc.type = input.type;
|
||||||
|
doc.value = input.value.trim();
|
||||||
|
doc.matchMode = input.matchMode;
|
||||||
|
doc.reason = input.reason;
|
||||||
|
doc.enabled = input.enabled ?? true;
|
||||||
|
doc.createdAt = now;
|
||||||
|
doc.updatedAt = now;
|
||||||
|
doc.createdBy = actor;
|
||||||
|
await doc.save();
|
||||||
|
await this.writeAudit('createBlockRule', actor, { rule: this.ruleFromDoc(doc) });
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
return this.ruleFromDoc(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateBlockRule(id: string, patch: Partial<Pick<ISecurityBlockRule, 'value' | 'matchMode' | 'reason' | 'enabled'>>, actor = 'system'): Promise<ISecurityBlockRule | null> {
|
||||||
|
const doc = await SecurityBlockRuleDoc.findById(id);
|
||||||
|
if (!doc) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (patch.value !== undefined) doc.value = patch.value.trim();
|
||||||
|
if (patch.matchMode !== undefined) doc.matchMode = patch.matchMode;
|
||||||
|
if (patch.reason !== undefined) doc.reason = patch.reason;
|
||||||
|
if (patch.enabled !== undefined) doc.enabled = patch.enabled;
|
||||||
|
doc.updatedAt = Date.now();
|
||||||
|
await doc.save();
|
||||||
|
await this.writeAudit('updateBlockRule', actor, { id, patch });
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
return this.ruleFromDoc(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteBlockRule(id: string, actor = 'system'): Promise<boolean> {
|
||||||
|
const doc = await SecurityBlockRuleDoc.findById(id);
|
||||||
|
if (!doc) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await doc.delete();
|
||||||
|
await this.writeAudit('deleteBlockRule', actor, { id });
|
||||||
|
await this.notifyPolicyChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compilePolicy(): Promise<ISecurityCompiledPolicy> {
|
||||||
|
const rules = await SecurityBlockRuleDoc.findEnabled();
|
||||||
|
const intelligenceDocs = await IpIntelligenceDoc.findAll();
|
||||||
|
const blockedIps = new Set<string>();
|
||||||
|
const blockedCidrs = new Set<string>();
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
const normalizedValue = rule.value.trim();
|
||||||
|
if (!normalizedValue) continue;
|
||||||
|
|
||||||
|
if (rule.type === 'ip') {
|
||||||
|
const ip = this.normalizeIp(normalizedValue);
|
||||||
|
if (ip && plugins.net.isIP(ip)) blockedIps.add(ip);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.type === 'cidr') {
|
||||||
|
const cidr = this.normalizeCidr(normalizedValue);
|
||||||
|
if (cidr) blockedCidrs.add(cidr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const doc of intelligenceDocs) {
|
||||||
|
if (!this.ruleMatchesIntelligence(rule, doc)) continue;
|
||||||
|
const cidr = this.normalizeCidr(doc.networkRange || '');
|
||||||
|
if (cidr) {
|
||||||
|
blockedCidrs.add(cidr);
|
||||||
|
} else if (this.normalizeIp(doc.ipAddress)) {
|
||||||
|
blockedIps.add(this.normalizeIp(doc.ipAddress)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockedIps: [...blockedIps].sort(),
|
||||||
|
blockedCidrs: [...blockedCidrs].sort(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compileSmartProxyPolicy(): Promise<ISecurityCompiledPolicy> {
|
||||||
|
return await this.compilePolicy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot | undefined> {
|
||||||
|
const policy = await this.compilePolicy();
|
||||||
|
const blockedIps = [
|
||||||
|
...policy.blockedIps.filter((ip) => plugins.net.isIP(ip) === 4),
|
||||||
|
...policy.blockedCidrs.filter((cidr) => plugins.net.isIP(cidr.split('/')[0]) === 4),
|
||||||
|
];
|
||||||
|
return blockedIps.length > 0 ? { blockedIps } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
|
||||||
|
const rules = await SecurityBlockRuleDoc.findEnabled();
|
||||||
|
return rules.some((rule) => rule.type === 'asn' || rule.type === 'organization'
|
||||||
|
? this.ruleMatchesIntelligence(rule, doc)
|
||||||
|
: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleMatchesIntelligence(rule: SecurityBlockRuleDoc, doc: IpIntelligenceDoc): boolean {
|
||||||
|
const value = rule.value.trim().toLowerCase();
|
||||||
|
if (!value) return false;
|
||||||
|
|
||||||
|
if (rule.type === 'asn') {
|
||||||
|
return String(doc.asn ?? '') === value.replace(/^as/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.type === 'organization') {
|
||||||
|
const candidates = [doc.asnOrg, doc.registrantOrg]
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((candidate) => candidate!.toLowerCase());
|
||||||
|
if (rule.matchMode === 'exact') {
|
||||||
|
return candidates.some((candidate) => candidate === value);
|
||||||
|
}
|
||||||
|
return candidates.some((candidate) => candidate.includes(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeIp(ipAddress: string): string | undefined {
|
||||||
|
const ip = ipAddress.trim();
|
||||||
|
if (ip.startsWith('::ffff:')) {
|
||||||
|
return ip.slice('::ffff:'.length);
|
||||||
|
}
|
||||||
|
return plugins.net.isIP(ip) ? ip : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeCidr(value: string): string | undefined {
|
||||||
|
const [rawIp, rawPrefix] = value.trim().split('/');
|
||||||
|
if (!rawIp || !rawPrefix) return undefined;
|
||||||
|
const ip = this.normalizeIp(rawIp);
|
||||||
|
if (!ip) return undefined;
|
||||||
|
const prefix = Number(rawPrefix);
|
||||||
|
const maxPrefix = plugins.net.isIP(ip) === 4 ? 32 : 128;
|
||||||
|
if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) return undefined;
|
||||||
|
return `${ip}/${prefix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPublicIp(ip: string): boolean {
|
||||||
|
const family = plugins.net.isIP(ip);
|
||||||
|
if (family === 4) {
|
||||||
|
const parts = ip.split('.').map((part) => Number(part));
|
||||||
|
const [a, b] = parts;
|
||||||
|
if (a === 10 || a === 127 || a === 0 || a >= 224) return false;
|
||||||
|
if (a === 100 && b >= 64 && b <= 127) return false;
|
||||||
|
if (a === 169 && b === 254) return false;
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return false;
|
||||||
|
if (a === 192 && b === 168) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (family === 6) {
|
||||||
|
const lower = ip.toLowerCase();
|
||||||
|
if (lower === '::1' || lower === '::') return false;
|
||||||
|
if (lower.startsWith('fe80:') || lower.startsWith('fc') || lower.startsWith('fd')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ruleFromDoc(doc: SecurityBlockRuleDoc): ISecurityBlockRule {
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
type: doc.type,
|
||||||
|
value: doc.value,
|
||||||
|
matchMode: doc.matchMode,
|
||||||
|
enabled: doc.enabled,
|
||||||
|
reason: doc.reason,
|
||||||
|
createdAt: doc.createdAt,
|
||||||
|
updatedAt: doc.updatedAt,
|
||||||
|
createdBy: doc.createdBy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeAudit(action: string, actor: string, details: Record<string, unknown>): Promise<void> {
|
||||||
|
const doc = new SecurityPolicyAuditDoc();
|
||||||
|
doc.id = plugins.uuid.v4();
|
||||||
|
doc.action = action;
|
||||||
|
doc.actor = actor;
|
||||||
|
doc.details = details;
|
||||||
|
doc.createdAt = Date.now();
|
||||||
|
await doc.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async notifyPolicyChanged(): Promise<void> {
|
||||||
|
if (this.onPolicyChanged) {
|
||||||
|
await this.onPolicyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,4 +18,10 @@ export {
|
|||||||
ThreatCategory,
|
ThreatCategory,
|
||||||
type IScanResult,
|
type IScanResult,
|
||||||
type IContentScannerOptions
|
type IContentScannerOptions
|
||||||
} from './classes.contentscanner.js';
|
} from './classes.contentscanner.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
SecurityPolicyManager,
|
||||||
|
type ISecurityPolicyManagerOptions,
|
||||||
|
type IRemoteIngressFirewallSnapshot,
|
||||||
|
} from './classes.security-policy-manager.js';
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ export class RemoteIngress {
|
|||||||
public name: string;
|
public name: string;
|
||||||
public secret: string;
|
public secret: string;
|
||||||
public listenPorts: number[];
|
public listenPorts: number[];
|
||||||
|
public listenPortsUdp?: number[];
|
||||||
public enabled: boolean;
|
public enabled: boolean;
|
||||||
public autoDerivePorts: boolean;
|
public autoDerivePorts: boolean;
|
||||||
public tags?: string[];
|
public tags?: string[];
|
||||||
public createdAt: number;
|
public createdAt: number;
|
||||||
public updatedAt: number;
|
public updatedAt: number;
|
||||||
public effectiveListenPorts?: number[];
|
public effectiveListenPorts?: number[];
|
||||||
|
public effectiveListenPortsUdp?: number[];
|
||||||
public manualPorts?: number[];
|
public manualPorts?: number[];
|
||||||
public derivedPorts?: number[];
|
public derivedPorts?: number[];
|
||||||
|
|
||||||
@@ -24,12 +26,14 @@ export class RemoteIngress {
|
|||||||
this.name = data.name;
|
this.name = data.name;
|
||||||
this.secret = data.secret;
|
this.secret = data.secret;
|
||||||
this.listenPorts = data.listenPorts;
|
this.listenPorts = data.listenPorts;
|
||||||
|
this.listenPortsUdp = data.listenPortsUdp;
|
||||||
this.enabled = data.enabled;
|
this.enabled = data.enabled;
|
||||||
this.autoDerivePorts = data.autoDerivePorts;
|
this.autoDerivePorts = data.autoDerivePorts;
|
||||||
this.tags = data.tags;
|
this.tags = data.tags;
|
||||||
this.createdAt = data.createdAt;
|
this.createdAt = data.createdAt;
|
||||||
this.updatedAt = data.updatedAt;
|
this.updatedAt = data.updatedAt;
|
||||||
this.effectiveListenPorts = data.effectiveListenPorts;
|
this.effectiveListenPorts = data.effectiveListenPorts;
|
||||||
|
this.effectiveListenPortsUdp = data.effectiveListenPortsUdp;
|
||||||
this.manualPorts = data.manualPorts;
|
this.manualPorts = data.manualPorts;
|
||||||
this.derivedPorts = data.derivedPorts;
|
this.derivedPorts = data.derivedPorts;
|
||||||
}
|
}
|
||||||
@@ -52,11 +56,13 @@ export class RemoteIngress {
|
|||||||
const edge = response.edge;
|
const edge = response.edge;
|
||||||
this.name = edge.name;
|
this.name = edge.name;
|
||||||
this.listenPorts = edge.listenPorts;
|
this.listenPorts = edge.listenPorts;
|
||||||
|
this.listenPortsUdp = edge.listenPortsUdp;
|
||||||
this.enabled = edge.enabled;
|
this.enabled = edge.enabled;
|
||||||
this.autoDerivePorts = edge.autoDerivePorts;
|
this.autoDerivePorts = edge.autoDerivePorts;
|
||||||
this.tags = edge.tags;
|
this.tags = edge.tags;
|
||||||
this.updatedAt = edge.updatedAt;
|
this.updatedAt = edge.updatedAt;
|
||||||
this.effectiveListenPorts = edge.effectiveListenPorts;
|
this.effectiveListenPorts = edge.effectiveListenPorts;
|
||||||
|
this.effectiveListenPortsUdp = edge.effectiveListenPortsUdp;
|
||||||
this.manualPorts = edge.manualPorts;
|
this.manualPorts = edge.manualPorts;
|
||||||
this.derivedPorts = edge.derivedPorts;
|
this.derivedPorts = edge.derivedPorts;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export * from './dns-provider.js';
|
|||||||
export * from './domain.js';
|
export * from './domain.js';
|
||||||
export * from './dns-record.js';
|
export * from './dns-record.js';
|
||||||
export * from './acme-config.js';
|
export * from './acme-config.js';
|
||||||
export * from './email-domain.js';
|
export * from './email-domain.js';
|
||||||
|
export * from './security-policy.js';
|
||||||
|
|||||||
@@ -36,6 +36,64 @@ export interface IRemoteIngressStatus {
|
|||||||
activeTunnels: number;
|
activeTunnels: number;
|
||||||
lastHeartbeat: number | null;
|
lastHeartbeat: number | null;
|
||||||
connectedAt: number | null;
|
connectedAt: number | null;
|
||||||
|
transportMode?: 'tcpTls' | 'quic' | 'quicWithFallback';
|
||||||
|
fallbackUsed?: boolean;
|
||||||
|
performance?: IRemoteIngressPerformanceEffective;
|
||||||
|
flowControl?: IRemoteIngressFlowControlStatus;
|
||||||
|
queues?: IRemoteIngressQueueStatus;
|
||||||
|
traffic?: IRemoteIngressTrafficStatus;
|
||||||
|
udp?: IRemoteIngressUdpStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TRemoteIngressPerformanceProfile = 'balanced' | 'throughput' | 'highConcurrency';
|
||||||
|
|
||||||
|
export interface IRemoteIngressPerformanceConfig {
|
||||||
|
profile?: TRemoteIngressPerformanceProfile;
|
||||||
|
maxStreamsPerEdge?: number;
|
||||||
|
totalWindowBudgetBytes?: number;
|
||||||
|
minStreamWindowBytes?: number;
|
||||||
|
maxStreamWindowBytes?: number;
|
||||||
|
sustainedStreamWindowBytes?: number;
|
||||||
|
quicDatagramReceiveBufferBytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoteIngressPerformanceEffective {
|
||||||
|
profile: TRemoteIngressPerformanceProfile;
|
||||||
|
maxStreamsPerEdge: number;
|
||||||
|
totalWindowBudgetBytes: number;
|
||||||
|
minStreamWindowBytes: number;
|
||||||
|
maxStreamWindowBytes: number;
|
||||||
|
sustainedStreamWindowBytes: number;
|
||||||
|
quicDatagramReceiveBufferBytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoteIngressFlowControlStatus {
|
||||||
|
applies: boolean;
|
||||||
|
currentWindowBytes: number;
|
||||||
|
minWindowBytes: number;
|
||||||
|
maxWindowBytes: number;
|
||||||
|
totalWindowBudgetBytes: number;
|
||||||
|
estimatedInFlightBytes: number;
|
||||||
|
stalledStreams: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoteIngressQueueStatus {
|
||||||
|
ctrlQueueDepth: number;
|
||||||
|
dataQueueDepth: number;
|
||||||
|
sustainedQueueDepth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoteIngressTrafficStatus {
|
||||||
|
bytesIn: number;
|
||||||
|
bytesOut: number;
|
||||||
|
streamsOpenedTotal: number;
|
||||||
|
streamsClosedTotal: number;
|
||||||
|
rejectedStreams: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRemoteIngressUdpStatus {
|
||||||
|
activeSessions: number;
|
||||||
|
droppedDatagrams: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { IIpIntelligenceResult } from '@push.rocks/smartnetwork';
|
||||||
|
|
||||||
|
export type TSecurityBlockRuleType = 'ip' | 'cidr' | 'asn' | 'organization';
|
||||||
|
export type TSecurityBlockRuleMatchMode = 'exact' | 'contains';
|
||||||
|
|
||||||
|
export interface IIpIntelligenceRecord extends IIpIntelligenceResult {
|
||||||
|
ipAddress: string;
|
||||||
|
firstSeenAt: number;
|
||||||
|
lastSeenAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
seenCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISecurityBlockRule {
|
||||||
|
id: string;
|
||||||
|
type: TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
enabled: boolean;
|
||||||
|
reason?: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
createdBy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISecurityCompiledPolicy {
|
||||||
|
blockedIps: string[];
|
||||||
|
blockedCidrs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISecurityPolicyAuditEvent {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
actor: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
createdAt: number;
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ export interface IConfigData {
|
|||||||
hubDomain: string | null;
|
hubDomain: string | null;
|
||||||
tlsMode: 'custom' | 'acme' | 'self-signed';
|
tlsMode: 'custom' | 'acme' | 'self-signed';
|
||||||
connectedEdgeIps: string[];
|
connectedEdgeIps: string[];
|
||||||
|
performance?: import('../data/remoteingress.js').IRemoteIngressPerformanceConfig;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ export * from './dns-providers.js';
|
|||||||
export * from './domains.js';
|
export * from './domains.js';
|
||||||
export * from './dns-records.js';
|
export * from './dns-records.js';
|
||||||
export * from './acme-config.js';
|
export * from './acme-config.js';
|
||||||
export * from './email-domains.js';
|
export * from './email-domains.js';
|
||||||
|
export * from './security-policy.js';
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type * as authInterfaces from '../data/auth.js';
|
||||||
|
import type {
|
||||||
|
IIpIntelligenceRecord,
|
||||||
|
ISecurityBlockRule,
|
||||||
|
ISecurityCompiledPolicy,
|
||||||
|
ISecurityPolicyAuditEvent,
|
||||||
|
TSecurityBlockRuleMatchMode,
|
||||||
|
TSecurityBlockRuleType,
|
||||||
|
} from '../data/security-policy.js';
|
||||||
|
|
||||||
|
export interface IReq_ListSecurityBlockRules extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListSecurityBlockRules
|
||||||
|
> {
|
||||||
|
method: 'listSecurityBlockRules';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
rules: ISecurityBlockRule[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_CreateSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CreateSecurityBlockRule
|
||||||
|
> {
|
||||||
|
method: 'createSecurityBlockRule';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
type: TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
rule?: ISecurityBlockRule;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_UpdateSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_UpdateSecurityBlockRule
|
||||||
|
> {
|
||||||
|
method: 'updateSecurityBlockRule';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
value?: string;
|
||||||
|
matchMode?: TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
rule?: ISecurityBlockRule;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_DeleteSecurityBlockRule extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DeleteSecurityBlockRule
|
||||||
|
> {
|
||||||
|
method: 'deleteSecurityBlockRule';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListIpIntelligence
|
||||||
|
> {
|
||||||
|
method: 'listIpIntelligence';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
records: IIpIntelligenceRecord[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_GetCompiledSecurityPolicy extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetCompiledSecurityPolicy
|
||||||
|
> {
|
||||||
|
method: 'getCompiledSecurityPolicy';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
policy: ISecurityCompiledPolicy;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_ListSecurityPolicyAudit extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ListSecurityPolicyAudit
|
||||||
|
> {
|
||||||
|
method: 'listSecurityPolicyAudit';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
events: ISecurityPolicyAuditEvent[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReq_RefreshIpIntelligence extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RefreshIpIntelligence
|
||||||
|
> {
|
||||||
|
method: 'refreshIpIntelligence';
|
||||||
|
request: {
|
||||||
|
identity: authInterfaces.IIdentity;
|
||||||
|
ipAddress: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
record?: IIpIntelligenceRecord;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.21.0',
|
version: '13.24.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
+236
-3
@@ -54,6 +54,7 @@ export interface INetworkState {
|
|||||||
topIPs: Array<{ ip: string; count: number }>;
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
|
topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
|
||||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||||
|
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
||||||
domainActivity: interfaces.data.IDomainActivity[];
|
domainActivity: interfaces.data.IDomainActivity[];
|
||||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
requestsPerSecond: number;
|
requestsPerSecond: number;
|
||||||
@@ -66,6 +67,16 @@ export interface INetworkState {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISecurityPolicyState {
|
||||||
|
rules: interfaces.data.ISecurityBlockRule[];
|
||||||
|
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
||||||
|
compiledPolicy: interfaces.data.ISecurityCompiledPolicy | null;
|
||||||
|
auditEvents: interfaces.data.ISecurityPolicyAuditEvent[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ICertificateState {
|
export interface ICertificateState {
|
||||||
certificates: interfaces.requests.ICertificateInfo[];
|
certificates: interfaces.requests.ICertificateInfo[];
|
||||||
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
||||||
@@ -164,6 +175,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
topIPs: [],
|
topIPs: [],
|
||||||
topIPsByBandwidth: [],
|
topIPsByBandwidth: [],
|
||||||
throughputByIP: [],
|
throughputByIP: [],
|
||||||
|
ipIntelligence: [],
|
||||||
domainActivity: [],
|
domainActivity: [],
|
||||||
throughputHistory: [],
|
throughputHistory: [],
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
@@ -178,6 +190,20 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const securityPolicyStatePart = await appState.getStatePart<ISecurityPolicyState>(
|
||||||
|
'securityPolicy',
|
||||||
|
{
|
||||||
|
rules: [],
|
||||||
|
ipIntelligence: [],
|
||||||
|
compiledPolicy: null,
|
||||||
|
auditEvents: [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
},
|
||||||
|
'soft',
|
||||||
|
);
|
||||||
|
|
||||||
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
|
||||||
'emailOps',
|
'emailOps',
|
||||||
{
|
{
|
||||||
@@ -517,9 +543,18 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
interfaces.requests.IReq_GetNetworkStats
|
interfaces.requests.IReq_GetNetworkStats
|
||||||
>('/typedrequest', 'getNetworkStats');
|
>('/typedrequest', 'getNetworkStats');
|
||||||
|
|
||||||
const networkStatsResponse = await networkStatsRequest.fire({
|
const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
identity: context.identity,
|
interfaces.requests.IReq_ListIpIntelligence
|
||||||
});
|
>('/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
|
||||||
@@ -561,6 +596,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 || [],
|
||||||
domainActivity: networkStatsResponse.domainActivity || [],
|
domainActivity: networkStatsResponse.domainActivity || [],
|
||||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||||
@@ -582,6 +618,182 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Security Policy Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
|
||||||
|
async (statePartArg): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
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');
|
||||||
|
const auditRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_ListSecurityPolicyAudit
|
||||||
|
>('/typedrequest', 'listSecurityPolicyAudit');
|
||||||
|
|
||||||
|
const [rulesResponse, intelligenceResponse, 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 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rules: rulesResponse.rules || [],
|
||||||
|
ipIntelligence: intelligenceResponse.records || [],
|
||||||
|
compiledPolicy: compiledPolicyResponse.policy,
|
||||||
|
auditEvents: auditResponse.events || [],
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch security policy',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
||||||
|
type: interfaces.data.TSecurityBlockRuleType;
|
||||||
|
value: string;
|
||||||
|
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_CreateSecurityBlockRule
|
||||||
|
>('/typedrequest', 'createSecurityBlockRule');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
type: dataArg.type,
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to create security block rule' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to create security block rule',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
|
||||||
|
id: string;
|
||||||
|
value?: string;
|
||||||
|
matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
|
||||||
|
reason?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
}>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_UpdateSecurityBlockRule
|
||||||
|
>('/typedrequest', 'updateSecurityBlockRule');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: dataArg.id,
|
||||||
|
value: dataArg.value,
|
||||||
|
matchMode: dataArg.matchMode,
|
||||||
|
reason: dataArg.reason,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to update security block rule' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to update security block rule',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteSecurityBlockRuleAction = securityPolicyStatePart.createAction<string>(
|
||||||
|
async (statePartArg, ruleId, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_DeleteSecurityBlockRule
|
||||||
|
>('/typedrequest', 'deleteSecurityBlockRule');
|
||||||
|
|
||||||
|
const response = await request.fire({ identity: context.identity, id: ruleId });
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to delete security block rule' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete security block rule',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<string>(
|
||||||
|
async (statePartArg, ipAddress, actionContext): Promise<ISecurityPolicyState> => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState()!;
|
||||||
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_RefreshIpIntelligence
|
||||||
|
>('/typedrequest', 'refreshIpIntelligence');
|
||||||
|
const response = await request.fire({ identity: context.identity, ipAddress });
|
||||||
|
if (!response.success) {
|
||||||
|
return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
|
||||||
|
}
|
||||||
|
return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to refresh IP intelligence',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Email Operations Actions
|
// Email Operations Actions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -2665,6 +2877,27 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentView === 'security') {
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
|||||||
@@ -255,6 +255,17 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.intelligenceBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')};
|
||||||
|
color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')};
|
||||||
|
}
|
||||||
|
|
||||||
.protocolChartGrid {
|
.protocolChartGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
@@ -345,6 +356,100 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatOptional(value: unknown): string {
|
||||||
|
if (value === null || value === undefined || value === '') return '-';
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDateTime(timestamp?: number | null): string {
|
||||||
|
return timestamp ? new Date(timestamp).toLocaleString() : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpIntelligence(ip: string): interfaces.data.IIpIntelligenceRecord | undefined {
|
||||||
|
return this.networkState.ipIntelligence?.find((record) => record.ipAddress === ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpOrganization(record?: interfaces.data.IIpIntelligenceRecord): string {
|
||||||
|
return record?.asnOrg || record?.registrantOrg || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpIntelligenceColumns(ip: string): Record<string, unknown> {
|
||||||
|
const record = this.getIpIntelligence(ip);
|
||||||
|
const organization = this.getIpOrganization(record);
|
||||||
|
return {
|
||||||
|
'Intelligence': record
|
||||||
|
? html`<span class="intelligenceBadge">${this.formatOptional(organization || record.countryCode || 'Known')}</span>`
|
||||||
|
: html`<span class="statusBadge warning">Enriching...</span>`,
|
||||||
|
'ASN': record?.asn ? `AS${record.asn}` : '-',
|
||||||
|
'Organization': this.formatOptional(organization),
|
||||||
|
'Country': this.formatOptional(record?.countryCode || record?.country),
|
||||||
|
'Network Range': this.formatOptional(record?.networkRange),
|
||||||
|
'Last Seen': this.formatDateTime(record?.lastSeenAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpDataActions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Refresh Intelligence',
|
||||||
|
iconName: 'lucide:refresh-cw',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const ip = actionData.item.ip;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, ip);
|
||||||
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block IP',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
await this.createBlockRuleDialog('ip', actionData.item.ip, 'Blocked from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Network Range',
|
||||||
|
iconName: 'lucide:network',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.networkRange),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = this.getIpIntelligence(actionData.item.ip);
|
||||||
|
await this.createBlockRuleDialog('cidr', record!.networkRange!, 'Blocked network range from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block ASN',
|
||||||
|
iconName: 'lucide:radio-tower',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.asn),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = this.getIpIntelligence(actionData.item.ip);
|
||||||
|
await this.createBlockRuleDialog('asn', String(record!.asn), 'Blocked ASN from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Organization',
|
||||||
|
iconName: 'lucide:building-2',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpOrganization(this.getIpIntelligence(actionData.item.ip))),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = this.getIpIntelligence(actionData.item.ip);
|
||||||
|
await this.createBlockRuleDialog('organization', this.getIpOrganization(record), 'Blocked organization from Network Activity');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'View Intelligence',
|
||||||
|
iconName: 'lucide:info',
|
||||||
|
type: ['doubleClick', 'contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
await this.showIpIntelligenceDetails(actionData.item.ip);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private calculateThroughput(): { in: number; out: number } {
|
private calculateThroughput(): { in: number; out: number } {
|
||||||
// Use real throughput data from network state
|
// Use real throughput data from network state
|
||||||
return {
|
return {
|
||||||
@@ -500,10 +605,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
||||||
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
||||||
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||||
|
...this.getIpIntelligenceColumns(ipData.ip),
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
.dataActions=${this.getIpDataActions()}
|
||||||
heading1="Top Connected IPs"
|
heading1="Top Connected IPs"
|
||||||
heading2="IPs with most active connections and bandwidth"
|
heading2="IPs with most active connections, bandwidth, and intelligence"
|
||||||
searchable
|
searchable
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.pagination=${false}
|
.pagination=${false}
|
||||||
@@ -529,10 +636,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
|
'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
|
||||||
'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
|
'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
|
||||||
'Connections': ipData.count,
|
'Connections': ipData.count,
|
||||||
|
...this.getIpIntelligenceColumns(ipData.ip),
|
||||||
};
|
};
|
||||||
}}
|
}}
|
||||||
|
.dataActions=${this.getIpDataActions()}
|
||||||
heading1="Top IPs by Bandwidth"
|
heading1="Top IPs by Bandwidth"
|
||||||
heading2="IPs with highest throughput"
|
heading2="IPs with highest throughput and intelligence"
|
||||||
searchable
|
searchable
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.pagination=${false}
|
.pagination=${false}
|
||||||
@@ -678,6 +787,114 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDropdownKey(value: any): string {
|
||||||
|
return typeof value === 'string' ? value : value?.key || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createBlockRuleDialog(
|
||||||
|
type: interfaces.data.TSecurityBlockRuleType,
|
||||||
|
value: string,
|
||||||
|
reason: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
const typeOptions = [
|
||||||
|
{ key: 'ip', option: 'IP address' },
|
||||||
|
{ key: 'cidr', option: 'CIDR / network range' },
|
||||||
|
{ key: 'asn', option: 'ASN' },
|
||||||
|
{ key: 'organization', option: 'Organization' },
|
||||||
|
];
|
||||||
|
const matchModeOptions = [
|
||||||
|
{ key: 'contains', option: 'Organization contains value' },
|
||||||
|
{ key: 'exact', option: 'Organization exactly matches value' },
|
||||||
|
];
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Create Security Block Rule',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'type'}
|
||||||
|
.label=${'Rule Type'}
|
||||||
|
.options=${typeOptions}
|
||||||
|
.selectedOption=${typeOptions.find((option) => option.key === type)}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'value'} .label=${'Value'} .value=${value} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'matchMode'}
|
||||||
|
.label=${'Organization Match Mode'}
|
||||||
|
.description=${'Only used for organization rules'}
|
||||||
|
.options=${matchModeOptions}
|
||||||
|
.selectedOption=${matchModeOptions[0]}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
<dees-input-text .key=${'reason'} .label=${'Reason'} .value=${reason}></dees-input-text>
|
||||||
|
<dees-input-checkbox .key=${'enabled'} .label=${'Enable immediately'} .value=${true}></dees-input-checkbox>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const data = await form.collectFormData();
|
||||||
|
const selectedType = this.getDropdownKey(data.type) as interfaces.data.TSecurityBlockRuleType;
|
||||||
|
const selectedValue = String(data.value || '').trim();
|
||||||
|
if (!selectedType || !selectedValue) return;
|
||||||
|
const matchMode = selectedType === 'organization'
|
||||||
|
? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode
|
||||||
|
: undefined;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, {
|
||||||
|
type: selectedType,
|
||||||
|
value: selectedValue,
|
||||||
|
matchMode,
|
||||||
|
reason: String(data.reason || '').trim() || undefined,
|
||||||
|
enabled: data.enabled !== false,
|
||||||
|
});
|
||||||
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showIpIntelligenceDetails(ip: string): Promise<void> {
|
||||||
|
const record = this.getIpIntelligence(ip);
|
||||||
|
if (!record) return;
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: `IP Intelligence: ${ip}`,
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<dees-dataview-codebox
|
||||||
|
.heading=${'Intelligence Record'}
|
||||||
|
progLang="json"
|
||||||
|
.codeToDisplay=${JSON.stringify(record, null, 2)}
|
||||||
|
></dees-dataview-codebox>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Copy Abuse Contact',
|
||||||
|
iconName: 'lucide:copy',
|
||||||
|
action: async () => {
|
||||||
|
if (record.abuseContact) await navigator.clipboard.writeText(record.abuseContact);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block IP',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
action: async () => {
|
||||||
|
await this.createBlockRuleDialog('ip', record.ipAddress, 'Blocked from IP intelligence details');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async updateNetworkData() {
|
private async updateNetworkData() {
|
||||||
// Track requests/sec history for the trend sparkline (moved out of render)
|
// Track requests/sec history for the trend sparkline (moved out of render)
|
||||||
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||||
|
|||||||
@@ -125,6 +125,18 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#047857', '#34d399')};
|
color: ${cssManager.bdTheme('#047857', '#34d399')};
|
||||||
border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')};
|
border: 1px dashed ${cssManager.bdTheme('#6ee7b7', '#065f46')};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metricStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metricMuted {
|
||||||
|
color: var(--text-muted, #6b7280);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -226,9 +238,13 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
.displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
|
.displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
|
||||||
name: edge.name,
|
name: edge.name,
|
||||||
status: this.getEdgeStatusHtml(edge),
|
status: this.getEdgeStatusHtml(edge),
|
||||||
|
transport: this.getTransportHtml(edge.id),
|
||||||
publicIp: this.getEdgePublicIp(edge.id),
|
publicIp: this.getEdgePublicIp(edge.id),
|
||||||
ports: this.getPortsHtml(edge),
|
ports: this.getPortsHtml(edge),
|
||||||
tunnels: this.getEdgeTunnelCount(edge.id),
|
tunnels: this.getEdgeTunnelCount(edge.id),
|
||||||
|
window: this.getWindowHtml(edge.id),
|
||||||
|
queues: this.getQueuesHtml(edge.id),
|
||||||
|
traffic: this.getTrafficHtml(edge.id),
|
||||||
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
||||||
})}
|
})}
|
||||||
.dataActions=${[
|
.dataActions=${[
|
||||||
@@ -459,6 +475,46 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
return status?.activeTunnels || 0;
|
return status?.activeTunnels || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getTransportHtml(edgeId: string): TemplateResult | string {
|
||||||
|
const status = this.getEdgeStatus(edgeId);
|
||||||
|
if (!status?.connected) return '-';
|
||||||
|
const mode = status.transportMode || 'unknown';
|
||||||
|
const label = mode === 'quic' ? 'QUIC' : mode === 'tcpTls' ? 'TCP/TLS' : mode;
|
||||||
|
return html`<div class="metricStack"><strong>${label}</strong><span class="metricMuted">${status.fallbackUsed ? 'fallback' : status.performance?.profile || 'default'}</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getWindowHtml(edgeId: string): TemplateResult | string {
|
||||||
|
const status = this.getEdgeStatus(edgeId);
|
||||||
|
if (!status?.connected || !status.flowControl) return '-';
|
||||||
|
if (!status.flowControl.applies) {
|
||||||
|
return html`<div class="metricStack"><span>native QUIC</span><span class="metricMuted">max ${status.performance?.maxStreamsPerEdge || '-'} streams</span></div>`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="metricStack">
|
||||||
|
<span>${this.formatBytes(status.flowControl.currentWindowBytes)} window</span>
|
||||||
|
<span class="metricMuted">${this.formatBytes(status.flowControl.estimatedInFlightBytes)} est. in-flight</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQueuesHtml(edgeId: string): TemplateResult | string {
|
||||||
|
const status = this.getEdgeStatus(edgeId);
|
||||||
|
if (!status?.connected || !status.queues) return '-';
|
||||||
|
return html`<div class="metricStack"><span>C ${status.queues.ctrlQueueDepth} / D ${status.queues.dataQueueDepth}</span><span class="metricMuted">S ${status.queues.sustainedQueueDepth}</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTrafficHtml(edgeId: string): TemplateResult | string {
|
||||||
|
const status = this.getEdgeStatus(edgeId);
|
||||||
|
if (!status?.connected || !status.traffic) return '-';
|
||||||
|
const drops = (status.traffic.rejectedStreams || 0) + (status.udp?.droppedDatagrams || 0);
|
||||||
|
return html`
|
||||||
|
<div class="metricStack">
|
||||||
|
<span>${this.formatBytes(status.traffic.bytesIn)} in / ${this.formatBytes(status.traffic.bytesOut)} out</span>
|
||||||
|
<span class="metricMuted">${drops} rejected/dropped</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private getLastHeartbeat(edgeId: string): string {
|
private getLastHeartbeat(edgeId: string): string {
|
||||||
const status = this.getEdgeStatus(edgeId);
|
const status = this.getEdgeStatus(edgeId);
|
||||||
if (!status?.lastHeartbeat) return '-';
|
if (!status?.lastHeartbeat) return '-';
|
||||||
@@ -467,4 +523,16 @@ export class OpsViewRemoteIngress extends DeesElement {
|
|||||||
if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`;
|
if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`;
|
||||||
return `${Math.floor(ago / 3600000)}h ago`;
|
return `${Math.floor(ago / 3600000)}h ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatBytes(bytes: number): string {
|
||||||
|
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
let value = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
value = value / 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from '../shared/css.js';
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -21,18 +22,23 @@ declare global {
|
|||||||
@customElement('ops-view-security-blocked')
|
@customElement('ops-view-security-blocked')
|
||||||
export class OpsViewSecurityBlocked extends DeesElement {
|
export class OpsViewSecurityBlocked extends DeesElement {
|
||||||
@state()
|
@state()
|
||||||
accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
|
accessor securityPolicyState: appstate.ISecurityPolicyState = appstate.securityPolicyStatePart.getState()!;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const sub = appstate.statsStatePart
|
const sub = appstate.securityPolicyStatePart
|
||||||
.select((s) => s)
|
.select((s) => s)
|
||||||
.subscribe((s) => {
|
.subscribe((s) => {
|
||||||
this.statsState = s;
|
this.securityPolicyState = s;
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(sub);
|
this.rxSubscriptions.push(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.fetchSecurityPolicyAction, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
viewHostCss,
|
viewHostCss,
|
||||||
@@ -40,79 +46,436 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
|||||||
dees-statsgrid {
|
dees-statsgrid {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sectionStack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.enabled {
|
||||||
|
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.disabled {
|
||||||
|
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
|
||||||
|
color: ${cssManager.bdTheme('#757575', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')};
|
||||||
|
color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMessage {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||||
|
color: ${cssManager.bdTheme('#b91c1c', '#fca5a5')};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
public render(): TemplateResult {
|
public render(): TemplateResult {
|
||||||
const metrics = this.statsState.securityMetrics;
|
const state = this.securityPolicyState;
|
||||||
|
const activeRules = state.rules.filter((rule) => rule.enabled);
|
||||||
if (!metrics) {
|
const disabledRules = state.rules.length - activeRules.length;
|
||||||
return html`
|
const compiledPolicy = state.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
|
||||||
<div class="loadingMessage">
|
|
||||||
<p>Loading security metrics...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockedIPs: string[] = metrics.blockedIPs || [];
|
|
||||||
|
|
||||||
const tiles: IStatsTile[] = [
|
const tiles: IStatsTile[] = [
|
||||||
{
|
{
|
||||||
id: 'totalBlocked',
|
id: 'activeRules',
|
||||||
title: 'Blocked IPs',
|
title: 'Active Rules',
|
||||||
value: blockedIPs.length,
|
value: activeRules.length,
|
||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lucide:ShieldBan',
|
icon: 'lucide:shield-check',
|
||||||
color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
|
color: activeRules.length > 0 ? '#ef4444' : '#22c55e',
|
||||||
description: 'Currently blocked addresses',
|
description: `${disabledRules} disabled`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compiledIps',
|
||||||
|
title: 'Compiled IPs',
|
||||||
|
value: compiledPolicy.blockedIps.length,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:server-off',
|
||||||
|
color: '#ef4444',
|
||||||
|
description: 'Direct IP blocks enforced by SmartProxy',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compiledCidrs',
|
||||||
|
title: 'Compiled CIDRs',
|
||||||
|
value: compiledPolicy.blockedCidrs.length,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:network',
|
||||||
|
color: '#f97316',
|
||||||
|
description: 'Network ranges pushed to enforcement layers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'intelligenceRecords',
|
||||||
|
title: 'IP Intelligence',
|
||||||
|
value: state.ipIntelligence.length,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lucide:radar',
|
||||||
|
color: '#6366f1',
|
||||||
|
description: 'Observed public IPs with enrichment',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-heading level="3">Blocked IPs</dees-heading>
|
<dees-heading level="3">Security Blocking</dees-heading>
|
||||||
|
|
||||||
|
${state.error ? html`<div class="errorMessage">${state.error}</div>` : html``}
|
||||||
|
|
||||||
<dees-statsgrid
|
<dees-statsgrid
|
||||||
.tiles=${tiles}
|
.tiles=${tiles}
|
||||||
.minTileWidth=${200}
|
.minTileWidth=${200}
|
||||||
></dees-statsgrid>
|
></dees-statsgrid>
|
||||||
|
|
||||||
|
<div class="sectionStack">
|
||||||
|
${this.renderRulesTable()}
|
||||||
|
${this.renderCompiledPolicyTable()}
|
||||||
|
${this.renderIpIntelligenceTable()}
|
||||||
|
${this.renderAuditTable()}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderRulesTable(): TemplateResult {
|
||||||
|
return html`
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Blocked IP Addresses'}
|
.heading1=${'Managed Block Rules'}
|
||||||
.heading2=${'IPs blocked due to suspicious activity'}
|
.heading2=${'Rules compiled into SmartProxy policy and remote ingress edge firewall snapshots'}
|
||||||
.data=${blockedIPs.map((ip) => ({ ip }))}
|
.data=${this.securityPolicyState.rules}
|
||||||
.displayFunction=${(item) => ({
|
.rowKey=${'id'}
|
||||||
'IP Address': item.ip,
|
.displayFunction=${(rule: interfaces.data.ISecurityBlockRule) => ({
|
||||||
'Reason': 'Suspicious activity',
|
'Type': html`<span class="typeBadge">${rule.type}</span>`,
|
||||||
|
'Value': rule.value,
|
||||||
|
'Match': rule.type === 'organization' ? (rule.matchMode || 'contains') : '-',
|
||||||
|
'Reason': rule.reason || '-',
|
||||||
|
'Status': html`<span class="statusBadge ${rule.enabled ? 'enabled' : 'disabled'}">${rule.enabled ? 'Enabled' : 'Disabled'}</span>`,
|
||||||
|
'Created': this.formatDateTime(rule.createdAt),
|
||||||
|
'Updated': this.formatDateTime(rule.updatedAt),
|
||||||
})}
|
})}
|
||||||
.dataActions=${[
|
.dataActions=${this.getRuleActions()}
|
||||||
{
|
searchable
|
||||||
name: 'Unblock',
|
.showColumnFilters=${true}
|
||||||
iconName: 'lucide:shield-off',
|
dataName="rule"
|
||||||
type: ['contextmenu' as const],
|
|
||||||
actionFunc: async (item) => {
|
|
||||||
await this.unblockIP(item.ip);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Clear All',
|
|
||||||
iconName: 'lucide:trash-2',
|
|
||||||
type: ['header' as const],
|
|
||||||
actionFunc: async () => {
|
|
||||||
await this.clearBlockedIPs();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
></dees-table>
|
></dees-table>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async clearBlockedIPs() {
|
private renderCompiledPolicyTable(): TemplateResult {
|
||||||
// SmartProxy manages IP blocking — not yet exposed via API
|
const policy = this.securityPolicyState.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
|
||||||
alert('Clearing blocked IPs is not yet supported from the UI.');
|
const rows = [
|
||||||
|
...policy.blockedIps.map((value) => ({ type: 'ip', value })),
|
||||||
|
...policy.blockedCidrs.map((value) => ({ type: 'cidr', value })),
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Compiled Enforcement Policy'}
|
||||||
|
.heading2=${'Concrete IPs and CIDRs currently sent to SmartProxy and remote ingress'}
|
||||||
|
.data=${rows}
|
||||||
|
.rowKey=${'value'}
|
||||||
|
.displayFunction=${(row: { type: string; value: string }) => ({
|
||||||
|
'Enforcement Type': html`<span class="typeBadge">${row.type}</span>`,
|
||||||
|
'Value': row.value,
|
||||||
|
})}
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
dataName="compiled rule"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unblockIP(ip: string) {
|
private renderIpIntelligenceTable(): TemplateResult {
|
||||||
// SmartProxy manages IP blocking — not yet exposed via API
|
return html`
|
||||||
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
|
<dees-table
|
||||||
|
.heading1=${'Observed IP Intelligence'}
|
||||||
|
.heading2=${'Public IPs observed in network metrics and enriched for ASN / organization matching'}
|
||||||
|
.data=${this.securityPolicyState.ipIntelligence}
|
||||||
|
.rowKey=${'ipAddress'}
|
||||||
|
.displayFunction=${(record: interfaces.data.IIpIntelligenceRecord) => ({
|
||||||
|
'IP Address': record.ipAddress,
|
||||||
|
'ASN': record.asn ? `AS${record.asn}` : '-',
|
||||||
|
'ASN Org': record.asnOrg || '-',
|
||||||
|
'Registrant Org': record.registrantOrg || '-',
|
||||||
|
'Country': record.countryCode || record.country || '-',
|
||||||
|
'Network Range': record.networkRange || '-',
|
||||||
|
'Abuse Contact': record.abuseContact || '-',
|
||||||
|
'Seen': record.seenCount,
|
||||||
|
'Last Seen': this.formatDateTime(record.lastSeenAt),
|
||||||
|
})}
|
||||||
|
.dataActions=${this.getIpIntelligenceActions()}
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
dataName="ip intelligence record"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAuditTable(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Policy Audit'}
|
||||||
|
.heading2=${'Recent security policy changes'}
|
||||||
|
.data=${this.securityPolicyState.auditEvents}
|
||||||
|
.rowKey=${'id'}
|
||||||
|
.displayFunction=${(event: interfaces.data.ISecurityPolicyAuditEvent) => ({
|
||||||
|
'Time': this.formatDateTime(event.createdAt),
|
||||||
|
'Action': event.action,
|
||||||
|
'Actor': event.actor,
|
||||||
|
'Details': this.formatAuditDetails(event.details),
|
||||||
|
})}
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
dataName="audit event"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRuleActions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Create Rule',
|
||||||
|
iconName: 'lucide:plus',
|
||||||
|
type: ['header'] as any,
|
||||||
|
actionFunc: async () => this.showRuleDialog(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Edit',
|
||||||
|
iconName: 'lucide:pencil',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => this.showRuleDialog(actionData.item),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Enable',
|
||||||
|
iconName: 'lucide:play',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
||||||
|
id: rule.id,
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Disable',
|
||||||
|
iconName: 'lucide:pause',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
||||||
|
id: rule.id,
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash-2',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const rule = actionData.item as interfaces.data.ISecurityBlockRule;
|
||||||
|
if (!window.confirm(`Delete block rule ${rule.type}:${rule.value}?`)) return;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.deleteSecurityBlockRuleAction, rule.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private getIpIntelligenceActions() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'Refresh Intelligence',
|
||||||
|
iconName: 'lucide:refresh-cw',
|
||||||
|
type: ['inRow', 'contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, record.ipAddress);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block IP',
|
||||||
|
iconName: 'lucide:shield-ban',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'ip',
|
||||||
|
value: record.ipAddress,
|
||||||
|
reason: 'Blocked from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Network Range',
|
||||||
|
iconName: 'lucide:network',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.networkRange),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'cidr',
|
||||||
|
value: record.networkRange || '',
|
||||||
|
reason: 'Blocked network range from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block ASN',
|
||||||
|
iconName: 'lucide:radio-tower',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.asn),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'asn',
|
||||||
|
value: String(record.asn),
|
||||||
|
reason: 'Blocked ASN from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Block Organization',
|
||||||
|
iconName: 'lucide:building-2',
|
||||||
|
type: ['contextmenu'] as any,
|
||||||
|
actionRelevancyCheckFunc: (actionData: any) => Boolean(actionData.item.asnOrg || actionData.item.registrantOrg),
|
||||||
|
actionFunc: async (actionData: any) => {
|
||||||
|
const record = actionData.item as interfaces.data.IIpIntelligenceRecord;
|
||||||
|
await this.showRuleDialog(undefined, {
|
||||||
|
type: 'organization',
|
||||||
|
value: record.asnOrg || record.registrantOrg || '',
|
||||||
|
reason: 'Blocked organization from IP intelligence table',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showRuleDialog(
|
||||||
|
rule?: interfaces.data.ISecurityBlockRule,
|
||||||
|
defaults: Partial<interfaces.data.ISecurityBlockRule> = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
const typeOptions = [
|
||||||
|
{ key: 'ip', option: 'IP address' },
|
||||||
|
{ key: 'cidr', option: 'CIDR / network range' },
|
||||||
|
{ key: 'asn', option: 'ASN' },
|
||||||
|
{ key: 'organization', option: 'Organization' },
|
||||||
|
];
|
||||||
|
const matchModeOptions = [
|
||||||
|
{ key: 'contains', option: 'Organization contains value' },
|
||||||
|
{ key: 'exact', option: 'Organization exactly matches value' },
|
||||||
|
];
|
||||||
|
const selectedType = rule?.type || defaults.type || 'ip';
|
||||||
|
const selectedMatchMode = rule?.matchMode || defaults.matchMode || 'contains';
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: rule ? `Edit Block Rule: ${rule.type}:${rule.value}` : 'Create Block Rule',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
${rule ? html`` : html`
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'type'}
|
||||||
|
.label=${'Rule Type'}
|
||||||
|
.options=${typeOptions}
|
||||||
|
.selectedOption=${typeOptions.find((option) => option.key === selectedType)}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
`}
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'value'}
|
||||||
|
.label=${'Value'}
|
||||||
|
.value=${rule?.value || defaults.value || ''}
|
||||||
|
.required=${true}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'matchMode'}
|
||||||
|
.label=${'Organization Match Mode'}
|
||||||
|
.description=${'Only used for organization rules'}
|
||||||
|
.options=${matchModeOptions}
|
||||||
|
.selectedOption=${matchModeOptions.find((option) => option.key === selectedMatchMode)}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'reason'}
|
||||||
|
.label=${'Reason'}
|
||||||
|
.value=${rule?.reason || defaults.reason || ''}
|
||||||
|
></dees-input-text>
|
||||||
|
<dees-input-checkbox
|
||||||
|
.key=${'enabled'}
|
||||||
|
.label=${'Enabled'}
|
||||||
|
.value=${rule ? rule.enabled : defaults.enabled !== false}
|
||||||
|
></dees-input-checkbox>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{ name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
|
{
|
||||||
|
name: rule ? 'Save' : 'Create',
|
||||||
|
iconName: rule ? 'lucide:check' : 'lucide:plus',
|
||||||
|
action: async (modalArg: any) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||||
|
if (!form) return;
|
||||||
|
const data = await form.collectFormData();
|
||||||
|
const type = (rule?.type || this.getDropdownKey(data.type)) as interfaces.data.TSecurityBlockRuleType;
|
||||||
|
const value = String(data.value || '').trim();
|
||||||
|
if (!type || !value) return;
|
||||||
|
const matchMode = type === 'organization'
|
||||||
|
? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode
|
||||||
|
: undefined;
|
||||||
|
const payload = {
|
||||||
|
value,
|
||||||
|
matchMode,
|
||||||
|
reason: String(data.reason || '').trim() || undefined,
|
||||||
|
enabled: data.enabled !== false,
|
||||||
|
};
|
||||||
|
if (rule) {
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.updateSecurityBlockRuleAction, {
|
||||||
|
id: rule.id,
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, {
|
||||||
|
type,
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await modalArg.destroy();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDropdownKey(value: any): string {
|
||||||
|
return typeof value === 'string' ? value : value?.key || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDateTime(timestamp?: number): string {
|
||||||
|
return timestamp ? new Date(timestamp).toLocaleString() : '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatAuditDetails(details: Record<string, unknown>): string {
|
||||||
|
const text = JSON.stringify(details);
|
||||||
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user