@serve.zone/dcrouter 13.22.0 → 13.23.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.
Files changed (42) hide show
  1. package/dist_serve/bundle.js +2 -2
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +4 -0
  4. package/dist_ts/classes.dcrouter.js +69 -2
  5. package/dist_ts/db/documents/classes.ip-intelligence.doc.d.ts +25 -0
  6. package/dist_ts/db/documents/classes.ip-intelligence.doc.js +175 -0
  7. package/dist_ts/db/documents/classes.security-block-rule.doc.d.ts +17 -0
  8. package/dist_ts/db/documents/classes.security-block-rule.doc.js +124 -0
  9. package/dist_ts/db/documents/classes.security-policy-audit.doc.d.ts +11 -0
  10. package/dist_ts/db/documents/classes.security-policy-audit.doc.js +95 -0
  11. package/dist_ts/db/documents/index.d.ts +3 -0
  12. package/dist_ts/db/documents/index.js +4 -1
  13. package/dist_ts/monitoring/classes.metricsmanager.js +2 -1
  14. package/dist_ts/opsserver/handlers/security.handler.js +42 -1
  15. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +10 -0
  16. package/dist_ts/remoteingress/classes.remoteingress-manager.js +9 -1
  17. package/dist_ts/security/classes.security-policy-manager.d.ts +41 -0
  18. package/dist_ts/security/classes.security-policy-manager.js +283 -0
  19. package/dist_ts/security/index.d.ts +1 -0
  20. package/dist_ts/security/index.js +2 -1
  21. package/dist_ts_interfaces/data/index.d.ts +1 -0
  22. package/dist_ts_interfaces/data/index.js +2 -1
  23. package/dist_ts_interfaces/data/security-policy.d.ts +32 -0
  24. package/dist_ts_interfaces/data/security-policy.js +2 -0
  25. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  26. package/dist_ts_interfaces/requests/index.js +2 -1
  27. package/dist_ts_interfaces/requests/security-policy.d.ts +64 -0
  28. package/dist_ts_interfaces/requests/security-policy.js +2 -0
  29. package/dist_ts_web/00_commitinfo_data.js +1 -1
  30. package/package.json +3 -3
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/classes.dcrouter.ts +91 -3
  33. package/ts/db/documents/classes.ip-intelligence.doc.ts +75 -0
  34. package/ts/db/documents/classes.security-block-rule.doc.ts +52 -0
  35. package/ts/db/documents/classes.security-policy-audit.doc.ts +33 -0
  36. package/ts/db/documents/index.ts +3 -0
  37. package/ts/monitoring/classes.metricsmanager.ts +2 -0
  38. package/ts/opsserver/handlers/security.handler.ts +69 -0
  39. package/ts/remoteingress/classes.remoteingress-manager.ts +15 -2
  40. package/ts/security/classes.security-policy-manager.ts +315 -0
  41. package/ts/security/index.ts +7 -1
  42. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -0,0 +1,75 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { DcRouterDb } from '../classes.dcrouter-db.js';
3
+ import type { IIpIntelligenceRecord } from '../../../ts_interfaces/data/security-policy.js';
4
+
5
+ const getDb = () => DcRouterDb.getInstance().getDb();
6
+
7
+ @plugins.smartdata.Collection(() => getDb())
8
+ export class IpIntelligenceDoc extends plugins.smartdata.SmartDataDbDoc<IpIntelligenceDoc, IpIntelligenceDoc> implements IIpIntelligenceRecord {
9
+ @plugins.smartdata.unI()
10
+ @plugins.smartdata.svDb()
11
+ public ipAddress!: string;
12
+
13
+ @plugins.smartdata.svDb()
14
+ public asn: number | null = null;
15
+
16
+ @plugins.smartdata.svDb()
17
+ public asnOrg: string | null = null;
18
+
19
+ @plugins.smartdata.svDb()
20
+ public registrantOrg: string | null = null;
21
+
22
+ @plugins.smartdata.svDb()
23
+ public registrantCountry: string | null = null;
24
+
25
+ @plugins.smartdata.svDb()
26
+ public networkRange: string | null = null;
27
+
28
+ @plugins.smartdata.svDb()
29
+ public abuseContact: string | null = null;
30
+
31
+ @plugins.smartdata.svDb()
32
+ public country: string | null = null;
33
+
34
+ @plugins.smartdata.svDb()
35
+ public countryCode: string | null = null;
36
+
37
+ @plugins.smartdata.svDb()
38
+ public city: string | null = null;
39
+
40
+ @plugins.smartdata.svDb()
41
+ public latitude: number | null = null;
42
+
43
+ @plugins.smartdata.svDb()
44
+ public longitude: number | null = null;
45
+
46
+ @plugins.smartdata.svDb()
47
+ public accuracyRadius: number | null = null;
48
+
49
+ @plugins.smartdata.svDb()
50
+ public timezone: string | null = null;
51
+
52
+ @plugins.smartdata.svDb()
53
+ public firstSeenAt: number = Date.now();
54
+
55
+ @plugins.smartdata.svDb()
56
+ public lastSeenAt: number = Date.now();
57
+
58
+ @plugins.smartdata.svDb()
59
+ public updatedAt: number = Date.now();
60
+
61
+ @plugins.smartdata.svDb()
62
+ public seenCount: number = 0;
63
+
64
+ constructor() {
65
+ super();
66
+ }
67
+
68
+ public static async findByIp(ipAddress: string): Promise<IpIntelligenceDoc | null> {
69
+ return await IpIntelligenceDoc.getInstance({ ipAddress });
70
+ }
71
+
72
+ public static async findAll(): Promise<IpIntelligenceDoc[]> {
73
+ return await IpIntelligenceDoc.getInstances({});
74
+ }
75
+ }
@@ -0,0 +1,52 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { DcRouterDb } from '../classes.dcrouter-db.js';
3
+ import type { ISecurityBlockRule, TSecurityBlockRuleMatchMode, TSecurityBlockRuleType } from '../../../ts_interfaces/data/security-policy.js';
4
+
5
+ const getDb = () => DcRouterDb.getInstance().getDb();
6
+
7
+ @plugins.smartdata.Collection(() => getDb())
8
+ export class SecurityBlockRuleDoc extends plugins.smartdata.SmartDataDbDoc<SecurityBlockRuleDoc, SecurityBlockRuleDoc> implements ISecurityBlockRule {
9
+ @plugins.smartdata.unI()
10
+ @plugins.smartdata.svDb()
11
+ public id!: string;
12
+
13
+ @plugins.smartdata.svDb()
14
+ public type!: TSecurityBlockRuleType;
15
+
16
+ @plugins.smartdata.svDb()
17
+ public value!: string;
18
+
19
+ @plugins.smartdata.svDb()
20
+ public matchMode?: TSecurityBlockRuleMatchMode;
21
+
22
+ @plugins.smartdata.svDb()
23
+ public enabled: boolean = true;
24
+
25
+ @plugins.smartdata.svDb()
26
+ public reason?: string;
27
+
28
+ @plugins.smartdata.svDb()
29
+ public createdAt: number = Date.now();
30
+
31
+ @plugins.smartdata.svDb()
32
+ public updatedAt: number = Date.now();
33
+
34
+ @plugins.smartdata.svDb()
35
+ public createdBy: string = 'system';
36
+
37
+ constructor() {
38
+ super();
39
+ }
40
+
41
+ public static async findById(id: string): Promise<SecurityBlockRuleDoc | null> {
42
+ return await SecurityBlockRuleDoc.getInstance({ id });
43
+ }
44
+
45
+ public static async findAll(): Promise<SecurityBlockRuleDoc[]> {
46
+ return await SecurityBlockRuleDoc.getInstances({});
47
+ }
48
+
49
+ public static async findEnabled(): Promise<SecurityBlockRuleDoc[]> {
50
+ return await SecurityBlockRuleDoc.getInstances({ enabled: true });
51
+ }
52
+ }
@@ -0,0 +1,33 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { DcRouterDb } from '../classes.dcrouter-db.js';
3
+ import type { ISecurityPolicyAuditEvent } from '../../../ts_interfaces/data/security-policy.js';
4
+
5
+ const getDb = () => DcRouterDb.getInstance().getDb();
6
+
7
+ @plugins.smartdata.Collection(() => getDb())
8
+ export class SecurityPolicyAuditDoc extends plugins.smartdata.SmartDataDbDoc<SecurityPolicyAuditDoc, SecurityPolicyAuditDoc> implements ISecurityPolicyAuditEvent {
9
+ @plugins.smartdata.unI()
10
+ @plugins.smartdata.svDb()
11
+ public id!: string;
12
+
13
+ @plugins.smartdata.svDb()
14
+ public action!: string;
15
+
16
+ @plugins.smartdata.svDb()
17
+ public actor!: string;
18
+
19
+ @plugins.smartdata.svDb()
20
+ public details!: Record<string, unknown>;
21
+
22
+ @plugins.smartdata.svDb()
23
+ public createdAt: number = Date.now();
24
+
25
+ constructor() {
26
+ super();
27
+ }
28
+
29
+ public static async findRecent(limit = 100): Promise<SecurityPolicyAuditDoc[]> {
30
+ const docs = await SecurityPolicyAuditDoc.getInstances({});
31
+ return docs.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
32
+ }
33
+ }
@@ -1,6 +1,9 @@
1
1
  // Cached/TTL document classes
2
2
  export * from './classes.cached.email.js';
3
3
  export * from './classes.cached.ip.reputation.js';
4
+ export * from './classes.ip-intelligence.doc.js';
5
+ export * from './classes.security-block-rule.doc.js';
6
+ export * from './classes.security-policy-audit.doc.js';
4
7
 
5
8
  // Config document classes
6
9
  export * from './classes.route.doc.js';
@@ -725,6 +725,8 @@ export class MetricsManager {
725
725
  .slice(0, 10)
726
726
  .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
727
727
 
728
+ void this.dcRouter.securityPolicyManager?.observeIps([...allIPData.keys()]);
729
+
728
730
  // Build domain activity using per-IP domain request counts from Rust engine
729
731
  const connectionsByRoute = proxyMetrics.connections.byRoute();
730
732
  const throughputByRoute = proxyMetrics.throughput.byRoute();
@@ -157,6 +157,75 @@ export class SecurityHandler {
157
157
  }
158
158
  )
159
159
  );
160
+
161
+ router.addTypedHandler(
162
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListSecurityBlockRules>(
163
+ 'listSecurityBlockRules',
164
+ async () => {
165
+ const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
166
+ return { rules: manager ? await manager.listBlockRules() : [] };
167
+ },
168
+ ),
169
+ );
170
+
171
+ router.addTypedHandler(
172
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListIpIntelligence>(
173
+ 'listIpIntelligence',
174
+ async () => {
175
+ const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
176
+ return { records: manager ? await manager.listIpIntelligence() : [] };
177
+ },
178
+ ),
179
+ );
180
+
181
+ const adminRouter = this.opsServerRef.adminRouter;
182
+
183
+ adminRouter.addTypedHandler(
184
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateSecurityBlockRule>(
185
+ 'createSecurityBlockRule',
186
+ async (dataArg) => {
187
+ const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
188
+ if (!manager) return { success: false, message: 'Security policy manager not initialized' };
189
+ const rule = await manager.createBlockRule({
190
+ type: dataArg.type,
191
+ value: dataArg.value,
192
+ matchMode: dataArg.matchMode,
193
+ reason: dataArg.reason,
194
+ enabled: dataArg.enabled,
195
+ }, dataArg.identity.userId);
196
+ return { success: true, rule };
197
+ },
198
+ ),
199
+ );
200
+
201
+ adminRouter.addTypedHandler(
202
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateSecurityBlockRule>(
203
+ 'updateSecurityBlockRule',
204
+ async (dataArg) => {
205
+ const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
206
+ if (!manager) return { success: false, message: 'Security policy manager not initialized' };
207
+ const rule = await manager.updateBlockRule(dataArg.id, {
208
+ value: dataArg.value,
209
+ matchMode: dataArg.matchMode,
210
+ reason: dataArg.reason,
211
+ enabled: dataArg.enabled,
212
+ }, dataArg.identity.userId);
213
+ return rule ? { success: true, rule } : { success: false, message: 'Rule not found' };
214
+ },
215
+ ),
216
+ );
217
+
218
+ adminRouter.addTypedHandler(
219
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteSecurityBlockRule>(
220
+ 'deleteSecurityBlockRule',
221
+ async (dataArg) => {
222
+ const manager = this.opsServerRef.dcRouterRef.securityPolicyManager;
223
+ if (!manager) return { success: false, message: 'Security policy manager not initialized' };
224
+ const success = await manager.deleteBlockRule(dataArg.id, dataArg.identity.userId);
225
+ return { success, message: success ? undefined : 'Rule not found' };
226
+ },
227
+ ),
228
+ );
160
229
  }
161
230
 
162
231
  private async collectSecurityMetrics(): Promise<{
@@ -2,6 +2,10 @@ import * as plugins from '../plugins.js';
2
2
  import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
3
3
  import { RemoteIngressEdgeDoc } from '../db/index.js';
4
4
 
5
+ interface IRemoteIngressFirewallConfig {
6
+ blockedIps?: string[];
7
+ }
8
+
5
9
  /**
6
10
  * Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
7
11
  */
@@ -31,6 +35,7 @@ function extractPorts(portRange: number | Array<number | { from: number; to: num
31
35
  export class RemoteIngressManager {
32
36
  private edges: Map<string, IRemoteIngress> = new Map();
33
37
  private routes: IDcRouterRouteConfig[] = [];
38
+ private firewallConfig?: IRemoteIngressFirewallConfig;
34
39
 
35
40
  constructor() {
36
41
  }
@@ -69,6 +74,13 @@ export class RemoteIngressManager {
69
74
  this.routes = routes;
70
75
  }
71
76
 
77
+ /**
78
+ * Set the full desired firewall snapshot pushed to all edges.
79
+ */
80
+ public setFirewallConfig(firewallConfig?: IRemoteIngressFirewallConfig): void {
81
+ this.firewallConfig = firewallConfig;
82
+ }
83
+
72
84
  /**
73
85
  * Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
74
86
  * When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
@@ -305,8 +317,8 @@ export class RemoteIngressManager {
305
317
  * Get the list of allowed edges (enabled only) for the Rust hub.
306
318
  * Includes listenPortsUdp when routes with transport 'udp' or 'all' are present.
307
319
  */
308
- public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> {
309
- const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[] }> = [];
320
+ public getAllowedEdges(): Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> {
321
+ const result: Array<{ id: string; secret: string; listenPorts: number[]; listenPortsUdp?: number[]; firewallConfig?: IRemoteIngressFirewallConfig }> = [];
310
322
  for (const edge of this.edges.values()) {
311
323
  if (edge.enabled) {
312
324
  const listenPortsUdp = this.getEffectiveListenPortsUdp(edge);
@@ -315,6 +327,7 @@ export class RemoteIngressManager {
315
327
  secret: edge.secret,
316
328
  listenPorts: this.getEffectiveListenPorts(edge),
317
329
  ...(listenPortsUdp.length > 0 ? { listenPortsUdp } : {}),
330
+ ...(this.firewallConfig ? { firewallConfig: this.firewallConfig } : {}),
318
331
  });
319
332
  }
320
333
  }
@@ -0,0 +1,315 @@
1
+ import * as plugins from '../plugins.js';
2
+ import { logger } from '../logger.js';
3
+ import { IpIntelligenceDoc, SecurityBlockRuleDoc, SecurityPolicyAuditDoc } from '../db/index.js';
4
+ import type {
5
+ IIpIntelligenceRecord,
6
+ ISecurityBlockRule,
7
+ ISecurityCompiledPolicy,
8
+ TSecurityBlockRuleMatchMode,
9
+ TSecurityBlockRuleType,
10
+ } from '../../ts_interfaces/data/security-policy.js';
11
+
12
+ export interface ISecurityPolicyManagerOptions {
13
+ intelligenceRefreshMs?: number;
14
+ onPolicyChanged?: () => void | Promise<void>;
15
+ }
16
+
17
+ export interface IRemoteIngressFirewallSnapshot {
18
+ blockedIps?: string[];
19
+ }
20
+
21
+ export class SecurityPolicyManager {
22
+ private readonly smartNetwork = new plugins.smartnetwork.SmartNetwork({
23
+ cacheTtl: 24 * 60 * 60 * 1000,
24
+ });
25
+ private readonly intelligenceRefreshMs: number;
26
+ private readonly inFlightObservations = new Set<string>();
27
+ private readonly onPolicyChanged?: () => void | Promise<void>;
28
+
29
+ constructor(options: ISecurityPolicyManagerOptions = {}) {
30
+ this.intelligenceRefreshMs = options.intelligenceRefreshMs ?? 24 * 60 * 60 * 1000;
31
+ this.onPolicyChanged = options.onPolicyChanged;
32
+ }
33
+
34
+ public async start(): Promise<void> {
35
+ logger.log('info', 'SecurityPolicyManager started');
36
+ }
37
+
38
+ public async stop(): Promise<void> {
39
+ await this.smartNetwork.stop();
40
+ }
41
+
42
+ public async observeIps(ips: string[]): Promise<void> {
43
+ const uniqueIps = [...new Set(ips.map((ip) => this.normalizeIp(ip)).filter(Boolean) as string[])];
44
+ await Promise.allSettled(uniqueIps.map((ip) => this.observeIp(ip)));
45
+ }
46
+
47
+ public async observeIp(ipAddress: string): Promise<void> {
48
+ const ip = this.normalizeIp(ipAddress);
49
+ if (!ip || !this.isPublicIp(ip) || this.inFlightObservations.has(ip)) {
50
+ return;
51
+ }
52
+
53
+ this.inFlightObservations.add(ip);
54
+ try {
55
+ const now = Date.now();
56
+ let doc = await IpIntelligenceDoc.findByIp(ip);
57
+ if (doc && now - doc.updatedAt < this.intelligenceRefreshMs) {
58
+ if (now - doc.lastSeenAt > 60_000) {
59
+ doc.lastSeenAt = now;
60
+ doc.seenCount = (doc.seenCount || 0) + 1;
61
+ await doc.save();
62
+ }
63
+ return;
64
+ }
65
+
66
+ const intelligence = await this.smartNetwork.getIpIntelligence(ip);
67
+ if (!doc) {
68
+ doc = new IpIntelligenceDoc();
69
+ doc.ipAddress = ip;
70
+ doc.firstSeenAt = now;
71
+ }
72
+ Object.assign(doc, intelligence);
73
+ doc.lastSeenAt = now;
74
+ doc.updatedAt = now;
75
+ doc.seenCount = (doc.seenCount || 0) + 1;
76
+ await doc.save();
77
+
78
+ if (await this.matchesAnyReactiveRule(doc)) {
79
+ await this.notifyPolicyChanged();
80
+ }
81
+ } catch (err) {
82
+ logger.log('warn', `Failed to enrich IP ${ip}: ${(err as Error).message}`);
83
+ } finally {
84
+ this.inFlightObservations.delete(ip);
85
+ }
86
+ }
87
+
88
+ public async listBlockRules(): Promise<ISecurityBlockRule[]> {
89
+ return (await SecurityBlockRuleDoc.findAll()).map((doc) => this.ruleFromDoc(doc));
90
+ }
91
+
92
+ public async listIpIntelligence(): Promise<IIpIntelligenceRecord[]> {
93
+ return (await IpIntelligenceDoc.findAll()).map((doc) => ({
94
+ ipAddress: doc.ipAddress,
95
+ asn: doc.asn,
96
+ asnOrg: doc.asnOrg,
97
+ registrantOrg: doc.registrantOrg,
98
+ registrantCountry: doc.registrantCountry,
99
+ networkRange: doc.networkRange,
100
+ abuseContact: doc.abuseContact,
101
+ country: doc.country,
102
+ countryCode: doc.countryCode,
103
+ city: doc.city,
104
+ latitude: doc.latitude,
105
+ longitude: doc.longitude,
106
+ accuracyRadius: doc.accuracyRadius,
107
+ timezone: doc.timezone,
108
+ firstSeenAt: doc.firstSeenAt,
109
+ lastSeenAt: doc.lastSeenAt,
110
+ updatedAt: doc.updatedAt,
111
+ seenCount: doc.seenCount,
112
+ }));
113
+ }
114
+
115
+ public async createBlockRule(input: {
116
+ type: TSecurityBlockRuleType;
117
+ value: string;
118
+ matchMode?: TSecurityBlockRuleMatchMode;
119
+ reason?: string;
120
+ enabled?: boolean;
121
+ }, actor = 'system'): Promise<ISecurityBlockRule> {
122
+ const now = Date.now();
123
+ const doc = new SecurityBlockRuleDoc();
124
+ doc.id = plugins.uuid.v4();
125
+ doc.type = input.type;
126
+ doc.value = input.value.trim();
127
+ doc.matchMode = input.matchMode;
128
+ doc.reason = input.reason;
129
+ doc.enabled = input.enabled ?? true;
130
+ doc.createdAt = now;
131
+ doc.updatedAt = now;
132
+ doc.createdBy = actor;
133
+ await doc.save();
134
+ await this.writeAudit('createBlockRule', actor, { rule: this.ruleFromDoc(doc) });
135
+ await this.notifyPolicyChanged();
136
+ return this.ruleFromDoc(doc);
137
+ }
138
+
139
+ public async updateBlockRule(id: string, patch: Partial<Pick<ISecurityBlockRule, 'value' | 'matchMode' | 'reason' | 'enabled'>>, actor = 'system'): Promise<ISecurityBlockRule | null> {
140
+ const doc = await SecurityBlockRuleDoc.findById(id);
141
+ if (!doc) {
142
+ return null;
143
+ }
144
+ if (patch.value !== undefined) doc.value = patch.value.trim();
145
+ if (patch.matchMode !== undefined) doc.matchMode = patch.matchMode;
146
+ if (patch.reason !== undefined) doc.reason = patch.reason;
147
+ if (patch.enabled !== undefined) doc.enabled = patch.enabled;
148
+ doc.updatedAt = Date.now();
149
+ await doc.save();
150
+ await this.writeAudit('updateBlockRule', actor, { id, patch });
151
+ await this.notifyPolicyChanged();
152
+ return this.ruleFromDoc(doc);
153
+ }
154
+
155
+ public async deleteBlockRule(id: string, actor = 'system'): Promise<boolean> {
156
+ const doc = await SecurityBlockRuleDoc.findById(id);
157
+ if (!doc) {
158
+ return false;
159
+ }
160
+ await doc.delete();
161
+ await this.writeAudit('deleteBlockRule', actor, { id });
162
+ await this.notifyPolicyChanged();
163
+ return true;
164
+ }
165
+
166
+ public async compilePolicy(): Promise<ISecurityCompiledPolicy> {
167
+ const rules = await SecurityBlockRuleDoc.findEnabled();
168
+ const intelligenceDocs = await IpIntelligenceDoc.findAll();
169
+ const blockedIps = new Set<string>();
170
+ const blockedCidrs = new Set<string>();
171
+
172
+ for (const rule of rules) {
173
+ const normalizedValue = rule.value.trim();
174
+ if (!normalizedValue) continue;
175
+
176
+ if (rule.type === 'ip') {
177
+ const ip = this.normalizeIp(normalizedValue);
178
+ if (ip && plugins.net.isIP(ip)) blockedIps.add(ip);
179
+ continue;
180
+ }
181
+
182
+ if (rule.type === 'cidr') {
183
+ const cidr = this.normalizeCidr(normalizedValue);
184
+ if (cidr) blockedCidrs.add(cidr);
185
+ continue;
186
+ }
187
+
188
+ for (const doc of intelligenceDocs) {
189
+ if (!this.ruleMatchesIntelligence(rule, doc)) continue;
190
+ const cidr = this.normalizeCidr(doc.networkRange || '');
191
+ if (cidr) {
192
+ blockedCidrs.add(cidr);
193
+ } else if (this.normalizeIp(doc.ipAddress)) {
194
+ blockedIps.add(this.normalizeIp(doc.ipAddress)!);
195
+ }
196
+ }
197
+ }
198
+
199
+ return {
200
+ blockedIps: [...blockedIps].sort(),
201
+ blockedCidrs: [...blockedCidrs].sort(),
202
+ };
203
+ }
204
+
205
+ public async compileSmartProxyPolicy(): Promise<ISecurityCompiledPolicy> {
206
+ return await this.compilePolicy();
207
+ }
208
+
209
+ public async compileRemoteIngressFirewall(): Promise<IRemoteIngressFirewallSnapshot | undefined> {
210
+ const policy = await this.compilePolicy();
211
+ const blockedIps = [
212
+ ...policy.blockedIps.filter((ip) => plugins.net.isIP(ip) === 4),
213
+ ...policy.blockedCidrs.filter((cidr) => plugins.net.isIP(cidr.split('/')[0]) === 4),
214
+ ];
215
+ return blockedIps.length > 0 ? { blockedIps } : undefined;
216
+ }
217
+
218
+ private async matchesAnyReactiveRule(doc: IpIntelligenceDoc): Promise<boolean> {
219
+ const rules = await SecurityBlockRuleDoc.findEnabled();
220
+ return rules.some((rule) => rule.type === 'asn' || rule.type === 'organization'
221
+ ? this.ruleMatchesIntelligence(rule, doc)
222
+ : false);
223
+ }
224
+
225
+ private ruleMatchesIntelligence(rule: SecurityBlockRuleDoc, doc: IpIntelligenceDoc): boolean {
226
+ const value = rule.value.trim().toLowerCase();
227
+ if (!value) return false;
228
+
229
+ if (rule.type === 'asn') {
230
+ return String(doc.asn ?? '') === value.replace(/^as/i, '');
231
+ }
232
+
233
+ if (rule.type === 'organization') {
234
+ const candidates = [doc.asnOrg, doc.registrantOrg]
235
+ .filter(Boolean)
236
+ .map((candidate) => candidate!.toLowerCase());
237
+ if (rule.matchMode === 'exact') {
238
+ return candidates.some((candidate) => candidate === value);
239
+ }
240
+ return candidates.some((candidate) => candidate.includes(value));
241
+ }
242
+
243
+ return false;
244
+ }
245
+
246
+ private normalizeIp(ipAddress: string): string | undefined {
247
+ const ip = ipAddress.trim();
248
+ if (ip.startsWith('::ffff:')) {
249
+ return ip.slice('::ffff:'.length);
250
+ }
251
+ return plugins.net.isIP(ip) ? ip : undefined;
252
+ }
253
+
254
+ private normalizeCidr(value: string): string | undefined {
255
+ const [rawIp, rawPrefix] = value.trim().split('/');
256
+ if (!rawIp || !rawPrefix) return undefined;
257
+ const ip = this.normalizeIp(rawIp);
258
+ if (!ip) return undefined;
259
+ const prefix = Number(rawPrefix);
260
+ const maxPrefix = plugins.net.isIP(ip) === 4 ? 32 : 128;
261
+ if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) return undefined;
262
+ return `${ip}/${prefix}`;
263
+ }
264
+
265
+ private isPublicIp(ip: string): boolean {
266
+ const family = plugins.net.isIP(ip);
267
+ if (family === 4) {
268
+ const parts = ip.split('.').map((part) => Number(part));
269
+ const [a, b] = parts;
270
+ if (a === 10 || a === 127 || a === 0 || a >= 224) return false;
271
+ if (a === 100 && b >= 64 && b <= 127) return false;
272
+ if (a === 169 && b === 254) return false;
273
+ if (a === 172 && b >= 16 && b <= 31) return false;
274
+ if (a === 192 && b === 168) return false;
275
+ return true;
276
+ }
277
+ if (family === 6) {
278
+ const lower = ip.toLowerCase();
279
+ if (lower === '::1' || lower === '::') return false;
280
+ if (lower.startsWith('fe80:') || lower.startsWith('fc') || lower.startsWith('fd')) return false;
281
+ return true;
282
+ }
283
+ return false;
284
+ }
285
+
286
+ private ruleFromDoc(doc: SecurityBlockRuleDoc): ISecurityBlockRule {
287
+ return {
288
+ id: doc.id,
289
+ type: doc.type,
290
+ value: doc.value,
291
+ matchMode: doc.matchMode,
292
+ enabled: doc.enabled,
293
+ reason: doc.reason,
294
+ createdAt: doc.createdAt,
295
+ updatedAt: doc.updatedAt,
296
+ createdBy: doc.createdBy,
297
+ };
298
+ }
299
+
300
+ private async writeAudit(action: string, actor: string, details: Record<string, unknown>): Promise<void> {
301
+ const doc = new SecurityPolicyAuditDoc();
302
+ doc.id = plugins.uuid.v4();
303
+ doc.action = action;
304
+ doc.actor = actor;
305
+ doc.details = details;
306
+ doc.createdAt = Date.now();
307
+ await doc.save();
308
+ }
309
+
310
+ private async notifyPolicyChanged(): Promise<void> {
311
+ if (this.onPolicyChanged) {
312
+ await this.onPolicyChanged();
313
+ }
314
+ }
315
+ }
@@ -18,4 +18,10 @@ export {
18
18
  ThreatCategory,
19
19
  type IScanResult,
20
20
  type IContentScannerOptions
21
- } from './classes.contentscanner.js';
21
+ } from './classes.contentscanner.js';
22
+
23
+ export {
24
+ SecurityPolicyManager,
25
+ type ISecurityPolicyManagerOptions,
26
+ type IRemoteIngressFirewallSnapshot,
27
+ } from './classes.security-policy-manager.js';
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.22.0',
6
+ version: '13.23.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }