@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.
@@ -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';
@@ -15,7 +16,7 @@ export interface ISecurityPolicyManagerOptions {
15
16
  }
16
17
 
17
18
  export interface IRemoteIngressFirewallSnapshot {
18
- blockedIps?: string[];
19
+ blockedIps: string[];
19
20
  }
20
21
 
21
22
  export class SecurityPolicyManager {
@@ -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,13 +91,38 @@ 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,
97
122
  registrantOrg: doc.registrantOrg,
98
123
  registrantCountry: doc.registrantCountry,
99
124
  networkRange: doc.networkRange,
125
+ networkCidrs: doc.networkCidrs,
100
126
  abuseContact: doc.abuseContact,
101
127
  country: doc.country,
102
128
  countryCode: doc.countryCode,
@@ -109,7 +135,7 @@ export class SecurityPolicyManager {
109
135
  lastSeenAt: doc.lastSeenAt,
110
136
  updatedAt: doc.updatedAt,
111
137
  seenCount: doc.seenCount,
112
- }));
138
+ };
113
139
  }
114
140
 
115
141
  public async createBlockRule(input: {
@@ -180,16 +206,22 @@ export class SecurityPolicyManager {
180
206
  }
181
207
 
182
208
  if (rule.type === 'cidr') {
183
- const cidr = this.normalizeCidr(normalizedValue);
184
- if (cidr) blockedCidrs.add(cidr);
209
+ for (const cidr of this.normalizeNetworkEntries(normalizedValue)) {
210
+ blockedCidrs.add(cidr);
211
+ }
185
212
  continue;
186
213
  }
187
214
 
188
215
  for (const doc of intelligenceDocs) {
189
216
  if (!this.ruleMatchesIntelligence(rule, doc)) continue;
190
- const cidr = this.normalizeCidr(doc.networkRange || '');
191
- if (cidr) {
192
- blockedCidrs.add(cidr);
217
+ const networkEntries = this.normalizeNetworkEntryList([
218
+ ...(doc.networkCidrs || []),
219
+ doc.networkRange,
220
+ ]);
221
+ if (networkEntries.length > 0) {
222
+ for (const cidr of networkEntries) {
223
+ blockedCidrs.add(cidr);
224
+ }
193
225
  } else if (this.normalizeIp(doc.ipAddress)) {
194
226
  blockedIps.add(this.normalizeIp(doc.ipAddress)!);
195
227
  }
@@ -206,13 +238,13 @@ export class SecurityPolicyManager {
206
238
  return await this.compilePolicy();
207
239
  }
208
240
 
209
- public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot | undefined> {
241
+ public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot> {
210
242
  const policy = await this.compilePolicy();
211
243
  const blockedIps = [
212
244
  ...policy.blockedIps.filter((ip) => plugins.net.isIP(ip) === 4),
213
245
  ...policy.blockedCidrs.filter((cidr) => plugins.net.isIP(cidr.split('/')[0]) === 4),
214
246
  ];
215
- return blockedIps.length > 0 ? { blockedIps } : undefined;
247
+ return { blockedIps };
216
248
  }
217
249
 
218
250
  private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
@@ -262,6 +294,81 @@ export class SecurityPolicyManager {
262
294
  return `${ip}/${prefix}`;
263
295
  }
264
296
 
297
+ private normalizeNetworkEntries(value: string): string[] {
298
+ const trimmed = value.trim();
299
+ if (!trimmed) return [];
300
+
301
+ const cidr = this.normalizeCidr(trimmed);
302
+ if (cidr) return [cidr];
303
+
304
+ const rangeParts = trimmed.split(/\s+-\s+/);
305
+ if (rangeParts.length === 2) {
306
+ return this.ipv4RangeToCidrs(rangeParts[0], rangeParts[1]);
307
+ }
308
+
309
+ return [];
310
+ }
311
+
312
+ private normalizeNetworkEntryList(values: Array<string | null | undefined>): string[] {
313
+ const cidrs = new Set<string>();
314
+ for (const value of values) {
315
+ if (!value) continue;
316
+ for (const entry of value.split(',').map((part) => part.trim()).filter(Boolean)) {
317
+ for (const cidr of this.normalizeNetworkEntries(entry)) {
318
+ cidrs.add(cidr);
319
+ }
320
+ }
321
+ }
322
+ return [...cidrs];
323
+ }
324
+
325
+ private ipv4RangeToCidrs(startIp: string, endIp: string): string[] {
326
+ const start = this.ipv4ToBigInt(startIp);
327
+ const end = this.ipv4ToBigInt(endIp);
328
+ if (start === undefined || end === undefined || start > end) return [];
329
+
330
+ const cidrs: string[] = [];
331
+ let current = start;
332
+ while (current <= end) {
333
+ let maxBlockSize = current === 0n ? 1n << 32n : current & -current;
334
+ const remaining = end - current + 1n;
335
+ while (maxBlockSize > remaining) {
336
+ maxBlockSize = maxBlockSize / 2n;
337
+ }
338
+ const prefixLength = 32 - this.powerOfTwoExponent(maxBlockSize);
339
+ cidrs.push(`${this.numberToIpv4(current)}/${prefixLength}`);
340
+ current += maxBlockSize;
341
+ }
342
+ return cidrs;
343
+ }
344
+
345
+ private ipv4ToBigInt(ip: string): bigint | undefined {
346
+ const normalized = this.normalizeIp(ip);
347
+ if (!normalized || plugins.net.isIP(normalized) !== 4) return undefined;
348
+ return normalized
349
+ .split('.')
350
+ .reduce((sum, part) => (sum * 256n) + BigInt(Number(part)), 0n);
351
+ }
352
+
353
+ private numberToIpv4(value: bigint): string {
354
+ return [
355
+ Number((value >> 24n) & 255n),
356
+ Number((value >> 16n) & 255n),
357
+ Number((value >> 8n) & 255n),
358
+ Number(value & 255n),
359
+ ].join('.');
360
+ }
361
+
362
+ private powerOfTwoExponent(value: bigint): number {
363
+ let exponent = 0;
364
+ let remaining = value;
365
+ while (remaining > 1n) {
366
+ remaining >>= 1n;
367
+ exponent++;
368
+ }
369
+ return exponent;
370
+ }
371
+
265
372
  private isPublicIp(ip: string): boolean {
266
373
  const family = plugins.net.isIP(ip);
267
374
  if (family === 4) {
@@ -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.25.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