@serve.zone/dcrouter 13.23.0 → 13.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ import type {
5
5
  IIpIntelligenceRecord,
6
6
  ISecurityBlockRule,
7
7
  ISecurityCompiledPolicy,
8
+ ISecurityPolicyAuditEvent,
8
9
  TSecurityBlockRuleMatchMode,
9
10
  TSecurityBlockRuleType,
10
11
  } from '../../ts_interfaces/data/security-policy.js';
@@ -44,7 +45,7 @@ export class SecurityPolicyManager {
44
45
  await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
45
46
  }
46
47
 
47
- public async observeIp(ipAddress: string): Promise<void> {
48
+ public async observeIp(ipAddress: string, options: { force?: boolean } = {}): Promise<void> {
48
49
  const ip = this.normalizeIp(ipAddress);
49
50
  if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
50
51
  return;
@@ -54,7 +55,7 @@ export class SecurityPolicyManager {
54
55
  try {
55
56
  const now = Date.now();
56
57
  let doc = await IpIntelligenceDoc.findByIp(ip);
57
- if (doc && now - doc.updatedAt < this.intelligenceRefreshMs) {
58
+ if (doc && !options.force && now - doc.updatedAt < this.intelligenceRefreshMs) {
58
59
  if (now - doc.lastSeenAt > 60_000) {
59
60
  doc.lastSeenAt = now;
60
61
  doc.seenCount = (doc.seenCount || 0) + 1;
@@ -90,7 +91,31 @@ export class SecurityPolicyManager {
90
91
  }
91
92
 
92
93
  public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
93
- return (await IpIntelligenceDoc.findAll()).map((doc) => ({
94
+ return (await IpIntelligenceDoc.findAll()).map((doc) => this.intelligenceFromDoc(doc));
95
+ }
96
+
97
+ public async refreshIpIntelligence(ipAddress: string): Promise<IIpIntelligenceRecord | null> {
98
+ const ip = this.normalizeIp(ipAddress);
99
+ if (!ip || !this.isPublicIp(ip)) {
100
+ return null;
101
+ }
102
+ await this.observeIp(ip, { force: true });
103
+ const doc = await IpIntelligenceDoc.findByIp(ip);
104
+ return doc ? this.intelligenceFromDoc(doc) : null;
105
+ }
106
+
107
+ public async listAuditEvents(limit = 100): Promise<ISecurityPolicyAuditEvent[]> {
108
+ return (await SecurityPolicyAuditDoc.findRecent(limit)).map((doc) => ({
109
+ id: doc.id,
110
+ action: doc.action,
111
+ actor: doc.actor,
112
+ details: doc.details,
113
+ createdAt: doc.createdAt,
114
+ }));
115
+ }
116
+
117
+ private intelligenceFromDoc(doc: IpIntelligenceDoc): IIpIntelligenceRecord {
118
+ return {
94
119
  ipAddress: doc.ipAddress,
95
120
  asn: doc.asn,
96
121
  asnOrg: doc.asnOrg,
@@ -109,7 +134,7 @@ export class SecurityPolicyManager {
109
134
  lastSeenAt: doc.lastSeenAt,
110
135
  updatedAt: doc.updatedAt,
111
136
  seenCount: doc.seenCount,
112
- }));
137
+ };
113
138
  }
114
139
 
115
140
  public async createBlockRule(input: {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.23.0',
6
+ version: '13.24.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -54,6 +54,7 @@ export interface INetworkState {
54
54
  topIPs: Array<{ ip: string; count: number }>;
55
55
  topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
56
56
  throughputByIP: Array<{ ip: string; in: number; out: number }>;
57
+ ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
57
58
  domainActivity: interfaces.data.IDomainActivity[];
58
59
  throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
59
60
  requestsPerSecond: number;
@@ -66,6 +67,16 @@ export interface INetworkState {
66
67
  error: string | null;
67
68
  }
68
69
 
70
+ export interface ISecurityPolicyState {
71
+ rules: interfaces.data.ISecurityBlockRule[];
72
+ ipIntelligence: interfaces.data.IIpIntelligenceRecord[];
73
+ compiledPolicy: interfaces.data.ISecurityCompiledPolicy | null;
74
+ auditEvents: interfaces.data.ISecurityPolicyAuditEvent[];
75
+ isLoading: boolean;
76
+ error: string | null;
77
+ lastUpdated: number;
78
+ }
79
+
69
80
  export interface ICertificateState {
70
81
  certificates: interfaces.requests.ICertificateInfo[];
71
82
  summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
@@ -164,6 +175,7 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
164
175
  topIPs: [],
165
176
  topIPsByBandwidth: [],
166
177
  throughputByIP: [],
178
+ ipIntelligence: [],
167
179
  domainActivity: [],
168
180
  throughputHistory: [],
169
181
  requestsPerSecond: 0,
@@ -178,6 +190,20 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
178
190
  'soft'
179
191
  );
180
192
 
193
+ export const securityPolicyStatePart = await appState.getStatePart<ISecurityPolicyState>(
194
+ 'securityPolicy',
195
+ {
196
+ rules: [],
197
+ ipIntelligence: [],
198
+ compiledPolicy: null,
199
+ auditEvents: [],
200
+ isLoading: false,
201
+ error: null,
202
+ lastUpdated: 0,
203
+ },
204
+ 'soft',
205
+ );
206
+
181
207
  export const emailOpsStatePart = await appState.getStatePart<IEmailOpsState>(
182
208
  'emailOps',
183
209
  {
@@ -517,9 +543,18 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
517
543
  interfaces.requests.IReq_GetNetworkStats
518
544
  >('/typedrequest', 'getNetworkStats');
519
545
 
520
- const networkStatsResponse = await networkStatsRequest.fire({
521
- identity: context.identity,
522
- });
546
+ const ipIntelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
547
+ interfaces.requests.IReq_ListIpIntelligence
548
+ >('/typedrequest', 'listIpIntelligence');
549
+
550
+ const [networkStatsResponse, ipIntelligenceResponse] = await Promise.all([
551
+ networkStatsRequest.fire({
552
+ identity: context.identity,
553
+ }),
554
+ ipIntelligenceRequest.fire({
555
+ identity: context.identity,
556
+ }),
557
+ ]);
523
558
 
524
559
  // Use the connections data for the connection list
525
560
  // and network stats for throughput and IP analytics
@@ -561,6 +596,7 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
561
596
  topIPs: networkStatsResponse.topIPs || [],
562
597
  topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
563
598
  throughputByIP: networkStatsResponse.throughputByIP || [],
599
+ ipIntelligence: ipIntelligenceResponse.records || [],
564
600
  domainActivity: networkStatsResponse.domainActivity || [],
565
601
  throughputHistory: networkStatsResponse.throughputHistory || [],
566
602
  requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
@@ -582,6 +618,182 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
582
618
  }
583
619
  });
584
620
 
621
+ // ============================================================================
622
+ // Security Policy Actions
623
+ // ============================================================================
624
+
625
+ export const fetchSecurityPolicyAction = securityPolicyStatePart.createAction(
626
+ async (statePartArg): Promise<ISecurityPolicyState> => {
627
+ const context = getActionContext();
628
+ const currentState = statePartArg.getState()!;
629
+ if (!context.identity) return currentState;
630
+
631
+ try {
632
+ const rulesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
633
+ interfaces.requests.IReq_ListSecurityBlockRules
634
+ >('/typedrequest', 'listSecurityBlockRules');
635
+ const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
636
+ interfaces.requests.IReq_ListIpIntelligence
637
+ >('/typedrequest', 'listIpIntelligence');
638
+ const compiledPolicyRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
639
+ interfaces.requests.IReq_GetCompiledSecurityPolicy
640
+ >('/typedrequest', 'getCompiledSecurityPolicy');
641
+ const auditRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
642
+ interfaces.requests.IReq_ListSecurityPolicyAudit
643
+ >('/typedrequest', 'listSecurityPolicyAudit');
644
+
645
+ const [rulesResponse, intelligenceResponse, compiledPolicyResponse, auditResponse] = await Promise.all([
646
+ rulesRequest.fire({ identity: context.identity }),
647
+ intelligenceRequest.fire({ identity: context.identity }),
648
+ compiledPolicyRequest.fire({ identity: context.identity }),
649
+ auditRequest.fire({ identity: context.identity, limit: 100 }),
650
+ ]);
651
+
652
+ return {
653
+ rules: rulesResponse.rules || [],
654
+ ipIntelligence: intelligenceResponse.records || [],
655
+ compiledPolicy: compiledPolicyResponse.policy,
656
+ auditEvents: auditResponse.events || [],
657
+ isLoading: false,
658
+ error: null,
659
+ lastUpdated: Date.now(),
660
+ };
661
+ } catch (error: unknown) {
662
+ return {
663
+ ...currentState,
664
+ isLoading: false,
665
+ error: error instanceof Error ? error.message : 'Failed to fetch security policy',
666
+ };
667
+ }
668
+ },
669
+ );
670
+
671
+ export const createSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
672
+ type: interfaces.data.TSecurityBlockRuleType;
673
+ value: string;
674
+ matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
675
+ reason?: string;
676
+ enabled?: boolean;
677
+ }>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
678
+ const context = getActionContext();
679
+ const currentState = statePartArg.getState()!;
680
+ if (!context.identity) return currentState;
681
+
682
+ try {
683
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
684
+ interfaces.requests.IReq_CreateSecurityBlockRule
685
+ >('/typedrequest', 'createSecurityBlockRule');
686
+
687
+ const response = await request.fire({
688
+ identity: context.identity,
689
+ type: dataArg.type,
690
+ value: dataArg.value,
691
+ matchMode: dataArg.matchMode,
692
+ reason: dataArg.reason,
693
+ enabled: dataArg.enabled,
694
+ });
695
+
696
+ if (!response.success) {
697
+ return { ...currentState, error: response.message || 'Failed to create security block rule' };
698
+ }
699
+
700
+ return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
701
+ } catch (error: unknown) {
702
+ return {
703
+ ...currentState,
704
+ error: error instanceof Error ? error.message : 'Failed to create security block rule',
705
+ };
706
+ }
707
+ });
708
+
709
+ export const updateSecurityBlockRuleAction = securityPolicyStatePart.createAction<{
710
+ id: string;
711
+ value?: string;
712
+ matchMode?: interfaces.data.TSecurityBlockRuleMatchMode;
713
+ reason?: string;
714
+ enabled?: boolean;
715
+ }>(async (statePartArg, dataArg, actionContext): Promise<ISecurityPolicyState> => {
716
+ const context = getActionContext();
717
+ const currentState = statePartArg.getState()!;
718
+ if (!context.identity) return currentState;
719
+
720
+ try {
721
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
722
+ interfaces.requests.IReq_UpdateSecurityBlockRule
723
+ >('/typedrequest', 'updateSecurityBlockRule');
724
+
725
+ const response = await request.fire({
726
+ identity: context.identity,
727
+ id: dataArg.id,
728
+ value: dataArg.value,
729
+ matchMode: dataArg.matchMode,
730
+ reason: dataArg.reason,
731
+ enabled: dataArg.enabled,
732
+ });
733
+
734
+ if (!response.success) {
735
+ return { ...currentState, error: response.message || 'Failed to update security block rule' };
736
+ }
737
+
738
+ return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
739
+ } catch (error: unknown) {
740
+ return {
741
+ ...currentState,
742
+ error: error instanceof Error ? error.message : 'Failed to update security block rule',
743
+ };
744
+ }
745
+ });
746
+
747
+ export const deleteSecurityBlockRuleAction = securityPolicyStatePart.createAction<string>(
748
+ async (statePartArg, ruleId, actionContext): Promise<ISecurityPolicyState> => {
749
+ const context = getActionContext();
750
+ const currentState = statePartArg.getState()!;
751
+ if (!context.identity) return currentState;
752
+
753
+ try {
754
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
755
+ interfaces.requests.IReq_DeleteSecurityBlockRule
756
+ >('/typedrequest', 'deleteSecurityBlockRule');
757
+
758
+ const response = await request.fire({ identity: context.identity, id: ruleId });
759
+ if (!response.success) {
760
+ return { ...currentState, error: response.message || 'Failed to delete security block rule' };
761
+ }
762
+
763
+ return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
764
+ } catch (error: unknown) {
765
+ return {
766
+ ...currentState,
767
+ error: error instanceof Error ? error.message : 'Failed to delete security block rule',
768
+ };
769
+ }
770
+ },
771
+ );
772
+
773
+ export const refreshIpIntelligenceAction = securityPolicyStatePart.createAction<string>(
774
+ async (statePartArg, ipAddress, actionContext): Promise<ISecurityPolicyState> => {
775
+ const context = getActionContext();
776
+ const currentState = statePartArg.getState()!;
777
+ if (!context.identity) return currentState;
778
+
779
+ try {
780
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
781
+ interfaces.requests.IReq_RefreshIpIntelligence
782
+ >('/typedrequest', 'refreshIpIntelligence');
783
+ const response = await request.fire({ identity: context.identity, ipAddress });
784
+ if (!response.success) {
785
+ return { ...currentState, error: response.message || 'Failed to refresh IP intelligence' };
786
+ }
787
+ return await actionContext!.dispatch(fetchSecurityPolicyAction, null);
788
+ } catch (error: unknown) {
789
+ return {
790
+ ...currentState,
791
+ error: error instanceof Error ? error.message : 'Failed to refresh IP intelligence',
792
+ };
793
+ }
794
+ },
795
+ );
796
+
585
797
  // ============================================================================
586
798
  // Email Operations Actions
587
799
  // ============================================================================
@@ -2665,6 +2877,27 @@ async function dispatchCombinedRefreshActionInner() {
2665
2877
  isLoading: false,
2666
2878
  error: null,
2667
2879
  });
2880
+
2881
+ try {
2882
+ const intelligenceRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
2883
+ interfaces.requests.IReq_ListIpIntelligence
2884
+ >('/typedrequest', 'listIpIntelligence');
2885
+ const intelligenceResponse = await intelligenceRequest.fire({ identity: context.identity });
2886
+ networkStatePart.setState({
2887
+ ...networkStatePart.getState()!,
2888
+ ipIntelligence: intelligenceResponse.records || [],
2889
+ });
2890
+ } catch (error) {
2891
+ console.error('IP intelligence refresh failed:', error);
2892
+ }
2893
+ }
2894
+
2895
+ if (currentView === 'security') {
2896
+ try {
2897
+ await securityPolicyStatePart.dispatchAction(fetchSecurityPolicyAction, null);
2898
+ } catch (error) {
2899
+ console.error('Security policy refresh failed:', error);
2900
+ }
2668
2901
  }
2669
2902
 
2670
2903
  // Refresh certificate data if on Domains > Certificates subview
@@ -255,6 +255,17 @@ export class OpsViewNetworkActivity extends DeesElement {
255
255
  color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
256
256
  }
257
257
 
258
+ .intelligenceBadge {
259
+ display: inline-flex;
260
+ align-items: center;
261
+ padding: 4px 8px;
262
+ border-radius: 999px;
263
+ font-size: 12px;
264
+ font-weight: 500;
265
+ background: ${cssManager.bdTheme('#eef2ff', '#1e1b4b')};
266
+ color: ${cssManager.bdTheme('#4338ca', '#a5b4fc')};
267
+ }
268
+
258
269
  .protocolChartGrid {
259
270
  display: grid;
260
271
  grid-template-columns: repeat(2, 1fr);
@@ -345,6 +356,100 @@ export class OpsViewNetworkActivity extends DeesElement {
345
356
  return `${size.toFixed(1)} ${units[unitIndex]}`;
346
357
  }
347
358
 
359
+ private formatOptional(value: unknown): string {
360
+ if (value === null || value === undefined || value === '') return '-';
361
+ return String(value);
362
+ }
363
+
364
+ private formatDateTime(timestamp?: number | null): string {
365
+ return timestamp ? new Date(timestamp).toLocaleString() : '-';
366
+ }
367
+
368
+ private getIpIntelligence(ip: string): interfaces.data.IIpIntelligenceRecord | undefined {
369
+ return this.networkState.ipIntelligence?.find((record) => record.ipAddress === ip);
370
+ }
371
+
372
+ private getIpOrganization(record?: interfaces.data.IIpIntelligenceRecord): string {
373
+ return record?.asnOrg || record?.registrantOrg || '';
374
+ }
375
+
376
+ private getIpIntelligenceColumns(ip: string): Record<string, unknown> {
377
+ const record = this.getIpIntelligence(ip);
378
+ const organization = this.getIpOrganization(record);
379
+ return {
380
+ 'Intelligence': record
381
+ ? html`<span class="intelligenceBadge">${this.formatOptional(organization || record.countryCode || 'Known')}</span>`
382
+ : html`<span class="statusBadge warning">Enriching...</span>`,
383
+ 'ASN': record?.asn ? `AS${record.asn}` : '-',
384
+ 'Organization': this.formatOptional(organization),
385
+ 'Country': this.formatOptional(record?.countryCode || record?.country),
386
+ 'Network Range': this.formatOptional(record?.networkRange),
387
+ 'Last Seen': this.formatDateTime(record?.lastSeenAt),
388
+ };
389
+ }
390
+
391
+ private getIpDataActions() {
392
+ return [
393
+ {
394
+ name: 'Refresh Intelligence',
395
+ iconName: 'lucide:refresh-cw',
396
+ type: ['inRow', 'contextmenu'] as any,
397
+ actionFunc: async (actionData: any) => {
398
+ const ip = actionData.item.ip;
399
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.refreshIpIntelligenceAction, ip);
400
+ await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
401
+ },
402
+ },
403
+ {
404
+ name: 'Block IP',
405
+ iconName: 'lucide:shield-ban',
406
+ type: ['inRow', 'contextmenu'] as any,
407
+ actionFunc: async (actionData: any) => {
408
+ await this.createBlockRuleDialog('ip', actionData.item.ip, 'Blocked from Network Activity');
409
+ },
410
+ },
411
+ {
412
+ name: 'Block Network Range',
413
+ iconName: 'lucide:network',
414
+ type: ['contextmenu'] as any,
415
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.networkRange),
416
+ actionFunc: async (actionData: any) => {
417
+ const record = this.getIpIntelligence(actionData.item.ip);
418
+ await this.createBlockRuleDialog('cidr', record!.networkRange!, 'Blocked network range from Network Activity');
419
+ },
420
+ },
421
+ {
422
+ name: 'Block ASN',
423
+ iconName: 'lucide:radio-tower',
424
+ type: ['contextmenu'] as any,
425
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)?.asn),
426
+ actionFunc: async (actionData: any) => {
427
+ const record = this.getIpIntelligence(actionData.item.ip);
428
+ await this.createBlockRuleDialog('asn', String(record!.asn), 'Blocked ASN from Network Activity');
429
+ },
430
+ },
431
+ {
432
+ name: 'Block Organization',
433
+ iconName: 'lucide:building-2',
434
+ type: ['contextmenu'] as any,
435
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpOrganization(this.getIpIntelligence(actionData.item.ip))),
436
+ actionFunc: async (actionData: any) => {
437
+ const record = this.getIpIntelligence(actionData.item.ip);
438
+ await this.createBlockRuleDialog('organization', this.getIpOrganization(record), 'Blocked organization from Network Activity');
439
+ },
440
+ },
441
+ {
442
+ name: 'View Intelligence',
443
+ iconName: 'lucide:info',
444
+ type: ['doubleClick', 'contextmenu'] as any,
445
+ actionRelevancyCheckFunc: (actionData: any) => Boolean(this.getIpIntelligence(actionData.item.ip)),
446
+ actionFunc: async (actionData: any) => {
447
+ await this.showIpIntelligenceDetails(actionData.item.ip);
448
+ },
449
+ },
450
+ ];
451
+ }
452
+
348
453
  private calculateThroughput(): { in: number; out: number } {
349
454
  // Use real throughput data from network state
350
455
  return {
@@ -500,10 +605,12 @@ export class OpsViewNetworkActivity extends DeesElement {
500
605
  'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
501
606
  'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
502
607
  'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
608
+ ...this.getIpIntelligenceColumns(ipData.ip),
503
609
  };
504
610
  }}
611
+ .dataActions=${this.getIpDataActions()}
505
612
  heading1="Top Connected IPs"
506
- heading2="IPs with most active connections and bandwidth"
613
+ heading2="IPs with most active connections, bandwidth, and intelligence"
507
614
  searchable
508
615
  .showColumnFilters=${true}
509
616
  .pagination=${false}
@@ -529,10 +636,12 @@ export class OpsViewNetworkActivity extends DeesElement {
529
636
  'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
530
637
  'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
531
638
  'Connections': ipData.count,
639
+ ...this.getIpIntelligenceColumns(ipData.ip),
532
640
  };
533
641
  }}
642
+ .dataActions=${this.getIpDataActions()}
534
643
  heading1="Top IPs by Bandwidth"
535
- heading2="IPs with highest throughput"
644
+ heading2="IPs with highest throughput and intelligence"
536
645
  searchable
537
646
  .showColumnFilters=${true}
538
647
  .pagination=${false}
@@ -678,6 +787,114 @@ export class OpsViewNetworkActivity extends DeesElement {
678
787
  });
679
788
  }
680
789
 
790
+ private getDropdownKey(value: any): string {
791
+ return typeof value === 'string' ? value : value?.key || '';
792
+ }
793
+
794
+ private async createBlockRuleDialog(
795
+ type: interfaces.data.TSecurityBlockRuleType,
796
+ value: string,
797
+ reason: string,
798
+ ): Promise<void> {
799
+ const { DeesModal } = await import('@design.estate/dees-catalog');
800
+ const typeOptions = [
801
+ { key: 'ip', option: 'IP address' },
802
+ { key: 'cidr', option: 'CIDR / network range' },
803
+ { key: 'asn', option: 'ASN' },
804
+ { key: 'organization', option: 'Organization' },
805
+ ];
806
+ const matchModeOptions = [
807
+ { key: 'contains', option: 'Organization contains value' },
808
+ { key: 'exact', option: 'Organization exactly matches value' },
809
+ ];
810
+
811
+ await DeesModal.createAndShow({
812
+ heading: 'Create Security Block Rule',
813
+ content: html`
814
+ <dees-form>
815
+ <dees-input-dropdown
816
+ .key=${'type'}
817
+ .label=${'Rule Type'}
818
+ .options=${typeOptions}
819
+ .selectedOption=${typeOptions.find((option) => option.key === type)}
820
+ ></dees-input-dropdown>
821
+ <dees-input-text .key=${'value'} .label=${'Value'} .value=${value} .required=${true}></dees-input-text>
822
+ <dees-input-dropdown
823
+ .key=${'matchMode'}
824
+ .label=${'Organization Match Mode'}
825
+ .description=${'Only used for organization rules'}
826
+ .options=${matchModeOptions}
827
+ .selectedOption=${matchModeOptions[0]}
828
+ ></dees-input-dropdown>
829
+ <dees-input-text .key=${'reason'} .label=${'Reason'} .value=${reason}></dees-input-text>
830
+ <dees-input-checkbox .key=${'enabled'} .label=${'Enable immediately'} .value=${true}></dees-input-checkbox>
831
+ </dees-form>
832
+ `,
833
+ menuOptions: [
834
+ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy() },
835
+ {
836
+ name: 'Create',
837
+ iconName: 'lucide:shield-ban',
838
+ action: async (modalArg: any) => {
839
+ const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
840
+ if (!form) return;
841
+ const data = await form.collectFormData();
842
+ const selectedType = this.getDropdownKey(data.type) as interfaces.data.TSecurityBlockRuleType;
843
+ const selectedValue = String(data.value || '').trim();
844
+ if (!selectedType || !selectedValue) return;
845
+ const matchMode = selectedType === 'organization'
846
+ ? this.getDropdownKey(data.matchMode) as interfaces.data.TSecurityBlockRuleMatchMode
847
+ : undefined;
848
+ await appstate.securityPolicyStatePart.dispatchAction(appstate.createSecurityBlockRuleAction, {
849
+ type: selectedType,
850
+ value: selectedValue,
851
+ matchMode,
852
+ reason: String(data.reason || '').trim() || undefined,
853
+ enabled: data.enabled !== false,
854
+ });
855
+ await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
856
+ await modalArg.destroy();
857
+ },
858
+ },
859
+ ],
860
+ });
861
+ }
862
+
863
+ private async showIpIntelligenceDetails(ip: string): Promise<void> {
864
+ const record = this.getIpIntelligence(ip);
865
+ if (!record) return;
866
+ const { DeesModal } = await import('@design.estate/dees-catalog');
867
+
868
+ await DeesModal.createAndShow({
869
+ heading: `IP Intelligence: ${ip}`,
870
+ content: html`
871
+ <div style="padding: 20px;">
872
+ <dees-dataview-codebox
873
+ .heading=${'Intelligence Record'}
874
+ progLang="json"
875
+ .codeToDisplay=${JSON.stringify(record, null, 2)}
876
+ ></dees-dataview-codebox>
877
+ </div>
878
+ `,
879
+ menuOptions: [
880
+ {
881
+ name: 'Copy Abuse Contact',
882
+ iconName: 'lucide:copy',
883
+ action: async () => {
884
+ if (record.abuseContact) await navigator.clipboard.writeText(record.abuseContact);
885
+ },
886
+ },
887
+ {
888
+ name: 'Block IP',
889
+ iconName: 'lucide:shield-ban',
890
+ action: async () => {
891
+ await this.createBlockRuleDialog('ip', record.ipAddress, 'Blocked from IP intelligence details');
892
+ },
893
+ },
894
+ ],
895
+ });
896
+ }
897
+
681
898
  private async updateNetworkData() {
682
899
  // Track requests/sec history for the trend sparkline (moved out of render)
683
900
  const reqPerSec = this.networkState.requestsPerSecond || 0;