@sentriflow/cli 0.1.7 → 0.1.9

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 (2) hide show
  1. package/dist/index.js +460 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10334,6 +10334,350 @@ function validateRegex(pattern, flags, path, ctx) {
10334
10334
  }
10335
10335
  }
10336
10336
 
10337
+ // ../core/src/ip/extractor.ts
10338
+ function isValidIPv4(ip) {
10339
+ if (!ip || typeof ip !== "string") return false;
10340
+ const octets = ip.split(".");
10341
+ if (octets.length !== 4) return false;
10342
+ for (const octet of octets) {
10343
+ if (!/^\d+$/.test(octet)) return false;
10344
+ if (octet.length > 1 && octet.startsWith("0")) return false;
10345
+ const num = parseInt(octet, 10);
10346
+ if (isNaN(num) || num < 0 || num > 255) return false;
10347
+ }
10348
+ return true;
10349
+ }
10350
+ function isValidIPv6(ip) {
10351
+ 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
+ }
10357
+ if (!addr.includes(":")) return false;
10358
+ if (addr.includes(":::")) return false;
10359
+ const doubleColonCount = (addr.match(/::/g) || []).length;
10360
+ if (doubleColonCount > 1) return false;
10361
+ const parts = addr.split(":");
10362
+ if (doubleColonCount === 1) {
10363
+ const nonEmptyParts = parts.filter((p) => p !== "");
10364
+ for (const part of nonEmptyParts) {
10365
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part)) return false;
10366
+ }
10367
+ if (nonEmptyParts.length > 7) return false;
10368
+ } else {
10369
+ if (parts.length !== 8) return false;
10370
+ for (const part of parts) {
10371
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part)) return false;
10372
+ }
10373
+ }
10374
+ return true;
10375
+ }
10376
+ function isValidSubnet(subnet) {
10377
+ if (!subnet || typeof subnet !== "string") return false;
10378
+ const slashIndex = subnet.lastIndexOf("/");
10379
+ if (slashIndex === -1) return false;
10380
+ const ip = subnet.substring(0, slashIndex);
10381
+ const prefixStr = subnet.substring(slashIndex + 1);
10382
+ if (!/^\d+$/.test(prefixStr)) return false;
10383
+ const prefix = parseInt(prefixStr, 10);
10384
+ if (isValidIPv4(ip)) {
10385
+ return prefix >= 0 && prefix <= 32;
10386
+ } else if (isValidIPv6(ip)) {
10387
+ return prefix >= 0 && prefix <= 128;
10388
+ }
10389
+ return false;
10390
+ }
10391
+ function normalizeIPv4(ip) {
10392
+ return ip.split(".").map((octet) => parseInt(octet, 10).toString()).join(".");
10393
+ }
10394
+ 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();
10401
+ if (addr.includes("::")) {
10402
+ const sides = addr.split("::");
10403
+ const left = sides[0] ? sides[0].split(":").filter((p) => p !== "") : [];
10404
+ const right = sides[1] ? sides[1].split(":").filter((p) => p !== "") : [];
10405
+ const zerosNeeded = 8 - left.length - right.length;
10406
+ const expanded = [];
10407
+ for (const part of left) {
10408
+ expanded.push(parseInt(part, 16).toString(16));
10409
+ }
10410
+ for (let i = 0; i < zerosNeeded; i++) {
10411
+ expanded.push("0");
10412
+ }
10413
+ for (const part of right) {
10414
+ expanded.push(parseInt(part, 16).toString(16));
10415
+ }
10416
+ return expanded.join(":");
10417
+ }
10418
+ const parts = addr.split(":");
10419
+ const result = [];
10420
+ for (const part of parts) {
10421
+ if (part !== "") {
10422
+ result.push(parseInt(part, 16).toString(16));
10423
+ }
10424
+ }
10425
+ return result.join(":");
10426
+ }
10427
+ function ipv4ToNumber(ip) {
10428
+ const octets = ip.split(".").map(Number);
10429
+ const o0 = octets[0] ?? 0;
10430
+ const o1 = octets[1] ?? 0;
10431
+ const o2 = octets[2] ?? 0;
10432
+ const o3 = octets[3] ?? 0;
10433
+ return (o0 << 24 >>> 0) + (o1 << 16) + (o2 << 8) + o3;
10434
+ }
10435
+ function compareIPv4(a, b) {
10436
+ const numA = ipv4ToNumber(a);
10437
+ const numB = ipv4ToNumber(b);
10438
+ return numA < numB ? -1 : numA > numB ? 1 : 0;
10439
+ }
10440
+ function expandIPv6(ip) {
10441
+ let addr = ip;
10442
+ const zoneIndex = ip.indexOf("%");
10443
+ if (zoneIndex !== -1) {
10444
+ addr = ip.substring(0, zoneIndex);
10445
+ }
10446
+ const parts = addr.split(":");
10447
+ const result = [];
10448
+ for (let i = 0; i < parts.length; i++) {
10449
+ if (parts[i] === "" && i > 0 && i < parts.length - 1) {
10450
+ const nonEmpty = parts.filter((p) => p !== "").length;
10451
+ const zeros = 8 - nonEmpty;
10452
+ for (let j = 0; j < zeros; j++) {
10453
+ result.push("0");
10454
+ }
10455
+ } else if (parts[i] !== "") {
10456
+ result.push(parts[i] ?? "0");
10457
+ } else if (i === 0 && parts[1] === "") {
10458
+ const nonEmpty = parts.filter((p) => p !== "").length;
10459
+ const zeros = 8 - nonEmpty;
10460
+ for (let j = 0; j < zeros; j++) {
10461
+ result.push("0");
10462
+ }
10463
+ } else if (i === parts.length - 1 && parts[i - 1] === "") {
10464
+ }
10465
+ }
10466
+ while (result.length < 8) {
10467
+ result.push("0");
10468
+ }
10469
+ return result.slice(0, 8);
10470
+ }
10471
+ function ipv6ToBigInt(ip) {
10472
+ const parts = expandIPv6(ip);
10473
+ let result = 0n;
10474
+ for (const part of parts) {
10475
+ result = (result << 16n) + BigInt(parseInt(part, 16) || 0);
10476
+ }
10477
+ return result;
10478
+ }
10479
+ function compareIPv6(a, b) {
10480
+ const bigA = ipv6ToBigInt(a);
10481
+ const bigB = ipv6ToBigInt(b);
10482
+ return bigA < bigB ? -1 : bigA > bigB ? 1 : 0;
10483
+ }
10484
+ function sortIPv4Addresses(ips) {
10485
+ return [...ips].sort(compareIPv4);
10486
+ }
10487
+ function sortIPv6Addresses(ips) {
10488
+ return [...ips].sort(compareIPv6);
10489
+ }
10490
+ function parseSubnet(subnet) {
10491
+ const slashIndex = subnet.lastIndexOf("/");
10492
+ return {
10493
+ network: subnet.substring(0, slashIndex),
10494
+ prefix: parseInt(subnet.substring(slashIndex + 1), 10)
10495
+ };
10496
+ }
10497
+ function sortSubnets(subnets, type) {
10498
+ const compare = type === "ipv4" ? compareIPv4 : compareIPv6;
10499
+ return [...subnets].sort((a, b) => {
10500
+ const subA = parseSubnet(a);
10501
+ const subB = parseSubnet(b);
10502
+ const netCompare = compare(subA.network, subB.network);
10503
+ if (netCompare !== 0) return netCompare;
10504
+ return subA.prefix - subB.prefix;
10505
+ });
10506
+ }
10507
+ function isSubnetMask(ip) {
10508
+ if (!ip.startsWith("255.")) return false;
10509
+ const octets = ip.split(".").map(Number);
10510
+ const num = ((octets[0] ?? 0) << 24) + ((octets[1] ?? 0) << 16) + ((octets[2] ?? 0) << 8) + (octets[3] ?? 0);
10511
+ const inverted = ~num >>> 0;
10512
+ return inverted === 0 || (inverted & inverted + 1) === 0;
10513
+ }
10514
+ function isWildcardMask(ip) {
10515
+ if (!ip.startsWith("0.")) return false;
10516
+ const octets = ip.split(".").map(Number);
10517
+ const num = ((octets[0] ?? 0) << 24) + ((octets[1] ?? 0) << 16) + ((octets[2] ?? 0) << 8) + (octets[3] ?? 0);
10518
+ return num === 0 || (num + 1 & num) === 0;
10519
+ }
10520
+ function maskToCidr(mask) {
10521
+ if (!isSubnetMask(mask)) return -1;
10522
+ const octets = mask.split(".").map(Number);
10523
+ const num = ((octets[0] ?? 0) << 24) + ((octets[1] ?? 0) << 16) + ((octets[2] ?? 0) << 8) + (octets[3] ?? 0);
10524
+ let prefix = 0;
10525
+ let n = num >>> 0;
10526
+ while (n & 2147483648) {
10527
+ prefix++;
10528
+ n = n << 1 >>> 0;
10529
+ }
10530
+ return prefix;
10531
+ }
10532
+ function wildcardToCidr(wildcard) {
10533
+ if (!isWildcardMask(wildcard)) return -1;
10534
+ const octets = wildcard.split(".").map(Number);
10535
+ const num = ((octets[0] ?? 0) << 24) + ((octets[1] ?? 0) << 16) + ((octets[2] ?? 0) << 8) + (octets[3] ?? 0);
10536
+ let prefix = 0;
10537
+ let n = num >>> 0;
10538
+ while (prefix < 32 && !(n & 2147483648)) {
10539
+ prefix++;
10540
+ n = n << 1 >>> 0;
10541
+ }
10542
+ return prefix;
10543
+ }
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
+ function createEmptyIPSummary() {
10552
+ return {
10553
+ ipv4Addresses: [],
10554
+ ipv6Addresses: [],
10555
+ ipv4Subnets: [],
10556
+ ipv6Subnets: [],
10557
+ counts: {
10558
+ ipv4: 0,
10559
+ ipv6: 0,
10560
+ ipv4Subnets: 0,
10561
+ ipv6Subnets: 0,
10562
+ total: 0
10563
+ }
10564
+ };
10565
+ }
10566
+ function extractIPSummary(content, options = {}) {
10567
+ if (!content || typeof content !== "string") {
10568
+ return createEmptyIPSummary();
10569
+ }
10570
+ const ipv4Set = /* @__PURE__ */ new Set();
10571
+ const ipv6Set = /* @__PURE__ */ new Set();
10572
+ const ipv4SubnetSet = /* @__PURE__ */ new Set();
10573
+ const ipv6SubnetSet = /* @__PURE__ */ new Set();
10574
+ const subnetNetworks = /* @__PURE__ */ new Set();
10575
+ const ipsWithMasks = /* @__PURE__ */ new Set();
10576
+ if (!options.skipSubnets) {
10577
+ const ipv4CidrMatches = content.matchAll(IPV4_CIDR_PATTERN);
10578
+ for (const match of ipv4CidrMatches) {
10579
+ const subnet = match[0];
10580
+ if (isValidSubnet(subnet)) {
10581
+ const { network } = parseSubnet(subnet);
10582
+ const normalizedNetwork = normalizeIPv4(network);
10583
+ ipv4SubnetSet.add(`${normalizedNetwork}/${parseSubnet(subnet).prefix}`);
10584
+ subnetNetworks.add(normalizedNetwork);
10585
+ }
10586
+ }
10587
+ const ipMaskMatches = content.matchAll(IPV4_WITH_MASK_PATTERN);
10588
+ for (const match of ipMaskMatches) {
10589
+ const ip = match[1];
10590
+ const mask = match[2];
10591
+ if (ip && mask && isValidIPv4(ip) && isSubnetMask(mask)) {
10592
+ const normalizedIP = normalizeIPv4(ip);
10593
+ const prefix = maskToCidr(mask);
10594
+ if (prefix >= 0) {
10595
+ ipv4SubnetSet.add(`${normalizedIP}/${prefix}`);
10596
+ ipsWithMasks.add(normalizedIP);
10597
+ }
10598
+ }
10599
+ }
10600
+ const ipMaskKeywordMatches = content.matchAll(IPV4_WITH_MASK_KEYWORD_PATTERN);
10601
+ for (const match of ipMaskKeywordMatches) {
10602
+ const ip = match[1];
10603
+ const mask = match[2];
10604
+ if (ip && mask && isValidIPv4(ip) && isSubnetMask(mask)) {
10605
+ const normalizedIP = normalizeIPv4(ip);
10606
+ const prefix = maskToCidr(mask);
10607
+ if (prefix >= 0) {
10608
+ ipv4SubnetSet.add(`${normalizedIP}/${prefix}`);
10609
+ ipsWithMasks.add(normalizedIP);
10610
+ }
10611
+ }
10612
+ }
10613
+ const ipWildcardMatches = content.matchAll(IPV4_WITH_WILDCARD_PATTERN);
10614
+ for (const match of ipWildcardMatches) {
10615
+ const ip = match[1];
10616
+ const wildcard = match[2];
10617
+ if (ip && wildcard && isValidIPv4(ip) && isWildcardMask(wildcard)) {
10618
+ const normalizedIP = normalizeIPv4(ip);
10619
+ const prefix = wildcardToCidr(wildcard);
10620
+ if (prefix >= 0) {
10621
+ ipv4SubnetSet.add(`${normalizedIP}/${prefix}`);
10622
+ ipsWithMasks.add(normalizedIP);
10623
+ }
10624
+ }
10625
+ }
10626
+ }
10627
+ const ipv4Matches = content.matchAll(IPV4_PATTERN);
10628
+ for (const match of ipv4Matches) {
10629
+ const ip = match[0];
10630
+ if (isValidIPv4(ip)) {
10631
+ const normalized = normalizeIPv4(ip);
10632
+ if (!subnetNetworks.has(normalized) && !ipsWithMasks.has(normalized) && !isSubnetMask(normalized) && !isWildcardMask(normalized)) {
10633
+ ipv4Set.add(normalized);
10634
+ }
10635
+ }
10636
+ }
10637
+ if (!options.skipIPv6) {
10638
+ if (!options.skipSubnets) {
10639
+ const ipv6CidrMatches = content.matchAll(IPV6_CIDR_PATTERN);
10640
+ for (const match of ipv6CidrMatches) {
10641
+ const subnet = match[0];
10642
+ if (isValidSubnet(subnet)) {
10643
+ const { network, prefix } = parseSubnet(subnet);
10644
+ const normalizedNetwork = normalizeIPv6(network);
10645
+ ipv6SubnetSet.add(`${normalizedNetwork}/${prefix}`);
10646
+ subnetNetworks.add(normalizedNetwork);
10647
+ }
10648
+ }
10649
+ }
10650
+ const ipv6Matches = content.matchAll(IPV6_PATTERN);
10651
+ for (const match of ipv6Matches) {
10652
+ const ip = match[0];
10653
+ if (isValidIPv6(ip)) {
10654
+ const normalized = normalizeIPv6(ip);
10655
+ if (!subnetNetworks.has(normalized)) {
10656
+ ipv6Set.add(normalized);
10657
+ }
10658
+ }
10659
+ }
10660
+ }
10661
+ const ipv4Addresses = sortIPv4Addresses([...ipv4Set]);
10662
+ const ipv6Addresses = sortIPv6Addresses([...ipv6Set]);
10663
+ const ipv4Subnets = sortSubnets([...ipv4SubnetSet], "ipv4");
10664
+ const ipv6Subnets = sortSubnets([...ipv6SubnetSet], "ipv6");
10665
+ const counts = {
10666
+ ipv4: ipv4Addresses.length,
10667
+ ipv6: ipv6Addresses.length,
10668
+ ipv4Subnets: ipv4Subnets.length,
10669
+ ipv6Subnets: ipv6Subnets.length,
10670
+ total: ipv4Addresses.length + ipv6Addresses.length + ipv4Subnets.length + ipv6Subnets.length
10671
+ };
10672
+ return {
10673
+ ipv4Addresses,
10674
+ ipv6Addresses,
10675
+ ipv4Subnets,
10676
+ ipv6Subnets,
10677
+ counts
10678
+ };
10679
+ }
10680
+
10337
10681
  // index.ts
10338
10682
  import { readFile } from "fs/promises";
10339
10683
  import { statSync as statSync2 } from "fs";
@@ -10341,7 +10685,7 @@ import { resolve as resolve4, dirname as dirname2, basename } from "path";
10341
10685
 
10342
10686
  // src/sarif.ts
10343
10687
  import { relative } from "path";
10344
- function generateSarif(results, filePath, rules, options = {}) {
10688
+ function generateSarif(results, filePath, rules, options = {}, ipSummary) {
10345
10689
  const fileUri = options.relativePaths ? relative(options.baseDir ?? process.cwd(), filePath) : filePath;
10346
10690
  const sarifResults = results.map((result) => {
10347
10691
  return {
@@ -10383,16 +10727,18 @@ function generateSarif(results, filePath, rules, options = {}) {
10383
10727
  kinds: ["superset"]
10384
10728
  }));
10385
10729
  }
10386
- if (secMeta?.cvssScore !== void 0 || secMeta?.cvssVector || secMeta?.tags) {
10730
+ const hasCvss = secMeta?.cvssScore !== void 0 || secMeta?.cvssVector;
10731
+ const hasTags = rule.metadata.tags && rule.metadata.tags.length > 0;
10732
+ if (hasCvss || hasTags) {
10387
10733
  base.properties = {};
10388
- if (secMeta.cvssScore !== void 0) {
10734
+ if (secMeta?.cvssScore !== void 0) {
10389
10735
  base.properties["security-severity"] = String(secMeta.cvssScore);
10390
10736
  }
10391
- if (secMeta.cvssVector) {
10737
+ if (secMeta?.cvssVector) {
10392
10738
  base.properties["cvss-vector"] = secMeta.cvssVector;
10393
10739
  }
10394
- if (secMeta.tags && secMeta.tags.length > 0) {
10395
- base.properties.tags = secMeta.tags;
10740
+ if (hasTags) {
10741
+ base.properties.tags = rule.metadata.tags.map((t) => t.label);
10396
10742
  }
10397
10743
  }
10398
10744
  return base;
@@ -10408,7 +10754,7 @@ function generateSarif(results, filePath, rules, options = {}) {
10408
10754
  tool: {
10409
10755
  driver: {
10410
10756
  name: "Sentriflow",
10411
- version: "0.1.7",
10757
+ version: "0.1.9",
10412
10758
  informationUri: "https://github.com/sentriflow/sentriflow",
10413
10759
  rules: sarifRules,
10414
10760
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -10435,13 +10781,61 @@ function generateSarif(results, filePath, rules, options = {}) {
10435
10781
  }
10436
10782
  ]
10437
10783
  },
10438
- results: sarifResults
10784
+ results: sarifResults,
10785
+ // Include IP summary in properties if available
10786
+ ...ipSummary && {
10787
+ properties: {
10788
+ ipSummary
10789
+ }
10790
+ }
10439
10791
  }
10440
10792
  ]
10441
10793
  };
10442
10794
  return JSON.stringify(report, null, 2);
10443
10795
  }
10796
+ function aggregateIPSummaries(summaries) {
10797
+ if (summaries.length === 0) return void 0;
10798
+ const ipv4Set = /* @__PURE__ */ new Set();
10799
+ const ipv6Set = /* @__PURE__ */ new Set();
10800
+ const ipv4SubnetSet = /* @__PURE__ */ new Set();
10801
+ const ipv6SubnetSet = /* @__PURE__ */ new Set();
10802
+ for (const summary of summaries) {
10803
+ for (const ip of summary.ipv4Addresses) ipv4Set.add(ip);
10804
+ for (const ip of summary.ipv6Addresses) ipv6Set.add(ip);
10805
+ for (const subnet of summary.ipv4Subnets) ipv4SubnetSet.add(subnet);
10806
+ for (const subnet of summary.ipv6Subnets) ipv6SubnetSet.add(subnet);
10807
+ }
10808
+ const ipv4Addresses = [...ipv4Set].sort((a, b) => {
10809
+ const aParts = a.split(".").map(Number);
10810
+ const bParts = b.split(".").map(Number);
10811
+ for (let i = 0; i < 4; i++) {
10812
+ if ((aParts[i] ?? 0) !== (bParts[i] ?? 0)) {
10813
+ return (aParts[i] ?? 0) - (bParts[i] ?? 0);
10814
+ }
10815
+ }
10816
+ return 0;
10817
+ });
10818
+ const ipv6Addresses = [...ipv6Set].sort();
10819
+ const ipv4Subnets = [...ipv4SubnetSet].sort();
10820
+ const ipv6Subnets = [...ipv6SubnetSet].sort();
10821
+ return {
10822
+ ipv4Addresses,
10823
+ ipv6Addresses,
10824
+ ipv4Subnets,
10825
+ ipv6Subnets,
10826
+ counts: {
10827
+ ipv4: ipv4Addresses.length,
10828
+ ipv6: ipv6Addresses.length,
10829
+ ipv4Subnets: ipv4Subnets.length,
10830
+ ipv6Subnets: ipv6Subnets.length,
10831
+ total: ipv4Addresses.length + ipv6Addresses.length + ipv4Subnets.length + ipv6Subnets.length
10832
+ }
10833
+ };
10834
+ }
10444
10835
  function generateMultiFileSarif(fileResults, rules, options = {}) {
10836
+ const aggregatedIpSummary = aggregateIPSummaries(
10837
+ fileResults.map((fr) => fr.ipSummary).filter((s) => !!s)
10838
+ );
10445
10839
  const allSarifResults = fileResults.flatMap(({ filePath, results }) => {
10446
10840
  const fileUri = options.relativePaths ? relative(options.baseDir ?? process.cwd(), filePath) : filePath;
10447
10841
  return results.map((result) => ({
@@ -10482,16 +10876,18 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10482
10876
  kinds: ["superset"]
10483
10877
  }));
10484
10878
  }
10485
- if (secMeta?.cvssScore !== void 0 || secMeta?.cvssVector || secMeta?.tags) {
10879
+ const hasCvss = secMeta?.cvssScore !== void 0 || secMeta?.cvssVector;
10880
+ const hasTags = rule.metadata.tags && rule.metadata.tags.length > 0;
10881
+ if (hasCvss || hasTags) {
10486
10882
  base.properties = {};
10487
- if (secMeta.cvssScore !== void 0) {
10883
+ if (secMeta?.cvssScore !== void 0) {
10488
10884
  base.properties["security-severity"] = String(secMeta.cvssScore);
10489
10885
  }
10490
- if (secMeta.cvssVector) {
10886
+ if (secMeta?.cvssVector) {
10491
10887
  base.properties["cvss-vector"] = secMeta.cvssVector;
10492
10888
  }
10493
- if (secMeta.tags && secMeta.tags.length > 0) {
10494
- base.properties.tags = secMeta.tags;
10889
+ if (hasTags) {
10890
+ base.properties.tags = rule.metadata.tags.map((t) => t.label);
10495
10891
  }
10496
10892
  }
10497
10893
  return base;
@@ -10514,7 +10910,7 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10514
10910
  tool: {
10515
10911
  driver: {
10516
10912
  name: "Sentriflow",
10517
- version: "0.1.7",
10913
+ version: "0.1.9",
10518
10914
  informationUri: "https://github.com/sentriflow/sentriflow",
10519
10915
  rules: sarifRules,
10520
10916
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -10545,7 +10941,13 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10545
10941
  }
10546
10942
  ]
10547
10943
  },
10548
- results: allSarifResults
10944
+ results: allSarifResults,
10945
+ // Include aggregated IP summary in properties if available
10946
+ ...aggregatedIpSummary && {
10947
+ properties: {
10948
+ ipSummary: aggregatedIpSummary
10949
+ }
10950
+ }
10549
10951
  }
10550
10952
  ]
10551
10953
  };
@@ -13086,9 +13488,12 @@ var cisco_json_rules_default = {
13086
13488
  description: "Trunk ports should disable DTP (Dynamic Trunking Protocol)",
13087
13489
  remediation: "Add 'switchport nonegotiate' to disable DTP on trunk ports",
13088
13490
  security: {
13089
- cwe: ["CWE-319"],
13090
- tags: ["vlan-hopping", "network-security"]
13091
- }
13491
+ cwe: ["CWE-319"]
13492
+ },
13493
+ tags: [
13494
+ { type: "security", label: "vlan-hopping" },
13495
+ { type: "security", label: "network-security" }
13496
+ ]
13092
13497
  },
13093
13498
  check: {
13094
13499
  type: "and",
@@ -13129,9 +13534,12 @@ var cisco_json_rules_default = {
13129
13534
  description: "VTY lines should have access-class configured for SSH access control",
13130
13535
  remediation: "Add 'access-class <acl> in' to restrict VTY access",
13131
13536
  security: {
13132
- cwe: ["CWE-284"],
13133
- tags: ["access-control", "remote-access"]
13134
- }
13537
+ cwe: ["CWE-284"]
13538
+ },
13539
+ tags: [
13540
+ { type: "security", label: "access-control" },
13541
+ { type: "security", label: "remote-access" }
13542
+ ]
13135
13543
  },
13136
13544
  check: {
13137
13545
  type: "child_not_exists",
@@ -13213,9 +13621,12 @@ var juniper_json_rules_default = {
13213
13621
  description: "SSH should be configured for version 2 only",
13214
13622
  remediation: "Configure 'set system services ssh protocol-version v2'",
13215
13623
  security: {
13216
- cwe: ["CWE-327"],
13217
- tags: ["ssh", "encryption"]
13218
- }
13624
+ cwe: ["CWE-327"]
13625
+ },
13626
+ tags: [
13627
+ { type: "security", label: "ssh" },
13628
+ { type: "security", label: "encryption" }
13629
+ ]
13219
13630
  },
13220
13631
  check: {
13221
13632
  type: "and",
@@ -13246,9 +13657,12 @@ var juniper_json_rules_default = {
13246
13657
  description: "Telnet service should be disabled",
13247
13658
  remediation: "Remove 'set system services telnet' or add 'delete system services telnet'",
13248
13659
  security: {
13249
- cwe: ["CWE-319"],
13250
- tags: ["telnet", "cleartext"]
13251
- }
13660
+ cwe: ["CWE-319"]
13661
+ },
13662
+ tags: [
13663
+ { type: "security", label: "telnet" },
13664
+ { type: "security", label: "cleartext" }
13665
+ ]
13252
13666
  },
13253
13667
  check: {
13254
13668
  type: "helper",
@@ -14459,7 +14873,7 @@ function isStdinRequested(files) {
14459
14873
 
14460
14874
  // index.ts
14461
14875
  var program = new Command();
14462
- program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.1.7").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(
14876
+ program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.1.9").argument("[files...]", "Path(s) to configuration file(s) (supports multiple files)").option("--ast", "Output the AST instead of rule results").option("-f, --format <format>", "Output format (json, sarif)", "json").option("-q, --quiet", "Only output failures (suppress passed results)").option("-c, --config <path>", "Path to config file (default: auto-detect)").option("--no-config", "Ignore config file").option("-r, --rules <path>", "Additional rules file to load (legacy)").option("-p, --rule-pack <path>", "Rule pack file to load").option(
14463
14877
  "--encrypted-pack <path...>",
14464
14878
  "SEC-012: Path(s) to encrypted rule pack(s) (.grpx), can specify multiple"
14465
14879
  ).option(
@@ -14752,10 +15166,12 @@ Parsing complete: ${allAsts.length} files`);
14752
15166
  const passed = results2.filter((r) => r.passed).length;
14753
15167
  totalFailures += failures;
14754
15168
  totalPassed += passed;
15169
+ const fileIpSummary = extractIPSummary(content2);
14755
15170
  allFileResults.push({
14756
15171
  filePath: filePath2,
14757
15172
  results: results2,
14758
- vendor: { id: vendor2.id, name: vendor2.name }
15173
+ vendor: { id: vendor2.id, name: vendor2.name },
15174
+ ipSummary: fileIpSummary
14759
15175
  });
14760
15176
  } catch (err) {
14761
15177
  const errMsg = err instanceof Error ? err.message : "Unknown error";
@@ -14787,7 +15203,8 @@ Parsing complete: ${allAsts.length} files`);
14787
15203
  files: allFileResults.map((fr) => ({
14788
15204
  file: fr.filePath,
14789
15205
  vendor: fr.vendor,
14790
- results: fr.results
15206
+ results: fr.results,
15207
+ ipSummary: fr.ipSummary
14791
15208
  }))
14792
15209
  };
14793
15210
  console.log(JSON.stringify(output, null, 2));
@@ -14863,17 +15280,19 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
14863
15280
  if (options.quiet) {
14864
15281
  results2 = results2.filter((r) => !r.passed);
14865
15282
  }
15283
+ const stdinIpSummary = extractIPSummary(content2);
14866
15284
  if (options.format === "sarif") {
14867
15285
  const sarifOptions = {
14868
15286
  relativePaths: options.relativePaths,
14869
15287
  baseDir: process.cwd()
14870
15288
  };
14871
- console.log(generateSarif(results2, "<stdin>", stdinRules, sarifOptions));
15289
+ console.log(generateSarif(results2, "<stdin>", stdinRules, sarifOptions, stdinIpSummary));
14872
15290
  } else {
14873
15291
  const output = {
14874
15292
  file: "<stdin>",
14875
15293
  vendor: { id: vendor2.id, name: vendor2.name },
14876
- results: results2
15294
+ results: results2,
15295
+ ipSummary: stdinIpSummary
14877
15296
  };
14878
15297
  console.log(JSON.stringify(output, null, 2));
14879
15298
  }
@@ -14932,10 +15351,12 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
14932
15351
  const passed = results2.filter((r) => r.passed).length;
14933
15352
  totalFailures += failures;
14934
15353
  totalPassed += passed;
15354
+ const fileIpSummary = extractIPSummary(content2);
14935
15355
  allFileResults.push({
14936
15356
  filePath: filePath2,
14937
15357
  results: results2,
14938
- vendor: { id: vendor2.id, name: vendor2.name }
15358
+ vendor: { id: vendor2.id, name: vendor2.name },
15359
+ ipSummary: fileIpSummary
14939
15360
  });
14940
15361
  } catch (err) {
14941
15362
  const errMsg = err instanceof Error ? err.message : "Unknown error";
@@ -14962,7 +15383,8 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
14962
15383
  files: allFileResults.map((fr) => ({
14963
15384
  file: fr.filePath,
14964
15385
  vendor: fr.vendor,
14965
- results: fr.results
15386
+ results: fr.results,
15387
+ ipSummary: fr.ipSummary
14966
15388
  }))
14967
15389
  };
14968
15390
  console.log(JSON.stringify(output, null, 2));
@@ -15047,13 +15469,14 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15047
15469
  if (options.quiet) {
15048
15470
  results = results.filter((r) => !r.passed);
15049
15471
  }
15472
+ const ipSummary = extractIPSummary(content);
15050
15473
  if (options.format === "sarif") {
15051
15474
  const sarifOptions = {
15052
15475
  relativePaths: options.relativePaths,
15053
15476
  baseDir: process.cwd()
15054
15477
  };
15055
15478
  console.log(
15056
- generateSarif(results, filePath, singleFileRules, sarifOptions)
15479
+ generateSarif(results, filePath, singleFileRules, sarifOptions, ipSummary)
15057
15480
  );
15058
15481
  } else {
15059
15482
  const output = {
@@ -15061,7 +15484,8 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
15061
15484
  id: vendor.id,
15062
15485
  name: vendor.name
15063
15486
  },
15064
- results
15487
+ results,
15488
+ ipSummary
15065
15489
  };
15066
15490
  console.log(JSON.stringify(output, null, 2));
15067
15491
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "SentriFlow CLI - Network configuration linter and validator",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",