@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.
- package/README.md +57 -2
- package/dist/index.js +236 -72
- 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");
|
|
@@ -10727,16 +10782,22 @@ function generateSarif(results, filePath, rules, options = {}, ipSummary) {
|
|
|
10727
10782
|
kinds: ["superset"]
|
|
10728
10783
|
}));
|
|
10729
10784
|
}
|
|
10730
|
-
|
|
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 (
|
|
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
|
|
10796
|
+
if (secMeta?.cvssVector) {
|
|
10736
10797
|
base.properties["cvss-vector"] = secMeta.cvssVector;
|
|
10737
10798
|
}
|
|
10738
|
-
if (
|
|
10739
|
-
base.properties.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.
|
|
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
|
-
|
|
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 (
|
|
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
|
|
10949
|
+
if (secMeta?.cvssVector) {
|
|
10883
10950
|
base.properties["cvss-vector"] = secMeta.cvssVector;
|
|
10884
10951
|
}
|
|
10885
|
-
if (
|
|
10886
|
-
base.properties.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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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("--
|
|
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.
|
|
14971
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|