@serve.zone/dcrouter 13.23.0 → 13.25.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.
@@ -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;