@sentriflow/core 0.1.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 (71) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +86 -0
  3. package/package.json +60 -0
  4. package/src/constants.ts +77 -0
  5. package/src/engine/RuleExecutor.ts +256 -0
  6. package/src/engine/Runner.ts +312 -0
  7. package/src/engine/SandboxedExecutor.ts +208 -0
  8. package/src/errors.ts +88 -0
  9. package/src/helpers/arista/helpers.ts +1220 -0
  10. package/src/helpers/arista/index.ts +12 -0
  11. package/src/helpers/aruba/helpers.ts +637 -0
  12. package/src/helpers/aruba/index.ts +13 -0
  13. package/src/helpers/cisco/helpers.ts +534 -0
  14. package/src/helpers/cisco/index.ts +11 -0
  15. package/src/helpers/common/helpers.ts +265 -0
  16. package/src/helpers/common/index.ts +5 -0
  17. package/src/helpers/common/validation.ts +280 -0
  18. package/src/helpers/cumulus/helpers.ts +676 -0
  19. package/src/helpers/cumulus/index.ts +12 -0
  20. package/src/helpers/extreme/helpers.ts +422 -0
  21. package/src/helpers/extreme/index.ts +12 -0
  22. package/src/helpers/fortinet/helpers.ts +892 -0
  23. package/src/helpers/fortinet/index.ts +12 -0
  24. package/src/helpers/huawei/helpers.ts +790 -0
  25. package/src/helpers/huawei/index.ts +11 -0
  26. package/src/helpers/index.ts +53 -0
  27. package/src/helpers/juniper/helpers.ts +756 -0
  28. package/src/helpers/juniper/index.ts +12 -0
  29. package/src/helpers/mikrotik/helpers.ts +722 -0
  30. package/src/helpers/mikrotik/index.ts +12 -0
  31. package/src/helpers/nokia/helpers.ts +856 -0
  32. package/src/helpers/nokia/index.ts +11 -0
  33. package/src/helpers/paloalto/helpers.ts +939 -0
  34. package/src/helpers/paloalto/index.ts +12 -0
  35. package/src/helpers/vyos/helpers.ts +429 -0
  36. package/src/helpers/vyos/index.ts +12 -0
  37. package/src/index.ts +30 -0
  38. package/src/json-rules/ExpressionEvaluator.ts +292 -0
  39. package/src/json-rules/HelperRegistry.ts +177 -0
  40. package/src/json-rules/JsonRuleCompiler.ts +339 -0
  41. package/src/json-rules/JsonRuleValidator.ts +371 -0
  42. package/src/json-rules/index.ts +97 -0
  43. package/src/json-rules/schema.json +350 -0
  44. package/src/json-rules/types.ts +303 -0
  45. package/src/pack-loader/PackLoader.ts +332 -0
  46. package/src/pack-loader/index.ts +17 -0
  47. package/src/pack-loader/types.ts +135 -0
  48. package/src/parser/IncrementalParser.ts +527 -0
  49. package/src/parser/Sanitizer.ts +104 -0
  50. package/src/parser/SchemaAwareParser.ts +504 -0
  51. package/src/parser/VendorSchema.ts +72 -0
  52. package/src/parser/vendors/arista-eos.ts +206 -0
  53. package/src/parser/vendors/aruba-aoscx.ts +123 -0
  54. package/src/parser/vendors/aruba-aosswitch.ts +113 -0
  55. package/src/parser/vendors/aruba-wlc.ts +173 -0
  56. package/src/parser/vendors/cisco-ios.ts +110 -0
  57. package/src/parser/vendors/cisco-nxos.ts +107 -0
  58. package/src/parser/vendors/cumulus-linux.ts +161 -0
  59. package/src/parser/vendors/extreme-exos.ts +154 -0
  60. package/src/parser/vendors/extreme-voss.ts +167 -0
  61. package/src/parser/vendors/fortinet-fortigate.ts +217 -0
  62. package/src/parser/vendors/huawei-vrp.ts +192 -0
  63. package/src/parser/vendors/index.ts +1521 -0
  64. package/src/parser/vendors/juniper-junos.ts +230 -0
  65. package/src/parser/vendors/mikrotik-routeros.ts +274 -0
  66. package/src/parser/vendors/nokia-sros.ts +251 -0
  67. package/src/parser/vendors/paloalto-panos.ts +264 -0
  68. package/src/parser/vendors/vyos-vyos.ts +454 -0
  69. package/src/types/ConfigNode.ts +72 -0
  70. package/src/types/DeclarativeRule.ts +158 -0
  71. package/src/types/IRule.ts +270 -0
@@ -0,0 +1,265 @@
1
+ // packages/rule-helpers/src/common/helpers.ts
2
+ // Common helper functions used across vendor-specific rules
3
+
4
+ import type { ConfigNode } from '../../types/ConfigNode';
5
+
6
+ // Re-export validation helpers for convenience
7
+ export {
8
+ equalsIgnoreCase,
9
+ includesIgnoreCase,
10
+ startsWithIgnoreCase,
11
+ parseInteger,
12
+ isInRange,
13
+ parsePort,
14
+ isValidPort,
15
+ parsePortRange,
16
+ parseVlanId,
17
+ isValidVlanId,
18
+ isDefaultVlan,
19
+ isReservedVlan,
20
+ isFeatureEnabled,
21
+ isFeatureDisabled,
22
+ isValidMacAddress,
23
+ normalizeMacAddress,
24
+ parseCidr,
25
+ isIpInNetwork,
26
+ isIpInCidr,
27
+ type CidrInfo,
28
+ } from './validation';
29
+
30
+ /**
31
+ * Parse an IP address string to a 32-bit unsigned integer.
32
+ * @param addr The IP address string (e.g., "10.0.0.1")
33
+ * @returns The IP as a 32-bit unsigned number, or null if invalid
34
+ */
35
+ export const parseIp = (addr: string): number | null => {
36
+ const parts = addr.split('.').map(Number);
37
+ if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255)) {
38
+ return null;
39
+ }
40
+ // Safe: we verified parts.length === 4 above
41
+ const [p0, p1, p2, p3] = parts as [number, number, number, number];
42
+ return ((p0 << 24) | (p1 << 16) | (p2 << 8) | p3) >>> 0;
43
+ };
44
+
45
+ /**
46
+ * Convert a 32-bit unsigned integer to an IP address string.
47
+ * @param num The IP as a 32-bit unsigned number
48
+ * @returns The IP address string
49
+ */
50
+ export const numToIp = (num: number): string => {
51
+ return [
52
+ (num >>> 24) & 255,
53
+ (num >>> 16) & 255,
54
+ (num >>> 8) & 255,
55
+ num & 255,
56
+ ].join('.');
57
+ };
58
+
59
+ /**
60
+ * Parse a CIDR prefix length to a subnet mask number.
61
+ * @param prefix The prefix length (e.g., 24)
62
+ * @returns The subnet mask as a 32-bit unsigned number
63
+ */
64
+ export const prefixToMask = (prefix: number): number => {
65
+ if (prefix < 0 || prefix > 32) return 0;
66
+ return prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
67
+ };
68
+
69
+ /**
70
+ * Convert a subnet mask to CIDR prefix length.
71
+ * @param mask The subnet mask as a 32-bit unsigned number
72
+ * @returns The prefix length
73
+ */
74
+ export const maskToPrefix = (mask: number): number => {
75
+ let count = 0;
76
+ let m = mask;
77
+ while (m & 0x80000000) {
78
+ count++;
79
+ m <<= 1;
80
+ }
81
+ return count;
82
+ };
83
+
84
+ /**
85
+ * Check if a node has a specific child command (case-insensitive prefix match).
86
+ * @param node The parent ConfigNode
87
+ * @param prefix The command prefix to search for
88
+ * @returns true if a matching child exists
89
+ */
90
+ export const hasChildCommand = (node: ConfigNode, prefix: string): boolean => {
91
+ return node.children.some((child) =>
92
+ child.id.toLowerCase().startsWith(prefix.toLowerCase())
93
+ );
94
+ };
95
+
96
+ /**
97
+ * Get a child command's node if it exists.
98
+ * @param node The parent ConfigNode
99
+ * @param prefix The command prefix to search for
100
+ * @returns The matching child node, or undefined
101
+ */
102
+ export const getChildCommand = (
103
+ node: ConfigNode,
104
+ prefix: string
105
+ ): ConfigNode | undefined => {
106
+ return node.children.find((child) =>
107
+ child.id.toLowerCase().startsWith(prefix.toLowerCase())
108
+ );
109
+ };
110
+
111
+ /**
112
+ * Get all child commands matching a prefix.
113
+ * @param node The parent ConfigNode
114
+ * @param prefix The command prefix to search for
115
+ * @returns Array of matching child nodes
116
+ */
117
+ export const getChildCommands = (
118
+ node: ConfigNode,
119
+ prefix: string
120
+ ): ConfigNode[] => {
121
+ return node.children.filter((child) =>
122
+ child.id.toLowerCase().startsWith(prefix.toLowerCase())
123
+ );
124
+ };
125
+
126
+ /**
127
+ * Check if a value is a valid IP address string.
128
+ * @param value The string to check
129
+ * @returns true if it's a valid IP address
130
+ */
131
+ export const isValidIpAddress = (value: string): boolean => {
132
+ return parseIp(value) !== null;
133
+ };
134
+
135
+ /**
136
+ * Check if an IP is in the multicast range (224.0.0.0 - 239.255.255.255).
137
+ * @param ipNum The IP as a 32-bit unsigned number
138
+ * @returns true if it's a multicast address
139
+ */
140
+ export const isMulticastAddress = (ipNum: number): boolean => {
141
+ const firstOctet = ipNum >>> 24;
142
+ return firstOctet >= 224 && firstOctet <= 239;
143
+ };
144
+
145
+ /**
146
+ * Check if an IP is the global broadcast address (255.255.255.255).
147
+ * @param ipNum The IP as a 32-bit unsigned number
148
+ * @returns true if it's the broadcast address
149
+ */
150
+ export const isBroadcastAddress = (ipNum: number): boolean => {
151
+ return ipNum === 0xffffffff;
152
+ };
153
+
154
+ /**
155
+ * Check if an IP is a private address (RFC 1918).
156
+ * @param ipNum The IP as a 32-bit unsigned number
157
+ * @returns true if it's a private address
158
+ */
159
+ export const isPrivateAddress = (ipNum: number): boolean => {
160
+ // 10.0.0.0/8
161
+ if ((ipNum & 0xff000000) >>> 0 === 0x0a000000) return true;
162
+ // 172.16.0.0/12
163
+ if ((ipNum & 0xfff00000) >>> 0 === 0xac100000) return true;
164
+ // 192.168.0.0/16
165
+ if ((ipNum & 0xffff0000) >>> 0 === 0xc0a80000) return true;
166
+ return false;
167
+ };
168
+
169
+ /**
170
+ * Extract a parameter value from a node's params array.
171
+ * @param node The ConfigNode
172
+ * @param keyword The keyword to find
173
+ * @returns The value after the keyword, or undefined
174
+ */
175
+ export const getParamValue = (
176
+ node: ConfigNode,
177
+ keyword: string
178
+ ): string | undefined => {
179
+ const idx = node.params.findIndex(
180
+ (p) => p.toLowerCase() === keyword.toLowerCase()
181
+ );
182
+ if (idx >= 0 && idx < node.params.length - 1) {
183
+ return node.params[idx + 1];
184
+ }
185
+ return undefined;
186
+ };
187
+
188
+ /**
189
+ * Check if an interface is administratively shutdown.
190
+ * Works for both Cisco ("shutdown") and Juniper ("disable") syntax.
191
+ * @param node The interface ConfigNode
192
+ * @returns true if the interface is shutdown/disabled
193
+ */
194
+ export const isShutdown = (node: ConfigNode): boolean => {
195
+ return node.children.some((child) => {
196
+ const id = child.id.toLowerCase().trim();
197
+ return id === 'shutdown' || id === 'disable';
198
+ });
199
+ };
200
+
201
+ /**
202
+ * Check if a node is an actual interface definition (not a reference or sub-command).
203
+ * Interface definitions are top-level sections that define physical/logical interfaces.
204
+ *
205
+ * This helper distinguishes real interface definitions from:
206
+ * - Interface references inside protocol blocks (OSPF, LLDP, etc.)
207
+ * - Sub-commands like "interface-type", "interface-mode"
208
+ * - Generic references like "interface all"
209
+ *
210
+ * @param node The ConfigNode to check
211
+ * @returns true if this is an actual interface definition
212
+ */
213
+ export const isInterfaceDefinition = (node: ConfigNode): boolean => {
214
+ const id = node.id.toLowerCase();
215
+
216
+ // Must be a section type (has children or is a block)
217
+ if (node.type !== 'section') {
218
+ return false;
219
+ }
220
+
221
+ // Must start with exactly "interface " followed by interface name
222
+ if (!id.startsWith('interface ')) {
223
+ return false;
224
+ }
225
+
226
+ // Skip "interface-type", "interface-mode", etc. (compound words)
227
+ if (id.startsWith('interface-')) {
228
+ return false;
229
+ }
230
+
231
+ // Get the interface name part
232
+ const ifName = id.slice('interface '.length).trim();
233
+
234
+ // Skip generic references like "interface all" (LLDP/CDP config)
235
+ if (ifName === 'all' || ifName === 'default') {
236
+ return false;
237
+ }
238
+
239
+ // For Juniper: interface references inside protocols/routing don't have meaningful children
240
+ // Real interface definitions in "interfaces { }" block have unit, family, etc.
241
+ // References inside ospf/lldp/etc. typically have no children or just simple options
242
+ // Check if this looks like a Juniper interface reference (inside protocols block)
243
+ // These typically have 0-1 children with simple options like "passive" or "interface-type"
244
+ if (node.children.length <= 1) {
245
+ const hasOnlySimpleChild = node.children.every((child) => {
246
+ const childId = child.id.toLowerCase();
247
+ return (
248
+ childId === 'passive' ||
249
+ childId.startsWith('interface-type') ||
250
+ childId.startsWith('metric') ||
251
+ childId.startsWith('hello-interval') ||
252
+ childId.startsWith('dead-interval') ||
253
+ childId.startsWith('priority') ||
254
+ childId.startsWith('authentication') ||
255
+ childId.startsWith('bfd-liveness')
256
+ );
257
+ });
258
+ // If only simple OSPF/routing options, this is likely a reference, not a definition
259
+ if (hasOnlySimpleChild && node.children.length > 0) {
260
+ return false;
261
+ }
262
+ }
263
+
264
+ return true;
265
+ };
@@ -0,0 +1,5 @@
1
+ // packages/rule-helpers/src/common/index.ts
2
+ // Re-export all common helpers
3
+
4
+ export * from './helpers';
5
+ export * from './validation';
@@ -0,0 +1,280 @@
1
+ // packages/rule-helpers/src/common/validation.ts
2
+ // Centralized validation helpers for network configuration rules
3
+
4
+ import { parseIp, prefixToMask } from './helpers';
5
+
6
+ // ============================================================================
7
+ // String Comparison Helpers
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Case-insensitive string equality check.
12
+ * @param a First string
13
+ * @param b Second string
14
+ * @returns true if strings are equal (case-insensitive)
15
+ */
16
+ export const equalsIgnoreCase = (a: string, b: string): boolean =>
17
+ a.toLowerCase() === b.toLowerCase();
18
+
19
+ /**
20
+ * Case-insensitive substring check.
21
+ * @param haystack String to search in
22
+ * @param needle String to search for
23
+ * @returns true if needle is found (case-insensitive)
24
+ */
25
+ export const includesIgnoreCase = (haystack: string, needle: string): boolean =>
26
+ haystack.toLowerCase().includes(needle.toLowerCase());
27
+
28
+ /**
29
+ * Case-insensitive prefix check.
30
+ * @param str String to check
31
+ * @param prefix Prefix to match
32
+ * @returns true if str starts with prefix (case-insensitive)
33
+ */
34
+ export const startsWithIgnoreCase = (str: string, prefix: string): boolean =>
35
+ str.toLowerCase().startsWith(prefix.toLowerCase());
36
+
37
+ // ============================================================================
38
+ // Numeric Validation Helpers
39
+ // ============================================================================
40
+
41
+ /**
42
+ * Parse a string to integer, returning null if invalid.
43
+ * @param value String to parse
44
+ * @returns Parsed integer or null if invalid
45
+ */
46
+ export const parseInteger = (value: string): number | null => {
47
+ const num = parseInt(value, 10);
48
+ return isNaN(num) ? null : num;
49
+ };
50
+
51
+ /**
52
+ * Check if a number is within a range (inclusive).
53
+ * @param value Number to check
54
+ * @param min Minimum value (inclusive)
55
+ * @param max Maximum value (inclusive)
56
+ * @returns true if value is in range
57
+ */
58
+ export const isInRange = (value: number, min: number, max: number): boolean =>
59
+ value >= min && value <= max;
60
+
61
+ /**
62
+ * Parse and validate a port number (1-65535).
63
+ * @param value Port string to parse
64
+ * @returns Port number or null if invalid
65
+ */
66
+ export const parsePort = (value: string): number | null => {
67
+ const port = parseInteger(value);
68
+ if (port === null || port < 1 || port > 65535) return null;
69
+ return port;
70
+ };
71
+
72
+ /**
73
+ * Check if a port number is valid (1-65535).
74
+ * @param port Port number to validate
75
+ * @returns true if valid port
76
+ */
77
+ export const isValidPort = (port: number): boolean =>
78
+ Number.isInteger(port) && port >= 1 && port <= 65535;
79
+
80
+ /**
81
+ * Parse a port range string (e.g., "1-24", "80,443", "1-10,20,30-32").
82
+ * @param portStr Port range string
83
+ * @returns Array of individual port numbers (empty array if no valid ports found)
84
+ */
85
+ export const parsePortRange = (portStr: string): number[] => {
86
+ const ports: number[] = [];
87
+ const parts = portStr.split(',');
88
+
89
+ for (const part of parts) {
90
+ const trimmed = part.trim();
91
+ if (trimmed.includes('-')) {
92
+ const rangeParts = trimmed.split('-').map((p) => parseInt(p.trim(), 10));
93
+ const start = rangeParts[0];
94
+ const end = rangeParts[1];
95
+ if (start !== undefined && end !== undefined && !isNaN(start) && !isNaN(end)) {
96
+ for (let i = start; i <= end; i++) {
97
+ ports.push(i);
98
+ }
99
+ }
100
+ } else {
101
+ const num = parseInt(trimmed, 10);
102
+ if (!isNaN(num)) {
103
+ ports.push(num);
104
+ }
105
+ }
106
+ }
107
+
108
+ return ports;
109
+ };
110
+
111
+ // ============================================================================
112
+ // VLAN Validation Helpers
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Parse a VLAN ID string to number.
117
+ * @param vlanStr VLAN ID string
118
+ * @returns VLAN number or null if invalid
119
+ */
120
+ export const parseVlanId = (vlanStr: string): number | null => {
121
+ const vlan = parseInteger(vlanStr);
122
+ if (vlan === null || vlan < 1 || vlan > 4094) return null;
123
+ return vlan;
124
+ };
125
+
126
+ /**
127
+ * Check if a VLAN ID is valid (1-4094).
128
+ * @param vlanId VLAN ID to validate
129
+ * @returns true if valid VLAN ID
130
+ */
131
+ export const isValidVlanId = (vlanId: number): boolean =>
132
+ Number.isInteger(vlanId) && vlanId >= 1 && vlanId <= 4094;
133
+
134
+ /**
135
+ * Check if VLAN is the default VLAN (VLAN 1).
136
+ * @param vlanId VLAN ID string or number
137
+ * @returns true if VLAN 1
138
+ */
139
+ export const isDefaultVlan = (vlanId: string | number): boolean => {
140
+ const id = typeof vlanId === 'string' ? parseInteger(vlanId) : vlanId;
141
+ return id === 1;
142
+ };
143
+
144
+ /**
145
+ * Check if VLAN is in the reserved range (1002-1005 for Cisco).
146
+ * @param vlanId VLAN ID
147
+ * @returns true if in reserved range
148
+ */
149
+ export const isReservedVlan = (vlanId: number): boolean =>
150
+ vlanId >= 1002 && vlanId <= 1005;
151
+
152
+ // ============================================================================
153
+ // Feature State Helpers
154
+ // ============================================================================
155
+
156
+ /**
157
+ * Check if a feature value represents "enabled" state.
158
+ * Handles common variations: "enable", "enabled", "yes", "true", "on", "1"
159
+ * @param value Feature state string
160
+ * @returns true if enabled
161
+ */
162
+ export const isFeatureEnabled = (value: string | undefined): boolean => {
163
+ if (!value) return false;
164
+ const normalized = value.toLowerCase().trim();
165
+ return ['enable', 'enabled', 'yes', 'true', 'on', '1'].includes(normalized);
166
+ };
167
+
168
+ /**
169
+ * Check if a feature value represents "disabled" state.
170
+ * Handles common variations: "disable", "disabled", "no", "false", "off", "0"
171
+ * @param value Feature state string
172
+ * @returns true if disabled
173
+ */
174
+ export const isFeatureDisabled = (value: string | undefined): boolean => {
175
+ if (!value) return false;
176
+ const normalized = value.toLowerCase().trim();
177
+ return ['disable', 'disabled', 'no', 'false', 'off', '0'].includes(normalized);
178
+ };
179
+
180
+ // ============================================================================
181
+ // MAC Address Validation Helpers
182
+ // ============================================================================
183
+
184
+ /**
185
+ * Validate MAC address format.
186
+ * Supports: XX:XX:XX:XX:XX:XX, XX-XX-XX-XX-XX-XX, XXXX.XXXX.XXXX
187
+ * @param mac MAC address string
188
+ * @returns true if valid MAC format
189
+ */
190
+ export const isValidMacAddress = (mac: string): boolean => {
191
+ // Colon-separated (Linux/Unix style)
192
+ if (/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/.test(mac)) return true;
193
+ // Hyphen-separated (Windows style)
194
+ if (/^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$/.test(mac)) return true;
195
+ // Dot-separated (Cisco style)
196
+ if (/^([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}$/.test(mac)) return true;
197
+ return false;
198
+ };
199
+
200
+ /**
201
+ * Normalize MAC address to lowercase colon-separated format.
202
+ * @param mac MAC address in any supported format
203
+ * @returns Normalized MAC or null if invalid
204
+ */
205
+ export const normalizeMacAddress = (mac: string): string | null => {
206
+ if (!isValidMacAddress(mac)) return null;
207
+
208
+ // Remove all separators and convert to lowercase
209
+ const hex = mac.replace(/[:\-.]/g, '').toLowerCase();
210
+
211
+ // Insert colons every 2 characters
212
+ const matched = hex.match(/.{2}/g);
213
+ return matched ? matched.join(':') : null;
214
+ };
215
+
216
+ // ============================================================================
217
+ // CIDR/Subnet Validation Helpers
218
+ // ============================================================================
219
+
220
+ /**
221
+ * Result of parsing CIDR notation.
222
+ */
223
+ export interface CidrInfo {
224
+ /** Network address as 32-bit unsigned number */
225
+ network: number;
226
+ /** CIDR prefix length (0-32) */
227
+ prefix: number;
228
+ /** Subnet mask as 32-bit unsigned number */
229
+ mask: number;
230
+ }
231
+
232
+ /**
233
+ * Parse CIDR notation (e.g., "10.0.0.0/24").
234
+ * @param cidr CIDR string
235
+ * @returns Object with network, prefix, mask or null if invalid
236
+ */
237
+ export const parseCidr = (cidr: string): CidrInfo | null => {
238
+ const parts = cidr.split('/');
239
+ if (parts.length !== 2) return null;
240
+
241
+ const ipPart = parts[0];
242
+ const prefixPart = parts[1];
243
+
244
+ if (!ipPart || !prefixPart) return null;
245
+
246
+ const network = parseIp(ipPart);
247
+ const prefix = parseInteger(prefixPart);
248
+
249
+ if (network === null || prefix === null) return null;
250
+ if (prefix < 0 || prefix > 32) return null;
251
+
252
+ const mask = prefixToMask(prefix);
253
+
254
+ return { network, prefix, mask };
255
+ };
256
+
257
+ /**
258
+ * Check if an IP is within a CIDR block.
259
+ * @param ip IP address (as 32-bit number)
260
+ * @param network Network address (as 32-bit number)
261
+ * @param mask Subnet mask (as 32-bit number)
262
+ * @returns true if IP is in the network
263
+ */
264
+ export const isIpInNetwork = (ip: number, network: number, mask: number): boolean =>
265
+ (ip & mask) === (network & mask);
266
+
267
+ /**
268
+ * Check if an IP string is within a CIDR block string.
269
+ * @param ipStr IP address string
270
+ * @param cidrStr CIDR notation string
271
+ * @returns true if IP is in the CIDR block, false if invalid or not in range
272
+ */
273
+ export const isIpInCidr = (ipStr: string, cidrStr: string): boolean => {
274
+ const ip = parseIp(ipStr);
275
+ const cidr = parseCidr(cidrStr);
276
+
277
+ if (ip === null || cidr === null) return false;
278
+
279
+ return isIpInNetwork(ip, cidr.network, cidr.mask);
280
+ };