@sentriflow/core 0.3.0 → 0.3.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "SentriFlow core engine for network configuration validation",
5
5
  "license": "Apache-2.0",
6
6
  "module": "src/index.ts",
@@ -30,8 +30,8 @@ export interface LicensePayload {
30
30
  /** Entitled feed IDs */
31
31
  feeds: string[];
32
32
 
33
- /** API URL for cloud updates */
34
- api: string;
33
+ /** API URL for cloud updates (optional - derived from domain at runtime if not set) */
34
+ api?: string;
35
35
 
36
36
  /** Expiration timestamp (Unix seconds) */
37
37
  exp: number;
@@ -229,7 +229,8 @@ export type EncryptedPackErrorCode =
229
229
  | 'DECRYPTION_FAILED'
230
230
  | 'MACHINE_MISMATCH'
231
231
  | 'NETWORK_ERROR'
232
- | 'API_ERROR';
232
+ | 'API_ERROR'
233
+ | 'ACTIVATION_FAILED';
233
234
 
234
235
  /**
235
236
  * Encrypted pack error
@@ -0,0 +1,416 @@
1
+ // packages/core/src/ip/classifier.ts
2
+
3
+ import type { IPSummary, IPCounts } from './types';
4
+
5
+ // ============================================================================
6
+ // IP Classification Types
7
+ // ============================================================================
8
+
9
+ /**
10
+ * Classification categories for IP addresses.
11
+ */
12
+ export type IPClassification =
13
+ | 'public' // Globally routable
14
+ | 'private' // RFC 1918 private ranges
15
+ | 'loopback' // 127.0.0.0/8, ::1
16
+ | 'link-local' // 169.254.0.0/16, fe80::/10
17
+ | 'multicast' // 224.0.0.0/4, ff00::/8
18
+ | 'reserved' // 240.0.0.0/4, other reserved
19
+ | 'unspecified' // 0.0.0.0, ::
20
+ | 'broadcast' // 255.255.255.255
21
+ | 'documentation' // 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32
22
+ | 'cgnat'; // 100.64.0.0/10 (Carrier-grade NAT)
23
+
24
+ /**
25
+ * Options for filtering IP addresses.
26
+ */
27
+ export interface IPFilterOptions {
28
+ /**
29
+ * Keep public (globally routable) addresses.
30
+ * @default true
31
+ */
32
+ keepPublic?: boolean;
33
+
34
+ /**
35
+ * Keep private (RFC 1918) addresses.
36
+ * @default true
37
+ */
38
+ keepPrivate?: boolean;
39
+
40
+ /**
41
+ * Keep loopback addresses (127.x.x.x, ::1).
42
+ * @default false
43
+ */
44
+ keepLoopback?: boolean;
45
+
46
+ /**
47
+ * Keep link-local addresses (169.254.x.x, fe80::).
48
+ * @default false
49
+ */
50
+ keepLinkLocal?: boolean;
51
+
52
+ /**
53
+ * Keep multicast addresses (224.x.x.x - 239.x.x.x, ff00::).
54
+ * @default false
55
+ */
56
+ keepMulticast?: boolean;
57
+
58
+ /**
59
+ * Keep reserved/future use addresses (240.x.x.x - 255.x.x.x).
60
+ * @default false
61
+ */
62
+ keepReserved?: boolean;
63
+
64
+ /**
65
+ * Keep unspecified addresses (0.0.0.0, ::).
66
+ * @default false
67
+ */
68
+ keepUnspecified?: boolean;
69
+
70
+ /**
71
+ * Keep broadcast address (255.255.255.255).
72
+ * @default false
73
+ */
74
+ keepBroadcast?: boolean;
75
+
76
+ /**
77
+ * Keep documentation addresses (TEST-NET ranges, 2001:db8::).
78
+ * @default false
79
+ */
80
+ keepDocumentation?: boolean;
81
+
82
+ /**
83
+ * Keep CGNAT addresses (100.64.0.0/10).
84
+ * @default true
85
+ */
86
+ keepCgnat?: boolean;
87
+ }
88
+
89
+ /**
90
+ * Default filter options - keep only public and private addresses.
91
+ */
92
+ export const DEFAULT_FILTER_OPTIONS: Required<IPFilterOptions> = {
93
+ keepPublic: true,
94
+ keepPrivate: true,
95
+ keepLoopback: false,
96
+ keepLinkLocal: false,
97
+ keepMulticast: false,
98
+ keepReserved: false,
99
+ keepUnspecified: false,
100
+ keepBroadcast: false,
101
+ keepDocumentation: false,
102
+ keepCgnat: true,
103
+ };
104
+
105
+ // ============================================================================
106
+ // IPv4 Classification
107
+ // ============================================================================
108
+
109
+ /**
110
+ * Convert IPv4 address string to 32-bit number.
111
+ */
112
+ function ipv4ToNumber(ip: string): number {
113
+ const parts = ip.split('.');
114
+ if (parts.length !== 4) return 0;
115
+
116
+ let result = 0;
117
+ for (let i = 0; i < 4; i++) {
118
+ const octet = parseInt(parts[i] ?? '0', 10);
119
+ if (isNaN(octet) || octet < 0 || octet > 255) return 0;
120
+ result = (result << 8) + octet;
121
+ }
122
+ return result >>> 0; // Ensure unsigned
123
+ }
124
+
125
+ /**
126
+ * Check if IPv4 address is in a given CIDR range.
127
+ */
128
+ function isInIPv4Range(ip: string, network: string, prefix: number): boolean {
129
+ const ipNum = ipv4ToNumber(ip);
130
+ const netNum = ipv4ToNumber(network);
131
+ const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
132
+ return (ipNum & mask) === (netNum & mask);
133
+ }
134
+
135
+ /**
136
+ * Classify an IPv4 address.
137
+ */
138
+ export function classifyIPv4(ip: string): IPClassification {
139
+ // Unspecified
140
+ if (ip === '0.0.0.0') return 'unspecified';
141
+
142
+ // Broadcast
143
+ if (ip === '255.255.255.255') return 'broadcast';
144
+
145
+ // Current network (0.0.0.0/8) - treat as unspecified
146
+ if (isInIPv4Range(ip, '0.0.0.0', 8)) return 'unspecified';
147
+
148
+ // Loopback (127.0.0.0/8)
149
+ if (isInIPv4Range(ip, '127.0.0.0', 8)) return 'loopback';
150
+
151
+ // Link-local (169.254.0.0/16)
152
+ if (isInIPv4Range(ip, '169.254.0.0', 16)) return 'link-local';
153
+
154
+ // Private ranges (RFC 1918)
155
+ if (isInIPv4Range(ip, '10.0.0.0', 8)) return 'private';
156
+ if (isInIPv4Range(ip, '172.16.0.0', 12)) return 'private';
157
+ if (isInIPv4Range(ip, '192.168.0.0', 16)) return 'private';
158
+
159
+ // CGNAT (100.64.0.0/10)
160
+ if (isInIPv4Range(ip, '100.64.0.0', 10)) return 'cgnat';
161
+
162
+ // Documentation ranges (TEST-NET)
163
+ if (isInIPv4Range(ip, '192.0.2.0', 24)) return 'documentation';
164
+ if (isInIPv4Range(ip, '198.51.100.0', 24)) return 'documentation';
165
+ if (isInIPv4Range(ip, '203.0.113.0', 24)) return 'documentation';
166
+
167
+ // Multicast (224.0.0.0/4)
168
+ if (isInIPv4Range(ip, '224.0.0.0', 4)) return 'multicast';
169
+
170
+ // Reserved for future use (240.0.0.0/4)
171
+ if (isInIPv4Range(ip, '240.0.0.0', 4)) return 'reserved';
172
+
173
+ // Everything else is public
174
+ return 'public';
175
+ }
176
+
177
+ /**
178
+ * Classify an IPv4 subnet (uses network address for classification).
179
+ */
180
+ export function classifyIPv4Subnet(subnet: string): IPClassification {
181
+ const slashIndex = subnet.lastIndexOf('/');
182
+ if (slashIndex === -1) return classifyIPv4(subnet);
183
+
184
+ const network = subnet.substring(0, slashIndex);
185
+ return classifyIPv4(network);
186
+ }
187
+
188
+ // ============================================================================
189
+ // IPv6 Classification
190
+ // ============================================================================
191
+
192
+ /**
193
+ * Expand IPv6 address to full form and return as array of 16-bit values.
194
+ */
195
+ function expandIPv6(ip: string): number[] {
196
+ // Strip zone ID if present
197
+ const zoneIndex = ip.indexOf('%');
198
+ const addr = zoneIndex !== -1 ? ip.substring(0, zoneIndex) : ip;
199
+
200
+ const parts = addr.split(':');
201
+ const result: number[] = [];
202
+
203
+ for (let i = 0; i < parts.length; i++) {
204
+ const part = parts[i] ?? '';
205
+ if (part === '' && i > 0 && i < parts.length - 1) {
206
+ // Middle :: - expand
207
+ const nonEmpty = parts.filter((p) => p !== '').length;
208
+ const zeros = 8 - nonEmpty;
209
+ for (let j = 0; j < zeros; j++) {
210
+ result.push(0);
211
+ }
212
+ } else if (part !== '') {
213
+ result.push(parseInt(part, 16) || 0);
214
+ } else if (i === 0 && (parts[1] ?? '') === '') {
215
+ // Leading ::
216
+ const nonEmpty = parts.filter((p) => p !== '').length;
217
+ const zeros = 8 - nonEmpty;
218
+ for (let j = 0; j < zeros; j++) {
219
+ result.push(0);
220
+ }
221
+ }
222
+ }
223
+
224
+ // Pad to 8 if needed
225
+ while (result.length < 8) {
226
+ result.push(0);
227
+ }
228
+
229
+ return result.slice(0, 8);
230
+ }
231
+
232
+ /**
233
+ * Check if IPv6 starts with a specific prefix.
234
+ */
235
+ function ipv6StartsWith(ip: string, prefixHex: number, prefixBits: number): boolean {
236
+ const parts = expandIPv6(ip);
237
+
238
+ // Calculate how many 16-bit groups we need to check
239
+ const fullGroups = Math.floor(prefixBits / 16);
240
+ const remainingBits = prefixBits % 16;
241
+
242
+ // Build the prefix value from parts
243
+ let value = 0;
244
+ for (let i = 0; i < fullGroups && i < parts.length; i++) {
245
+ value = (value << 16) | (parts[i] ?? 0);
246
+ }
247
+
248
+ if (remainingBits > 0 && fullGroups < parts.length) {
249
+ const mask = (~0 << (16 - remainingBits)) & 0xffff;
250
+ value = (value << remainingBits) | (((parts[fullGroups] ?? 0) & mask) >> (16 - remainingBits));
251
+ }
252
+
253
+ return value === prefixHex;
254
+ }
255
+
256
+ /**
257
+ * Classify an IPv6 address.
258
+ */
259
+ export function classifyIPv6(ip: string): IPClassification {
260
+ const parts = expandIPv6(ip);
261
+
262
+ // Unspecified (::)
263
+ if (parts.every((p) => p === 0)) return 'unspecified';
264
+
265
+ // Loopback (::1)
266
+ if (parts.slice(0, 7).every((p) => p === 0) && parts[7] === 1) return 'loopback';
267
+
268
+ // Link-local (fe80::/10)
269
+ if ((parts[0] ?? 0) >= 0xfe80 && (parts[0] ?? 0) <= 0xfebf) return 'link-local';
270
+
271
+ // Multicast (ff00::/8)
272
+ if (((parts[0] ?? 0) & 0xff00) === 0xff00) return 'multicast';
273
+
274
+ // Documentation (2001:db8::/32)
275
+ if (parts[0] === 0x2001 && parts[1] === 0x0db8) return 'documentation';
276
+
277
+ // Unique local (fc00::/7) - similar to private
278
+ if (((parts[0] ?? 0) & 0xfe00) === 0xfc00) return 'private';
279
+
280
+ // Everything else is public
281
+ return 'public';
282
+ }
283
+
284
+ /**
285
+ * Classify an IPv6 subnet (uses network address for classification).
286
+ */
287
+ export function classifyIPv6Subnet(subnet: string): IPClassification {
288
+ const slashIndex = subnet.lastIndexOf('/');
289
+ if (slashIndex === -1) return classifyIPv6(subnet);
290
+
291
+ const network = subnet.substring(0, slashIndex);
292
+ return classifyIPv6(network);
293
+ }
294
+
295
+ // ============================================================================
296
+ // Filtering Functions
297
+ // ============================================================================
298
+
299
+ /**
300
+ * Check if a classification should be kept based on filter options.
301
+ */
302
+ function shouldKeepClassification(
303
+ classification: IPClassification,
304
+ options: Required<IPFilterOptions>
305
+ ): boolean {
306
+ switch (classification) {
307
+ case 'public':
308
+ return options.keepPublic;
309
+ case 'private':
310
+ return options.keepPrivate;
311
+ case 'loopback':
312
+ return options.keepLoopback;
313
+ case 'link-local':
314
+ return options.keepLinkLocal;
315
+ case 'multicast':
316
+ return options.keepMulticast;
317
+ case 'reserved':
318
+ return options.keepReserved;
319
+ case 'unspecified':
320
+ return options.keepUnspecified;
321
+ case 'broadcast':
322
+ return options.keepBroadcast;
323
+ case 'documentation':
324
+ return options.keepDocumentation;
325
+ case 'cgnat':
326
+ return options.keepCgnat;
327
+ default:
328
+ return true;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Filter an array of IPv4 addresses based on classification.
334
+ */
335
+ export function filterIPv4Addresses(
336
+ addresses: string[],
337
+ options: IPFilterOptions = {}
338
+ ): string[] {
339
+ const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
340
+ return addresses.filter((ip) => {
341
+ const classification = classifyIPv4(ip);
342
+ return shouldKeepClassification(classification, opts);
343
+ });
344
+ }
345
+
346
+ /**
347
+ * Filter an array of IPv6 addresses based on classification.
348
+ */
349
+ export function filterIPv6Addresses(
350
+ addresses: string[],
351
+ options: IPFilterOptions = {}
352
+ ): string[] {
353
+ const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
354
+ return addresses.filter((ip) => {
355
+ const classification = classifyIPv6(ip);
356
+ return shouldKeepClassification(classification, opts);
357
+ });
358
+ }
359
+
360
+ /**
361
+ * Filter an array of IPv4 subnets based on classification.
362
+ */
363
+ export function filterIPv4Subnets(
364
+ subnets: string[],
365
+ options: IPFilterOptions = {}
366
+ ): string[] {
367
+ const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
368
+ return subnets.filter((subnet) => {
369
+ const classification = classifyIPv4Subnet(subnet);
370
+ return shouldKeepClassification(classification, opts);
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Filter an array of IPv6 subnets based on classification.
376
+ */
377
+ export function filterIPv6Subnets(
378
+ subnets: string[],
379
+ options: IPFilterOptions = {}
380
+ ): string[] {
381
+ const opts: Required<IPFilterOptions> = { ...DEFAULT_FILTER_OPTIONS, ...options };
382
+ return subnets.filter((subnet) => {
383
+ const classification = classifyIPv6Subnet(subnet);
384
+ return shouldKeepClassification(classification, opts);
385
+ });
386
+ }
387
+
388
+ /**
389
+ * Filter an entire IPSummary based on classification options.
390
+ * Returns a new IPSummary with filtered results and updated counts.
391
+ */
392
+ export function filterIPSummary(
393
+ summary: IPSummary,
394
+ options: IPFilterOptions = {}
395
+ ): IPSummary {
396
+ const ipv4Addresses = filterIPv4Addresses(summary.ipv4Addresses, options);
397
+ const ipv6Addresses = filterIPv6Addresses(summary.ipv6Addresses, options);
398
+ const ipv4Subnets = filterIPv4Subnets(summary.ipv4Subnets, options);
399
+ const ipv6Subnets = filterIPv6Subnets(summary.ipv6Subnets, options);
400
+
401
+ const counts: IPCounts = {
402
+ ipv4: ipv4Addresses.length,
403
+ ipv6: ipv6Addresses.length,
404
+ ipv4Subnets: ipv4Subnets.length,
405
+ ipv6Subnets: ipv6Subnets.length,
406
+ total: ipv4Addresses.length + ipv6Addresses.length + ipv4Subnets.length + ipv6Subnets.length,
407
+ };
408
+
409
+ return {
410
+ ipv4Addresses,
411
+ ipv6Addresses,
412
+ ipv4Subnets,
413
+ ipv6Subnets,
414
+ counts,
415
+ };
416
+ }
package/src/ip/index.ts CHANGED
@@ -25,3 +25,19 @@ export type {
25
25
  } from './types';
26
26
 
27
27
  export { InputValidationError, DEFAULT_MAX_CONTENT_SIZE } from './types';
28
+
29
+ // IP Classification and Filtering
30
+ export {
31
+ classifyIPv4,
32
+ classifyIPv6,
33
+ classifyIPv4Subnet,
34
+ classifyIPv6Subnet,
35
+ filterIPv4Addresses,
36
+ filterIPv6Addresses,
37
+ filterIPv4Subnets,
38
+ filterIPv6Subnets,
39
+ filterIPSummary,
40
+ DEFAULT_FILTER_OPTIONS,
41
+ } from './classifier';
42
+
43
+ export type { IPClassification, IPFilterOptions } from './classifier';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://sentriflow.io/schemas/json-rules/v1.0.json",
3
+ "$id": "https://sentriflow.com.au/schemas/json-rules/v1.0.json",
4
4
  "title": "SentriFlow JSON Rules",
5
5
  "description": "Schema for SentriFlow JSON rule files",
6
6
  "type": "object",
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  export * from './types';
17
- export { loadEncryptedPack, validatePackFormat } from './PackLoader';
17
+ export { loadEncryptedPack, validatePackFormat, compileNativeCheckFunction } from './PackLoader';
18
18
 
19
19
  // Pack format detection (shared with CLI and VS Code)
20
20
  export {