@vaultcompass/vault-guard 1.3.0 → 1.4.1

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/dist/cli-entry.js CHANGED
@@ -8,6 +8,12 @@ if (nodeMajor < 22) {
8
8
  'Some features may not work correctly.');
9
9
  }
10
10
  const program = (0, cli_1.buildCli)();
11
+ // npx (and some wrappers) forward a leading `--` into argv. Commander treats
12
+ // that as end-of-options, so `--format sarif` is ignored and text banners
13
+ // pollute machine-readable stdout. Drop a single leading separator.
14
+ if (process.argv[2] === '--') {
15
+ process.argv.splice(2, 1);
16
+ }
11
17
  // Parse arguments and execute command
12
18
  program.parseAsync().catch((error) => {
13
19
  // Handle errors
package/dist/cli.js CHANGED
@@ -79,9 +79,10 @@ function buildCli() {
79
79
  .argument('[path]', 'Path to scan', '.')
80
80
  .option('-f, --format <format>', 'Output format: text | json | sarif', 'text')
81
81
  .option('--staged', 'Scan git staged files only (uses index vs HEAD)', false)
82
+ .option('--fail-on <severity>', 'Minimum severity that fails the scan: critical | high | medium | low | none (default: medium, or fail_on in .vault-guard.json)')
82
83
  .action(async (path, options) => {
83
84
  const format = options.format ?? 'text';
84
- const exitCode = await (0, scan_1.scanCommand)(path, format, Boolean(options.staged));
85
+ const exitCode = await (0, scan_1.scanCommand)(path, format, Boolean(options.staged), options.failOn);
85
86
  setExitCode(exitCode);
86
87
  });
87
88
  program
@@ -149,8 +150,9 @@ function buildCli() {
149
150
  .command('check')
150
151
  .description('Scan files with config and baselines')
151
152
  .argument('[files...]', 'Files to check')
152
- .action(async (files) => {
153
- const exitCode = await (0, check_1.checkCommand)(files);
153
+ .option('--fail-on <severity>', 'Minimum severity that fails the check: critical | high | medium | low | none (default: medium, or fail_on in .vault-guard.json)')
154
+ .action(async (files, options) => {
155
+ const exitCode = await (0, check_1.checkCommand)(files, options.failOn);
154
156
  setExitCode(exitCode);
155
157
  });
156
158
  program
@@ -1 +1 @@
1
- export declare function checkCommand(files: string[]): Promise<number>;
1
+ export declare function checkCommand(files: string[], failOn?: string): Promise<number>;
@@ -2,6 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkCommand = checkCommand;
4
4
  const scan_1 = require("./scan");
5
- async function checkCommand(files) {
6
- return (0, scan_1.scanCommand)(files.length > 0 ? files : '.', 'text', false);
5
+ async function checkCommand(files, failOn) {
6
+ return (0, scan_1.scanCommand)(files.length > 0 ? files : '.', 'text', false, failOn);
7
7
  }
@@ -1,2 +1,2 @@
1
1
  export type OutputFormat = 'text' | 'json' | 'sarif';
2
- export declare function scanCommand(targetPath: string | string[], format?: OutputFormat, staged?: boolean): Promise<number>;
2
+ export declare function scanCommand(targetPath: string | string[], format?: OutputFormat, staged?: boolean, failOnFlag?: string): Promise<number>;
@@ -7,7 +7,7 @@ exports.scanCommand = scanCommand;
7
7
  const vault_guard_core_1 = require("@vaultcompass/vault-guard-core");
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const scan_utils_1 = require("../utils/scan-utils");
10
- async function scanCommand(targetPath, format = 'text', staged = false) {
10
+ async function scanCommand(targetPath, format = 'text', staged = false, failOnFlag) {
11
11
  const cwd = process.cwd();
12
12
  const targetPaths = Array.isArray(targetPath) ? targetPath : [targetPath];
13
13
  const targetLabel = targetPaths.length === 1 ? targetPaths[0] : `${targetPaths.length} paths`;
@@ -25,6 +25,18 @@ async function scanCommand(targetPath, format = 'text', staged = false) {
25
25
  }
26
26
  throw e;
27
27
  }
28
+ // Resolve the gate threshold before scanning so an invalid value fails fast
29
+ // rather than after a long scan.
30
+ const failOnResolved = (0, vault_guard_core_1.resolveFailOn)(failOnFlag, config.fail_on);
31
+ if (!failOnResolved.ok) {
32
+ console.error(chalk_1.default.red('❌ Invalid fail-on value:'), chalk_1.default.white(failOnResolved.invalid));
33
+ console.error(chalk_1.default.gray(` Expected one of: ${vault_guard_core_1.FAIL_ON_VALUES.join(' | ')}\n`));
34
+ return 1;
35
+ }
36
+ const failOn = failOnResolved.threshold;
37
+ // True when neither the flag nor the config chose a threshold. Drives the
38
+ // 1.4.0 upgrade notice below: users who picked a value have already decided.
39
+ const gateIsImplicitDefault = failOnFlag === undefined && config.fail_on === undefined;
28
40
  const scanner = new vault_guard_core_1.SecretScanner(config);
29
41
  // Merge config ignore paths and patterns into a single list for file filtering.
30
42
  const configIgnorePatterns = [
@@ -125,21 +137,36 @@ async function scanCommand(targetPath, format = 'text', staged = false) {
125
137
  const { results: afterBaseline, suppressed: baselineSuppressed } = (0, vault_guard_core_1.filterResultsByBaseline)(process.cwd(), results, baselineLoad.fingerprints);
126
138
  results = afterBaseline;
127
139
  const durationMs = Date.now() - t0;
140
+ const totalMatches = results.reduce((n, r) => n + r.matches.length, 0);
141
+ const blocking = (0, vault_guard_core_1.countBlockingMatches)(results, failOn);
128
142
  const run = {
129
143
  duration_ms: durationMs,
130
144
  files_scanned: stats.filesScanned,
131
145
  bytes_scanned: stats.bytesScanned,
132
146
  patterns_active: scanner.getActivePatternCount(),
133
147
  diagnostics_count: diagnostics.length,
148
+ fail_on: failOn,
149
+ blocking_matches: blocking,
134
150
  ...(baselineSuppressed > 0 ? { baseline_suppressed: baselineSuppressed } : {}),
135
151
  };
152
+ // Upgrade notice for the 1.4.0 default change. Before 1.4.0 any finding
153
+ // failed the scan; now the implicit default is `medium`. When that
154
+ // difference is what decides this run's outcome (findings exist, none
155
+ // block, and the user never chose a threshold), say so once on stderr —
156
+ // stderr so JSON/SARIF stdout stays parseable, and only for the implicit
157
+ // default so setting `fail_on` anywhere silences it for good.
158
+ if (gateIsImplicitDefault && totalMatches > 0 && blocking === 0) {
159
+ console.error(chalk_1.default.yellow(`note: earlier vault-guard versions failed on any finding; since 1.4.0 the default gate is "medium".`));
160
+ console.error(chalk_1.default.gray(` This run would have failed before. Set "fail_on" in .vault-guard.json ("low" restores the old\n` +
161
+ ` behaviour, "medium" keeps this one) to silence this note.`));
162
+ }
136
163
  if (format === 'json') {
137
164
  process.stdout.write((0, scan_utils_1.formatJson)(results, { diagnostics, run }) + '\n');
138
- return results.reduce((n, r) => n + r.matches.length, 0) === 0 ? 0 : 1;
165
+ return blocking === 0 ? 0 : 1;
139
166
  }
140
167
  if (format === 'sarif') {
141
168
  process.stdout.write((0, scan_utils_1.formatSarif)(results, { diagnostics, run }) + '\n');
142
- return results.reduce((n, r) => n + r.matches.length, 0) === 0 ? 0 : 1;
169
+ return blocking === 0 ? 0 : 1;
143
170
  }
144
171
  // Text mode: print one-line diagnostic summary when any non-fatal issues occurred
145
172
  if (diagnostics.length > 0) {
@@ -149,7 +176,14 @@ async function scanCommand(targetPath, format = 'text', staged = false) {
149
176
  console.log(chalk_1.default.green.bold('✅ SUCCESS:'), chalk_1.default.white('No secrets found\n'));
150
177
  return 0;
151
178
  }
152
- (0, scan_utils_1.displayScanResults)(results);
179
+ (0, scan_utils_1.displayScanResults)(results, blocking);
180
+ if (blocking === 0) {
181
+ // Findings exist but all sit below the gate. Say so explicitly — a silent
182
+ // exit 0 after printing findings reads like a bug.
183
+ console.log(chalk_1.default.white(`${totalMatches} finding(s), none at or above severity "${failOn}" — not failing the gate.`));
184
+ console.log(chalk_1.default.gray(` Tighten with --fail-on low or "fail_on": "low" in .vault-guard.json\n`));
185
+ return 0;
186
+ }
153
187
  return 1;
154
188
  }
155
189
  catch (error) {
@@ -70,4 +70,4 @@ export declare function scanFiles(targetPaths: string[], scanner: SecretScanner,
70
70
  * - The redacted match value (`sk-a…(37c)`) is shown last and intentionally
71
71
  * low-information.
72
72
  */
73
- export declare function displayScanResults(results: ScanResult[]): void;
73
+ export declare function displayScanResults(results: ScanResult[], blocking?: number): void;
@@ -296,13 +296,22 @@ function scanFiles(targetPaths, scanner, options = {}) {
296
296
  * - The redacted match value (`sk-a…(37c)`) is shown last and intentionally
297
297
  * low-information.
298
298
  */
299
- function displayScanResults(results) {
299
+ function displayScanResults(results, blocking) {
300
300
  if (results.length === 0) {
301
301
  console.log(chalk_1.default.green.bold('✅ SUCCESS:'), chalk_1.default.white('No secrets found\n'));
302
302
  return;
303
303
  }
304
304
  const totalSecrets = results.reduce((sum, r) => sum + r.matches.length, 0);
305
- console.log(chalk_1.default.red.bold('🚨 BLOCKED:'), chalk_1.default.white(`Found ${totalSecrets} secret${totalSecrets > 1 ? 's' : ''}\n`));
305
+ // `blocking` is how many findings sit at or above the `--fail-on` threshold.
306
+ // When none do we still list everything, but the headline must not say
307
+ // "BLOCKED" over a run that is about to exit 0.
308
+ const willBlock = blocking === undefined || blocking > 0;
309
+ if (willBlock) {
310
+ console.log(chalk_1.default.red.bold('🚨 BLOCKED:'), chalk_1.default.white(`Found ${totalSecrets} secret${totalSecrets > 1 ? 's' : ''}\n`));
311
+ }
312
+ else {
313
+ console.log(chalk_1.default.yellow.bold('⚠️ REPORT:'), chalk_1.default.white(`Found ${totalSecrets} finding${totalSecrets > 1 ? 's' : ''} below the fail threshold\n`));
314
+ }
306
315
  for (const { file, matches } of results) {
307
316
  const relativePath = relativeForDisplay(file);
308
317
  for (const match of matches) {
@@ -313,7 +322,9 @@ function displayScanResults(results) {
313
322
  }
314
323
  }
315
324
  console.log('');
316
- console.log(chalk_1.default.red.bold('❌ BLOCKED:'), chalk_1.default.white('Commit blocked — remove secrets before pushing\n'));
325
+ if (willBlock) {
326
+ console.log(chalk_1.default.red.bold('❌ BLOCKED:'), chalk_1.default.white('Commit blocked — remove secrets before pushing\n'));
327
+ }
317
328
  }
318
329
  /** cwd-relative when inside cwd, absolute otherwise. Matches scan-output behaviour. */
319
330
  function relativeForDisplay(file) {
@@ -346,8 +357,10 @@ function getSeverityEmoji(severity) {
346
357
  return '⚠️';
347
358
  case 'medium':
348
359
  return 'ℹ️';
360
+ // Not a checkmark: every line here is a finding, and a green tick beside
361
+ // one reads as "this file is clean".
349
362
  case 'low':
350
- return '';
363
+ return '🔵';
351
364
  default:
352
365
  return '•';
353
366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaultcompass/vault-guard",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Block secrets at commit and in CI. Pre-commit hooks, SARIF output, and fast staged-file scans for AI-native workflows.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -36,8 +36,8 @@
36
36
  "dependencies": {
37
37
  "chalk": "^4.1.2",
38
38
  "commander": "^12.0.0",
39
- "@vaultcompass/vault-guard-core": "1.3.0",
40
- "@vaultcompass/vault-guard-telemetry": "1.3.0"
39
+ "@vaultcompass/vault-guard-core": "1.4.1",
40
+ "@vaultcompass/vault-guard-telemetry": "1.4.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/jest": "^30.0.0",