@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/core",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "SentriFlow core engine for network configuration validation",
5
5
  "license": "Apache-2.0",
6
6
  "module": "src/index.ts",
@@ -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
- let addr = ip;
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
- let addr = ip;
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
- let addr = ip;
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: parseInt(subnet.substring(slashIndex + 1), 10),
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(IPV4_CIDR_PATTERN);
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(IPV4_WITH_MASK_PATTERN);
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(IPV4_WITH_MASK_KEYWORD_PATTERN);
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(IPV4_WITH_WILDCARD_PATTERN);
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(IPV4_PATTERN);
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(IPV6_CIDR_PATTERN);
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(IPV6_PATTERN);
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
@@ -21,4 +21,7 @@ export type {
21
21
  IPSummary,
22
22
  IPCounts,
23
23
  ExtractOptions,
24
+ InputValidationErrorCode,
24
25
  } from './types';
26
+
27
+ export { InputValidationError, DEFAULT_MAX_CONTENT_SIZE } from './types';
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
  },
@@ -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
  /**