@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.
- package/README.md +57 -2
- package/dist/index.js +200 -52
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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("--
|
|
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.
|
|
14987
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|