2026-04-08 07:45:26 +00:00
|
|
|
import * as appstate from '../../appstate.js';
|
2026-04-26 19:51:08 +00:00
|
|
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
2026-04-08 08:24:55 +00:00
|
|
|
import { viewHostCss } from '../shared/css.js';
|
2026-04-08 07:45:26 +00:00
|
|
|
|
|
|
|
|
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()
|
2026-04-26 19:51:08 +00:00
|
|
|
accessor securityPolicyState: appstate.ISecurityPolicyState = appstate.securityPolicyStatePart.getState()!;
|
2026-04-08 07:45:26 +00:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
2026-04-26 19:51:08 +00:00
|
|
|
const sub = appstate.securityPolicyStatePart
|
2026-04-08 07:45:26 +00:00
|
|
|
.select((s) => s)
|
|
|
|
|
.subscribe((s) => {
|
2026-04-26 19:51:08 +00:00
|
|
|
this.securityPolicyState = s;
|
2026-04-08 07:45:26 +00:00
|
|
|
});
|
|
|
|
|
this.rxSubscriptions.push(sub);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 19:51:08 +00:00
|
|
|
public async connectedCallback() {
|
|
|
|
|
await super.connectedCallback();
|
|
|
|
|
await appstate.securityPolicyStatePart.dispatchAction(appstate.fetchSecurityPolicyAction, null);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 07:45:26 +00:00
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
2026-04-08 08:24:55 +00:00
|
|
|
viewHostCss,
|
2026-04-08 07:45:26 +00:00
|
|
|
css`
|
|
|
|
|
dees-statsgrid {
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
}
|
2026-04-26 19:51:08 +00:00
|
|
|
|
|
|
|
|
.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')};
|
|
|
|
|
}
|
2026-04-08 07:45:26 +00:00
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
2026-04-26 19:51:08 +00:00
|
|
|
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: [] };
|
2026-04-08 07:45:26 +00:00
|
|
|
|
|
|
|
|
const tiles: IStatsTile[] = [
|
|
|
|
|
{
|
2026-04-26 19:51:08 +00:00
|
|
|
id: 'activeRules',
|
|
|
|
|
title: 'Active Rules',
|
|
|
|
|
value: activeRules.length,
|
2026-04-08 07:45:26 +00:00
|
|
|
type: 'number',
|
2026-04-26 19:51:08 +00:00
|
|
|
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',
|
2026-04-08 07:45:26 +00:00
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return html`
|
2026-04-26 19:51:08 +00:00
|
|
|
<dees-heading level="3">Security Blocking</dees-heading>
|
|
|
|
|
|
|
|
|
|
${state.error ? html`<div class="errorMessage">${state.error}</div>` : html``}
|
2026-04-08 07:45:26 +00:00
|
|
|
|
|
|
|
|
<dees-statsgrid
|
|
|
|
|
.tiles=${tiles}
|
|
|
|
|
.minTileWidth=${200}
|
|
|
|
|
></dees-statsgrid>
|
|
|
|
|
|
2026-04-26 19:51:08 +00:00
|
|
|
<div class="sectionStack">
|
|
|
|
|
${this.renderRulesTable()}
|
|
|
|
|
${this.renderCompiledPolicyTable()}
|
|
|
|
|
${this.renderIpIntelligenceTable()}
|
|
|
|
|
${this.renderAuditTable()}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private renderRulesTable(): TemplateResult {
|
|
|
|
|
return html`
|
2026-04-08 07:45:26 +00:00
|
|
|
<dees-table
|
2026-04-26 19:51:08 +00:00
|
|
|
.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),
|
2026-04-08 07:45:26 +00:00
|
|
|
})}
|
2026-04-26 19:51:08 +00:00
|
|
|
.dataActions=${this.getRuleActions()}
|
|
|
|
|
searchable
|
|
|
|
|
.showColumnFilters=${true}
|
|
|
|
|
dataName="rule"
|
|
|
|
|
></dees-table>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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"
|
2026-04-08 07:45:26 +00:00
|
|
|
></dees-table>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-26 19:51:08 +00:00
|
|
|
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() : '-';
|
2026-04-08 07:45:26 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-26 19:51:08 +00:00
|
|
|
private formatAuditDetails(details: Record<string, unknown>): string {
|
|
|
|
|
const text = JSON.stringify(details);
|
|
|
|
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
2026-04-08 07:45:26 +00:00
|
|
|
}
|
|
|
|
|
}
|