@sentriflow/cli 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.
Files changed (3) hide show
  1. package/README.md +57 -2
  2. package/dist/index.js +236 -72
  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");
@@ -10727,16 +10782,22 @@ function generateSarif(results, filePath, rules, options = {}, ipSummary) {
10727
10782
  kinds: ["superset"]
10728
10783
  }));
10729
10784
  }
10730
- if (secMeta?.cvssScore !== void 0 || secMeta?.cvssVector || secMeta?.tags) {
10785
+ const hasCvss = secMeta?.cvssScore !== void 0 || secMeta?.cvssVector;
10786
+ const hasTags = rule.metadata.tags && rule.metadata.tags.length > 0;
10787
+ const hasCategory = rule.category !== void 0;
10788
+ if (hasCvss || hasTags || hasCategory) {
10731
10789
  base.properties = {};
10732
- if (secMeta.cvssScore !== void 0) {
10790
+ if (hasCategory) {
10791
+ base.properties.category = rule.category;
10792
+ }
10793
+ if (secMeta?.cvssScore !== void 0) {
10733
10794
  base.properties["security-severity"] = String(secMeta.cvssScore);
10734
10795
  }
10735
- if (secMeta.cvssVector) {
10796
+ if (secMeta?.cvssVector) {
10736
10797
  base.properties["cvss-vector"] = secMeta.cvssVector;
10737
10798
  }
10738
- if (secMeta.tags && secMeta.tags.length > 0) {
10739
- base.properties.tags = secMeta.tags;
10799
+ if (hasTags) {
10800
+ base.properties.tags = rule.metadata.tags.map((t) => t.label);
10740
10801
  }
10741
10802
  }
10742
10803
  return base;
@@ -10752,7 +10813,7 @@ function generateSarif(results, filePath, rules, options = {}, ipSummary) {
10752
10813
  tool: {
10753
10814
  driver: {
10754
10815
  name: "Sentriflow",
10755
- version: "0.1.8",
10816
+ version: "0.2.0",
10756
10817
  informationUri: "https://github.com/sentriflow/sentriflow",
10757
10818
  rules: sarifRules,
10758
10819
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -10874,16 +10935,22 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10874
10935
  kinds: ["superset"]
10875
10936
  }));
10876
10937
  }
10877
- if (secMeta?.cvssScore !== void 0 || secMeta?.cvssVector || secMeta?.tags) {
10938
+ const hasCvss = secMeta?.cvssScore !== void 0 || secMeta?.cvssVector;
10939
+ const hasTags = rule.metadata.tags && rule.metadata.tags.length > 0;
10940
+ const hasCategory = rule.category !== void 0;
10941
+ if (hasCvss || hasTags || hasCategory) {
10878
10942
  base.properties = {};
10879
- if (secMeta.cvssScore !== void 0) {
10943
+ if (hasCategory) {
10944
+ base.properties.category = rule.category;
10945
+ }
10946
+ if (secMeta?.cvssScore !== void 0) {
10880
10947
  base.properties["security-severity"] = String(secMeta.cvssScore);
10881
10948
  }
10882
- if (secMeta.cvssVector) {
10949
+ if (secMeta?.cvssVector) {
10883
10950
  base.properties["cvss-vector"] = secMeta.cvssVector;
10884
10951
  }
10885
- if (secMeta.tags && secMeta.tags.length > 0) {
10886
- base.properties.tags = secMeta.tags;
10952
+ if (hasTags) {
10953
+ base.properties.tags = rule.metadata.tags.map((t) => t.label);
10887
10954
  }
10888
10955
  }
10889
10956
  return base;
@@ -10906,7 +10973,7 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10906
10973
  tool: {
10907
10974
  driver: {
10908
10975
  name: "Sentriflow",
10909
- version: "0.1.8",
10976
+ version: "0.2.0",
10910
10977
  informationUri: "https://github.com/sentriflow/sentriflow",
10911
10978
  rules: sarifRules,
10912
10979
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -13484,9 +13551,12 @@ var cisco_json_rules_default = {
13484
13551
  description: "Trunk ports should disable DTP (Dynamic Trunking Protocol)",
13485
13552
  remediation: "Add 'switchport nonegotiate' to disable DTP on trunk ports",
13486
13553
  security: {
13487
- cwe: ["CWE-319"],
13488
- tags: ["vlan-hopping", "network-security"]
13489
- }
13554
+ cwe: ["CWE-319"]
13555
+ },
13556
+ tags: [
13557
+ { type: "security", label: "vlan-hopping" },
13558
+ { type: "security", label: "network-security" }
13559
+ ]
13490
13560
  },
13491
13561
  check: {
13492
13562
  type: "and",
@@ -13527,9 +13597,12 @@ var cisco_json_rules_default = {
13527
13597
  description: "VTY lines should have access-class configured for SSH access control",
13528
13598
  remediation: "Add 'access-class <acl> in' to restrict VTY access",
13529
13599
  security: {
13530
- cwe: ["CWE-284"],
13531
- tags: ["access-control", "remote-access"]
13532
- }
13600
+ cwe: ["CWE-284"]
13601
+ },
13602
+ tags: [
13603
+ { type: "security", label: "access-control" },
13604
+ { type: "security", label: "remote-access" }
13605
+ ]
13533
13606
  },
13534
13607
  check: {
13535
13608
  type: "child_not_exists",
@@ -13611,9 +13684,12 @@ var juniper_json_rules_default = {
13611
13684
  description: "SSH should be configured for version 2 only",
13612
13685
  remediation: "Configure 'set system services ssh protocol-version v2'",
13613
13686
  security: {
13614
- cwe: ["CWE-327"],
13615
- tags: ["ssh", "encryption"]
13616
- }
13687
+ cwe: ["CWE-327"]
13688
+ },
13689
+ tags: [
13690
+ { type: "security", label: "ssh" },
13691
+ { type: "security", label: "encryption" }
13692
+ ]
13617
13693
  },
13618
13694
  check: {
13619
13695
  type: "and",
@@ -13644,9 +13720,12 @@ var juniper_json_rules_default = {
13644
13720
  description: "Telnet service should be disabled",
13645
13721
  remediation: "Remove 'set system services telnet' or add 'delete system services telnet'",
13646
13722
  security: {
13647
- cwe: ["CWE-319"],
13648
- tags: ["telnet", "cleartext"]
13649
- }
13723
+ cwe: ["CWE-319"]
13724
+ },
13725
+ tags: [
13726
+ { type: "security", label: "telnet" },
13727
+ { type: "security", label: "cleartext" }
13728
+ ]
13650
13729
  },
13651
13730
  check: {
13652
13731
  type: "helper",
@@ -14856,8 +14935,19 @@ function isStdinRequested(files) {
14856
14935
  }
14857
14936
 
14858
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
+ }
14859
14949
  var program = new Command();
14860
- program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.1.8").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(
14861
14951
  "--encrypted-pack <path...>",
14862
14952
  "SEC-012: Path(s) to encrypted rule pack(s) (.grpx), can specify multiple"
14863
14953
  ).option(
@@ -14873,7 +14963,14 @@ program.name("sentriflow").description("SentriFlow Network Configuration Validat
14873
14963
  "-d, --disable <ids>",
14874
14964
  "Comma-separated rule IDs to disable",
14875
14965
  (val) => val.split(",")
14876
- ).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(
14877
14974
  "--allow-external",
14878
14975
  "Allow reading files outside the current directory (use with caution)"
14879
14976
  ).option(
@@ -14967,17 +15064,73 @@ Use: sentriflow --vendor <vendor> <file>`);
14967
15064
  allowedBaseDirs
14968
15065
  // SEC-011: Pass allowed base dirs for rule file validation
14969
15066
  });
14970
- if (options.listRules) {
14971
- console.log("Active rules:\n");
15067
+ if (options.listCategories) {
15068
+ const counts = /* @__PURE__ */ new Map();
14972
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 {
14973
15115
  console.log(
14974
- ` ${rule.id} [${rule.metadata.level}] - ${rule.metadata.obu}`
15116
+ "ID CATEGORY VENDOR LEVEL OBU"
14975
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
+ }
14976
15131
  }
14977
- console.log(`
14978
- Total: ${rules.length} rules`);
14979
15132
  const configFile = findConfigFile(configSearchDir);
14980
- if (configFile) {
15133
+ if (configFile && options.listFormat === "table") {
14981
15134
  console.log(`
14982
15135
  Config file: ${configFile}`);
14983
15136
  }
@@ -15150,7 +15303,7 @@ Parsing complete: ${allAsts.length} files`);
15150
15303
  const passed = results2.filter((r) => r.passed).length;
15151
15304
  totalFailures += failures;
15152
15305
  totalPassed += passed;
15153
- const fileIpSummary = extractIPSummary(content2);
15306
+ const fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
15154
15307
  allFileResults.push({
15155
15308
  filePath: filePath2,
15156
15309
  results: results2,
@@ -15187,7 +15340,7 @@ Parsing complete: ${allAsts.length} files`);
15187
15340
  files: allFileResults.map((fr) => ({
15188
15341
  file: fr.filePath,
15189
15342
  vendor: fr.vendor,
15190
- results: fr.results,
15343
+ results: enrichResultsWithRuleMetadata(fr.results, rules),
15191
15344
  ipSummary: fr.ipSummary
15192
15345
  }))
15193
15346
  };
@@ -15264,7 +15417,16 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15264
15417
  if (options.quiet) {
15265
15418
  results2 = results2.filter((r) => !r.passed);
15266
15419
  }
15267
- 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
+ }
15268
15430
  if (options.format === "sarif") {
15269
15431
  const sarifOptions = {
15270
15432
  relativePaths: options.relativePaths,
@@ -15275,7 +15437,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15275
15437
  const output = {
15276
15438
  file: "<stdin>",
15277
15439
  vendor: { id: vendor2.id, name: vendor2.name },
15278
- results: results2,
15440
+ results: enrichResultsWithRuleMetadata(results2, stdinRules),
15279
15441
  ipSummary: stdinIpSummary
15280
15442
  };
15281
15443
  console.log(JSON.stringify(output, null, 2));
@@ -15335,7 +15497,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15335
15497
  const passed = results2.filter((r) => r.passed).length;
15336
15498
  totalFailures += failures;
15337
15499
  totalPassed += passed;
15338
- const fileIpSummary = extractIPSummary(content2);
15500
+ const fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
15339
15501
  allFileResults.push({
15340
15502
  filePath: filePath2,
15341
15503
  results: results2,
@@ -15367,7 +15529,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15367
15529
  files: allFileResults.map((fr) => ({
15368
15530
  file: fr.filePath,
15369
15531
  vendor: fr.vendor,
15370
- results: fr.results,
15532
+ results: enrichResultsWithRuleMetadata(fr.results, rules),
15371
15533
  ipSummary: fr.ipSummary
15372
15534
  }))
15373
15535
  };
@@ -15453,7 +15615,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15453
15615
  if (options.quiet) {
15454
15616
  results = results.filter((r) => !r.passed);
15455
15617
  }
15456
- const ipSummary = extractIPSummary(content);
15618
+ const ipSummary = extractIPSummary(content, { includeSubnetNetworks: true });
15457
15619
  if (options.format === "sarif") {
15458
15620
  const sarifOptions = {
15459
15621
  relativePaths: options.relativePaths,
@@ -15468,7 +15630,7 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15468
15630
  id: vendor.id,
15469
15631
  name: vendor.name
15470
15632
  },
15471
- results,
15633
+ results: enrichResultsWithRuleMetadata(results, singleFileRules),
15472
15634
  ipSummary
15473
15635
  };
15474
15636
  console.log(JSON.stringify(output, null, 2));
@@ -15480,6 +15642,8 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15480
15642
  } catch (error) {
15481
15643
  if (error instanceof SentriflowError) {
15482
15644
  console.error(`Error: ${error.toUserMessage()}`);
15645
+ } else if (error instanceof InputValidationError) {
15646
+ console.error(`Input validation error: ${error.message}`);
15483
15647
  } else {
15484
15648
  console.error("Error: An unexpected error occurred");
15485
15649
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/cli",
3
- "version": "0.1.8",
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",