import * as appstate from '../../appstate.js'; import * as interfaces from '../../../dist_ts_interfaces/index.js'; import { viewHostCss } from '../shared/css.js'; import { DeesElement, customElement, html, state, css, cssManager, type TemplateResult, } from '@design.estate/dees-element'; import { type IStatsTile } from '@design.estate/dees-catalog'; declare global { interface HTMLElementTagNameMap { 'ops-view-security-blocked': OpsViewSecurityBlocked; } } @customElement('ops-view-security-blocked') export class OpsViewSecurityBlocked extends DeesElement { @state() accessor securityPolicyState: appstate.ISecurityPolicyState = appstate.securityPolicyStatePart.getState()!; constructor() { super(); const sub = appstate.securityPolicyStatePart .select((s) => s) .subscribe((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, css` 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 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: 'activeRules', title: 'Active Rules', value: activeRules.length, type: 'number', 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` Security Blocking ${state.error ? html`
${state.error}
` : html``}
${this.renderRulesTable()} ${this.renderCompiledPolicyTable()} ${this.renderIpIntelligenceTable()} ${this.renderAuditTable()}
`; } private renderRulesTable(): TemplateResult { return html` ({ 'Type': html`${rule.type}`, 'Value': rule.value, 'Match': rule.type === 'organization' ? (rule.matchMode || 'contains') : '-', 'Reason': rule.reason || '-', 'Status': html`${rule.enabled ? 'Enabled' : 'Disabled'}`, 'Created': this.formatDateTime(rule.createdAt), 'Updated': this.formatDateTime(rule.updatedAt), })} .dataActions=${this.getRuleActions()} searchable .showColumnFilters=${true} dataName="rule" > `; } 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` ({ 'Enforcement Type': html`${row.type}`, 'Value': row.value, })} searchable .showColumnFilters=${true} dataName="compiled rule" > `; } private renderIpIntelligenceTable(): TemplateResult { return html` ({ '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" > `; } private renderAuditTable(): TemplateResult { return html` ({ 'Time': this.formatDateTime(event.createdAt), 'Action': event.action, 'Actor': event.actor, 'Details': this.formatAuditDetails(event.details), })} searchable .showColumnFilters=${true} dataName="audit event" > `; } 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 = {}, ): Promise { 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` ${rule ? html`` : html` option.key === selectedType)} > `} option.key === selectedMatchMode)} > `, 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 { const text = JSON.stringify(details); return text.length > 160 ? `${text.slice(0, 157)}...` : text; } }