@vaultcompass/vault-guard-core 1.2.3 → 1.3.0

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/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export * from './scan-output';
10
10
  export * from './diagnostics';
11
11
  export { shannonEntropy, DEFAULT_ENTROPY_THRESHOLD } from './utils/entropy';
12
12
  export { isPlaceholderSecret, isNonSecretConnectionString, isSampleJwt, isRedactedTemplateValue, isEnvVarNameToken } from './utils/placeholder';
13
- export { getGitStagedFilePaths, isInsideGitWorkTree } from './utils/git-utils';
13
+ export { getGitStagedFilePaths, readGitIndexFile, isInsideGitWorkTree } from './utils/git-utils';
14
14
  export { validateRegexSafety, validateRegexLength, mapRegexSafetyReasonToDiagnosticCode, mapPatternRejectionReasonToDiagnosticCode, REGEX_REASON_TO_DIAGNOSTIC_CODE, REGEX_MAX_LENGTH, REGEX_MAX_QUANTIFIERS, } from './utils/regex-safety';
15
15
  export { scanTextFileAsync, scanTextFileSync } from './utils/scan-file';
16
16
  export { applyPathAwareSeverity, isTestFilePath } from './utils/path-severity';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.isTestFilePath = exports.applyPathAwareSeverity = exports.scanTextFileSync = exports.scanTextFileAsync = exports.REGEX_MAX_QUANTIFIERS = exports.REGEX_MAX_LENGTH = exports.REGEX_REASON_TO_DIAGNOSTIC_CODE = exports.mapPatternRejectionReasonToDiagnosticCode = exports.mapRegexSafetyReasonToDiagnosticCode = exports.validateRegexLength = exports.validateRegexSafety = exports.isInsideGitWorkTree = exports.getGitStagedFilePaths = exports.isEnvVarNameToken = exports.isRedactedTemplateValue = exports.isSampleJwt = exports.isNonSecretConnectionString = exports.isPlaceholderSecret = exports.DEFAULT_ENTROPY_THRESHOLD = exports.shannonEntropy = exports.fingerprintForMatch = void 0;
17
+ exports.isTestFilePath = exports.applyPathAwareSeverity = exports.scanTextFileSync = exports.scanTextFileAsync = exports.REGEX_MAX_QUANTIFIERS = exports.REGEX_MAX_LENGTH = exports.REGEX_REASON_TO_DIAGNOSTIC_CODE = exports.mapPatternRejectionReasonToDiagnosticCode = exports.mapRegexSafetyReasonToDiagnosticCode = exports.validateRegexLength = exports.validateRegexSafety = exports.isInsideGitWorkTree = exports.readGitIndexFile = exports.getGitStagedFilePaths = exports.isEnvVarNameToken = exports.isRedactedTemplateValue = exports.isSampleJwt = exports.isNonSecretConnectionString = exports.isPlaceholderSecret = exports.DEFAULT_ENTROPY_THRESHOLD = exports.shannonEntropy = exports.fingerprintForMatch = void 0;
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./errors"), exports);
20
20
  __exportStar(require("./scanners"), exports);
@@ -37,6 +37,7 @@ Object.defineProperty(exports, "isRedactedTemplateValue", { enumerable: true, ge
37
37
  Object.defineProperty(exports, "isEnvVarNameToken", { enumerable: true, get: function () { return placeholder_1.isEnvVarNameToken; } });
38
38
  var git_utils_1 = require("./utils/git-utils");
39
39
  Object.defineProperty(exports, "getGitStagedFilePaths", { enumerable: true, get: function () { return git_utils_1.getGitStagedFilePaths; } });
40
+ Object.defineProperty(exports, "readGitIndexFile", { enumerable: true, get: function () { return git_utils_1.readGitIndexFile; } });
40
41
  Object.defineProperty(exports, "isInsideGitWorkTree", { enumerable: true, get: function () { return git_utils_1.isInsideGitWorkTree; } });
41
42
  var regex_safety_1 = require("./utils/regex-safety");
42
43
  Object.defineProperty(exports, "validateRegexSafety", { enumerable: true, get: function () { return regex_safety_1.validateRegexSafety; } });
@@ -18,6 +18,10 @@ export declare class PreCommitHook {
18
18
  * Absolute path to the \`pre-commit\` hook file for the given manager.
19
19
  */
20
20
  getPreCommitHookPath(cwd: string, manager?: HookManager): string;
21
+ /**
22
+ * Absolute path to the Windows \`pre-commit.cmd\` companion (native manager only).
23
+ */
24
+ getPreCommitCmdPath(cwd: string): string;
21
25
  install(options?: InstallHookOptions): {
22
26
  success: boolean;
23
27
  message: string;
@@ -28,6 +32,12 @@ export declare class PreCommitHook {
28
32
  message: string;
29
33
  };
30
34
  isInstalled(options?: InstallHookOptions): boolean;
35
+ /**
36
+ * Write or refresh our `pre-commit.cmd`. Never overwrites a foreign file.
37
+ * @returns whether our companion is present afterwards
38
+ */
39
+ private writeNativeCmdCompanion;
40
+ private removeNativeCmdCompanion;
31
41
  private installNative;
32
42
  private uninstallNative;
33
43
  private installHusky;
@@ -35,6 +35,30 @@ echo "❌ COMMIT BLOCKED: secrets detected in staged files"
35
35
  echo "💡 Fix or unstage, then retry. Emergency bypass (discouraged): git commit --no-verify"
36
36
  exit 1
37
37
  `;
38
+ /**
39
+ * Optional `pre-commit.cmd` beside the POSIX hook. Git for Windows runs the
40
+ * extensionless `pre-commit` via sh; a few clients may invoke `.cmd` directly.
41
+ */
42
+ const NATIVE_HOOK_CMD = `@echo off
43
+ REM vault-guard pre-commit (installed by @vaultcompass/vault-guard)
44
+ where vault-guard >nul 2>&1
45
+ if errorlevel 1 (
46
+ echo ❌ vault-guard: command not found ^(install: npm i -g @vaultcompass/vault-guard^)
47
+ exit /b 1
48
+ )
49
+
50
+ echo 🔍 vault-guard: scanning staged files...
51
+ call vault-guard scan --staged
52
+ if errorlevel 1 (
53
+ echo.
54
+ echo ❌ COMMIT BLOCKED: secrets detected in staged files
55
+ echo 💡 Fix or unstage, then retry. Emergency bypass ^(discouraged^): git commit --no-verify
56
+ exit /b 1
57
+ )
58
+
59
+ echo ✅ vault-guard: no secrets in staged files
60
+ exit /b 0
61
+ `;
38
62
  /** Husky-friendly hook (sources \`_/husky.sh\` when present). */
39
63
  const HUSKY_HOOK_SCRIPT = `#!/usr/bin/env sh
40
64
  if [ -f "$(dirname "$0")/_/husky.sh" ]; then
@@ -110,6 +134,12 @@ class PreCommitHook {
110
134
  }
111
135
  return path_1.default.join(this.getEffectiveHooksDir(cwd).hooksDir, 'pre-commit');
112
136
  }
137
+ /**
138
+ * Absolute path to the Windows \`pre-commit.cmd\` companion (native manager only).
139
+ */
140
+ getPreCommitCmdPath(cwd) {
141
+ return path_1.default.join(this.getEffectiveHooksDir(cwd).hooksDir, 'pre-commit.cmd');
142
+ }
113
143
  install(options = {}) {
114
144
  const cwd = options.cwd ?? process.cwd();
115
145
  const manager = options.manager ?? 'native';
@@ -160,9 +190,37 @@ class PreCommitHook {
160
190
  // -------------------------------------------------------------------------
161
191
  // native (Git hooks / core.hooksPath)
162
192
  // -------------------------------------------------------------------------
193
+ /**
194
+ * Write or refresh our `pre-commit.cmd`. Never overwrites a foreign file.
195
+ * @returns whether our companion is present afterwards
196
+ */
197
+ writeNativeCmdCompanion(hooksDir) {
198
+ const cmdPath = path_1.default.join(hooksDir, 'pre-commit.cmd');
199
+ if (fs_1.default.existsSync(cmdPath)) {
200
+ const existing = fs_1.default.readFileSync(cmdPath, 'utf-8');
201
+ const isOurs = existing.includes('vault-guard') && existing.includes('scan --staged');
202
+ if (!isOurs) {
203
+ return false;
204
+ }
205
+ }
206
+ fs_1.default.writeFileSync(cmdPath, NATIVE_HOOK_CMD, { encoding: 'utf-8' });
207
+ return true;
208
+ }
209
+ removeNativeCmdCompanion(hooksDir) {
210
+ const cmdPath = path_1.default.join(hooksDir, 'pre-commit.cmd');
211
+ if (!fs_1.default.existsSync(cmdPath))
212
+ return false;
213
+ const content = fs_1.default.readFileSync(cmdPath, 'utf-8');
214
+ if (!content.includes('vault-guard') || !content.includes('scan --staged')) {
215
+ return false;
216
+ }
217
+ fs_1.default.unlinkSync(cmdPath);
218
+ return true;
219
+ }
163
220
  installNative(cwd) {
164
221
  const { hooksDir, viaHooksPath } = this.getEffectiveHooksDir(cwd);
165
222
  const hookPath = path_1.default.join(hooksDir, 'pre-commit');
223
+ const cmdPath = path_1.default.join(hooksDir, 'pre-commit.cmd');
166
224
  try {
167
225
  if (!fs_1.default.existsSync(hooksDir)) {
168
226
  fs_1.default.mkdirSync(hooksDir, { recursive: true });
@@ -170,18 +228,31 @@ class PreCommitHook {
170
228
  if (fs_1.default.existsSync(hookPath)) {
171
229
  const existing = fs_1.default.readFileSync(hookPath, 'utf-8');
172
230
  if (existing.includes('vault-guard') && existing.includes('scan --staged')) {
231
+ // Refresh the Windows companion if missing or stale (never clobber foreign).
232
+ const cmdOk = this.writeNativeCmdCompanion(hooksDir);
173
233
  return {
174
234
  success: true,
175
- message: 'Hook already installed',
235
+ message: cmdOk
236
+ ? 'Hook already installed (POSIX + Windows .cmd companion)'
237
+ : fs_1.default.existsSync(cmdPath)
238
+ ? 'Hook already installed (left foreign pre-commit.cmd untouched)'
239
+ : 'Hook already installed',
176
240
  hookPath,
177
241
  };
178
242
  }
179
243
  }
180
244
  fs_1.default.writeFileSync(hookPath, NATIVE_HOOK_SCRIPT, { mode: 0o755 });
245
+ const cmdOk = this.writeNativeCmdCompanion(hooksDir);
181
246
  const hint = viaHooksPath
182
247
  ? `Installed to hooksPath: ${hooksDir}`
183
- : 'Installed to .git/hooks/pre-commit';
184
- return { success: true, message: `Pre-commit hook installed (${hint})`, hookPath };
248
+ : cmdOk
249
+ ? 'Installed to .git/hooks/pre-commit (+ pre-commit.cmd)'
250
+ : 'Installed to .git/hooks/pre-commit';
251
+ return {
252
+ success: true,
253
+ message: `Pre-commit hook installed (${hint})`,
254
+ hookPath,
255
+ };
185
256
  }
186
257
  catch (error) {
187
258
  const hookError = new errors_1.HookError(`Failed to install hook: ${error}`, 'install');
@@ -189,17 +260,34 @@ class PreCommitHook {
189
260
  }
190
261
  }
191
262
  uninstallNative(cwd) {
192
- const hookPath = this.getPreCommitHookPath(cwd, 'native');
263
+ const { hooksDir } = this.getEffectiveHooksDir(cwd);
264
+ const hookPath = path_1.default.join(hooksDir, 'pre-commit');
265
+ const cmdRemoved = this.removeNativeCmdCompanion(hooksDir);
193
266
  if (!fs_1.default.existsSync(hookPath)) {
194
- return { success: true, message: 'No hook to remove' };
267
+ return {
268
+ success: true,
269
+ message: cmdRemoved
270
+ ? 'Removed Windows pre-commit.cmd companion'
271
+ : 'No hook to remove',
272
+ };
195
273
  }
196
274
  const content = fs_1.default.readFileSync(hookPath, 'utf-8');
197
275
  if (!content.includes('vault-guard')) {
198
- return { success: true, message: 'No vault-guard hook to remove' };
276
+ return {
277
+ success: true,
278
+ message: cmdRemoved
279
+ ? 'Removed Windows pre-commit.cmd companion'
280
+ : 'No vault-guard hook to remove',
281
+ };
199
282
  }
200
283
  try {
201
284
  fs_1.default.unlinkSync(hookPath);
202
- return { success: true, message: 'Pre-commit hook removed' };
285
+ return {
286
+ success: true,
287
+ message: cmdRemoved
288
+ ? 'Pre-commit hook and Windows .cmd companion removed'
289
+ : 'Pre-commit hook removed',
290
+ };
203
291
  }
204
292
  catch (error) {
205
293
  const hookError = new errors_1.HookError(`Failed to remove hook: ${error}`, 'uninstall');
@@ -96,8 +96,9 @@ class TokenCounter {
96
96
  return ['.png', '.jpg', '.jpeg', '.gif', '.pdf', '.zip', '.lock', '.log'].includes(ext);
97
97
  }
98
98
  getExtension(filePath) {
99
- const parts = filePath.split('.');
100
- return parts.length > 1 ? `.${parts[parts.length - 1]}` : '(no ext)';
99
+ // Use basename so parent dirs with '.' (e.g. macOS temp paths) are ignored.
100
+ const ext = path_1.default.extname(path_1.default.basename(filePath));
101
+ return ext.length > 0 ? ext : '(no ext)';
101
102
  }
102
103
  }
103
104
  exports.TokenCounter = TokenCounter;
@@ -1,11 +1,20 @@
1
1
  /**
2
2
  * Return absolute paths of files staged for commit (cached index vs HEAD).
3
- * Excludes deleted paths; only returns paths that still exist on disk.
3
+ *
4
+ * Uses `--diff-filter=ACMRT` so deleted index entries are excluded, but
5
+ * **does not** require the path to exist in the worktree. A staged add that
6
+ * was later deleted from disk (`AD` in `git status`) still appears — that
7
+ * blob will be committed and must be scanned.
4
8
  *
5
9
  * Throws `GitError` on git failure rather than returning an empty list.
6
10
  * Returning `[]` silently on git failure would produce a false "✅ nothing
7
11
  * staged" result in pre-commit, letting secrets through undetected.
8
12
  */
9
13
  export declare function getGitStagedFilePaths(cwd?: string): string[];
14
+ /**
15
+ * Read a staged blob from the index (`git show :path`), not the worktree.
16
+ * `relativePath` may use OS separators; it is normalized to git's `/` form.
17
+ */
18
+ export declare function readGitIndexFile(cwd: string, relativePath: string): string;
10
19
  /** True when `cwd` is inside a work tree with a `.git` directory or file. */
11
20
  export declare function isInsideGitWorkTree(cwd?: string): boolean;
@@ -4,24 +4,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getGitStagedFilePaths = getGitStagedFilePaths;
7
+ exports.readGitIndexFile = readGitIndexFile;
7
8
  exports.isInsideGitWorkTree = isInsideGitWorkTree;
8
9
  const child_process_1 = require("child_process");
9
- const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const errors_1 = require("../errors");
12
12
  /**
13
13
  * Return absolute paths of files staged for commit (cached index vs HEAD).
14
- * Excludes deleted paths; only returns paths that still exist on disk.
14
+ *
15
+ * Uses `--diff-filter=ACMRT` so deleted index entries are excluded, but
16
+ * **does not** require the path to exist in the worktree. A staged add that
17
+ * was later deleted from disk (`AD` in `git status`) still appears — that
18
+ * blob will be committed and must be scanned.
15
19
  *
16
20
  * Throws `GitError` on git failure rather than returning an empty list.
17
21
  * Returning `[]` silently on git failure would produce a false "✅ nothing
18
22
  * staged" result in pre-commit, letting secrets through undetected.
19
23
  */
20
24
  function getGitStagedFilePaths(cwd = process.cwd()) {
21
- const cmd = 'git diff --cached --name-only --diff-filter=ACMRT';
25
+ const args = ['diff', '--cached', '--name-only', '--diff-filter=ACMRT', '-z'];
22
26
  let out;
23
27
  try {
24
- out = (0, child_process_1.execSync)(cmd, {
28
+ out = (0, child_process_1.execFileSync)('git', args, {
25
29
  cwd,
26
30
  encoding: 'utf-8',
27
31
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -29,19 +33,37 @@ function getGitStagedFilePaths(cwd = process.cwd()) {
29
33
  }
30
34
  catch (err) {
31
35
  throw new errors_1.GitError(`Failed to list staged files — is this a git repository? (cwd: ${cwd})\n` +
32
- `Run 'git status' to verify.\nUnderlying error: ${String(err)}`, cmd, err);
36
+ `Run 'git status' to verify.\nUnderlying error: ${String(err)}`, `git ${args.join(' ')}`, err);
33
37
  }
34
38
  return out
35
- .split('\n')
39
+ .split('\0')
36
40
  .map((line) => line.trim())
37
41
  .filter(Boolean)
38
- .map((rel) => path_1.default.resolve(cwd, rel))
39
- .filter((abs) => fs_1.default.existsSync(abs) && fs_1.default.statSync(abs).isFile());
42
+ .map((rel) => path_1.default.resolve(cwd, rel));
43
+ }
44
+ /**
45
+ * Read a staged blob from the index (`git show :path`), not the worktree.
46
+ * `relativePath` may use OS separators; it is normalized to git's `/` form.
47
+ */
48
+ function readGitIndexFile(cwd, relativePath) {
49
+ const normalized = relativePath.split(path_1.default.sep).join('/');
50
+ const args = ['show', `:${normalized}`];
51
+ try {
52
+ return (0, child_process_1.execFileSync)('git', args, {
53
+ cwd,
54
+ encoding: 'utf-8',
55
+ stdio: ['ignore', 'pipe', 'pipe'],
56
+ maxBuffer: 32 * 1024 * 1024,
57
+ });
58
+ }
59
+ catch (err) {
60
+ throw new errors_1.GitError(`Failed to read staged blob for ${normalized}\nUnderlying error: ${String(err)}`, `git ${args.join(' ')}`, err);
61
+ }
40
62
  }
41
63
  /** True when `cwd` is inside a work tree with a `.git` directory or file. */
42
64
  function isInsideGitWorkTree(cwd = process.cwd()) {
43
65
  try {
44
- (0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', {
66
+ (0, child_process_1.execFileSync)('git', ['rev-parse', '--is-inside-work-tree'], {
45
67
  cwd,
46
68
  encoding: 'utf-8',
47
69
  stdio: ['ignore', 'pipe', 'pipe'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaultcompass/vault-guard-core",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "Secret-scanning engine: vendor-anchored patterns, entropy gating, baselines, SARIF/JSON, hook helpers.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",