@vaultcompass/vault-guard-core 1.1.0 → 1.1.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.
@@ -22,6 +22,6 @@ function relativeFingerprintKey(file, cwd) {
22
22
  */
23
23
  function fingerprintForMatch(cwd, fileAbs, m) {
24
24
  const relKey = relativeFingerprintKey(fileAbs, cwd);
25
- const payload = `${relKey}|${m.type}|${m.line}|${m.column}|${m.matchLength}`;
25
+ const payload = `${relKey}|${m.type}|${m.line}|${m.offset}|${m.matchLength}`;
26
26
  return (0, crypto_1.createHash)('sha256').update(payload, 'utf8').digest('hex');
27
27
  }
@@ -33,10 +33,13 @@ export interface JsonOutput {
33
33
  type: string;
34
34
  severity: string;
35
35
  line: number;
36
+ /** 0-based line-relative column. */
36
37
  column: number;
38
+ /** 0-based absolute UTF-16 offset in the scanned content. */
39
+ offset: number;
37
40
  /** Redacted form, e.g. `sk-a…(37c)`. Never the raw secret. */
38
41
  value: string;
39
- /** SHA-256 hex of `relPath|type|line|column|matchLength` for baselines (no raw secret). */
42
+ /** SHA-256 hex of `relPath|type|line|offset|matchLength` for baselines (no raw secret). */
40
43
  fingerprint: string;
41
44
  }>;
42
45
  }>;
@@ -43,6 +43,7 @@ function formatJson(results, opts = {}) {
43
43
  severity: m.severity,
44
44
  line: m.line,
45
45
  column: m.column,
46
+ offset: m.offset,
46
47
  value: m.value,
47
48
  fingerprint: (0, match_fingerprint_1.fingerprintForMatch)(fpCwd, file, m),
48
49
  })),
@@ -30,10 +30,15 @@ const regex_safety_1 = require("../utils/regex-safety");
30
30
  const BUILTIN_PATTERNS = new Map([
31
31
  // --- AI / ML providers ---
32
32
  ['anthropic', { regex: /sk-ant-[a-zA-Z0-9_-]{20,}/g, severity: 'critical' }],
33
- // OpenAI key formats. All modern keys embed the T3BlbkFJ watermark (base64 "OpenAI").
33
+ // OpenAI key formats. All current keys embed the T3BlbkFJ watermark (base64 "OpenAI").
34
34
  // Specific prefixes are ordered first so they get their own rule title (blast radius differs).
35
35
  // The legacy sk- catch-all uses the watermark + token-boundary so it does not shadow the
36
36
  // prefixed rules and avoids matching short benign identifiers.
37
+ //
38
+ // DELIBERATE: we do NOT match the pre-2023 bare `sk-<48 alphanumerics>` format (no watermark).
39
+ // That pattern fires on any base64/hex blob following `sk-` and floods false positives; the
40
+ // watermark is the only reliable discriminator, matching the gitleaks/trufflehog consensus.
41
+ // Do not re-add a bare `sk-[A-Za-z0-9]{N,}` rule without an entropy gate and a bench FP guard.
37
42
  ['openai-project', { regex: /sk-proj-[A-Za-z0-9_-]{20,100}T3BlbkFJ[A-Za-z0-9_-]{20,100}/g, severity: 'critical' }],
38
43
  ['openai-svcacct', { regex: /sk-svcacct-[A-Za-z0-9_-]{20,100}T3BlbkFJ[A-Za-z0-9_-]{20,100}/g, severity: 'critical' }],
39
44
  ['openai-admin', { regex: /sk-admin-[A-Za-z0-9_-]{20,100}T3BlbkFJ[A-Za-z0-9_-]{20,100}/g, severity: 'critical' }],
@@ -307,7 +312,8 @@ class SecretScanner {
307
312
  type,
308
313
  value: this.maskValue(fullMatch),
309
314
  line,
310
- column: match.index,
315
+ column: match.index - (lineIndex[line - 1] ?? 0),
316
+ offset: match.index,
311
317
  matchLength: fullMatch.length,
312
318
  severity,
313
319
  });
@@ -396,15 +402,15 @@ class SecretScanner {
396
402
  if (matches.length <= 1)
397
403
  return matches;
398
404
  // Sort by start offset so we can do a linear sweep.
399
- const sorted = [...matches].sort((a, b) => a.column - b.column || a.line - b.line);
405
+ const sorted = [...matches].sort((a, b) => a.offset - b.offset || a.line - b.line);
400
406
  const kept = [];
401
407
  for (const candidate of sorted) {
402
- const cStart = candidate.column;
408
+ const cStart = candidate.offset;
403
409
  const cEnd = cStart + candidate.matchLength;
404
410
  let dominated = false;
405
411
  for (let i = kept.length - 1; i >= 0; i--) {
406
412
  const existing = kept[i];
407
- const eStart = existing.column;
413
+ const eStart = existing.offset;
408
414
  const eEnd = eStart + existing.matchLength;
409
415
  // No possible overlap once we've passed the candidate start by more
410
416
  // than the max pattern length (optimisation — safe upper bound: 512).
package/dist/types.d.ts CHANGED
@@ -5,9 +5,11 @@ export interface SecretMatch {
5
5
  value: string;
6
6
  /** 1-based line number in the file. */
7
7
  line: number;
8
- /** 0-based absolute byte offset in the file (used for deduplication). */
8
+ /** 0-based line-relative UTF-16 column, for CLI/SARIF/editor display. */
9
9
  column: number;
10
- /** Byte length of the original match (used for SARIF region output). */
10
+ /** 0-based absolute UTF-16 offset in the scanned content, for dedupe/baselines. */
11
+ offset: number;
12
+ /** UTF-16 code-unit length of the original match (used for SARIF region output). */
11
13
  matchLength: number;
12
14
  severity: 'critical' | 'high' | 'medium' | 'low';
13
15
  }
@@ -89,7 +89,7 @@ async function scanTextFileAsync(scanner, filePath, options) {
89
89
  const found = scanner.scanContent(slice).map(m => ({
90
90
  ...m,
91
91
  line: lineNo,
92
- column: utf16Offset + m.column,
92
+ offset: utf16Offset + m.offset,
93
93
  }));
94
94
  raw.push(...found);
95
95
  utf16Offset += line.length + 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaultcompass/vault-guard-core",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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",
@@ -34,7 +34,7 @@
34
34
  "license": "MIT",
35
35
  "devDependencies": {
36
36
  "@types/jest": "^30.0.0",
37
- "@types/node": "^25.9.1",
37
+ "@types/node": "^25.9.3",
38
38
  "jest": "^30.4.2",
39
39
  "ts-jest": "^29.4.11",
40
40
  "typescript": "^5.0.0"