@sentriflow/cli 0.1.2 → 0.1.5

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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sentriflow/cli
2
2
 
3
- Command-line interface for SentriFlow - lint and validate network configurations.
3
+ Command-line interface for SentriFlow - check network configurations for compliance against best practices or organization-specific policies.
4
4
 
5
5
  ## Installation
6
6
 
@@ -17,10 +17,10 @@ bun add -g @sentriflow/cli
17
17
  ## Quick Start
18
18
 
19
19
  ```bash
20
- # Validate a single configuration file
20
+ # Check a single configuration file
21
21
  sentriflow router.conf
22
22
 
23
- # Validate with specific vendor
23
+ # Check with specific vendor
24
24
  sentriflow -v cisco-ios router.conf
25
25
 
26
26
  # Scan a directory of configs
@@ -44,7 +44,7 @@ sentriflow --list-rules
44
44
  ```
45
45
  Usage: sentriflow [options] [file]
46
46
 
47
- SentriFlow Network Configuration Validator
47
+ SentriFlow Network Configuration Compliance Checker
48
48
 
49
49
  Arguments:
50
50
  file Path to the configuration file
@@ -164,7 +164,7 @@ sentriflow router.conf -f sarif > results.sarif
164
164
  ### GitHub Actions
165
165
 
166
166
  ```yaml
167
- - name: Lint network configs
167
+ - name: Check network config compliance
168
168
  run: |
169
169
  npx @sentriflow/cli -D configs/ -R -f sarif > results.sarif
170
170
 
@@ -190,8 +190,8 @@ SentriFlow automatically looks for `.sentriflowrc` or `.sentriflowrc.json` in th
190
190
 
191
191
  ## Related Packages
192
192
 
193
- - [`@sentriflow/core`](https://github.com/sentriflow/sentriflow/tree/main/packages/core) - Core parsing engine
194
- - [`@sentriflow/rules-default`](https://github.com/sentriflow/sentriflow/tree/main/packages/rules-default) - Default validation rules
193
+ - [`@sentriflow/core`](https://github.com/sentriflow/sentriflow/tree/main/packages/core) - Core parsing and compliance engine
194
+ - [`@sentriflow/rules-default`](https://github.com/sentriflow/sentriflow/tree/main/packages/rules-default) - Default compliance rules
195
195
 
196
196
  ## License
197
197
 
package/dist/index.js CHANGED
@@ -10407,7 +10407,7 @@ function generateSarif(results, filePath, rules, options = {}) {
10407
10407
  tool: {
10408
10408
  driver: {
10409
10409
  name: "Sentriflow",
10410
- version: "0.1.2",
10410
+ version: "0.1.5",
10411
10411
  informationUri: "https://github.com/sentriflow/sentriflow",
10412
10412
  rules: sarifRules,
10413
10413
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -10513,7 +10513,7 @@ function generateMultiFileSarif(fileResults, rules, options = {}) {
10513
10513
  tool: {
10514
10514
  driver: {
10515
10515
  name: "Sentriflow",
10516
- version: "0.1.2",
10516
+ version: "0.1.5",
10517
10517
  informationUri: "https://github.com/sentriflow/sentriflow",
10518
10518
  rules: sarifRules,
10519
10519
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -10740,86 +10740,9 @@ var InterfaceDescriptionRequired = {
10740
10740
  };
10741
10741
  }
10742
10742
  };
10743
- var NoPlaintextPasswords = {
10744
- id: "NET-SEC-001",
10745
- selector: "password",
10746
- vendor: "common",
10747
- metadata: {
10748
- level: "error",
10749
- obu: "Security",
10750
- owner: "SecOps",
10751
- remediation: 'Use "secret" instead of "password", or ensure password is encrypted (type 7 or higher).'
10752
- },
10753
- check: (node) => {
10754
- const params = node.params;
10755
- const nodeId = node.id;
10756
- if (includesIgnoreCase(nodeId, "encryption") || includesIgnoreCase(nodeId, "service")) {
10757
- return {
10758
- passed: true,
10759
- message: "Global password configuration command.",
10760
- ruleId: "NET-SEC-001",
10761
- nodeId: node.id,
10762
- level: "info",
10763
- loc: node.loc
10764
- };
10765
- }
10766
- if (params.length >= 2) {
10767
- const typeOrValue = params[1];
10768
- if (!typeOrValue) {
10769
- return {
10770
- passed: false,
10771
- message: 'Possible plaintext password detected. Use encryption type 7 or "secret" command.',
10772
- ruleId: "NET-SEC-001",
10773
- nodeId: node.id,
10774
- level: "error",
10775
- loc: node.loc
10776
- };
10777
- }
10778
- if (typeOrValue === "7" || typeOrValue === "5" || typeOrValue === "8" || typeOrValue === "9") {
10779
- return {
10780
- passed: true,
10781
- message: "Password is encrypted.",
10782
- ruleId: "NET-SEC-001",
10783
- nodeId: node.id,
10784
- level: "info",
10785
- loc: node.loc
10786
- };
10787
- }
10788
- if (typeOrValue === "0") {
10789
- return {
10790
- passed: false,
10791
- message: "Plaintext password detected (type 0).",
10792
- ruleId: "NET-SEC-001",
10793
- nodeId: node.id,
10794
- level: "error",
10795
- loc: node.loc
10796
- };
10797
- }
10798
- if (!/^\d+$/.test(typeOrValue)) {
10799
- return {
10800
- passed: false,
10801
- message: 'Possible plaintext password detected. Use encryption type 7 or "secret" command.',
10802
- ruleId: "NET-SEC-001",
10803
- nodeId: node.id,
10804
- level: "error",
10805
- loc: node.loc
10806
- };
10807
- }
10808
- }
10809
- return {
10810
- passed: true,
10811
- message: "Password check passed.",
10812
- ruleId: "NET-SEC-001",
10813
- nodeId: node.id,
10814
- level: "info",
10815
- loc: node.loc
10816
- };
10817
- }
10818
- };
10819
10743
  var allCommonRules = [
10820
10744
  NoMulticastBroadcastIp,
10821
- InterfaceDescriptionRequired,
10822
- NoPlaintextPasswords
10745
+ InterfaceDescriptionRequired
10823
10746
  ];
10824
10747
 
10825
10748
  // ../rules-default/src/cisco/ios-rules.ts
@@ -10921,12 +10844,89 @@ var EnableSecretStrong = {
10921
10844
  return { passed: true, message: "Enable secret check passed.", ruleId: "NET-AAA-003", nodeId: node.id, level: "info", loc: node.loc };
10922
10845
  }
10923
10846
  };
10847
+ var CiscoNoPlaintextPasswords = {
10848
+ id: "NET-SEC-001",
10849
+ selector: "password",
10850
+ vendor: ["cisco-ios", "cisco-nxos"],
10851
+ metadata: {
10852
+ level: "error",
10853
+ obu: "Security",
10854
+ owner: "SecOps",
10855
+ remediation: 'Use "secret" instead of "password", or ensure password is encrypted (type 7 or higher).'
10856
+ },
10857
+ check: (node) => {
10858
+ const params = node.params;
10859
+ const nodeId = node.id;
10860
+ if (includesIgnoreCase(nodeId, "encryption") || includesIgnoreCase(nodeId, "service")) {
10861
+ return {
10862
+ passed: true,
10863
+ message: "Global password configuration command.",
10864
+ ruleId: "NET-SEC-001",
10865
+ nodeId: node.id,
10866
+ level: "info",
10867
+ loc: node.loc
10868
+ };
10869
+ }
10870
+ if (params.length >= 2) {
10871
+ const typeOrValue = params[1];
10872
+ if (!typeOrValue) {
10873
+ return {
10874
+ passed: false,
10875
+ message: 'Possible plaintext password detected. Use encryption type 7 or "secret" command.',
10876
+ ruleId: "NET-SEC-001",
10877
+ nodeId: node.id,
10878
+ level: "error",
10879
+ loc: node.loc
10880
+ };
10881
+ }
10882
+ if (typeOrValue === "7" || typeOrValue === "5" || typeOrValue === "8" || typeOrValue === "9") {
10883
+ return {
10884
+ passed: true,
10885
+ message: "Password is encrypted.",
10886
+ ruleId: "NET-SEC-001",
10887
+ nodeId: node.id,
10888
+ level: "info",
10889
+ loc: node.loc
10890
+ };
10891
+ }
10892
+ if (typeOrValue === "0") {
10893
+ return {
10894
+ passed: false,
10895
+ message: "Plaintext password detected (type 0).",
10896
+ ruleId: "NET-SEC-001",
10897
+ nodeId: node.id,
10898
+ level: "error",
10899
+ loc: node.loc
10900
+ };
10901
+ }
10902
+ if (!/^\d+$/.test(typeOrValue)) {
10903
+ return {
10904
+ passed: false,
10905
+ message: 'Possible plaintext password detected. Use encryption type 7 or "secret" command.',
10906
+ ruleId: "NET-SEC-001",
10907
+ nodeId: node.id,
10908
+ level: "error",
10909
+ loc: node.loc
10910
+ };
10911
+ }
10912
+ }
10913
+ return {
10914
+ passed: true,
10915
+ message: "Password check passed.",
10916
+ ruleId: "NET-SEC-001",
10917
+ nodeId: node.id,
10918
+ level: "info",
10919
+ loc: node.loc
10920
+ };
10921
+ }
10922
+ };
10924
10923
  var allCiscoRules = [
10925
10924
  // Layer 2 Trunk
10926
10925
  TrunkNoDTP,
10927
10926
  // Layer 2 Access
10928
10927
  AccessExplicitMode,
10929
- // Service Hardening
10928
+ // Security
10929
+ CiscoNoPlaintextPasswords,
10930
10930
  EnableSecretStrong
10931
10931
  ];
10932
10932
 
@@ -11759,6 +11759,40 @@ var VyosHostnameRequired = {
11759
11759
  };
11760
11760
  }
11761
11761
  };
11762
+ var VyosNoPlaintextPassword = {
11763
+ id: "VYOS-SEC-001",
11764
+ selector: "authentication",
11765
+ vendor: "vyos",
11766
+ metadata: {
11767
+ level: "error",
11768
+ obu: "Security",
11769
+ owner: "SecOps",
11770
+ remediation: 'Use "encrypted-password" with a pre-hashed password, or let VyOS hash it during configuration.'
11771
+ },
11772
+ check: (node) => {
11773
+ const hasPlaintext = node.children.some(
11774
+ (child) => startsWithIgnoreCase(child.id, "plaintext-password")
11775
+ );
11776
+ if (hasPlaintext) {
11777
+ return {
11778
+ passed: false,
11779
+ message: "Plaintext password found in configuration. VyOS should store hashed passwords only.",
11780
+ ruleId: "VYOS-SEC-001",
11781
+ nodeId: node.id,
11782
+ level: "error",
11783
+ loc: node.loc
11784
+ };
11785
+ }
11786
+ return {
11787
+ passed: true,
11788
+ message: "No plaintext passwords in configuration.",
11789
+ ruleId: "VYOS-SEC-001",
11790
+ nodeId: node.id,
11791
+ level: "info",
11792
+ loc: node.loc
11793
+ };
11794
+ }
11795
+ };
11762
11796
  var VyosInterfaceDescription = {
11763
11797
  id: "VYOS-IF-001",
11764
11798
  selector: "interfaces",
@@ -11855,6 +11889,8 @@ var VyosFirewallDefaultAction = {
11855
11889
  var allVyosRules = [
11856
11890
  // System
11857
11891
  VyosHostnameRequired,
11892
+ // Security
11893
+ VyosNoPlaintextPassword,
11858
11894
  // Firewall
11859
11895
  VyosFirewallDefaultAction,
11860
11896
  // Interfaces
@@ -13093,55 +13129,6 @@ var common_json_rules_default = {
13093
13129
  ]
13094
13130
  },
13095
13131
  failureMessage: "Interface {nodeId} is missing documentation (description)"
13096
- },
13097
- {
13098
- id: "JSON-COMMON-002",
13099
- selector: "interface",
13100
- vendor: "common",
13101
- metadata: {
13102
- level: "warning",
13103
- obu: "Network Engineering",
13104
- owner: "NetOps",
13105
- description: "Shutdown interfaces should have a description explaining why",
13106
- remediation: "Add a description to shutdown interfaces documenting the reason"
13107
- },
13108
- check: {
13109
- type: "and",
13110
- conditions: [
13111
- {
13112
- type: "helper",
13113
- helper: "isInterfaceDefinition",
13114
- args: [{ $ref: "node" }]
13115
- },
13116
- {
13117
- type: "helper",
13118
- helper: "isShutdown",
13119
- args: [{ $ref: "node" }]
13120
- },
13121
- {
13122
- type: "child_not_exists",
13123
- selector: "description"
13124
- }
13125
- ]
13126
- },
13127
- failureMessage: "Shutdown interface {nodeId} should have a description explaining why it's disabled"
13128
- },
13129
- {
13130
- id: "JSON-COMMON-003",
13131
- selector: "ntp",
13132
- vendor: "common",
13133
- metadata: {
13134
- level: "warning",
13135
- obu: "Operations",
13136
- owner: "SysOps",
13137
- description: "NTP should be configured for time synchronization",
13138
- remediation: "Configure NTP servers for accurate time synchronization"
13139
- },
13140
- check: {
13141
- type: "child_not_exists",
13142
- selector: "server"
13143
- },
13144
- failureMessage: "NTP configuration is missing server entries"
13145
13132
  }
13146
13133
  ]
13147
13134
  };
@@ -13307,6 +13294,16 @@ var allJsonRules = [
13307
13294
  ...commonJsonRules,
13308
13295
  ...juniperJsonRules
13309
13296
  ];
13297
+ function getJsonRulesByVendor(vendorId) {
13298
+ return allJsonRules.filter((rule) => {
13299
+ if (!rule.vendor) return true;
13300
+ if (rule.vendor === "common") return true;
13301
+ if (Array.isArray(rule.vendor)) {
13302
+ return rule.vendor.includes(vendorId) || rule.vendor.includes("common");
13303
+ }
13304
+ return rule.vendor === vendorId;
13305
+ });
13306
+ }
13310
13307
 
13311
13308
  // ../rules-default/src/index.ts
13312
13309
  var allRules = [
@@ -13341,25 +13338,25 @@ var allRules = [
13341
13338
  ];
13342
13339
  var vendorRulesRegistry = {
13343
13340
  // Cisco platforms share the same rules
13344
- "cisco-ios": () => [...allCommonRules, ...allCiscoRules],
13345
- "cisco-nxos": () => [...allCommonRules, ...allCiscoRules],
13341
+ "cisco-ios": () => [...allCommonRules, ...allCiscoRules, ...getJsonRulesByVendor("cisco-ios")],
13342
+ "cisco-nxos": () => [...allCommonRules, ...allCiscoRules, ...getJsonRulesByVendor("cisco-nxos")],
13346
13343
  // Juniper
13347
- "juniper-junos": () => [...allCommonRules, ...allJuniperRules],
13344
+ "juniper-junos": () => [...allCommonRules, ...allJuniperRules, ...getJsonRulesByVendor("juniper-junos")],
13348
13345
  // Aruba platforms have variant-specific rules
13349
- "aruba-aoscx": () => getRulesByArubaVendor("aruba-aoscx"),
13350
- "aruba-aosswitch": () => getRulesByArubaVendor("aruba-aosswitch"),
13351
- "aruba-wlc": () => getRulesByArubaVendor("aruba-wlc"),
13346
+ "aruba-aoscx": () => [...getRulesByArubaVendor("aruba-aoscx"), ...getJsonRulesByVendor("aruba-aoscx")],
13347
+ "aruba-aosswitch": () => [...getRulesByArubaVendor("aruba-aosswitch"), ...getJsonRulesByVendor("aruba-aosswitch")],
13348
+ "aruba-wlc": () => [...getRulesByArubaVendor("aruba-wlc"), ...getJsonRulesByVendor("aruba-wlc")],
13352
13349
  // Other vendors
13353
- "paloalto-panos": () => getRulesByPaloAltoVendor(),
13354
- "arista-eos": () => getRulesByAristaVendor(),
13355
- "vyos": () => getRulesByVyosVendor(),
13356
- "fortinet-fortigate": () => getRulesByFortinetVendor(),
13357
- "extreme-exos": () => getRulesByExtremeVendor("extreme-exos"),
13358
- "extreme-voss": () => getRulesByExtremeVendor("extreme-voss"),
13359
- "huawei-vrp": () => getRulesByHuaweiVendor(),
13360
- "mikrotik-routeros": () => getRulesByMikroTikVendor(),
13361
- "nokia-sros": () => getRulesByNokiaVendor(),
13362
- "cumulus-linux": () => getRulesByCumulusVendor()
13350
+ "paloalto-panos": () => [...getRulesByPaloAltoVendor(), ...getJsonRulesByVendor("paloalto-panos")],
13351
+ "arista-eos": () => [...getRulesByAristaVendor(), ...getJsonRulesByVendor("arista-eos")],
13352
+ "vyos": () => [...getRulesByVyosVendor(), ...getJsonRulesByVendor("vyos")],
13353
+ "fortinet-fortigate": () => [...getRulesByFortinetVendor(), ...getJsonRulesByVendor("fortinet-fortigate")],
13354
+ "extreme-exos": () => [...getRulesByExtremeVendor("extreme-exos"), ...getJsonRulesByVendor("extreme-exos")],
13355
+ "extreme-voss": () => [...getRulesByExtremeVendor("extreme-voss"), ...getJsonRulesByVendor("extreme-voss")],
13356
+ "huawei-vrp": () => [...getRulesByHuaweiVendor(), ...getJsonRulesByVendor("huawei-vrp")],
13357
+ "mikrotik-routeros": () => [...getRulesByMikroTikVendor(), ...getJsonRulesByVendor("mikrotik-routeros")],
13358
+ "nokia-sros": () => [...getRulesByNokiaVendor(), ...getJsonRulesByVendor("nokia-sros")],
13359
+ "cumulus-linux": () => [...getRulesByCumulusVendor(), ...getJsonRulesByVendor("cumulus-linux")]
13363
13360
  };
13364
13361
  function getRulesByVendor(vendorId) {
13365
13362
  const getRules = vendorRulesRegistry[vendorId];
@@ -13493,6 +13490,43 @@ function validateInputFilePath(filePath, maxSize = 10 * 1024 * 1024, baseDirs) {
13493
13490
  });
13494
13491
  }
13495
13492
 
13493
+ // src/loaders/index.ts
13494
+ function validatePathOrThrow(path, pathValidator, errorContext, baseDirs) {
13495
+ const validation = pathValidator(path, baseDirs);
13496
+ if (!validation.valid) {
13497
+ throw new SentriflowConfigError(
13498
+ `Invalid ${errorContext} path: ${validation.error}`
13499
+ );
13500
+ }
13501
+ return validation.canonicalPath;
13502
+ }
13503
+ async function wrapLoadError(operation, errorContext) {
13504
+ try {
13505
+ return await operation();
13506
+ } catch (error) {
13507
+ if (error instanceof SentriflowConfigError) {
13508
+ throw error;
13509
+ }
13510
+ throw new SentriflowConfigError(`Failed to load ${errorContext} file`);
13511
+ }
13512
+ }
13513
+ async function loadAndValidate(options) {
13514
+ const { path, baseDirs, pathValidator, loader, validator, errorContext } = options;
13515
+ const canonicalPath = validatePathOrThrow(
13516
+ path,
13517
+ pathValidator,
13518
+ errorContext,
13519
+ baseDirs
13520
+ );
13521
+ return wrapLoadError(async () => {
13522
+ const data = await loader(canonicalPath);
13523
+ if (!validator(data)) {
13524
+ throw new SentriflowConfigError(`Invalid ${errorContext} structure`);
13525
+ }
13526
+ return data;
13527
+ }, errorContext);
13528
+ }
13529
+
13496
13530
  // src/config.ts
13497
13531
  var CONFIG_FILES = [
13498
13532
  "sentriflow.config.ts",
@@ -13658,36 +13692,30 @@ function isValidRulePack(pack) {
13658
13692
  return true;
13659
13693
  }
13660
13694
  async function loadConfigFile(configPath, baseDirs) {
13661
- const validation = validateConfigPath(configPath, baseDirs);
13662
- if (!validation.valid) {
13663
- throw new SentriflowConfigError(`Invalid config path: ${validation.error}`);
13664
- }
13665
- try {
13666
- const module = await import(validation.canonicalPath);
13667
- const config = module.default ?? module;
13668
- if (!isValidSentriflowConfig(config)) {
13669
- throw new SentriflowConfigError("Invalid configuration structure");
13670
- }
13671
- return config;
13672
- } catch (error) {
13673
- if (error instanceof SentriflowConfigError) {
13674
- throw error;
13675
- }
13676
- throw new SentriflowConfigError("Failed to load configuration file");
13677
- }
13695
+ return loadAndValidate({
13696
+ path: configPath,
13697
+ baseDirs,
13698
+ pathValidator: validateConfigPath,
13699
+ loader: async (p) => {
13700
+ const m = await import(p);
13701
+ return m.default ?? m;
13702
+ },
13703
+ validator: isValidSentriflowConfig,
13704
+ errorContext: "config"
13705
+ });
13678
13706
  }
13679
13707
  async function loadExternalRules(rulesPath, baseDirs) {
13680
- const validation = validateConfigPath(rulesPath, baseDirs);
13681
- if (!validation.valid) {
13682
- throw new SentriflowConfigError(`Invalid rules path: ${validation.error}`);
13683
- }
13684
- try {
13685
- const module = await import(validation.canonicalPath);
13708
+ const canonicalPath = validatePathOrThrow(
13709
+ rulesPath,
13710
+ validateConfigPath,
13711
+ "rules",
13712
+ baseDirs
13713
+ );
13714
+ return wrapLoadError(async () => {
13715
+ const module = await import(canonicalPath);
13686
13716
  const rules = module.default ?? module.rules ?? module;
13687
13717
  if (!Array.isArray(rules)) {
13688
- throw new SentriflowConfigError(
13689
- "Rules file must export an array of rules"
13690
- );
13718
+ throw new SentriflowConfigError("Rules file must export an array of rules");
13691
13719
  }
13692
13720
  const validRules = [];
13693
13721
  for (const rule of rules) {
@@ -13695,29 +13723,24 @@ async function loadExternalRules(rulesPath, baseDirs) {
13695
13723
  validRules.push(rule);
13696
13724
  } else {
13697
13725
  const safeRuleId = typeof rule === "object" && rule !== null && "id" in rule ? String(rule.id).slice(0, 50) : "unknown";
13698
- console.warn(
13699
- `Skipping invalid rule: ${safeRuleId} (validation failed)`
13700
- );
13726
+ console.warn(`Skipping invalid rule: ${safeRuleId} (validation failed)`);
13701
13727
  }
13702
13728
  }
13703
13729
  if (validRules.length === 0 && rules.length > 0) {
13704
13730
  throw new SentriflowConfigError("No valid rules found in rules file");
13705
13731
  }
13706
13732
  return validRules;
13707
- } catch (error) {
13708
- if (error instanceof SentriflowConfigError) {
13709
- throw error;
13710
- }
13711
- throw new SentriflowConfigError("Failed to load rules file");
13712
- }
13733
+ }, "rules");
13713
13734
  }
13714
13735
  async function loadJsonRules(jsonPath, baseDirs) {
13715
- const validation = validateJsonRulesPath(jsonPath, baseDirs);
13716
- if (!validation.valid) {
13717
- throw new SentriflowConfigError(`Invalid JSON rules path: ${validation.error}`);
13718
- }
13719
- try {
13720
- const content = await readFileAsync(validation.canonicalPath, "utf-8");
13736
+ const canonicalPath = validatePathOrThrow(
13737
+ jsonPath,
13738
+ validateJsonRulesPath,
13739
+ "JSON rules",
13740
+ baseDirs
13741
+ );
13742
+ return wrapLoadError(async () => {
13743
+ const content = await readFileAsync(canonicalPath, "utf-8");
13721
13744
  let jsonData;
13722
13745
  try {
13723
13746
  jsonData = JSON.parse(content);
@@ -13726,16 +13749,12 @@ async function loadJsonRules(jsonPath, baseDirs) {
13726
13749
  }
13727
13750
  const validationResult = validateJsonRuleFile(jsonData);
13728
13751
  if (!validationResult.valid) {
13729
- const errorMessages = validationResult.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
13730
- throw new SentriflowConfigError(
13731
- `Invalid JSON rules file:
13732
- ${errorMessages}`
13733
- );
13752
+ const errors = validationResult.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
13753
+ throw new SentriflowConfigError(`Invalid JSON rules file:
13754
+ ${errors}`);
13734
13755
  }
13735
- if (validationResult.warnings.length > 0) {
13736
- for (const warning of validationResult.warnings) {
13737
- console.warn(`[JSON Rules] Warning: ${warning.path}: ${warning.message}`);
13738
- }
13756
+ for (const warning of validationResult.warnings) {
13757
+ console.warn(`[JSON Rules] Warning: ${warning.path}: ${warning.message}`);
13739
13758
  }
13740
13759
  const ruleFile = jsonData;
13741
13760
  const compiledRules = compileJsonRules(ruleFile.rules);
@@ -13743,12 +13762,7 @@ ${errorMessages}`
13743
13762
  throw new SentriflowConfigError("No valid rules compiled from JSON file");
13744
13763
  }
13745
13764
  return compiledRules;
13746
- } catch (error) {
13747
- if (error instanceof SentriflowConfigError) {
13748
- throw error;
13749
- }
13750
- throw new SentriflowConfigError("Failed to load JSON rules file");
13751
- }
13765
+ }, "JSON rules");
13752
13766
  }
13753
13767
  function ruleAppliesToVendor(rule, vendorId) {
13754
13768
  if (!rule.vendor) return true;
@@ -13769,70 +13783,52 @@ function isDefaultRuleDisabled(ruleId, vendorId, packs, legacyDisableIds) {
13769
13783
  return false;
13770
13784
  }
13771
13785
  async function loadRulePackFile(packPath, baseDirs) {
13772
- const validation = validateConfigPath(packPath, baseDirs);
13773
- if (!validation.valid) {
13774
- throw new SentriflowConfigError(
13775
- `Invalid rule pack path: ${validation.error}`
13776
- );
13777
- }
13778
- try {
13779
- const module = await import(validation.canonicalPath);
13780
- const pack = module.default ?? module;
13781
- if (!isValidRulePack(pack)) {
13782
- throw new SentriflowConfigError("Invalid rule pack structure");
13783
- }
13784
- return pack;
13785
- } catch (error) {
13786
- if (error instanceof SentriflowConfigError) throw error;
13787
- throw new SentriflowConfigError("Failed to load rule pack file");
13788
- }
13786
+ return loadAndValidate({
13787
+ path: packPath,
13788
+ baseDirs,
13789
+ pathValidator: validateConfigPath,
13790
+ loader: async (p) => {
13791
+ const m = await import(p);
13792
+ return m.default ?? m;
13793
+ },
13794
+ validator: isValidRulePack,
13795
+ errorContext: "rule pack"
13796
+ });
13797
+ }
13798
+ function mapPackLoadError(error) {
13799
+ const messages = {
13800
+ DECRYPTION_FAILED: "Invalid license key for encrypted pack",
13801
+ EXPIRED: "Encrypted pack has expired",
13802
+ MACHINE_MISMATCH: "License is not valid for this machine",
13803
+ ACTIVATION_LIMIT: "Maximum activations exceeded for this license"
13804
+ };
13805
+ throw new SentriflowConfigError(
13806
+ messages[error.code] ?? `Failed to load encrypted pack: ${error.message}`
13807
+ );
13789
13808
  }
13790
13809
  async function loadEncryptedRulePack(packPath, licenseKey, baseDirs) {
13791
- const validation = validateEncryptedPackPath(packPath, baseDirs);
13792
- if (!validation.valid) {
13793
- throw new SentriflowConfigError(
13794
- `Invalid encrypted pack path: ${validation.error}`
13795
- );
13796
- }
13810
+ const canonicalPath = validatePathOrThrow(
13811
+ packPath,
13812
+ validateEncryptedPackPath,
13813
+ "encrypted pack",
13814
+ baseDirs
13815
+ );
13797
13816
  try {
13798
- const packData = await readFileAsync(validation.canonicalPath);
13817
+ const packData = await readFileAsync(canonicalPath);
13799
13818
  if (!validatePackFormat(packData)) {
13800
13819
  throw new SentriflowConfigError("Invalid encrypted pack format");
13801
13820
  }
13802
13821
  const loadedPack = await loadEncryptedPack(packData, {
13803
13822
  licenseKey,
13804
13823
  timeout: 1e4
13805
- // 10 seconds for pack validation
13806
13824
  });
13807
13825
  return {
13808
13826
  ...loadedPack.metadata,
13809
13827
  priority: 200,
13810
- // High priority for licensed packs
13811
13828
  rules: loadedPack.rules
13812
13829
  };
13813
13830
  } catch (error) {
13814
- if (error instanceof PackLoadError) {
13815
- switch (error.code) {
13816
- case "DECRYPTION_FAILED":
13817
- throw new SentriflowConfigError(
13818
- "Invalid license key for encrypted pack"
13819
- );
13820
- case "EXPIRED":
13821
- throw new SentriflowConfigError("Encrypted pack has expired");
13822
- case "MACHINE_MISMATCH":
13823
- throw new SentriflowConfigError(
13824
- "License is not valid for this machine"
13825
- );
13826
- case "ACTIVATION_LIMIT":
13827
- throw new SentriflowConfigError(
13828
- "Maximum activations exceeded for this license"
13829
- );
13830
- default:
13831
- throw new SentriflowConfigError(
13832
- `Failed to load encrypted pack: ${error.message}`
13833
- );
13834
- }
13835
- }
13831
+ if (error instanceof PackLoadError) mapPackLoadError(error);
13836
13832
  if (error instanceof SentriflowConfigError) throw error;
13837
13833
  throw new SentriflowConfigError("Failed to load encrypted rule pack");
13838
13834
  }
@@ -14194,7 +14190,7 @@ function validateDirectoryPath(dirPath, allowedBaseDirs) {
14194
14190
 
14195
14191
  // index.ts
14196
14192
  var program = new Command();
14197
- program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.1.2").argument("[file]", "Path to the configuration file").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(
14193
+ program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.1.5").argument("[file]", "Path to the configuration file").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(
14198
14194
  "--encrypted-pack <path...>",
14199
14195
  "SEC-012: Path(s) to encrypted rule pack(s) (.grpx), can specify multiple"
14200
14196
  ).option(
@@ -14601,4 +14597,19 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
14601
14597
  process.exit(2);
14602
14598
  }
14603
14599
  });
14604
- program.parse();
14600
+ async function loadLicensingExtension() {
14601
+ try {
14602
+ const licensingModulePath = "@sentriflow/licensing/cli";
14603
+ const licensing = await import(
14604
+ /* @vite-ignore */
14605
+ licensingModulePath
14606
+ );
14607
+ if (licensing.registerCommands) {
14608
+ licensing.registerCommands(program);
14609
+ }
14610
+ } catch {
14611
+ }
14612
+ }
14613
+ loadLicensingExtension().finally(() => {
14614
+ program.parse();
14615
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "SentriFlow CLI - Network configuration linter and validator",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "scripts": {
36
- "build": "node build.mjs",
36
+ "build": "bun build.mjs",
37
37
  "prepublishOnly": "bun run build"
38
38
  },
39
39
  "dependencies": {