@sentriflow/cli 0.1.9 → 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.
Files changed (3) hide show
  1. package/README.md +57 -2
  2. package/dist/index.js +200 -52
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,6 +37,15 @@ sentriflow --list-vendors
37
37
 
38
38
  # List active rules
39
39
  sentriflow --list-rules
40
+
41
+ # List rules by category
42
+ sentriflow --list-rules --category authentication
43
+
44
+ # List all categories
45
+ sentriflow --list-categories
46
+
47
+ # Read from stdin
48
+ cat router.conf | sentriflow -
40
49
  ```
41
50
 
42
51
  ## Usage
@@ -47,7 +56,7 @@ Usage: sentriflow [options] [file]
47
56
  SentriFlow Network Configuration Compliance Checker
48
57
 
49
58
  Arguments:
50
- file Path to the configuration file
59
+ file Path to the configuration file (use - for stdin)
51
60
 
52
61
  Options:
53
62
  -V, --version output the version number
@@ -80,10 +89,20 @@ Supported vendors: `cisco-ios`, `juniper-junos`, `palo-alto`, `fortinet`, `arist
80
89
  | `--no-config` | Ignore config file |
81
90
  | `-d, --disable <ids>` | Comma-separated rule IDs to disable |
82
91
  | `--list-rules` | List all active rules and exit |
92
+ | `--list-categories` | List all rule categories with counts |
93
+ | `--category <name>` | Filter `--list-rules` by category |
94
+ | `--list-format <fmt>` | Format for `--list-rules`: `table` (default), `json`, `csv` |
83
95
  | `-p, --rule-pack <path>` | Rule pack file to load |
84
96
  | `--json-rules <path...>` | Path(s) to JSON rules file(s) |
85
97
  | `-r, --rules <path>` | Additional rules file (legacy) |
86
98
 
99
+ ### IP Extraction
100
+
101
+ | Option | Description |
102
+ |--------|-------------|
103
+ | `--extract-ips` | Extract and display all IP addresses/subnets from configuration |
104
+ | `--copy-ips` | Copy extracted IPs to clipboard (requires xclip/pbcopy) |
105
+
87
106
  ### Encrypted Rule Packs
88
107
 
89
108
  | Option | Description |
@@ -125,7 +144,11 @@ Supported vendors: `cisco-ios`, `juniper-junos`, `palo-alto`, `fortinet`, `arist
125
144
  "passed": false,
126
145
  "message": "Telnet is enabled - use SSH instead",
127
146
  "line": 12,
128
- "column": 1
147
+ "column": 1,
148
+ "category": "authentication",
149
+ "tags": [
150
+ { "type": "security", "label": "plaintext-protocol" }
151
+ ]
129
152
  }
130
153
  ]
131
154
  }
@@ -159,6 +182,38 @@ Produces SARIF 2.1.0 compliant output for integration with GitHub Code Scanning,
159
182
  sentriflow router.conf -f sarif > results.sarif
160
183
  ```
161
184
 
185
+ SARIF output includes rule categories and tags in the `properties` block:
186
+
187
+ ```json
188
+ {
189
+ "rules": [{
190
+ "id": "SEC-001",
191
+ "properties": {
192
+ "category": "authentication",
193
+ "tags": ["security:plaintext-protocol"]
194
+ }
195
+ }]
196
+ }
197
+ ```
198
+
199
+ ## Rule Categories
200
+
201
+ List all available categories:
202
+
203
+ ```bash
204
+ sentriflow --list-categories
205
+ ```
206
+
207
+ Filter rules by category:
208
+
209
+ ```bash
210
+ # List only authentication rules
211
+ sentriflow --list-rules --category authentication
212
+
213
+ # Output as JSON
214
+ sentriflow --list-rules --category encryption --list-format json
215
+ ```
216
+
162
217
  ## CI/CD Integration
163
218
 
164
219
  ### GitHub Actions
package/dist/index.js CHANGED
@@ -10334,7 +10334,42 @@ function validateRegex(pattern, flags, path, ctx) {
10334
10334
  }
10335
10335
  }
10336
10336
 
10337
+ // ../core/src/ip/types.ts
10338
+ var DEFAULT_MAX_CONTENT_SIZE = 50 * 1024 * 1024;
10339
+ var InputValidationError = class extends Error {
10340
+ constructor(message, code) {
10341
+ super(message);
10342
+ this.code = code;
10343
+ this.name = "InputValidationError";
10344
+ }
10345
+ };
10346
+
10337
10347
  // ../core/src/ip/extractor.ts
10348
+ function stripZoneId(ip) {
10349
+ const zoneIndex = ip.indexOf("%");
10350
+ return zoneIndex !== -1 ? ip.substring(0, zoneIndex) : ip;
10351
+ }
10352
+ function createIPv4Pattern() {
10353
+ 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;
10354
+ }
10355
+ function createIPv4CidrPattern() {
10356
+ 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;
10357
+ }
10358
+ function createIPv4WithMaskPattern() {
10359
+ 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;
10360
+ }
10361
+ function createIPv4WithMaskKeywordPattern() {
10362
+ 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;
10363
+ }
10364
+ function createIPv4WithWildcardPattern() {
10365
+ 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;
10366
+ }
10367
+ function createIPv6Pattern() {
10368
+ 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;
10369
+ }
10370
+ function createIPv6CidrPattern() {
10371
+ 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;
10372
+ }
10338
10373
  function isValidIPv4(ip) {
10339
10374
  if (!ip || typeof ip !== "string") return false;
10340
10375
  const octets = ip.split(".");
@@ -10349,11 +10384,7 @@ function isValidIPv4(ip) {
10349
10384
  }
10350
10385
  function isValidIPv6(ip) {
10351
10386
  if (!ip || typeof ip !== "string") return false;
10352
- let addr = ip;
10353
- const zoneIndex = ip.indexOf("%");
10354
- if (zoneIndex !== -1) {
10355
- addr = ip.substring(0, zoneIndex);
10356
- }
10387
+ const addr = stripZoneId(ip);
10357
10388
  if (!addr.includes(":")) return false;
10358
10389
  if (addr.includes(":::")) return false;
10359
10390
  const doubleColonCount = (addr.match(/::/g) || []).length;
@@ -10392,12 +10423,7 @@ function normalizeIPv4(ip) {
10392
10423
  return ip.split(".").map((octet) => parseInt(octet, 10).toString()).join(".");
10393
10424
  }
10394
10425
  function normalizeIPv6(ip) {
10395
- let addr = ip;
10396
- const zoneIndex = ip.indexOf("%");
10397
- if (zoneIndex !== -1) {
10398
- addr = ip.substring(0, zoneIndex);
10399
- }
10400
- addr = addr.toLowerCase();
10426
+ const addr = stripZoneId(ip).toLowerCase();
10401
10427
  if (addr.includes("::")) {
10402
10428
  const sides = addr.split("::");
10403
10429
  const left = sides[0] ? sides[0].split(":").filter((p) => p !== "") : [];
@@ -10424,8 +10450,17 @@ function normalizeIPv6(ip) {
10424
10450
  }
10425
10451
  return result.join(":");
10426
10452
  }
10453
+ function clampOctet(n) {
10454
+ if (!Number.isInteger(n)) {
10455
+ return 0;
10456
+ }
10457
+ if (n < 0 || n > 255) {
10458
+ return 0;
10459
+ }
10460
+ return n;
10461
+ }
10427
10462
  function ipv4ToNumber(ip) {
10428
- const octets = ip.split(".").map(Number);
10463
+ const octets = ip.split(".").map((n) => clampOctet(Number(n)));
10429
10464
  const o0 = octets[0] ?? 0;
10430
10465
  const o1 = octets[1] ?? 0;
10431
10466
  const o2 = octets[2] ?? 0;
@@ -10438,11 +10473,7 @@ function compareIPv4(a, b) {
10438
10473
  return numA < numB ? -1 : numA > numB ? 1 : 0;
10439
10474
  }
10440
10475
  function expandIPv6(ip) {
10441
- let addr = ip;
10442
- const zoneIndex = ip.indexOf("%");
10443
- if (zoneIndex !== -1) {
10444
- addr = ip.substring(0, zoneIndex);
10445
- }
10476
+ const addr = stripZoneId(ip);
10446
10477
  const parts = addr.split(":");
10447
10478
  const result = [];
10448
10479
  for (let i = 0; i < parts.length; i++) {
@@ -10489,9 +10520,23 @@ function sortIPv6Addresses(ips) {
10489
10520
  }
10490
10521
  function parseSubnet(subnet) {
10491
10522
  const slashIndex = subnet.lastIndexOf("/");
10523
+ if (slashIndex === -1) {
10524
+ throw new InputValidationError(
10525
+ `Invalid subnet format (missing /): ${subnet}`,
10526
+ "INVALID_FORMAT"
10527
+ );
10528
+ }
10529
+ const prefixStr = subnet.substring(slashIndex + 1);
10530
+ const prefix = parseInt(prefixStr, 10);
10531
+ if (isNaN(prefix)) {
10532
+ throw new InputValidationError(
10533
+ `Invalid subnet prefix: ${prefixStr}`,
10534
+ "INVALID_FORMAT"
10535
+ );
10536
+ }
10492
10537
  return {
10493
10538
  network: subnet.substring(0, slashIndex),
10494
- prefix: parseInt(subnet.substring(slashIndex + 1), 10)
10539
+ prefix
10495
10540
  };
10496
10541
  }
10497
10542
  function sortSubnets(subnets, type) {
@@ -10541,13 +10586,6 @@ function wildcardToCidr(wildcard) {
10541
10586
  }
10542
10587
  return prefix;
10543
10588
  }
10544
- var IPV4_PATTERN = /\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;
10545
- var IPV4_CIDR_PATTERN = /\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;
10546
- var IPV4_WITH_MASK_PATTERN = /\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;
10547
- var IPV4_WITH_MASK_KEYWORD_PATTERN = /\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;
10548
- var IPV4_WITH_WILDCARD_PATTERN = /\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;
10549
- var IPV6_PATTERN = /(?:[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;
10550
- var IPV6_CIDR_PATTERN = /(?:(?:[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;
10551
10589
  function createEmptyIPSummary() {
10552
10590
  return {
10553
10591
  ipv4Addresses: [],
@@ -10567,6 +10605,13 @@ function extractIPSummary(content, options = {}) {
10567
10605
  if (!content || typeof content !== "string") {
10568
10606
  return createEmptyIPSummary();
10569
10607
  }
10608
+ const maxSize = options.maxContentSize ?? DEFAULT_MAX_CONTENT_SIZE;
10609
+ if (content.length > maxSize) {
10610
+ throw new InputValidationError(
10611
+ `Content exceeds maximum size of ${maxSize} bytes`,
10612
+ "SIZE_LIMIT_EXCEEDED"
10613
+ );
10614
+ }
10570
10615
  const ipv4Set = /* @__PURE__ */ new Set();
10571
10616
  const ipv6Set = /* @__PURE__ */ new Set();
10572
10617
  const ipv4SubnetSet = /* @__PURE__ */ new Set();
@@ -10574,7 +10619,7 @@ function extractIPSummary(content, options = {}) {
10574
10619
  const subnetNetworks = /* @__PURE__ */ new Set();
10575
10620
  const ipsWithMasks = /* @__PURE__ */ new Set();
10576
10621
  if (!options.skipSubnets) {
10577
- const ipv4CidrMatches = content.matchAll(IPV4_CIDR_PATTERN);
10622
+ const ipv4CidrMatches = content.matchAll(createIPv4CidrPattern());
10578
10623
  for (const match of ipv4CidrMatches) {
10579
10624
  const subnet = match[0];
10580
10625
  if (isValidSubnet(subnet)) {
@@ -10584,7 +10629,7 @@ function extractIPSummary(content, options = {}) {
10584
10629
  subnetNetworks.add(normalizedNetwork);
10585
10630
  }
10586
10631
  }
10587
- const ipMaskMatches = content.matchAll(IPV4_WITH_MASK_PATTERN);
10632
+ const ipMaskMatches = content.matchAll(createIPv4WithMaskPattern());
10588
10633
  for (const match of ipMaskMatches) {
10589
10634
  const ip = match[1];
10590
10635
  const mask = match[2];
@@ -10597,7 +10642,7 @@ function extractIPSummary(content, options = {}) {
10597
10642
  }
10598
10643
  }
10599
10644
  }
10600
- const ipMaskKeywordMatches = content.matchAll(IPV4_WITH_MASK_KEYWORD_PATTERN);
10645
+ const ipMaskKeywordMatches = content.matchAll(createIPv4WithMaskKeywordPattern());
10601
10646
  for (const match of ipMaskKeywordMatches) {
10602
10647
  const ip = match[1];
10603
10648
  const mask = match[2];
@@ -10610,7 +10655,7 @@ function extractIPSummary(content, options = {}) {
10610
10655
  }
10611
10656
  }
10612
10657
  }
10613
- const ipWildcardMatches = content.matchAll(IPV4_WITH_WILDCARD_PATTERN);
10658
+ const ipWildcardMatches = content.matchAll(createIPv4WithWildcardPattern());
10614
10659
  for (const match of ipWildcardMatches) {
10615
10660
  const ip = match[1];
10616
10661
  const wildcard = match[2];
@@ -10624,7 +10669,7 @@ function extractIPSummary(content, options = {}) {
10624
10669
  }
10625
10670
  }
10626
10671
  }
10627
- const ipv4Matches = content.matchAll(IPV4_PATTERN);
10672
+ const ipv4Matches = content.matchAll(createIPv4Pattern());
10628
10673
  for (const match of ipv4Matches) {
10629
10674
  const ip = match[0];
10630
10675
  if (isValidIPv4(ip)) {
@@ -10636,7 +10681,7 @@ function extractIPSummary(content, options = {}) {
10636
10681
  }
10637
10682
  if (!options.skipIPv6) {
10638
10683
  if (!options.skipSubnets) {
10639
- const ipv6CidrMatches = content.matchAll(IPV6_CIDR_PATTERN);
10684
+ const ipv6CidrMatches = content.matchAll(createIPv6CidrPattern());
10640
10685
  for (const match of ipv6CidrMatches) {
10641
10686
  const subnet = match[0];
10642
10687
  if (isValidSubnet(subnet)) {
@@ -10647,7 +10692,7 @@ function extractIPSummary(content, options = {}) {
10647
10692
  }
10648
10693
  }
10649
10694
  }
10650
- const ipv6Matches = content.matchAll(IPV6_PATTERN);
10695
+ const ipv6Matches = content.matchAll(createIPv6Pattern());
10651
10696
  for (const match of ipv6Matches) {
10652
10697
  const ip = match[0];
10653
10698
  if (isValidIPv6(ip)) {
@@ -10658,6 +10703,16 @@ function extractIPSummary(content, options = {}) {
10658
10703
  }
10659
10704
  }
10660
10705
  }
10706
+ if (options.includeSubnetNetworks) {
10707
+ for (const subnet of ipv4SubnetSet) {
10708
+ const { network } = parseSubnet(subnet);
10709
+ ipv4Set.add(network);
10710
+ }
10711
+ for (const subnet of ipv6SubnetSet) {
10712
+ const { network } = parseSubnet(subnet);
10713
+ ipv6Set.add(network);
10714
+ }
10715
+ }
10661
10716
  const ipv4Addresses = sortIPv4Addresses([...ipv4Set]);
10662
10717
  const ipv6Addresses = sortIPv6Addresses([...ipv6Set]);
10663
10718
  const ipv4Subnets = sortSubnets([...ipv4SubnetSet], "ipv4");
@@ -10729,8 +10784,12 @@ function generateSarif(results, filePath, rules, options = {}, ipSummary) {
10729
10784
  }
10730
10785
  const hasCvss = secMeta?.cvssScore !== void 0 || secMeta?.cvssVector;
10731
10786
  const hasTags = rule.metadata.tags && rule.metadata.tags.length > 0;
10732
- if (hasCvss || hasTags) {
10787
+ const hasCategory = rule.category !== void 0;
10788
+ if (hasCvss || hasTags || hasCategory) {
10733
10789
  base.properties = {};
10790
+ if (hasCategory) {
10791
+ base.properties.category = rule.category;
10792
+ }
10734
10793
  if (secMeta?.cvssScore !== void 0) {
10735
10794
  base.properties["security-severity"] = String(secMeta.cvssScore);
10736
10795
  }
@@ -10754,7 +10813,7 @@ function generateSarif(results, filePath, rules, options = {}, ipSummary) {
10754
10813
  tool: {
10755
10814
  driver: {
10756
10815
  name: "Sentriflow",
10757
- version: "0.1.9",
10816
+ version: "0.2.0",
10758
10817
  informationUri: "https://github.com/sentriflow/sentriflow",
10759
10818
  rules: sarifRules,
10760
10819
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -10878,8 +10937,12 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10878
10937
  }
10879
10938
  const hasCvss = secMeta?.cvssScore !== void 0 || secMeta?.cvssVector;
10880
10939
  const hasTags = rule.metadata.tags && rule.metadata.tags.length > 0;
10881
- if (hasCvss || hasTags) {
10940
+ const hasCategory = rule.category !== void 0;
10941
+ if (hasCvss || hasTags || hasCategory) {
10882
10942
  base.properties = {};
10943
+ if (hasCategory) {
10944
+ base.properties.category = rule.category;
10945
+ }
10883
10946
  if (secMeta?.cvssScore !== void 0) {
10884
10947
  base.properties["security-severity"] = String(secMeta.cvssScore);
10885
10948
  }
@@ -10910,7 +10973,7 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10910
10973
  tool: {
10911
10974
  driver: {
10912
10975
  name: "Sentriflow",
10913
- version: "0.1.9",
10976
+ version: "0.2.0",
10914
10977
  informationUri: "https://github.com/sentriflow/sentriflow",
10915
10978
  rules: sarifRules,
10916
10979
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -14872,8 +14935,19 @@ function isStdinRequested(files) {
14872
14935
  }
14873
14936
 
14874
14937
  // index.ts
14938
+ function enrichResultsWithRuleMetadata(results, rules) {
14939
+ const ruleMap = new Map(rules.map((r) => [r.id, r]));
14940
+ return results.map((result) => {
14941
+ const rule = ruleMap.get(result.ruleId);
14942
+ return {
14943
+ ...result,
14944
+ category: rule?.category,
14945
+ tags: rule?.metadata.tags ?? []
14946
+ };
14947
+ });
14948
+ }
14875
14949
  var program = new Command();
14876
- program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.1.9").argument("[files...]", "Path(s) to configuration file(s) (supports multiple files)").option("--ast", "Output the AST instead of rule results").option("-f, --format <format>", "Output format (json, sarif)", "json").option("-q, --quiet", "Only output failures (suppress passed results)").option("-c, --config <path>", "Path to config file (default: auto-detect)").option("--no-config", "Ignore config file").option("-r, --rules <path>", "Additional rules file to load (legacy)").option("-p, --rule-pack <path>", "Rule pack file to load").option(
14950
+ program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.2.0").argument("[files...]", "Path(s) to configuration file(s) (supports multiple files)").option("--ast", "Output the AST instead of rule results").option("-f, --format <format>", "Output format (json, sarif)", "json").option("-q, --quiet", "Only output failures (suppress passed results)").option("-c, --config <path>", "Path to config file (default: auto-detect)").option("--no-config", "Ignore config file").option("-r, --rules <path>", "Additional rules file to load (legacy)").option("-p, --rule-pack <path>", "Rule pack file to load").option(
14877
14951
  "--encrypted-pack <path...>",
14878
14952
  "SEC-012: Path(s) to encrypted rule pack(s) (.grpx), can specify multiple"
14879
14953
  ).option(
@@ -14889,7 +14963,14 @@ program.name("sentriflow").description("SentriFlow Network Configuration Validat
14889
14963
  "-d, --disable <ids>",
14890
14964
  "Comma-separated rule IDs to disable",
14891
14965
  (val) => val.split(",")
14892
- ).option("--list-rules", "List all active rules and exit").option("--relative-paths", "Use relative paths in SARIF output").option(
14966
+ ).option("--list-rules", "List all active rules and exit").option("--list-categories", "List all rule categories with counts and exit").option(
14967
+ "--category <category>",
14968
+ "Filter rules by category (use with --list-rules)"
14969
+ ).option(
14970
+ "--list-format <format>",
14971
+ "Output format for --list-rules: table, json, csv (default: table)",
14972
+ "table"
14973
+ ).option("--relative-paths", "Use relative paths in SARIF output").option(
14893
14974
  "--allow-external",
14894
14975
  "Allow reading files outside the current directory (use with caution)"
14895
14976
  ).option(
@@ -14983,17 +15064,73 @@ Use: sentriflow --vendor <vendor> <file>`);
14983
15064
  allowedBaseDirs
14984
15065
  // SEC-011: Pass allowed base dirs for rule file validation
14985
15066
  });
14986
- if (options.listRules) {
14987
- console.log("Active rules:\n");
15067
+ if (options.listCategories) {
15068
+ const counts = /* @__PURE__ */ new Map();
14988
15069
  for (const rule of rules) {
15070
+ const cats = Array.isArray(rule.category) ? rule.category : [rule.category ?? "uncategorized"];
15071
+ for (const cat of cats) {
15072
+ counts.set(cat, (counts.get(cat) ?? 0) + 1);
15073
+ }
15074
+ }
15075
+ const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]);
15076
+ console.log("CATEGORY COUNT");
15077
+ console.log("\u2500".repeat(35));
15078
+ for (const [cat, count] of sorted) {
15079
+ console.log(`${cat.padEnd(22)}${count}`);
15080
+ }
15081
+ console.log("\u2500".repeat(35));
15082
+ console.log(`TOTAL ${rules.length}`);
15083
+ return;
15084
+ }
15085
+ if (options.listRules) {
15086
+ let filteredRules = rules;
15087
+ if (options.category) {
15088
+ filteredRules = rules.filter((r) => {
15089
+ const cats = Array.isArray(r.category) ? r.category : [r.category];
15090
+ return cats.includes(options.category);
15091
+ });
15092
+ }
15093
+ if (options.listFormat === "json") {
15094
+ const output = filteredRules.map((r) => ({
15095
+ id: r.id,
15096
+ category: r.category,
15097
+ vendor: r.vendor,
15098
+ level: r.metadata.level,
15099
+ obu: r.metadata.obu,
15100
+ description: r.metadata.description,
15101
+ tags: r.metadata.tags
15102
+ }));
15103
+ console.log(JSON.stringify(output, null, 2));
15104
+ } else if (options.listFormat === "csv") {
15105
+ console.log("id,category,vendor,level,obu,description");
15106
+ for (const rule of filteredRules) {
15107
+ const cat = Array.isArray(rule.category) ? rule.category.join(";") : rule.category ?? "";
15108
+ const vendor2 = Array.isArray(rule.vendor) ? rule.vendor.join(";") : rule.vendor ?? "common";
15109
+ const desc = (rule.metadata.description ?? "").replace(/"/g, '""');
15110
+ console.log(
15111
+ `"${rule.id}","${cat}","${vendor2}","${rule.metadata.level}","${rule.metadata.obu}","${desc}"`
15112
+ );
15113
+ }
15114
+ } else {
14989
15115
  console.log(
14990
- ` ${rule.id} [${rule.metadata.level}] - ${rule.metadata.obu}`
15116
+ "ID CATEGORY VENDOR LEVEL OBU"
14991
15117
  );
15118
+ console.log("\u2500".repeat(85));
15119
+ for (const rule of filteredRules) {
15120
+ const cat = Array.isArray(rule.category) ? rule.category[0] ?? "general" : rule.category ?? "general";
15121
+ const vendor2 = Array.isArray(rule.vendor) ? rule.vendor[0] ?? "common" : rule.vendor ?? "common";
15122
+ console.log(
15123
+ `${rule.id.padEnd(18)}${cat.padEnd(22)}${vendor2.padEnd(16)}${rule.metadata.level.padEnd(9)}${rule.metadata.obu}`
15124
+ );
15125
+ }
15126
+ console.log("\u2500".repeat(85));
15127
+ console.log(`Total: ${filteredRules.length} rules`);
15128
+ if (options.category) {
15129
+ console.log(`Filtered by category: ${options.category}`);
15130
+ }
14992
15131
  }
14993
- console.log(`
14994
- Total: ${rules.length} rules`);
14995
15132
  const configFile = findConfigFile(configSearchDir);
14996
- if (configFile) {
15133
+ if (configFile && options.listFormat === "table") {
14997
15134
  console.log(`
14998
15135
  Config file: ${configFile}`);
14999
15136
  }
@@ -15166,7 +15303,7 @@ Parsing complete: ${allAsts.length} files`);
15166
15303
  const passed = results2.filter((r) => r.passed).length;
15167
15304
  totalFailures += failures;
15168
15305
  totalPassed += passed;
15169
- const fileIpSummary = extractIPSummary(content2);
15306
+ const fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
15170
15307
  allFileResults.push({
15171
15308
  filePath: filePath2,
15172
15309
  results: results2,
@@ -15203,7 +15340,7 @@ Parsing complete: ${allAsts.length} files`);
15203
15340
  files: allFileResults.map((fr) => ({
15204
15341
  file: fr.filePath,
15205
15342
  vendor: fr.vendor,
15206
- results: fr.results,
15343
+ results: enrichResultsWithRuleMetadata(fr.results, rules),
15207
15344
  ipSummary: fr.ipSummary
15208
15345
  }))
15209
15346
  };
@@ -15280,7 +15417,16 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15280
15417
  if (options.quiet) {
15281
15418
  results2 = results2.filter((r) => !r.passed);
15282
15419
  }
15283
- const stdinIpSummary = extractIPSummary(content2);
15420
+ let stdinIpSummary;
15421
+ try {
15422
+ stdinIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
15423
+ } catch (error) {
15424
+ if (error instanceof InputValidationError) {
15425
+ console.error(`Input validation error: ${error.message}`);
15426
+ process.exit(2);
15427
+ }
15428
+ throw error;
15429
+ }
15284
15430
  if (options.format === "sarif") {
15285
15431
  const sarifOptions = {
15286
15432
  relativePaths: options.relativePaths,
@@ -15291,7 +15437,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15291
15437
  const output = {
15292
15438
  file: "<stdin>",
15293
15439
  vendor: { id: vendor2.id, name: vendor2.name },
15294
- results: results2,
15440
+ results: enrichResultsWithRuleMetadata(results2, stdinRules),
15295
15441
  ipSummary: stdinIpSummary
15296
15442
  };
15297
15443
  console.log(JSON.stringify(output, null, 2));
@@ -15351,7 +15497,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15351
15497
  const passed = results2.filter((r) => r.passed).length;
15352
15498
  totalFailures += failures;
15353
15499
  totalPassed += passed;
15354
- const fileIpSummary = extractIPSummary(content2);
15500
+ const fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
15355
15501
  allFileResults.push({
15356
15502
  filePath: filePath2,
15357
15503
  results: results2,
@@ -15383,7 +15529,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15383
15529
  files: allFileResults.map((fr) => ({
15384
15530
  file: fr.filePath,
15385
15531
  vendor: fr.vendor,
15386
- results: fr.results,
15532
+ results: enrichResultsWithRuleMetadata(fr.results, rules),
15387
15533
  ipSummary: fr.ipSummary
15388
15534
  }))
15389
15535
  };
@@ -15469,7 +15615,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15469
15615
  if (options.quiet) {
15470
15616
  results = results.filter((r) => !r.passed);
15471
15617
  }
15472
- const ipSummary = extractIPSummary(content);
15618
+ const ipSummary = extractIPSummary(content, { includeSubnetNetworks: true });
15473
15619
  if (options.format === "sarif") {
15474
15620
  const sarifOptions = {
15475
15621
  relativePaths: options.relativePaths,
@@ -15484,7 +15630,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15484
15630
  id: vendor.id,
15485
15631
  name: vendor.name
15486
15632
  },
15487
- results,
15633
+ results: enrichResultsWithRuleMetadata(results, singleFileRules),
15488
15634
  ipSummary
15489
15635
  };
15490
15636
  console.log(JSON.stringify(output, null, 2));
@@ -15496,6 +15642,8 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15496
15642
  } catch (error) {
15497
15643
  if (error instanceof SentriflowError) {
15498
15644
  console.error(`Error: ${error.toUserMessage()}`);
15645
+ } else if (error instanceof InputValidationError) {
15646
+ console.error(`Input validation error: ${error.message}`);
15499
15647
  } else {
15500
15648
  console.error("Error: An unexpected error occurred");
15501
15649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/cli",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "SentriFlow CLI - Network configuration linter and validator",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",