@sentriflow/cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +35 -0
  2. package/dist/index.js +274 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -151,6 +151,41 @@ sentriflow --show-machine-id
151
151
  # Output: Machine ID: a1b2c3d4...
152
152
  ```
153
153
 
154
+ ### Cloud Licensing Commands
155
+
156
+ Cloud licensing features require the `@sentriflow/licensing` package, which is provided to customers after purchasing a license. Visit [sentriflow.com.au/pricing](https://sentriflow.com.au/pricing) for more information.
157
+
158
+ | Command | Description |
159
+ |---------|-------------|
160
+ | `sentriflow activate --license-key <key>` | Activate license and download entitled packs |
161
+ | `sentriflow update` | Check for and download pack updates |
162
+ | `sentriflow offline --bundle <path>` | Create offline bundle for air-gapped environments |
163
+ | `sentriflow license` | Show license status and entitled feeds |
164
+
165
+ **Activate a license:**
166
+
167
+ ```bash
168
+ # Activate license and download all entitled packs
169
+ sentriflow activate --license-key eyJhbGciOiJIUzI1Ni...
170
+
171
+ # Or use environment variable
172
+ export SENTRIFLOW_LICENSE_KEY=eyJhbGciOiJIUzI1Ni...
173
+ sentriflow activate
174
+ ```
175
+
176
+ **Check for updates:**
177
+
178
+ ```bash
179
+ # Check and download available pack updates
180
+ sentriflow update
181
+ ```
182
+
183
+ **Offline mode:**
184
+
185
+ Downloaded packs are cached in `~/.sentriflow/cache/` and work offline for 72 hours (entitlement cache). The pack files themselves work indefinitely once downloaded.
186
+
187
+ If `@sentriflow/licensing` is not installed, these commands display a message with information on how to obtain access.
188
+
154
189
  ### Directory Scanning
155
190
 
156
191
  | Option | Description |
package/dist/index.js CHANGED
@@ -12129,6 +12129,178 @@ function extractIPSummary(content, options = {}) {
12129
12129
  };
12130
12130
  }
12131
12131
 
12132
+ // ../core/src/ip/classifier.ts
12133
+ var DEFAULT_FILTER_OPTIONS = {
12134
+ keepPublic: true,
12135
+ keepPrivate: true,
12136
+ keepLoopback: false,
12137
+ keepLinkLocal: false,
12138
+ keepMulticast: false,
12139
+ keepReserved: false,
12140
+ keepUnspecified: false,
12141
+ keepBroadcast: false,
12142
+ keepDocumentation: false,
12143
+ keepCgnat: true
12144
+ };
12145
+ function ipv4ToNumber2(ip) {
12146
+ const parts = ip.split(".");
12147
+ if (parts.length !== 4) return 0;
12148
+ let result = 0;
12149
+ for (let i = 0; i < 4; i++) {
12150
+ const octet = parseInt(parts[i] ?? "0", 10);
12151
+ if (isNaN(octet) || octet < 0 || octet > 255) return 0;
12152
+ result = (result << 8) + octet;
12153
+ }
12154
+ return result >>> 0;
12155
+ }
12156
+ function isInIPv4Range(ip, network, prefix) {
12157
+ const ipNum = ipv4ToNumber2(ip);
12158
+ const netNum = ipv4ToNumber2(network);
12159
+ const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
12160
+ return (ipNum & mask) === (netNum & mask);
12161
+ }
12162
+ function classifyIPv4(ip) {
12163
+ if (ip === "0.0.0.0") return "unspecified";
12164
+ if (ip === "255.255.255.255") return "broadcast";
12165
+ if (isInIPv4Range(ip, "0.0.0.0", 8)) return "unspecified";
12166
+ if (isInIPv4Range(ip, "127.0.0.0", 8)) return "loopback";
12167
+ if (isInIPv4Range(ip, "169.254.0.0", 16)) return "link-local";
12168
+ if (isInIPv4Range(ip, "10.0.0.0", 8)) return "private";
12169
+ if (isInIPv4Range(ip, "172.16.0.0", 12)) return "private";
12170
+ if (isInIPv4Range(ip, "192.168.0.0", 16)) return "private";
12171
+ if (isInIPv4Range(ip, "100.64.0.0", 10)) return "cgnat";
12172
+ if (isInIPv4Range(ip, "192.0.2.0", 24)) return "documentation";
12173
+ if (isInIPv4Range(ip, "198.51.100.0", 24)) return "documentation";
12174
+ if (isInIPv4Range(ip, "203.0.113.0", 24)) return "documentation";
12175
+ if (isInIPv4Range(ip, "224.0.0.0", 4)) return "multicast";
12176
+ if (isInIPv4Range(ip, "240.0.0.0", 4)) return "reserved";
12177
+ return "public";
12178
+ }
12179
+ function classifyIPv4Subnet(subnet) {
12180
+ const slashIndex = subnet.lastIndexOf("/");
12181
+ if (slashIndex === -1) return classifyIPv4(subnet);
12182
+ const network = subnet.substring(0, slashIndex);
12183
+ return classifyIPv4(network);
12184
+ }
12185
+ function expandIPv62(ip) {
12186
+ const zoneIndex = ip.indexOf("%");
12187
+ const addr = zoneIndex !== -1 ? ip.substring(0, zoneIndex) : ip;
12188
+ const parts = addr.split(":");
12189
+ const result = [];
12190
+ for (let i = 0; i < parts.length; i++) {
12191
+ const part = parts[i] ?? "";
12192
+ if (part === "" && i > 0 && i < parts.length - 1) {
12193
+ const nonEmpty = parts.filter((p) => p !== "").length;
12194
+ const zeros = 8 - nonEmpty;
12195
+ for (let j = 0; j < zeros; j++) {
12196
+ result.push(0);
12197
+ }
12198
+ } else if (part !== "") {
12199
+ result.push(parseInt(part, 16) || 0);
12200
+ } else if (i === 0 && (parts[1] ?? "") === "") {
12201
+ const nonEmpty = parts.filter((p) => p !== "").length;
12202
+ const zeros = 8 - nonEmpty;
12203
+ for (let j = 0; j < zeros; j++) {
12204
+ result.push(0);
12205
+ }
12206
+ }
12207
+ }
12208
+ while (result.length < 8) {
12209
+ result.push(0);
12210
+ }
12211
+ return result.slice(0, 8);
12212
+ }
12213
+ function classifyIPv6(ip) {
12214
+ const parts = expandIPv62(ip);
12215
+ if (parts.every((p) => p === 0)) return "unspecified";
12216
+ if (parts.slice(0, 7).every((p) => p === 0) && parts[7] === 1) return "loopback";
12217
+ if ((parts[0] ?? 0) >= 65152 && (parts[0] ?? 0) <= 65215) return "link-local";
12218
+ if (((parts[0] ?? 0) & 65280) === 65280) return "multicast";
12219
+ if (parts[0] === 8193 && parts[1] === 3512) return "documentation";
12220
+ if (((parts[0] ?? 0) & 65024) === 64512) return "private";
12221
+ return "public";
12222
+ }
12223
+ function classifyIPv6Subnet(subnet) {
12224
+ const slashIndex = subnet.lastIndexOf("/");
12225
+ if (slashIndex === -1) return classifyIPv6(subnet);
12226
+ const network = subnet.substring(0, slashIndex);
12227
+ return classifyIPv6(network);
12228
+ }
12229
+ function shouldKeepClassification(classification, options) {
12230
+ switch (classification) {
12231
+ case "public":
12232
+ return options.keepPublic;
12233
+ case "private":
12234
+ return options.keepPrivate;
12235
+ case "loopback":
12236
+ return options.keepLoopback;
12237
+ case "link-local":
12238
+ return options.keepLinkLocal;
12239
+ case "multicast":
12240
+ return options.keepMulticast;
12241
+ case "reserved":
12242
+ return options.keepReserved;
12243
+ case "unspecified":
12244
+ return options.keepUnspecified;
12245
+ case "broadcast":
12246
+ return options.keepBroadcast;
12247
+ case "documentation":
12248
+ return options.keepDocumentation;
12249
+ case "cgnat":
12250
+ return options.keepCgnat;
12251
+ default:
12252
+ return true;
12253
+ }
12254
+ }
12255
+ function filterIPv4Addresses(addresses, options = {}) {
12256
+ const opts = { ...DEFAULT_FILTER_OPTIONS, ...options };
12257
+ return addresses.filter((ip) => {
12258
+ const classification = classifyIPv4(ip);
12259
+ return shouldKeepClassification(classification, opts);
12260
+ });
12261
+ }
12262
+ function filterIPv6Addresses(addresses, options = {}) {
12263
+ const opts = { ...DEFAULT_FILTER_OPTIONS, ...options };
12264
+ return addresses.filter((ip) => {
12265
+ const classification = classifyIPv6(ip);
12266
+ return shouldKeepClassification(classification, opts);
12267
+ });
12268
+ }
12269
+ function filterIPv4Subnets(subnets, options = {}) {
12270
+ const opts = { ...DEFAULT_FILTER_OPTIONS, ...options };
12271
+ return subnets.filter((subnet) => {
12272
+ const classification = classifyIPv4Subnet(subnet);
12273
+ return shouldKeepClassification(classification, opts);
12274
+ });
12275
+ }
12276
+ function filterIPv6Subnets(subnets, options = {}) {
12277
+ const opts = { ...DEFAULT_FILTER_OPTIONS, ...options };
12278
+ return subnets.filter((subnet) => {
12279
+ const classification = classifyIPv6Subnet(subnet);
12280
+ return shouldKeepClassification(classification, opts);
12281
+ });
12282
+ }
12283
+ function filterIPSummary(summary, options = {}) {
12284
+ const ipv4Addresses = filterIPv4Addresses(summary.ipv4Addresses, options);
12285
+ const ipv6Addresses = filterIPv6Addresses(summary.ipv6Addresses, options);
12286
+ const ipv4Subnets = filterIPv4Subnets(summary.ipv4Subnets, options);
12287
+ const ipv6Subnets = filterIPv6Subnets(summary.ipv6Subnets, options);
12288
+ const counts = {
12289
+ ipv4: ipv4Addresses.length,
12290
+ ipv6: ipv6Addresses.length,
12291
+ ipv4Subnets: ipv4Subnets.length,
12292
+ ipv6Subnets: ipv6Subnets.length,
12293
+ total: ipv4Addresses.length + ipv6Addresses.length + ipv4Subnets.length + ipv6Subnets.length
12294
+ };
12295
+ return {
12296
+ ipv4Addresses,
12297
+ ipv6Addresses,
12298
+ ipv4Subnets,
12299
+ ipv6Subnets,
12300
+ counts
12301
+ };
12302
+ }
12303
+
12132
12304
  // ../core/src/validation/rule-validation.ts
12133
12305
  function validateRule2(rule) {
12134
12306
  if (typeof rule !== "object" || rule === null) {
@@ -12328,7 +12500,7 @@ function generateSarif(results, filePath, rules, options = {}, ipSummary) {
12328
12500
  tool: {
12329
12501
  driver: {
12330
12502
  name: "Sentriflow",
12331
- version: "0.3.0",
12503
+ version: "0.3.2",
12332
12504
  informationUri: "https://github.com/sentriflow/sentriflow",
12333
12505
  rules: sarifRules,
12334
12506
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -12488,7 +12660,7 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
12488
12660
  tool: {
12489
12661
  driver: {
12490
12662
  name: "Sentriflow",
12491
- version: "0.3.0",
12663
+ version: "0.3.2",
12492
12664
  informationUri: "https://github.com/sentriflow/sentriflow",
12493
12665
  rules: sarifRules,
12494
12666
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -15418,7 +15590,7 @@ function getRulesByCumulusVendor() {
15418
15590
 
15419
15591
  // ../rules-default/src/json/cisco-json-rules.json
15420
15592
  var cisco_json_rules_default = {
15421
- $schema: "https://sentriflow.io/schemas/json-rules/v1.0.json",
15593
+ $schema: "https://sentriflow.com.au/schemas/json-rules/v1.0.json",
15422
15594
  version: "1.0",
15423
15595
  meta: {
15424
15596
  name: "Cisco IOS JSON Rules",
@@ -15615,7 +15787,7 @@ var cisco_json_rules_default = {
15615
15787
 
15616
15788
  // ../rules-default/src/json/common-json-rules.json
15617
15789
  var common_json_rules_default = {
15618
- $schema: "https://sentriflow.io/schemas/json-rules/v1.0.json",
15790
+ $schema: "https://sentriflow.com.au/schemas/json-rules/v1.0.json",
15619
15791
  version: "1.0",
15620
15792
  meta: {
15621
15793
  name: "Common JSON Rules",
@@ -15663,7 +15835,7 @@ var common_json_rules_default = {
15663
15835
 
15664
15836
  // ../rules-default/src/json/juniper-json-rules.json
15665
15837
  var juniper_json_rules_default = {
15666
- $schema: "https://sentriflow.io/schemas/json-rules/v1.0.json",
15838
+ $schema: "https://sentriflow.com.au/schemas/json-rules/v1.0.json",
15667
15839
  version: "1.0",
15668
15840
  meta: {
15669
15841
  name: "Juniper JunOS JSON Rules",
@@ -16172,6 +16344,9 @@ function isValidSentriflowConfig(config) {
16172
16344
  return false;
16173
16345
  }
16174
16346
  }
16347
+ if (obj.filterSpecialIps !== void 0 && typeof obj.filterSpecialIps !== "boolean") {
16348
+ return false;
16349
+ }
16175
16350
  return true;
16176
16351
  }
16177
16352
  function isValidDirectoryConfig(config) {
@@ -16967,7 +17142,7 @@ function enrichResultsWithRuleMetadata(results, rules) {
16967
17142
  });
16968
17143
  }
16969
17144
  var program = new Command();
16970
- program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.3.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(
17145
+ program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.3.2").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(
16971
17146
  "--pack <path...>",
16972
17147
  "Path(s) to rule pack(s) (auto-detects format: .grx2, .grpx, or unencrypted)"
16973
17148
  ).option(
@@ -17025,7 +17200,10 @@ program.name("sentriflow").description("SentriFlow Network Configuration Validat
17025
17200
  "--max-depth <number>",
17026
17201
  "Maximum recursion depth for directory scanning (use with -R)",
17027
17202
  (val) => parseInt(val, 10)
17028
- ).option("--progress", "Show progress during directory scanning").action(async (files, options) => {
17203
+ ).option("--progress", "Show progress during directory scanning").option(
17204
+ "--filter-special-ips",
17205
+ "Filter out special IP ranges (loopback, multicast, reserved, broadcast) from IP summary"
17206
+ ).action(async (files, options) => {
17029
17207
  try {
17030
17208
  if (options.showMachineId) {
17031
17209
  try {
@@ -17099,6 +17277,19 @@ Use: sentriflow --vendor <vendor> <file>`);
17099
17277
  allowedBaseDirs
17100
17278
  // SEC-011: Pass allowed base dirs for rule file validation
17101
17279
  });
17280
+ let filterSpecialIps = options.filterSpecialIps ?? false;
17281
+ if (!options.filterSpecialIps && options.config !== false) {
17282
+ const configPath = options.config ?? findConfigFile(configSearchDir);
17283
+ if (configPath) {
17284
+ try {
17285
+ const config = await loadConfigFile(configPath, allowedBaseDirs);
17286
+ if (config.filterSpecialIps) {
17287
+ filterSpecialIps = true;
17288
+ }
17289
+ } catch {
17290
+ }
17291
+ }
17292
+ }
17102
17293
  if (options.listCategories) {
17103
17294
  const counts = /* @__PURE__ */ new Map();
17104
17295
  for (const rule of rules) {
@@ -17338,7 +17529,21 @@ Parsing complete: ${allAsts.length} files`);
17338
17529
  const passed = results2.filter((r) => r.passed).length;
17339
17530
  totalFailures += failures;
17340
17531
  totalPassed += passed;
17341
- const fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
17532
+ let fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
17533
+ if (filterSpecialIps) {
17534
+ fileIpSummary = filterIPSummary(fileIpSummary, {
17535
+ keepPublic: true,
17536
+ keepPrivate: true,
17537
+ keepCgnat: true,
17538
+ keepLoopback: false,
17539
+ keepLinkLocal: false,
17540
+ keepMulticast: false,
17541
+ keepReserved: false,
17542
+ keepUnspecified: false,
17543
+ keepBroadcast: false,
17544
+ keepDocumentation: false
17545
+ });
17546
+ }
17342
17547
  allFileResults.push({
17343
17548
  filePath: filePath2,
17344
17549
  results: results2,
@@ -17454,6 +17659,20 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
17454
17659
  let stdinIpSummary;
17455
17660
  try {
17456
17661
  stdinIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
17662
+ if (filterSpecialIps) {
17663
+ stdinIpSummary = filterIPSummary(stdinIpSummary, {
17664
+ keepPublic: true,
17665
+ keepPrivate: true,
17666
+ keepCgnat: true,
17667
+ keepLoopback: false,
17668
+ keepLinkLocal: false,
17669
+ keepMulticast: false,
17670
+ keepReserved: false,
17671
+ keepUnspecified: false,
17672
+ keepBroadcast: false,
17673
+ keepDocumentation: false
17674
+ });
17675
+ }
17457
17676
  } catch (error) {
17458
17677
  if (error instanceof InputValidationError) {
17459
17678
  console.error(`Input validation error: ${error.message}`);
@@ -17531,7 +17750,21 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
17531
17750
  const passed = results2.filter((r) => r.passed).length;
17532
17751
  totalFailures += failures;
17533
17752
  totalPassed += passed;
17534
- const fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
17753
+ let fileIpSummary = extractIPSummary(content2, { includeSubnetNetworks: true });
17754
+ if (filterSpecialIps) {
17755
+ fileIpSummary = filterIPSummary(fileIpSummary, {
17756
+ keepPublic: true,
17757
+ keepPrivate: true,
17758
+ keepCgnat: true,
17759
+ keepLoopback: false,
17760
+ keepLinkLocal: false,
17761
+ keepMulticast: false,
17762
+ keepReserved: false,
17763
+ keepUnspecified: false,
17764
+ keepBroadcast: false,
17765
+ keepDocumentation: false
17766
+ });
17767
+ }
17535
17768
  allFileResults.push({
17536
17769
  filePath: filePath2,
17537
17770
  results: results2,
@@ -17647,7 +17880,21 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
17647
17880
  if (options.quiet) {
17648
17881
  results = results.filter((r) => !r.passed);
17649
17882
  }
17650
- const ipSummary = extractIPSummary(content, { includeSubnetNetworks: true });
17883
+ let ipSummary = extractIPSummary(content, { includeSubnetNetworks: true });
17884
+ if (filterSpecialIps) {
17885
+ ipSummary = filterIPSummary(ipSummary, {
17886
+ keepPublic: true,
17887
+ keepPrivate: true,
17888
+ keepCgnat: true,
17889
+ keepLoopback: false,
17890
+ keepLinkLocal: false,
17891
+ keepMulticast: false,
17892
+ keepReserved: false,
17893
+ keepUnspecified: false,
17894
+ keepBroadcast: false,
17895
+ keepDocumentation: false
17896
+ });
17897
+ }
17651
17898
  if (options.format === "sarif") {
17652
17899
  const sarifOptions = {
17653
17900
  relativePaths: options.relativePaths,
@@ -17693,6 +17940,23 @@ async function loadLicensingExtension() {
17693
17940
  licensing.registerCommands(program);
17694
17941
  }
17695
17942
  } catch {
17943
+ const licensingMessage = `
17944
+ Cloud licensing features require the @sentriflow/licensing package.
17945
+
17946
+ This package is provided to customers after purchasing a license.
17947
+ Visit https://sentriflow.com.au/pricing for more information.
17948
+
17949
+ Once you have a license, you'll receive access to the private package
17950
+ and can enable cloud features like pack downloads and license activation.
17951
+ `.trim();
17952
+ const fallbackAction = () => {
17953
+ console.log(licensingMessage);
17954
+ process.exit(0);
17955
+ };
17956
+ program.command("activate").description("Activate your SentriFlow license (requires @sentriflow/licensing)").action(fallbackAction);
17957
+ program.command("update").description("Check and download pack updates (requires @sentriflow/licensing)").action(fallbackAction);
17958
+ program.command("offline").description("Manage offline bundles (requires @sentriflow/licensing)").action(fallbackAction);
17959
+ program.command("license").description("Show license status (requires @sentriflow/licensing)").action(fallbackAction);
17696
17960
  }
17697
17961
  }
17698
17962
  loadLicensingExtension().finally(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "SentriFlow CLI - Network configuration linter and validator",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",