@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.
- package/LICENSE +190 -0
- package/README.md +86 -0
- package/package.json +60 -0
- package/src/constants.ts +77 -0
- package/src/engine/RuleExecutor.ts +256 -0
- package/src/engine/Runner.ts +312 -0
- package/src/engine/SandboxedExecutor.ts +208 -0
- package/src/errors.ts +88 -0
- package/src/helpers/arista/helpers.ts +1220 -0
- package/src/helpers/arista/index.ts +12 -0
- package/src/helpers/aruba/helpers.ts +637 -0
- package/src/helpers/aruba/index.ts +13 -0
- package/src/helpers/cisco/helpers.ts +534 -0
- package/src/helpers/cisco/index.ts +11 -0
- package/src/helpers/common/helpers.ts +265 -0
- package/src/helpers/common/index.ts +5 -0
- package/src/helpers/common/validation.ts +280 -0
- package/src/helpers/cumulus/helpers.ts +676 -0
- package/src/helpers/cumulus/index.ts +12 -0
- package/src/helpers/extreme/helpers.ts +422 -0
- package/src/helpers/extreme/index.ts +12 -0
- package/src/helpers/fortinet/helpers.ts +892 -0
- package/src/helpers/fortinet/index.ts +12 -0
- package/src/helpers/huawei/helpers.ts +790 -0
- package/src/helpers/huawei/index.ts +11 -0
- package/src/helpers/index.ts +53 -0
- package/src/helpers/juniper/helpers.ts +756 -0
- package/src/helpers/juniper/index.ts +12 -0
- package/src/helpers/mikrotik/helpers.ts +722 -0
- package/src/helpers/mikrotik/index.ts +12 -0
- package/src/helpers/nokia/helpers.ts +856 -0
- package/src/helpers/nokia/index.ts +11 -0
- package/src/helpers/paloalto/helpers.ts +939 -0
- package/src/helpers/paloalto/index.ts +12 -0
- package/src/helpers/vyos/helpers.ts +429 -0
- package/src/helpers/vyos/index.ts +12 -0
- package/src/index.ts +30 -0
- package/src/json-rules/ExpressionEvaluator.ts +292 -0
- package/src/json-rules/HelperRegistry.ts +177 -0
- package/src/json-rules/JsonRuleCompiler.ts +339 -0
- package/src/json-rules/JsonRuleValidator.ts +371 -0
- package/src/json-rules/index.ts +97 -0
- package/src/json-rules/schema.json +350 -0
- package/src/json-rules/types.ts +303 -0
- package/src/pack-loader/PackLoader.ts +332 -0
- package/src/pack-loader/index.ts +17 -0
- package/src/pack-loader/types.ts +135 -0
- package/src/parser/IncrementalParser.ts +527 -0
- package/src/parser/Sanitizer.ts +104 -0
- package/src/parser/SchemaAwareParser.ts +504 -0
- package/src/parser/VendorSchema.ts +72 -0
- package/src/parser/vendors/arista-eos.ts +206 -0
- package/src/parser/vendors/aruba-aoscx.ts +123 -0
- package/src/parser/vendors/aruba-aosswitch.ts +113 -0
- package/src/parser/vendors/aruba-wlc.ts +173 -0
- package/src/parser/vendors/cisco-ios.ts +110 -0
- package/src/parser/vendors/cisco-nxos.ts +107 -0
- package/src/parser/vendors/cumulus-linux.ts +161 -0
- package/src/parser/vendors/extreme-exos.ts +154 -0
- package/src/parser/vendors/extreme-voss.ts +167 -0
- package/src/parser/vendors/fortinet-fortigate.ts +217 -0
- package/src/parser/vendors/huawei-vrp.ts +192 -0
- package/src/parser/vendors/index.ts +1521 -0
- package/src/parser/vendors/juniper-junos.ts +230 -0
- package/src/parser/vendors/mikrotik-routeros.ts +274 -0
- package/src/parser/vendors/nokia-sros.ts +251 -0
- package/src/parser/vendors/paloalto-panos.ts +264 -0
- package/src/parser/vendors/vyos-vyos.ts +454 -0
- package/src/types/ConfigNode.ts +72 -0
- package/src/types/DeclarativeRule.ts +158 -0
- 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,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
|
+
};
|