feat(security): add security policy management and IP intelligence operations to the ops UI
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user