@sentriflow/cli 0.1.5 → 0.1.6

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 +431 -12
  2. package/package.json +1 -1
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.5",
10410
+ version: "0.1.6",
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.5",
10516
+ version: "0.1.6",
10517
10517
  informationUri: "https://github.com/sentriflow/sentriflow",
10518
10518
  rules: sarifRules,
10519
10519
  // SEC-007: Include CWE taxonomy when rules reference it
@@ -13602,8 +13602,110 @@ function isValidSentriflowConfig(config) {
13602
13602
  }
13603
13603
  }
13604
13604
  }
13605
+ if (obj.directory !== void 0) {
13606
+ if (!isValidDirectoryConfig(obj.directory)) {
13607
+ return false;
13608
+ }
13609
+ }
13610
+ return true;
13611
+ }
13612
+ function isValidDirectoryConfig(config) {
13613
+ if (config === null || config === void 0) {
13614
+ return false;
13615
+ }
13616
+ if (typeof config !== "object") {
13617
+ return false;
13618
+ }
13619
+ const obj = config;
13620
+ if (obj.excludePatterns !== void 0) {
13621
+ if (!Array.isArray(obj.excludePatterns)) {
13622
+ return false;
13623
+ }
13624
+ for (const pattern of obj.excludePatterns) {
13625
+ if (typeof pattern !== "string") {
13626
+ return false;
13627
+ }
13628
+ try {
13629
+ new RegExp(pattern);
13630
+ } catch {
13631
+ return false;
13632
+ }
13633
+ }
13634
+ }
13635
+ if (obj.extensions !== void 0) {
13636
+ if (!Array.isArray(obj.extensions)) {
13637
+ return false;
13638
+ }
13639
+ for (const ext of obj.extensions) {
13640
+ if (typeof ext !== "string") {
13641
+ return false;
13642
+ }
13643
+ }
13644
+ }
13645
+ if (obj.recursive !== void 0 && typeof obj.recursive !== "boolean") {
13646
+ return false;
13647
+ }
13648
+ if (obj.maxDepth !== void 0) {
13649
+ if (typeof obj.maxDepth !== "number") {
13650
+ return false;
13651
+ }
13652
+ if (obj.maxDepth < 0 || obj.maxDepth > 1e3) {
13653
+ return false;
13654
+ }
13655
+ }
13656
+ if (obj.exclude !== void 0) {
13657
+ if (!Array.isArray(obj.exclude)) {
13658
+ return false;
13659
+ }
13660
+ for (const pattern of obj.exclude) {
13661
+ if (typeof pattern !== "string") {
13662
+ return false;
13663
+ }
13664
+ }
13665
+ }
13605
13666
  return true;
13606
13667
  }
13668
+ function mergeDirectoryOptions(cliOptions, configOptions) {
13669
+ const result = {};
13670
+ if (cliOptions.excludePatterns) {
13671
+ result.excludePatterns = [...cliOptions.excludePatterns];
13672
+ } else {
13673
+ result.excludePatterns = [];
13674
+ }
13675
+ if (configOptions?.excludePatterns) {
13676
+ for (const pattern of configOptions.excludePatterns) {
13677
+ try {
13678
+ const regex = new RegExp(pattern);
13679
+ result.excludePatterns.push(regex);
13680
+ } catch {
13681
+ }
13682
+ }
13683
+ }
13684
+ const excludeSet = /* @__PURE__ */ new Set();
13685
+ if (cliOptions.exclude) {
13686
+ for (const p of cliOptions.exclude) excludeSet.add(p);
13687
+ }
13688
+ if (configOptions?.exclude) {
13689
+ for (const p of configOptions.exclude) excludeSet.add(p);
13690
+ }
13691
+ result.exclude = [...excludeSet];
13692
+ if (cliOptions.recursive !== void 0) {
13693
+ result.recursive = cliOptions.recursive;
13694
+ } else if (configOptions?.recursive !== void 0) {
13695
+ result.recursive = configOptions.recursive;
13696
+ }
13697
+ if (cliOptions.maxDepth !== void 0) {
13698
+ result.maxDepth = cliOptions.maxDepth;
13699
+ } else if (configOptions?.maxDepth !== void 0) {
13700
+ result.maxDepth = configOptions.maxDepth;
13701
+ }
13702
+ if (cliOptions.extensions !== void 0) {
13703
+ result.extensions = cliOptions.extensions;
13704
+ } else if (configOptions?.extensions !== void 0) {
13705
+ result.extensions = configOptions.extensions;
13706
+ }
13707
+ return result;
13708
+ }
13607
13709
  function isValidRule(rule) {
13608
13710
  if (typeof rule !== "object" || rule === null) {
13609
13711
  return false;
@@ -14048,6 +14150,19 @@ function isExcluded(relativePath, excludePatterns) {
14048
14150
  if (excludePatterns.length === 0) return false;
14049
14151
  return excludePatterns.some((pattern) => matchesPattern(relativePath, pattern));
14050
14152
  }
14153
+ function validateRegexPattern(pattern) {
14154
+ try {
14155
+ const regex = new RegExp(pattern);
14156
+ return { valid: true, regex };
14157
+ } catch (error) {
14158
+ const message = error instanceof Error ? error.message : "Invalid regex pattern";
14159
+ return { valid: false, error: message };
14160
+ }
14161
+ }
14162
+ function isExcludedByRegex(relativePath, patterns) {
14163
+ if (patterns.length === 0 || relativePath === "") return false;
14164
+ return patterns.some((pattern) => pattern.test(relativePath));
14165
+ }
14051
14166
  async function scanDirectory(dirPath, options = {}) {
14052
14167
  const {
14053
14168
  recursive = false,
@@ -14056,7 +14171,8 @@ async function scanDirectory(dirPath, options = {}) {
14056
14171
  maxFileSize = MAX_CONFIG_SIZE,
14057
14172
  maxDepth = 100,
14058
14173
  allowedBaseDirs,
14059
- exclude = []
14174
+ exclude = [],
14175
+ excludePatterns = []
14060
14176
  } = options;
14061
14177
  const result = {
14062
14178
  files: [],
@@ -14102,7 +14218,11 @@ async function scanDirectory(dirPath, options = {}) {
14102
14218
  for (const entry of entries) {
14103
14219
  const fullPath = join(currentDir, entry);
14104
14220
  const relativePath = join(basePath, entry);
14105
- if (isExcluded(relativePath, exclude)) {
14221
+ const normalizedRelativePath = normalizeSeparators2(relativePath);
14222
+ if (isExcluded(normalizedRelativePath, exclude)) {
14223
+ continue;
14224
+ }
14225
+ if (isExcludedByRegex(normalizedRelativePath, excludePatterns)) {
14106
14226
  continue;
14107
14227
  }
14108
14228
  let stats;
@@ -14188,9 +14308,96 @@ function validateDirectoryPath(dirPath, allowedBaseDirs) {
14188
14308
  }
14189
14309
  }
14190
14310
 
14311
+ // src/loaders/stdin.ts
14312
+ async function readStdin() {
14313
+ return new Promise((resolve5) => {
14314
+ const stdin = process.stdin;
14315
+ const chunks = [];
14316
+ let totalSize = 0;
14317
+ let sizeLimitExceeded = false;
14318
+ stdin.setEncoding("utf8");
14319
+ if (stdin.isTTY) {
14320
+ resolve5({
14321
+ success: false,
14322
+ error: "No input received from stdin"
14323
+ });
14324
+ return;
14325
+ }
14326
+ stdin.on("data", (chunk) => {
14327
+ if (sizeLimitExceeded) return;
14328
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, "utf8");
14329
+ totalSize += buffer.length;
14330
+ if (totalSize > MAX_CONFIG_SIZE) {
14331
+ sizeLimitExceeded = true;
14332
+ resolve5({
14333
+ success: false,
14334
+ error: `Input exceeds maximum size (${totalSize} > ${MAX_CONFIG_SIZE} bytes)`
14335
+ });
14336
+ stdin.destroy();
14337
+ return;
14338
+ }
14339
+ chunks.push(buffer);
14340
+ });
14341
+ stdin.on("end", () => {
14342
+ if (sizeLimitExceeded) return;
14343
+ const content = Buffer.concat(chunks).toString("utf8");
14344
+ if (content.length === 0) {
14345
+ resolve5({
14346
+ success: false,
14347
+ error: "No input received from stdin"
14348
+ });
14349
+ return;
14350
+ }
14351
+ resolve5({
14352
+ success: true,
14353
+ content
14354
+ });
14355
+ });
14356
+ stdin.on("error", (err) => {
14357
+ resolve5({
14358
+ success: false,
14359
+ error: `Failed to read from stdin: ${err.message}`
14360
+ });
14361
+ });
14362
+ const timeout = setTimeout(() => {
14363
+ if (chunks.length === 0 && !sizeLimitExceeded) {
14364
+ stdin.destroy();
14365
+ resolve5({
14366
+ success: false,
14367
+ error: "No input received from stdin (timeout)"
14368
+ });
14369
+ }
14370
+ }, 100);
14371
+ stdin.on("end", () => clearTimeout(timeout));
14372
+ stdin.on("error", () => clearTimeout(timeout));
14373
+ });
14374
+ }
14375
+ function validateStdinArgument(files, hasDirectory) {
14376
+ const hasStdin = files.includes("-");
14377
+ if (!hasStdin) {
14378
+ return { valid: true };
14379
+ }
14380
+ if (files.length > 1) {
14381
+ return {
14382
+ valid: false,
14383
+ error: "Cannot combine stdin (-) with other file arguments"
14384
+ };
14385
+ }
14386
+ if (hasDirectory) {
14387
+ return {
14388
+ valid: false,
14389
+ error: "Cannot combine stdin (-) with directory mode (-D)"
14390
+ };
14391
+ }
14392
+ return { valid: true };
14393
+ }
14394
+ function isStdinRequested(files) {
14395
+ return files.length === 1 && files[0] === "-";
14396
+ }
14397
+
14191
14398
  // index.ts
14192
14399
  var program = new Command();
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(
14400
+ program.name("sentriflow").description("SentriFlow Network Configuration Validator").version("0.1.6").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(
14194
14401
  "--encrypted-pack <path...>",
14195
14402
  "SEC-012: Path(s) to encrypted rule pack(s) (.grpx), can specify multiple"
14196
14403
  ).option(
@@ -14231,7 +14438,14 @@ program.name("sentriflow").description("SentriFlow Network Configuration Validat
14231
14438
  "--exclude <patterns>",
14232
14439
  "Exclude patterns (comma-separated glob patterns)",
14233
14440
  (val) => val.split(",")
14234
- ).option("--progress", "Show progress during directory scanning").action(async (file, options) => {
14441
+ ).option(
14442
+ "--exclude-pattern <pattern...>",
14443
+ "Regex pattern(s) to exclude files (JavaScript regex syntax, can specify multiple)"
14444
+ ).option(
14445
+ "--max-depth <number>",
14446
+ "Maximum recursion depth for directory scanning (use with -R)",
14447
+ (val) => parseInt(val, 10)
14448
+ ).option("--progress", "Show progress during directory scanning").action(async (files, options) => {
14235
14449
  try {
14236
14450
  if (options.listVendors) {
14237
14451
  console.log("Supported vendors:\n");
@@ -14258,8 +14472,21 @@ Use: sentriflow --vendor <vendor> <file>`);
14258
14472
  }
14259
14473
  const workingDir = process.cwd();
14260
14474
  const allowedBaseDirs = options.allowExternal ? void 0 : [workingDir];
14475
+ const excludePatterns = [];
14476
+ if (options.excludePattern) {
14477
+ for (const pattern of options.excludePattern) {
14478
+ const result = validateRegexPattern(pattern);
14479
+ if (!result.valid) {
14480
+ console.error(`Error: Invalid regex pattern '${pattern}'`);
14481
+ console.error(` ${result.error}`);
14482
+ process.exit(2);
14483
+ }
14484
+ excludePatterns.push(result.regex);
14485
+ }
14486
+ }
14261
14487
  const licenseKey = options.licenseKey || process.env.SENTRIFLOW_LICENSE_KEY;
14262
- const configSearchDir = file ? dirname2(resolve4(file)) : workingDir;
14488
+ const firstFile = files.length > 0 ? files[0] : void 0;
14489
+ const configSearchDir = firstFile ? dirname2(resolve4(firstFile)) : workingDir;
14263
14490
  const rules = await resolveRules({
14264
14491
  configPath: options.config,
14265
14492
  noConfig: options.config === false,
@@ -14313,18 +14540,44 @@ Config file: ${configFile}`);
14313
14540
  process.exit(2);
14314
14541
  }
14315
14542
  const canonicalDir = dirValidation.canonicalPath;
14543
+ let directoryConfig;
14544
+ if (options.config !== false) {
14545
+ const configPath = options.config ?? findConfigFile(canonicalDir);
14546
+ if (configPath) {
14547
+ try {
14548
+ const config = await loadConfigFile(configPath, allowedBaseDirs);
14549
+ directoryConfig = config.directory;
14550
+ } catch (err) {
14551
+ if (options.progress) {
14552
+ const msg = err instanceof Error ? err.message : "Unknown error";
14553
+ console.error(`Warning: Failed to load config: ${msg}`);
14554
+ }
14555
+ }
14556
+ }
14557
+ }
14558
+ const cliDirOptions = {
14559
+ recursive: options.recursive,
14560
+ extensions: options.extensions,
14561
+ exclude: options.exclude,
14562
+ excludePatterns: excludePatterns.length > 0 ? excludePatterns : void 0,
14563
+ maxDepth: options.maxDepth
14564
+ };
14565
+ const mergedOptions = mergeDirectoryOptions(cliDirOptions, directoryConfig);
14316
14566
  if (options.progress) {
14567
+ const recursive = mergedOptions.recursive ?? false;
14317
14568
  console.error(
14318
- `Scanning directory: ${canonicalDir}${options.recursive ? " (recursive)" : ""}`
14569
+ `Scanning directory: ${canonicalDir}${recursive ? " (recursive)" : ""}`
14319
14570
  );
14320
14571
  }
14321
14572
  const scanResult = await scanDirectory(canonicalDir, {
14322
- recursive: options.recursive ?? false,
14573
+ recursive: mergedOptions.recursive ?? false,
14323
14574
  patterns: options.glob ? [options.glob] : [],
14324
- extensions: options.extensions ?? DEFAULT_CONFIG_EXTENSIONS,
14575
+ extensions: mergedOptions.extensions ?? DEFAULT_CONFIG_EXTENSIONS,
14325
14576
  maxFileSize: MAX_CONFIG_SIZE,
14577
+ maxDepth: mergedOptions.maxDepth,
14326
14578
  allowedBaseDirs,
14327
- exclude: options.exclude ?? []
14579
+ exclude: mergedOptions.exclude ?? [],
14580
+ excludePatterns: mergedOptions.excludePatterns ?? []
14328
14581
  });
14329
14582
  if (scanResult.errors.length > 0 && options.progress) {
14330
14583
  console.error(`
@@ -14488,10 +14741,176 @@ Scan complete: ${allFileResults.length} files, ${totalFailures} failures, ${tota
14488
14741
  }
14489
14742
  return;
14490
14743
  }
14491
- if (!file) {
14744
+ if (files.length === 0) {
14492
14745
  program.help();
14493
14746
  return;
14494
14747
  }
14748
+ const stdinValidation = validateStdinArgument(files, !!options.directory);
14749
+ if (!stdinValidation.valid) {
14750
+ console.error(`Error: ${stdinValidation.error}`);
14751
+ process.exit(2);
14752
+ }
14753
+ if (isStdinRequested(files)) {
14754
+ const stdinResult = await readStdin();
14755
+ if (!stdinResult.success) {
14756
+ console.error(`Error: ${stdinResult.error}`);
14757
+ process.exit(2);
14758
+ }
14759
+ const content2 = stdinResult.content;
14760
+ let vendor2;
14761
+ if (options.vendor === "auto") {
14762
+ vendor2 = detectVendor(content2);
14763
+ if (!options.quiet && !options.ast) {
14764
+ console.error(`Detected vendor: ${vendor2.name} (${vendor2.id})`);
14765
+ }
14766
+ } else {
14767
+ try {
14768
+ vendor2 = getVendor(options.vendor);
14769
+ } catch {
14770
+ console.error(`Error: Unknown vendor '${options.vendor}'`);
14771
+ console.error(`Available vendors: ${getAvailableVendors().join(", ")}, auto`);
14772
+ process.exit(2);
14773
+ }
14774
+ }
14775
+ const stdinRules = await resolveRules({
14776
+ configPath: options.config,
14777
+ noConfig: options.config === false,
14778
+ rulesPath: options.rules,
14779
+ rulePackPath: options.rulePack,
14780
+ encryptedPackPaths: options.encryptedPack,
14781
+ licenseKey,
14782
+ strictPacks: options.strictPacks,
14783
+ jsonRulesPaths: options.jsonRules,
14784
+ disableIds: options.disable ?? [],
14785
+ vendorId: vendor2.id,
14786
+ cwd: workingDir,
14787
+ allowedBaseDirs
14788
+ });
14789
+ const parser2 = new SchemaAwareParser({ vendor: vendor2 });
14790
+ const nodes2 = parser2.parse(content2);
14791
+ if (options.ast) {
14792
+ const output = {
14793
+ vendor: { id: vendor2.id, name: vendor2.name },
14794
+ ast: nodes2
14795
+ };
14796
+ console.log(JSON.stringify(output, null, 2));
14797
+ return;
14798
+ }
14799
+ const engine2 = new RuleEngine();
14800
+ let results2 = engine2.run(nodes2, stdinRules);
14801
+ if (options.quiet) {
14802
+ results2 = results2.filter((r) => !r.passed);
14803
+ }
14804
+ if (options.format === "sarif") {
14805
+ const sarifOptions = {
14806
+ relativePaths: options.relativePaths,
14807
+ baseDir: process.cwd()
14808
+ };
14809
+ console.log(generateSarif(results2, "<stdin>", stdinRules, sarifOptions));
14810
+ } else {
14811
+ const output = {
14812
+ file: "<stdin>",
14813
+ vendor: { id: vendor2.id, name: vendor2.name },
14814
+ results: results2
14815
+ };
14816
+ console.log(JSON.stringify(output, null, 2));
14817
+ }
14818
+ const hasFailures2 = results2.some((r) => !r.passed);
14819
+ if (hasFailures2) {
14820
+ process.exit(1);
14821
+ }
14822
+ return;
14823
+ }
14824
+ if (files.length > 1) {
14825
+ const allFileResults = [];
14826
+ let totalFailures = 0;
14827
+ let totalPassed = 0;
14828
+ const engine2 = new RuleEngine();
14829
+ for (let i = 0; i < files.length; i++) {
14830
+ const file2 = files[i];
14831
+ if (!file2) continue;
14832
+ const fileValidation2 = validateInputFilePath(
14833
+ file2,
14834
+ MAX_CONFIG_SIZE,
14835
+ allowedBaseDirs
14836
+ );
14837
+ if (!fileValidation2.valid) {
14838
+ console.error(`Error processing ${file2}: ${fileValidation2.error}`);
14839
+ allFileResults.push({
14840
+ filePath: file2,
14841
+ results: []
14842
+ });
14843
+ continue;
14844
+ }
14845
+ const filePath2 = fileValidation2.canonicalPath;
14846
+ try {
14847
+ const stats2 = statSync2(filePath2);
14848
+ if (stats2.size > MAX_CONFIG_SIZE) {
14849
+ console.error(`Error: ${file2} exceeds maximum size`);
14850
+ allFileResults.push({ filePath: file2, results: [] });
14851
+ continue;
14852
+ }
14853
+ const content2 = await readFile(filePath2, "utf-8");
14854
+ let vendor2;
14855
+ if (options.vendor === "auto") {
14856
+ vendor2 = detectVendor(content2);
14857
+ } else {
14858
+ vendor2 = getVendor(options.vendor);
14859
+ }
14860
+ const fileRules = rules.filter(
14861
+ (rule) => ruleAppliesToVendor(rule, vendor2.id)
14862
+ );
14863
+ const parser2 = new SchemaAwareParser({ vendor: vendor2 });
14864
+ const nodes2 = parser2.parse(content2);
14865
+ let results2 = engine2.run(nodes2, fileRules);
14866
+ if (options.quiet) {
14867
+ results2 = results2.filter((r) => !r.passed);
14868
+ }
14869
+ const failures = results2.filter((r) => !r.passed).length;
14870
+ const passed = results2.filter((r) => r.passed).length;
14871
+ totalFailures += failures;
14872
+ totalPassed += passed;
14873
+ allFileResults.push({
14874
+ filePath: filePath2,
14875
+ results: results2,
14876
+ vendor: { id: vendor2.id, name: vendor2.name }
14877
+ });
14878
+ } catch (err) {
14879
+ const errMsg = err instanceof Error ? err.message : "Unknown error";
14880
+ console.error(`Error processing ${basename(file2)}: ${errMsg}`);
14881
+ allFileResults.push({ filePath: file2, results: [] });
14882
+ }
14883
+ }
14884
+ if (options.format === "sarif") {
14885
+ const sarifOptions = {
14886
+ relativePaths: options.relativePaths,
14887
+ baseDir: process.cwd()
14888
+ };
14889
+ console.log(
14890
+ generateMultiFileSarif(allFileResults, rules, sarifOptions)
14891
+ );
14892
+ } else {
14893
+ const output = {
14894
+ summary: {
14895
+ filesScanned: allFileResults.length,
14896
+ totalResults: totalFailures + totalPassed,
14897
+ failures: totalFailures,
14898
+ passed: totalPassed
14899
+ },
14900
+ files: allFileResults.map((fr) => ({
14901
+ file: fr.filePath,
14902
+ vendor: fr.vendor,
14903
+ results: fr.results
14904
+ }))
14905
+ };
14906
+ console.log(JSON.stringify(output, null, 2));
14907
+ }
14908
+ if (totalFailures > 0) {
14909
+ process.exit(1);
14910
+ }
14911
+ return;
14912
+ }
14913
+ const file = files[0];
14495
14914
  const fileValidation = validateInputFilePath(
14496
14915
  file,
14497
14916
  MAX_CONFIG_SIZE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentriflow/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "SentriFlow CLI - Network configuration linter and validator",
5
5
  "license": "Apache-2.0",
6
6
  "main": "dist/index.js",