feat(security): add security policy management and IP intelligence operations to the ops UI

This commit is contained in:
2026-04-26 19:51:08 +00:00
parent 1567606c49
commit e5c3578163
9 changed files with 991 additions and 62 deletions
@@ -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;