Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df9cc3e49b | |||
| 7f3ab2499d | |||
| 89ab918826 | |||
| e5c3578163 |
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-26 - 13.25.0 - feat(security)
|
||||
compile network ranges and CIDR arrays into edge firewall policies
|
||||
|
||||
- add support for storing intelligence network CIDR arrays alongside single network ranges
|
||||
- convert start-end IPv4 ranges into CIDR blocks when compiling security policies
|
||||
- always return an explicit remote ingress firewall snapshot with a blockedIps array
|
||||
- add tests covering range normalization, ASN-derived CIDRs, and empty firewall snapshots
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "13.23.0",
|
||||
"version": "13.25.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
@@ -51,7 +51,7 @@
|
||||
"@push.rocks/smartmetrics": "^3.0.3",
|
||||
"@push.rocks/smartmigration": "1.2.0",
|
||||
"@push.rocks/smartmta": "^5.3.3",
|
||||
"@push.rocks/smartnetwork": "^4.6.0",
|
||||
"@push.rocks/smartnetwork": "^4.7.0",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartproxy": "^27.9.0",
|
||||
|
||||
Generated
+8
-8
@@ -72,8 +72,8 @@ importers:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
'@push.rocks/smartnetwork':
|
||||
specifier: ^4.6.0
|
||||
version: 4.6.0
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0
|
||||
'@push.rocks/smartpath':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
@@ -1257,8 +1257,8 @@ packages:
|
||||
'@push.rocks/smartmustache@3.0.2':
|
||||
resolution: {integrity: sha512-G3LyRXoJhyM+iQhkvP/MR/2WYMvC9U7zc2J44JxUM5tPdkQ+o3++FbfRtnZj6rz5X/A7q03//vsxPitVQwoi2Q==}
|
||||
|
||||
'@push.rocks/smartnetwork@4.6.0':
|
||||
resolution: {integrity: sha512-ubaO/Qp8r30A+qwk33M/0+nQi+o8gNHEI9zq3jv1MwqiLxhiV1hnbr4CL9AUcvs4EhwUBiw0EswKjCJROwDqvQ==}
|
||||
'@push.rocks/smartnetwork@4.7.0':
|
||||
resolution: {integrity: sha512-WZ46pJlklDRcw1AqkyyBhmGSNSK3i7IYM9D9vcVJOUhlLmgUSai8o1NbpWlb7HvOkp1IhQ7iZeuJV2JiWLtl1g==}
|
||||
|
||||
'@push.rocks/smartnftables@1.1.0':
|
||||
resolution: {integrity: sha512-7JNzerlW20HEl2wKMBIHltwneCQRpXiD2lJkXZZc02ctnfjgFejXVDIeWomhPx6PZ0Z6zmqdF6rrFDtDHyqqfA==}
|
||||
@@ -5163,7 +5163,7 @@ snapshots:
|
||||
'@push.rocks/smartjson': 6.0.0
|
||||
'@push.rocks/smartlog': 3.2.2
|
||||
'@push.rocks/smartmongo': 5.1.1(socks@2.8.7)
|
||||
'@push.rocks/smartnetwork': 4.6.0
|
||||
'@push.rocks/smartnetwork': 4.7.0
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartrequest': 5.0.1
|
||||
@@ -5966,7 +5966,7 @@ snapshots:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartdns': 7.9.0
|
||||
'@push.rocks/smartlog': 3.2.2
|
||||
'@push.rocks/smartnetwork': 4.6.0
|
||||
'@push.rocks/smartnetwork': 4.7.0
|
||||
'@push.rocks/smartstring': 4.1.0
|
||||
'@push.rocks/smarttime': 4.2.3
|
||||
'@push.rocks/smartunique': 3.0.9
|
||||
@@ -6433,7 +6433,7 @@ snapshots:
|
||||
dependencies:
|
||||
handlebars: 4.7.9
|
||||
|
||||
'@push.rocks/smartnetwork@4.6.0':
|
||||
'@push.rocks/smartnetwork@4.7.0':
|
||||
dependencies:
|
||||
'@push.rocks/smartdns': 7.9.0
|
||||
'@push.rocks/smartrust': 1.3.2
|
||||
@@ -6499,7 +6499,7 @@ snapshots:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartfs': 1.5.0
|
||||
'@push.rocks/smartjimp': 1.2.0
|
||||
'@push.rocks/smartnetwork': 4.6.0
|
||||
'@push.rocks/smartnetwork': 4.7.0
|
||||
'@push.rocks/smartpath': 6.0.0
|
||||
'@push.rocks/smartpromise': 4.2.3
|
||||
'@push.rocks/smartpuppeteer': 2.0.5(typescript@6.0.2)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import { DcRouterDb, IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../ts/db/index.js';
|
||||
import { SecurityPolicyManager } from '../ts/security/index.js';
|
||||
|
||||
const createTestDb = async () => {
|
||||
const storagePath = plugins.path.join(
|
||||
plugins.os.tmpdir(),
|
||||
`dcrouter-security-policy-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
);
|
||||
|
||||
DcRouterDb.resetInstance();
|
||||
const db = DcRouterDb.getInstance({
|
||||
storagePath,
|
||||
dbName: `dcrouter-security-policy-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
});
|
||||
await db.start();
|
||||
await db.getDb().mongoDb.createCollection('__test_init');
|
||||
|
||||
return {
|
||||
async cleanup() {
|
||||
await db.stop();
|
||||
DcRouterDb.resetInstance();
|
||||
await plugins.fs.promises.rm(storagePath, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const testDbPromise = createTestDb();
|
||||
|
||||
const clearTestState = async () => {
|
||||
for (const rule of await SecurityBlockRuleDoc.findAll()) {
|
||||
await rule.delete();
|
||||
}
|
||||
for (const record of await IpIntelligenceDoc.findAll()) {
|
||||
await record.delete();
|
||||
}
|
||||
for (const event of await SecurityPolicyAuditDoc.findRecent(1000)) {
|
||||
await event.delete();
|
||||
}
|
||||
};
|
||||
|
||||
tap.test('SecurityPolicyManager compiles start-end CIDR rules for edge firewall snapshots', async () => {
|
||||
await testDbPromise;
|
||||
await clearTestState();
|
||||
const manager = new SecurityPolicyManager();
|
||||
|
||||
await manager.createBlockRule({
|
||||
type: 'cidr',
|
||||
value: '203.0.113.0 - 203.0.113.255',
|
||||
reason: 'test range',
|
||||
});
|
||||
|
||||
const policy = await manager.compilePolicy();
|
||||
expect(policy.blockedCidrs).toEqual(['203.0.113.0/24']);
|
||||
|
||||
const firewall = await manager.compileRemoteIngressFirewall();
|
||||
expect(firewall.blockedIps).toEqual(['203.0.113.0/24']);
|
||||
});
|
||||
|
||||
tap.test('SecurityPolicyManager compiles intelligence network ranges for ASN rules', async () => {
|
||||
await testDbPromise;
|
||||
await clearTestState();
|
||||
const manager = new SecurityPolicyManager();
|
||||
|
||||
const intelligenceDoc = new IpIntelligenceDoc();
|
||||
intelligenceDoc.ipAddress = '198.51.100.23';
|
||||
intelligenceDoc.asn = 64500;
|
||||
intelligenceDoc.asnOrg = 'Example Network';
|
||||
intelligenceDoc.networkRange = '198.51.100.0 - 198.51.100.127';
|
||||
intelligenceDoc.firstSeenAt = Date.now();
|
||||
intelligenceDoc.lastSeenAt = Date.now();
|
||||
intelligenceDoc.updatedAt = Date.now();
|
||||
intelligenceDoc.seenCount = 1;
|
||||
await intelligenceDoc.save();
|
||||
|
||||
await manager.createBlockRule({
|
||||
type: 'asn',
|
||||
value: 'AS64500',
|
||||
reason: 'test asn range',
|
||||
});
|
||||
|
||||
const policy = await manager.compilePolicy();
|
||||
expect(policy.blockedCidrs).toEqual(['198.51.100.0/25']);
|
||||
});
|
||||
|
||||
tap.test('SecurityPolicyManager compiles intelligence CIDR arrays for ASN rules', async () => {
|
||||
await testDbPromise;
|
||||
await clearTestState();
|
||||
const manager = new SecurityPolicyManager();
|
||||
|
||||
const intelligenceDoc = new IpIntelligenceDoc();
|
||||
intelligenceDoc.ipAddress = '198.51.100.130';
|
||||
intelligenceDoc.asn = 64501;
|
||||
intelligenceDoc.asnOrg = 'Example Split Network';
|
||||
intelligenceDoc.networkRange = null;
|
||||
intelligenceDoc.networkCidrs = ['198.51.100.128/25', '198.51.101.0/24'];
|
||||
intelligenceDoc.firstSeenAt = Date.now();
|
||||
intelligenceDoc.lastSeenAt = Date.now();
|
||||
intelligenceDoc.updatedAt = Date.now();
|
||||
intelligenceDoc.seenCount = 1;
|
||||
await intelligenceDoc.save();
|
||||
|
||||
await manager.createBlockRule({
|
||||
type: 'asn',
|
||||
value: 'AS64501',
|
||||
reason: 'test asn cidr array',
|
||||
});
|
||||
|
||||
const policy = await manager.compilePolicy();
|
||||
expect(policy.blockedCidrs).toEqual(['198.51.100.128/25', '198.51.101.0/24']);
|
||||
});
|
||||
|
||||
tap.test('SecurityPolicyManager returns an explicit empty edge firewall snapshot', async () => {
|
||||
await testDbPromise;
|
||||
await clearTestState();
|
||||
const manager = new SecurityPolicyManager();
|
||||
|
||||
const firewall = await manager.compileRemoteIngressFirewall();
|
||||
expect(firewall).toEqual({ blockedIps: [] });
|
||||
});
|
||||
|
||||
tap.test('cleanup security policy test db', async () => {
|
||||
const dbHandle = await testDbPromise;
|
||||
await clearTestState();
|
||||
await dbHandle.cleanup();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.23.0',
|
||||
version: '13.25.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ export class IpIntelligenceDoc extends plugins.smartdata.SmartDataDbDoc<IpIntell
|
||||
@plugins.smartdata.svDb()
|
||||
public networkRange: string | null = null;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public networkCidrs: string[] | null = null;
|
||||
|
||||
@plugins.smartdata.svDb()
|
||||
public abuseContact: string | null = null;
|
||||
|
||||
|
||||
@@ -178,6 +178,30 @@ export class SecurityHandler {
|
||||
),
|
||||
);
|
||||
|
||||
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(
|
||||
@@ -226,6 +250,20 @@ export class SecurityHandler {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
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<{
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
IIpIntelligenceRecord,
|
||||
ISecurityBlockRule,
|
||||
ISecurityCompiledPolicy,
|
||||
ISecurityPolicyAuditEvent,
|
||||
TSecurityBlockRuleMatchMode,
|
||||
TSecurityBlockRuleType,
|
||||
} from '../../ts_interfaces/data/security-policy.js';
|
||||
@@ -15,7 +16,7 @@ export interface ISecurityPolicyManagerOptions {
|
||||
}
|
||||
|
||||
export interface IRemoteIngressFirewallSnapshot {
|
||||
blockedIps?: string[];
|
||||
blockedIps: string[];
|
||||
}
|
||||
|
||||
export class SecurityPolicyManager {
|
||||
@@ -44,7 +45,7 @@ export class SecurityPolicyManager {
|
||||
await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
|
||||
}
|
||||
|
||||
public async observeIp(ipAddress: string): Promise<void> {
|
||||
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;
|
||||
@@ -54,7 +55,7 @@ export class SecurityPolicyManager {
|
||||
try {
|
||||
const now = Date.now();
|
||||
let doc = await IpIntelligenceDoc.findByIp(ip);
|
||||
if (doc && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
||||
if (doc && !options.force && now - doc.updatedAt < this.intelligenceRefreshMs) {
|
||||
if (now - doc.lastSeenAt > 60_000) {
|
||||
doc.lastSeenAt = now;
|
||||
doc.seenCount = (doc.seenCount || 0) + 1;
|
||||
@@ -90,13 +91,38 @@ export class SecurityPolicyManager {
|
||||
}
|
||||
|
||||
public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
|
||||
return (await IpIntelligenceDoc.findAll()).map((doc) => ({
|
||||
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,
|
||||
networkCidrs: doc.networkCidrs,
|
||||
abuseContact: doc.abuseContact,
|
||||
country: doc.country,
|
||||
countryCode: doc.countryCode,
|
||||
@@ -109,7 +135,7 @@ export class SecurityPolicyManager {
|
||||
lastSeenAt: doc.lastSeenAt,
|
||||
updatedAt: doc.updatedAt,
|
||||
seenCount: doc.seenCount,
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
public async createBlockRule(input: {
|
||||
@@ -180,16 +206,22 @@ export class SecurityPolicyManager {
|
||||
}
|
||||
|
||||
if (rule.type === 'cidr') {
|
||||
const cidr = this.normalizeCidr(normalizedValue);
|
||||
if (cidr) blockedCidrs.add(cidr);
|
||||
for (const cidr of this.normalizeNetworkEntries(normalizedValue)) {
|
||||
blockedCidrs.add(cidr);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const doc of intelligenceDocs) {
|
||||
if (!this.ruleMatchesIntelligence(rule, doc)) continue;
|
||||
const cidr = this.normalizeCidr(doc.networkRange || '');
|
||||
if (cidr) {
|
||||
const networkEntries = this.normalizeNetworkEntryList([
|
||||
...(doc.networkCidrs || []),
|
||||
doc.networkRange,
|
||||
]);
|
||||
if (networkEntries.length > 0) {
|
||||
for (const cidr of networkEntries) {
|
||||
blockedCidrs.add(cidr);
|
||||
}
|
||||
} else if (this.normalizeIp(doc.ipAddress)) {
|
||||
blockedIps.add(this.normalizeIp(doc.ipAddress)!);
|
||||
}
|
||||
@@ -206,13 +238,13 @@ export class SecurityPolicyManager {
|
||||
return await this.compilePolicy();
|
||||
}
|
||||
|
||||
public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot | undefined> {
|
||||
public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot> {
|
||||
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;
|
||||
return { blockedIps };
|
||||
}
|
||||
|
||||
private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
|
||||
@@ -262,6 +294,81 @@ export class SecurityPolicyManager {
|
||||
return `${ip}/${prefix}`;
|
||||
}
|
||||
|
||||
private normalizeNetworkEntries(value: string): string[] {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return [];
|
||||
|
||||
const cidr = this.normalizeCidr(trimmed);
|
||||
if (cidr) return [cidr];
|
||||
|
||||
const rangeParts = trimmed.split(/\s+-\s+/);
|
||||
if (rangeParts.length === 2) {
|
||||
return this.ipv4RangeToCidrs(rangeParts[0], rangeParts[1]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private normalizeNetworkEntryList(values: Array<string | null | undefined>): string[] {
|
||||
const cidrs = new Set<string>();
|
||||
for (const value of values) {
|
||||
if (!value) continue;
|
||||
for (const entry of value.split(',').map((part) => part.trim()).filter(Boolean)) {
|
||||
for (const cidr of this.normalizeNetworkEntries(entry)) {
|
||||
cidrs.add(cidr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...cidrs];
|
||||
}
|
||||
|
||||
private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
|
||||
const start = this.ipv4ToBigInt(startIp);
|
||||
const end = this.ipv4ToBigInt(endIp);
|
||||
if (start === undefined || end === undefined || start > end) return [];
|
||||
|
||||
const cidrs: string[] = [];
|
||||
let current = start;
|
||||
while (current <= end) {
|
||||
let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
|
||||
const remaining = end - current + 1n;
|
||||
while (maxBlockSize > remaining) {
|
||||
maxBlockSize = maxBlockSize / 2n;
|
||||
}
|
||||
const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
|
||||
cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
|
||||
current += maxBlockSize;
|
||||
}
|
||||
return cidrs;
|
||||
}
|
||||
|
||||
private ipv4ToBigInt(ip: string): bigint | undefined {
|
||||
const normalized = this.normalizeIp(ip);
|
||||
if (!normalized || plugins.net.isIP(normalized) !== 4) return undefined;
|
||||
return normalized
|
||||
.split('.')
|
||||
.reduce((sum, part) => (sum * 256n) + BigInt(Number(part)), 0n);
|
||||
}
|
||||
|
||||
private numberToIpv4(value: bigint): string {
|
||||
return [
|
||||
Number((value >> 24n) & 255n),
|
||||
Number((value >> 16n) & 255n),
|
||||
Number((value >> 8n) & 255n),
|
||||
Number(value & 255n),
|
||||
].join('.');
|
||||
}
|
||||
|
||||
private powerOfTwoExponent(value: bigint): number {
|
||||
let exponent = 0;
|
||||
let remaining = value;
|
||||
while (remaining > 1n) {
|
||||
remaining >>= 1n;
|
||||
exponent++;
|
||||
}
|
||||
return exponent;
|
||||
}
|
||||
|
||||
private isPublicIp(ip: string): boolean {
|
||||
const family = plugins.net.isIP(ip);
|
||||
if (family === 4) {
|
||||
|
||||
@@ -3,6 +3,8 @@ import type * as authInterfaces from '../data/auth.js';
|
||||
import type {
|
||||
IIpIntelligenceRecord,
|
||||
ISecurityBlockRule,
|
||||
ISecurityCompiledPolicy,
|
||||
ISecurityPolicyAuditEvent,
|
||||
TSecurityBlockRuleMatchMode,
|
||||
TSecurityBlockRuleType,
|
||||
} from '../data/security-policy.js';
|
||||
@@ -87,3 +89,46 @@ export interface IReq_ListIpIntelligence extends plugins.typedrequestInterfaces.
|
||||
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 = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.23.0',
|
||||
version: '13.25.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
+235
-2
@@ -54,6 +54,7 @@ export interface INetworkState {
|
||||
topIPs: Array<{ ip: string; count: number }>;
|
||||
topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
|
||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||
ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
|
||||
domainActivity: interfaces.data.IDomainActivity[];
|
||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond: number;
|
||||
@@ -66,6 +67,16 @@ export interface INetworkState {
|
||||
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 {
|
||||
certificates: interfaces.requests.ICertificateInfo[];
|
||||
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: [],
|
||||
topIPsByBandwidth: [],
|
||||
throughputByIP: [],
|
||||
ipIntelligence: [],
|
||||
domainActivity: [],
|
||||
throughputHistory: [],
|
||||
requestsPerSecond: 0,
|
||||
@@ -178,6 +190,20 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
'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>(
|
||||
'emailOps',
|
||||
{
|
||||
@@ -517,9 +543,18 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
interfaces.requests.IReq_GetNetworkStats
|
||||
>('/typedrequest', 'getNetworkStats');
|
||||
|
||||
const networkStatsResponse = await networkStatsRequest.fire({
|
||||
const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_ListIpIntelligence
|
||||
>('/typedrequest', 'listIpIntelligence');
|
||||
|
||||
const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
|
||||
networkStatsRequest.fire({
|
||||
identity: context.identity,
|
||||
});
|
||||
}),
|
||||
ipIntelligenceRequest.fire({
|
||||
identity: context.identity,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Use the connections data for the connection list
|
||||
// and network stats for throughput and IP analytics
|
||||
@@ -561,6 +596,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
||||
topIPs: networkStatsResponse.topIPs || [],
|
||||
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
|
||||
throughputByIP: networkStatsResponse.throughputByIP || [],
|
||||
ipIntelligence: ipIntelligenceResponse.records || [],
|
||||
domainActivity: networkStatsResponse.domainActivity || [],
|
||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -2665,6 +2877,27 @@ async function dispatchCombinedRefreshActionInner() {
|
||||
isLoading: false,
|
||||
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
|
||||
|
||||
@@ -255,6 +255,17 @@ export class OpsViewNetworkActivity extends DeesElement {
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -345,6 +356,100 @@ export class OpsViewNetworkActivity extends DeesElement {
|
||||
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 } {
|
||||
// Use real throughput data from network state
|
||||
return {
|
||||
@@ -500,10 +605,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
||||
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
||||
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
||||
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||
...this.getIpIntelligenceColumns(ipData.ip),
|
||||
};
|
||||
}}
|
||||
.dataActions=${this.getIpDataActions()}
|
||||
heading1="Top Connected IPs"
|
||||
heading2="IPs with most active connections and bandwidth"
|
||||
heading2="IPs with most active connections, bandwidth, and intelligence"
|
||||
searchable
|
||||
.showColumnFilters=${true}
|
||||
.pagination=${false}
|
||||
@@ -529,10 +636,12 @@ export class OpsViewNetworkActivity extends DeesElement {
|
||||
'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
|
||||
'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
|
||||
'Connections': ipData.count,
|
||||
...this.getIpIntelligenceColumns(ipData.ip),
|
||||
};
|
||||
}}
|
||||
.dataActions=${this.getIpDataActions()}
|
||||
heading1="Top IPs by Bandwidth"
|
||||
heading2="IPs with highest throughput"
|
||||
heading2="IPs with highest throughput and intelligence"
|
||||
searchable
|
||||
.showColumnFilters=${true}
|
||||
.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() {
|
||||
// Track requests/sec history for the trend sparkline (moved out of render)
|
||||
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as appstate from '../../appstate.js';
|
||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||
import { viewHostCss } from '../shared/css.js';
|
||||
|
||||
import {
|
||||
@@ -21,18 +22,23 @@ declare global {
|
||||
@customElement('ops-view-security-blocked')
|
||||
export class OpsViewSecurityBlocked extends DeesElement {
|
||||
@state()
|
||||
accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
|
||||
accessor securityPolicyState: appstate.ISecurityPolicyState = appstate.securityPolicyStatePart.getState()!;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const sub = appstate.statsStatePart
|
||||
const sub = appstate.securityPolicyStatePart
|
||||
.select((s) => s)
|
||||
.subscribe((s) => {
|
||||
this.statsState = s;
|
||||
this.securityPolicyState = s;
|
||||
});
|
||||
this.rxSubscriptions.push(sub);
|
||||
}
|
||||
|
||||
public async connectedCallback() {
|
||||
await super.connectedCallback();
|
||||
await appstate.securityPolicyStatePart.dispatchAction(appstate.fetchSecurityPolicyAction, null);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
@@ -40,79 +46,436 @@ export class OpsViewSecurityBlocked extends DeesElement {
|
||||
dees-statsgrid {
|
||||
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 {
|
||||
const metrics = this.statsState.securityMetrics;
|
||||
|
||||
if (!metrics) {
|
||||
return html`
|
||||
<div class="loadingMessage">
|
||||
<p>Loading security metrics...</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const blockedIPs: string[] = metrics.blockedIPs || [];
|
||||
const state = this.securityPolicyState;
|
||||
const activeRules = state.rules.filter((rule) => rule.enabled);
|
||||
const disabledRules = state.rules.length - activeRules.length;
|
||||
const compiledPolicy = state.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'totalBlocked',
|
||||
title: 'Blocked IPs',
|
||||
value: blockedIPs.length,
|
||||
id: 'activeRules',
|
||||
title: 'Active Rules',
|
||||
value: activeRules.length,
|
||||
type: 'number',
|
||||
icon: 'lucide:ShieldBan',
|
||||
color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
|
||||
description: 'Currently blocked addresses',
|
||||
icon: 'lucide:shield-check',
|
||||
color: activeRules.length > 0 ? '#ef4444' : '#22c55e',
|
||||
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`
|
||||
<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
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${200}
|
||||
></dees-statsgrid>
|
||||
|
||||
<div class="sectionStack">
|
||||
${this.renderRulesTable()}
|
||||
${this.renderCompiledPolicyTable()}
|
||||
${this.renderIpIntelligenceTable()}
|
||||
${this.renderAuditTable()}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderRulesTable(): TemplateResult {
|
||||
return html`
|
||||
<dees-table
|
||||
.heading1=${'Blocked IP Addresses'}
|
||||
.heading2=${'IPs blocked due to suspicious activity'}
|
||||
.data=${blockedIPs.map((ip) => ({ ip }))}
|
||||
.displayFunction=${(item) => ({
|
||||
'IP Address': item.ip,
|
||||
'Reason': 'Suspicious activity',
|
||||
.heading1=${'Managed Block Rules'}
|
||||
.heading2=${'Rules compiled into SmartProxy policy and remote ingress edge firewall snapshots'}
|
||||
.data=${this.securityPolicyState.rules}
|
||||
.rowKey=${'id'}
|
||||
.displayFunction=${(rule: interfaces.data.ISecurityBlockRule) => ({
|
||||
'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=${[
|
||||
{
|
||||
name: 'Unblock',
|
||||
iconName: 'lucide:shield-off',
|
||||
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();
|
||||
},
|
||||
},
|
||||
]}
|
||||
.dataActions=${this.getRuleActions()}
|
||||
searchable
|
||||
.showColumnFilters=${true}
|
||||
dataName="rule"
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private async clearBlockedIPs() {
|
||||
// SmartProxy manages IP blocking — not yet exposed via API
|
||||
alert('Clearing blocked IPs is not yet supported from the UI.');
|
||||
private renderCompiledPolicyTable(): TemplateResult {
|
||||
const policy = this.securityPolicyState.compiledPolicy || { blockedIps: [], blockedCidrs: [] };
|
||||
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) {
|
||||
// SmartProxy manages IP blocking — not yet exposed via API
|
||||
alert(`Unblocking IP ${ip} is not yet supported from the UI.`);
|
||||
private renderIpIntelligenceTable(): TemplateResult {
|
||||
return html`
|
||||
<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