@sentriflow/core 0.1.8 → 0.2.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/README.md +29 -0
- package/package.json +1 -1
- package/src/ip/extractor.ts +151 -61
- package/src/ip/index.ts +3 -0
- package/src/ip/types.ts +51 -0
- package/src/json-rules/schema.json +35 -5
- package/src/types/IRule.ts +27 -6
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ bun add @sentriflow/core
|
|
|
19
19
|
- **Multi-vendor support**: Cisco IOS/NX-OS, Juniper JunOS, Arista EOS, Fortinet FortiGate, Palo Alto PAN-OS, and more
|
|
20
20
|
- **AST-based parsing**: Converts configurations into a vendor-agnostic Abstract Syntax Tree
|
|
21
21
|
- **Extensible rule engine**: Define compliance rules for best practices or organization-specific policies
|
|
22
|
+
- **IP/Subnet Extraction**: Extract and deduplicate IP addresses and CIDR subnets from configurations
|
|
22
23
|
- **TypeScript native**: Full type safety with comprehensive type definitions
|
|
23
24
|
|
|
24
25
|
## Supported Vendors
|
|
@@ -75,6 +76,34 @@ Checks an AST for compliance against a set of rules.
|
|
|
75
76
|
|
|
76
77
|
Auto-detects the vendor/platform from configuration content.
|
|
77
78
|
|
|
79
|
+
### `extractIPSummary(content: string, options?: ExtractOptions): IPSummary`
|
|
80
|
+
|
|
81
|
+
Extracts all IP addresses and subnets from configuration text.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { extractIPSummary } from '@sentriflow/core';
|
|
85
|
+
|
|
86
|
+
const config = `
|
|
87
|
+
interface GigabitEthernet0/1
|
|
88
|
+
ip address 192.168.1.1 255.255.255.0
|
|
89
|
+
ip route 10.0.0.0/24 via 192.168.1.254
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
const summary = extractIPSummary(config);
|
|
93
|
+
// {
|
|
94
|
+
// ipv4Addresses: ['192.168.1.1', '192.168.1.254'],
|
|
95
|
+
// ipv6Addresses: [],
|
|
96
|
+
// ipv4Subnets: ['10.0.0.0/24'],
|
|
97
|
+
// ipv6Subnets: [],
|
|
98
|
+
// counts: { total: 3, ipv4: 2, ipv6: 0, ipv4Subnets: 1, ipv6Subnets: 0 }
|
|
99
|
+
// }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Options:**
|
|
103
|
+
- `maxContentSize`: Maximum input size in bytes (default: 50MB) - prevents DoS
|
|
104
|
+
- `includeSubnetNetworks`: Include subnet network addresses in address lists
|
|
105
|
+
- `skipIPv4`, `skipIPv6`, `skipSubnets`: Skip specific extraction types
|
|
106
|
+
|
|
78
107
|
## Related Packages
|
|
79
108
|
|
|
80
109
|
- [`@sentriflow/cli`](https://github.com/sentriflow/sentriflow/tree/main/packages/cli) - Command-line interface
|
package/package.json
CHANGED
package/src/ip/extractor.ts
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
// packages/core/src/ip/extractor.ts
|
|
2
2
|
|
|
3
3
|
import type { IPAddressType, IPSummary, IPCounts, ExtractOptions } from './types';
|
|
4
|
+
import { InputValidationError, DEFAULT_MAX_CONTENT_SIZE } from './types';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Utility Functions
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Strip zone ID from IPv6 address.
|
|
12
|
+
* e.g., "fe80::1%eth0" -> "fe80::1"
|
|
13
|
+
*
|
|
14
|
+
* @param ip - IPv6 address string (with or without zone ID)
|
|
15
|
+
* @returns IPv6 address without zone ID
|
|
16
|
+
*/
|
|
17
|
+
function stripZoneId(ip: string): string {
|
|
18
|
+
const zoneIndex = ip.indexOf('%');
|
|
19
|
+
return zoneIndex !== -1 ? ip.substring(0, zoneIndex) : ip;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Regex Factory Functions
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create IPv4 pattern regex.
|
|
28
|
+
* Using factory function prevents lastIndex state issues with global flag.
|
|
29
|
+
*/
|
|
30
|
+
function createIPv4Pattern(): RegExp {
|
|
31
|
+
return /\b(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b/g;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create IPv4 CIDR pattern regex.
|
|
36
|
+
*/
|
|
37
|
+
function createIPv4CidrPattern(): RegExp {
|
|
38
|
+
return /\b(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(?:3[0-2]|[12]?[0-9])\b/g;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create IPv4 + subnet mask pattern regex.
|
|
43
|
+
*/
|
|
44
|
+
function createIPv4WithMaskPattern(): RegExp {
|
|
45
|
+
return /\b((?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\s+(255\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\b/g;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create IPv4 + "mask" keyword + subnet mask pattern regex.
|
|
50
|
+
*/
|
|
51
|
+
function createIPv4WithMaskKeywordPattern(): RegExp {
|
|
52
|
+
return /\b((?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\s+mask\s+(255\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\b/gi;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create IPv4 + wildcard mask pattern regex.
|
|
57
|
+
*/
|
|
58
|
+
function createIPv4WithWildcardPattern(): RegExp {
|
|
59
|
+
return /\b((?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\s+(0\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\b/g;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create IPv6 pattern regex.
|
|
64
|
+
*/
|
|
65
|
+
function createIPv6Pattern(): RegExp {
|
|
66
|
+
return /(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::/g;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create IPv6 CIDR pattern regex.
|
|
71
|
+
* Uses negative lookahead (?!\d) to prevent matching partial prefixes like /12 from /129
|
|
72
|
+
*/
|
|
73
|
+
function createIPv6CidrPattern(): RegExp {
|
|
74
|
+
return /(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])(?!\d)/g;
|
|
75
|
+
}
|
|
4
76
|
|
|
5
77
|
// ============================================================================
|
|
6
78
|
// Validation Functions
|
|
@@ -42,11 +114,7 @@ export function isValidIPv6(ip: string): boolean {
|
|
|
42
114
|
if (!ip || typeof ip !== 'string') return false;
|
|
43
115
|
|
|
44
116
|
// Strip zone ID if present (e.g., fe80::1%eth0)
|
|
45
|
-
|
|
46
|
-
const zoneIndex = ip.indexOf('%');
|
|
47
|
-
if (zoneIndex !== -1) {
|
|
48
|
-
addr = ip.substring(0, zoneIndex);
|
|
49
|
-
}
|
|
117
|
+
const addr = stripZoneId(ip);
|
|
50
118
|
|
|
51
119
|
// Must have at least one colon
|
|
52
120
|
if (!addr.includes(':')) return false;
|
|
@@ -144,14 +212,8 @@ export function normalizeIPv4(ip: string): string {
|
|
|
144
212
|
* @returns Normalized IPv6 string (lowercase, no zone ID, fully expanded)
|
|
145
213
|
*/
|
|
146
214
|
export function normalizeIPv6(ip: string): string {
|
|
147
|
-
// Strip zone ID
|
|
148
|
-
|
|
149
|
-
const zoneIndex = ip.indexOf('%');
|
|
150
|
-
if (zoneIndex !== -1) {
|
|
151
|
-
addr = ip.substring(0, zoneIndex);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
addr = addr.toLowerCase();
|
|
215
|
+
// Strip zone ID and convert to lowercase
|
|
216
|
+
const addr = stripZoneId(ip).toLowerCase();
|
|
155
217
|
|
|
156
218
|
// Handle :: expansion
|
|
157
219
|
if (addr.includes('::')) {
|
|
@@ -199,11 +261,31 @@ export function normalizeIPv6(ip: string): string {
|
|
|
199
261
|
// Comparison Functions
|
|
200
262
|
// ============================================================================
|
|
201
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Validate and clamp octet value to valid range.
|
|
266
|
+
* T019/T020/T021: Defensive bounds checking for IPv4 octets.
|
|
267
|
+
*
|
|
268
|
+
* @param n - Raw number value
|
|
269
|
+
* @returns Valid octet value (0-255), clamped if out of range
|
|
270
|
+
*/
|
|
271
|
+
function clampOctet(n: number): number {
|
|
272
|
+
// T021: Check if value is an integer
|
|
273
|
+
if (!Number.isInteger(n)) {
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
// T019/T020: Clamp to valid octet range
|
|
277
|
+
if (n < 0 || n > 255) {
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
return n;
|
|
281
|
+
}
|
|
282
|
+
|
|
202
283
|
/**
|
|
203
284
|
* Convert IPv4 to 32-bit number for comparison.
|
|
285
|
+
* Uses defensive octet validation to handle malformed input.
|
|
204
286
|
*/
|
|
205
287
|
function ipv4ToNumber(ip: string): number {
|
|
206
|
-
const octets = ip.split('.').map(Number);
|
|
288
|
+
const octets = ip.split('.').map((n) => clampOctet(Number(n)));
|
|
207
289
|
const o0 = octets[0] ?? 0;
|
|
208
290
|
const o1 = octets[1] ?? 0;
|
|
209
291
|
const o2 = octets[2] ?? 0;
|
|
@@ -227,12 +309,7 @@ export function compareIPv4(a: string, b: string): number {
|
|
|
227
309
|
*/
|
|
228
310
|
function expandIPv6(ip: string): string[] {
|
|
229
311
|
// Strip zone ID
|
|
230
|
-
|
|
231
|
-
const zoneIndex = ip.indexOf('%');
|
|
232
|
-
if (zoneIndex !== -1) {
|
|
233
|
-
addr = ip.substring(0, zoneIndex);
|
|
234
|
-
}
|
|
235
|
-
|
|
312
|
+
const addr = stripZoneId(ip);
|
|
236
313
|
const parts = addr.split(':');
|
|
237
314
|
const result: string[] = [];
|
|
238
315
|
|
|
@@ -315,12 +392,37 @@ export function sortIPv6Addresses(ips: string[]): string[] {
|
|
|
315
392
|
|
|
316
393
|
/**
|
|
317
394
|
* Parse subnet into network and prefix.
|
|
395
|
+
* T016/T017/T018: Validates format before parsing.
|
|
396
|
+
*
|
|
397
|
+
* @param subnet - CIDR notation string (e.g., "10.0.0.0/24")
|
|
398
|
+
* @returns Parsed network address and prefix length
|
|
399
|
+
* @throws InputValidationError if format is invalid
|
|
318
400
|
*/
|
|
319
401
|
function parseSubnet(subnet: string): { network: string; prefix: number } {
|
|
320
402
|
const slashIndex = subnet.lastIndexOf('/');
|
|
403
|
+
|
|
404
|
+
// T016: Validate slash presence
|
|
405
|
+
if (slashIndex === -1) {
|
|
406
|
+
throw new InputValidationError(
|
|
407
|
+
`Invalid subnet format (missing /): ${subnet}`,
|
|
408
|
+
'INVALID_FORMAT'
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const prefixStr = subnet.substring(slashIndex + 1);
|
|
413
|
+
const prefix = parseInt(prefixStr, 10);
|
|
414
|
+
|
|
415
|
+
// T017: Validate prefix is a valid number
|
|
416
|
+
if (isNaN(prefix)) {
|
|
417
|
+
throw new InputValidationError(
|
|
418
|
+
`Invalid subnet prefix: ${prefixStr}`,
|
|
419
|
+
'INVALID_FORMAT'
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
321
423
|
return {
|
|
322
424
|
network: subnet.substring(0, slashIndex),
|
|
323
|
-
prefix
|
|
425
|
+
prefix,
|
|
324
426
|
};
|
|
325
427
|
}
|
|
326
428
|
|
|
@@ -449,39 +551,6 @@ function wildcardToCidr(wildcard: string): number {
|
|
|
449
551
|
return prefix;
|
|
450
552
|
}
|
|
451
553
|
|
|
452
|
-
// IPv4 pattern: 4 octets, each 0-255, no leading zeros
|
|
453
|
-
const IPV4_PATTERN =
|
|
454
|
-
/\b(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\b/g;
|
|
455
|
-
|
|
456
|
-
// IPv4 CIDR pattern
|
|
457
|
-
const IPV4_CIDR_PATTERN =
|
|
458
|
-
/\b(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\/(?:3[0-2]|[12]?[0-9])\b/g;
|
|
459
|
-
|
|
460
|
-
// IPv4 + subnet mask pattern (e.g., "ip address 192.168.1.1 255.255.255.0")
|
|
461
|
-
// Captures IP address followed by space and subnet mask
|
|
462
|
-
const IPV4_WITH_MASK_PATTERN =
|
|
463
|
-
/\b((?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\s+(255\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\b/g;
|
|
464
|
-
|
|
465
|
-
// IPv4 + "mask" keyword + subnet mask pattern (e.g., "network 192.168.1.0 mask 255.255.255.0" in BGP)
|
|
466
|
-
// Captures IP address followed by "mask" keyword and subnet mask
|
|
467
|
-
const IPV4_WITH_MASK_KEYWORD_PATTERN =
|
|
468
|
-
/\b((?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\s+mask\s+(255\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\b/gi;
|
|
469
|
-
|
|
470
|
-
// IPv4 + wildcard mask pattern (e.g., "access-list 10 permit 192.168.1.0 0.0.0.255")
|
|
471
|
-
// Captures network address followed by space and wildcard mask
|
|
472
|
-
const IPV4_WITH_WILDCARD_PATTERN =
|
|
473
|
-
/\b((?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\s+(0\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9]))\b/g;
|
|
474
|
-
|
|
475
|
-
// IPv6 pattern - matches most common formats
|
|
476
|
-
// Order matters: longer patterns first to prevent partial matching
|
|
477
|
-
const IPV6_PATTERN =
|
|
478
|
-
/(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::/g;
|
|
479
|
-
|
|
480
|
-
// IPv6 CIDR pattern - matches IPv6/prefix notation
|
|
481
|
-
// Order matters: longer patterns first
|
|
482
|
-
const IPV6_CIDR_PATTERN =
|
|
483
|
-
/(?:(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|:(?::[0-9a-fA-F]{1,4}){1,7}|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|(?:[0-9a-fA-F]{1,4}:){1,7}:|::)\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])/g;
|
|
484
|
-
|
|
485
554
|
/**
|
|
486
555
|
* Create an empty IP summary.
|
|
487
556
|
*/
|
|
@@ -513,6 +582,15 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
513
582
|
return createEmptyIPSummary();
|
|
514
583
|
}
|
|
515
584
|
|
|
585
|
+
// T011/T012: Validate content size to prevent DoS via memory exhaustion
|
|
586
|
+
const maxSize = options.maxContentSize ?? DEFAULT_MAX_CONTENT_SIZE;
|
|
587
|
+
if (content.length > maxSize) {
|
|
588
|
+
throw new InputValidationError(
|
|
589
|
+
`Content exceeds maximum size of ${maxSize} bytes`,
|
|
590
|
+
'SIZE_LIMIT_EXCEEDED'
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
516
594
|
const ipv4Set = new Set<string>();
|
|
517
595
|
const ipv6Set = new Set<string>();
|
|
518
596
|
const ipv4SubnetSet = new Set<string>();
|
|
@@ -527,7 +605,7 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
527
605
|
// Extract IPv4 subnets first (so we can exclude their network addresses from standalone IPs)
|
|
528
606
|
if (!options.skipSubnets) {
|
|
529
607
|
// Extract CIDR notation subnets (e.g., 10.0.0.0/24)
|
|
530
|
-
const ipv4CidrMatches = content.matchAll(
|
|
608
|
+
const ipv4CidrMatches = content.matchAll(createIPv4CidrPattern());
|
|
531
609
|
for (const match of ipv4CidrMatches) {
|
|
532
610
|
const subnet = match[0];
|
|
533
611
|
if (isValidSubnet(subnet)) {
|
|
@@ -540,7 +618,7 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
540
618
|
|
|
541
619
|
// Extract IP + mask pairs (e.g., "192.168.1.1 255.255.255.0")
|
|
542
620
|
// These are interface addresses with their subnet info
|
|
543
|
-
const ipMaskMatches = content.matchAll(
|
|
621
|
+
const ipMaskMatches = content.matchAll(createIPv4WithMaskPattern());
|
|
544
622
|
for (const match of ipMaskMatches) {
|
|
545
623
|
const ip = match[1];
|
|
546
624
|
const mask = match[2];
|
|
@@ -556,7 +634,7 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
556
634
|
}
|
|
557
635
|
|
|
558
636
|
// Extract IP + "mask" + mask pairs (e.g., "network 10.0.0.0 mask 255.0.0.0" in BGP)
|
|
559
|
-
const ipMaskKeywordMatches = content.matchAll(
|
|
637
|
+
const ipMaskKeywordMatches = content.matchAll(createIPv4WithMaskKeywordPattern());
|
|
560
638
|
for (const match of ipMaskKeywordMatches) {
|
|
561
639
|
const ip = match[1];
|
|
562
640
|
const mask = match[2];
|
|
@@ -573,7 +651,7 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
573
651
|
|
|
574
652
|
// Extract IP + wildcard pairs (e.g., "192.168.1.0 0.0.0.255" in ACLs)
|
|
575
653
|
// These define network ranges in access lists
|
|
576
|
-
const ipWildcardMatches = content.matchAll(
|
|
654
|
+
const ipWildcardMatches = content.matchAll(createIPv4WithWildcardPattern());
|
|
577
655
|
for (const match of ipWildcardMatches) {
|
|
578
656
|
const ip = match[1];
|
|
579
657
|
const wildcard = match[2];
|
|
@@ -590,7 +668,7 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
590
668
|
}
|
|
591
669
|
|
|
592
670
|
// Extract IPv4 addresses (excluding those that are subnet network addresses)
|
|
593
|
-
const ipv4Matches = content.matchAll(
|
|
671
|
+
const ipv4Matches = content.matchAll(createIPv4Pattern());
|
|
594
672
|
for (const match of ipv4Matches) {
|
|
595
673
|
const ip = match[0];
|
|
596
674
|
if (isValidIPv4(ip)) {
|
|
@@ -615,7 +693,7 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
615
693
|
if (!options.skipIPv6) {
|
|
616
694
|
// Extract IPv6 subnets first
|
|
617
695
|
if (!options.skipSubnets) {
|
|
618
|
-
const ipv6CidrMatches = content.matchAll(
|
|
696
|
+
const ipv6CidrMatches = content.matchAll(createIPv6CidrPattern());
|
|
619
697
|
for (const match of ipv6CidrMatches) {
|
|
620
698
|
const subnet = match[0];
|
|
621
699
|
if (isValidSubnet(subnet)) {
|
|
@@ -628,7 +706,7 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
628
706
|
}
|
|
629
707
|
|
|
630
708
|
// Extract IPv6 addresses
|
|
631
|
-
const ipv6Matches = content.matchAll(
|
|
709
|
+
const ipv6Matches = content.matchAll(createIPv6Pattern());
|
|
632
710
|
for (const match of ipv6Matches) {
|
|
633
711
|
const ip = match[0];
|
|
634
712
|
if (isValidIPv6(ip)) {
|
|
@@ -640,6 +718,18 @@ export function extractIPSummary(content: string, options: ExtractOptions = {}):
|
|
|
640
718
|
}
|
|
641
719
|
}
|
|
642
720
|
|
|
721
|
+
// If includeSubnetNetworks is true, add subnet network addresses to the address sets
|
|
722
|
+
if (options.includeSubnetNetworks) {
|
|
723
|
+
for (const subnet of ipv4SubnetSet) {
|
|
724
|
+
const { network } = parseSubnet(subnet);
|
|
725
|
+
ipv4Set.add(network);
|
|
726
|
+
}
|
|
727
|
+
for (const subnet of ipv6SubnetSet) {
|
|
728
|
+
const { network } = parseSubnet(subnet);
|
|
729
|
+
ipv6Set.add(network);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
643
733
|
// Convert sets to sorted arrays
|
|
644
734
|
const ipv4Addresses = sortIPv4Addresses([...ipv4Set]);
|
|
645
735
|
const ipv6Addresses = sortIPv6Addresses([...ipv6Set]);
|
package/src/ip/index.ts
CHANGED
package/src/ip/types.ts
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
// packages/core/src/ip/types.ts
|
|
2
2
|
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Constants
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default maximum content size for IP extraction (50MB).
|
|
9
|
+
* Prevents DoS attacks via memory exhaustion from processing very large files.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MAX_CONTENT_SIZE = 50 * 1024 * 1024; // 50MB
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Error Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Error codes for input validation failures.
|
|
19
|
+
*/
|
|
20
|
+
export type InputValidationErrorCode = 'SIZE_LIMIT_EXCEEDED' | 'INVALID_FORMAT';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Error thrown when input validation fails.
|
|
24
|
+
* Used for size limits and malformed input detection.
|
|
25
|
+
*/
|
|
26
|
+
export class InputValidationError extends Error {
|
|
27
|
+
constructor(
|
|
28
|
+
message: string,
|
|
29
|
+
public readonly code: InputValidationErrorCode
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'InputValidationError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Core Types
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
3
40
|
/**
|
|
4
41
|
* Discriminator for IPv4 vs IPv6 addresses.
|
|
5
42
|
*/
|
|
@@ -97,4 +134,18 @@ export interface ExtractOptions {
|
|
|
97
134
|
* @default false
|
|
98
135
|
*/
|
|
99
136
|
skipSubnets?: boolean;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Include subnet network addresses in the addresses lists.
|
|
140
|
+
* When true, a subnet like 10.0.0.0/24 will add 10.0.0.0 to ipv4Addresses.
|
|
141
|
+
* @default false
|
|
142
|
+
*/
|
|
143
|
+
includeSubnetNetworks?: boolean;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Maximum content size in bytes.
|
|
147
|
+
* Content exceeding this limit will throw InputValidationError.
|
|
148
|
+
* @default DEFAULT_MAX_CONTENT_SIZE (50MB)
|
|
149
|
+
*/
|
|
150
|
+
maxContentSize?: number;
|
|
100
151
|
}
|
|
@@ -135,6 +135,41 @@
|
|
|
135
135
|
},
|
|
136
136
|
"security": {
|
|
137
137
|
"$ref": "#/definitions/SecurityMetadata"
|
|
138
|
+
},
|
|
139
|
+
"tags": {
|
|
140
|
+
"type": "array",
|
|
141
|
+
"items": { "$ref": "#/definitions/Tag" },
|
|
142
|
+
"description": "Typed tags for multi-dimensional rule categorization"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"TagType": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"enum": ["security", "operational", "compliance", "general"],
|
|
149
|
+
"description": "Tag classification type"
|
|
150
|
+
},
|
|
151
|
+
"Tag": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"required": ["type", "label"],
|
|
154
|
+
"additionalProperties": false,
|
|
155
|
+
"properties": {
|
|
156
|
+
"type": {
|
|
157
|
+
"$ref": "#/definitions/TagType"
|
|
158
|
+
},
|
|
159
|
+
"label": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"minLength": 1,
|
|
162
|
+
"description": "Short identifier/label for the tag"
|
|
163
|
+
},
|
|
164
|
+
"text": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"description": "Optional extended description"
|
|
167
|
+
},
|
|
168
|
+
"score": {
|
|
169
|
+
"type": "number",
|
|
170
|
+
"minimum": 0,
|
|
171
|
+
"maximum": 10,
|
|
172
|
+
"description": "Optional severity/priority score (0-10 range)"
|
|
138
173
|
}
|
|
139
174
|
}
|
|
140
175
|
},
|
|
@@ -156,11 +191,6 @@
|
|
|
156
191
|
"cvssVector": {
|
|
157
192
|
"type": "string",
|
|
158
193
|
"description": "CVSS v3.1 vector string"
|
|
159
|
-
},
|
|
160
|
-
"tags": {
|
|
161
|
-
"type": "array",
|
|
162
|
-
"items": { "type": "string" },
|
|
163
|
-
"description": "Security-related tags"
|
|
164
194
|
}
|
|
165
195
|
}
|
|
166
196
|
},
|
package/src/types/IRule.ts
CHANGED
|
@@ -179,9 +179,34 @@ export interface RulePack extends RulePackMetadata {
|
|
|
179
179
|
disables?: PackDisableConfig;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Tag type classification for rule categorization.
|
|
184
|
+
* Allows multi-dimensional tagging beyond security-only metadata.
|
|
185
|
+
*/
|
|
186
|
+
export type TagType = 'security' | 'operational' | 'compliance' | 'general';
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* A typed classification object for categorizing rules.
|
|
190
|
+
* Replaces the simpler string-based security tags with structured metadata.
|
|
191
|
+
*/
|
|
192
|
+
export interface Tag {
|
|
193
|
+
/** Tag classification type */
|
|
194
|
+
type: TagType;
|
|
195
|
+
|
|
196
|
+
/** Short identifier/label for the tag (e.g., "vlan-hopping", "access-control") */
|
|
197
|
+
label: string;
|
|
198
|
+
|
|
199
|
+
/** Optional extended description */
|
|
200
|
+
text?: string;
|
|
201
|
+
|
|
202
|
+
/** Optional severity/priority score (0-10 range) */
|
|
203
|
+
score?: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
182
206
|
/**
|
|
183
207
|
* SEC-007: Security metadata for SARIF integration.
|
|
184
208
|
* Provides CWE mappings and CVSS scores for security-related rules.
|
|
209
|
+
* Note: tags field has been moved to RuleMetadata.tags for generalization.
|
|
185
210
|
*/
|
|
186
211
|
export interface SecurityMetadata {
|
|
187
212
|
/**
|
|
@@ -201,12 +226,6 @@ export interface SecurityMetadata {
|
|
|
201
226
|
* Example: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H'
|
|
202
227
|
*/
|
|
203
228
|
cvssVector?: string;
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Security-related tags for categorization.
|
|
207
|
-
* Example: ['authentication', 'hardcoded-credentials', 'encryption']
|
|
208
|
-
*/
|
|
209
|
-
tags?: string[];
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
/**
|
|
@@ -226,6 +245,8 @@ export interface RuleMetadata {
|
|
|
226
245
|
remediation?: string;
|
|
227
246
|
/** SEC-007: Optional security metadata for SARIF integration */
|
|
228
247
|
security?: SecurityMetadata;
|
|
248
|
+
/** Typed tags for multi-dimensional rule categorization */
|
|
249
|
+
tags?: Tag[];
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
/**
|