@vaultcompass/vault-guard-core 1.3.0 → 1.4.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/config-validate.js +5 -0
- package/dist/config.d.ts +8 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +12 -1
- package/dist/scan-output.d.ts +8 -0
- package/dist/scanners/pre-commit-hook.js +10 -2
- package/dist/scanners/secret-scanner.js +66 -1
- package/dist/utils/fail-on.d.ts +47 -0
- package/dist/utils/fail-on.js +79 -0
- package/dist/utils/path-severity.d.ts +10 -0
- package/dist/utils/path-severity.js +35 -1
- package/dist/utils/placeholder.d.ts +50 -0
- package/dist/utils/placeholder.js +108 -1
- package/package.json +1 -1
package/dist/config-validate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.validateVaultGuardConfig = validateVaultGuardConfig;
|
|
4
|
+
const fail_on_1 = require("./utils/fail-on");
|
|
4
5
|
const SEVERITIES = new Set(['critical', 'high', 'medium', 'low', 'off']);
|
|
5
6
|
/**
|
|
6
7
|
* Structural validation for parsed `.vault-guard.json` (no file I/O).
|
|
@@ -19,6 +20,7 @@ function validateVaultGuardConfig(value) {
|
|
|
19
20
|
'extra_patterns',
|
|
20
21
|
'extra_patterns_unsafe',
|
|
21
22
|
'entropy_threshold',
|
|
23
|
+
'fail_on',
|
|
22
24
|
]);
|
|
23
25
|
if (!allowed.has(key)) {
|
|
24
26
|
errors.push(`unknown top-level key: ${JSON.stringify(key)}`);
|
|
@@ -67,6 +69,9 @@ function validateVaultGuardConfig(value) {
|
|
|
67
69
|
errors.push('entropy_threshold must be a finite number');
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
if (o.fail_on !== undefined && !(0, fail_on_1.isFailOnThreshold)(o.fail_on)) {
|
|
73
|
+
errors.push('fail_on must be critical|high|medium|low|none');
|
|
74
|
+
}
|
|
70
75
|
if (o.extra_patterns !== undefined) {
|
|
71
76
|
if (!Array.isArray(o.extra_patterns)) {
|
|
72
77
|
errors.push('extra_patterns must be an array');
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SecretMatch } from './types';
|
|
2
|
+
import type { FailOnThreshold } from './utils/fail-on';
|
|
2
3
|
/**
|
|
3
4
|
* Shape of .vault-guard.json in a repository root.
|
|
4
5
|
*
|
|
@@ -40,6 +41,13 @@ export interface VaultGuardConfig {
|
|
|
40
41
|
* generic catch-all patterns. Lower = more matches but more false positives.
|
|
41
42
|
*/
|
|
42
43
|
entropy_threshold?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Minimum severity that fails the scan (non-zero exit). Findings below the
|
|
46
|
+
* threshold are still reported in text/JSON/SARIF, they just do not break
|
|
47
|
+
* the build. `"none"` reports everything and never fails, for advisory
|
|
48
|
+
* rollouts. Defaults to `"medium"`; overridden by `--fail-on`.
|
|
49
|
+
*/
|
|
50
|
+
fail_on?: FailOnThreshold;
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
45
53
|
* Directories searched for `.vault-guard.json` / baseline files, in order
|
package/dist/index.d.ts
CHANGED
|
@@ -9,8 +9,9 @@ export { fingerprintForMatch } from './match-fingerprint';
|
|
|
9
9
|
export * from './scan-output';
|
|
10
10
|
export * from './diagnostics';
|
|
11
11
|
export { shannonEntropy, DEFAULT_ENTROPY_THRESHOLD } from './utils/entropy';
|
|
12
|
-
export { isPlaceholderSecret, isNonSecretConnectionString, isSampleJwt, isRedactedTemplateValue, isEnvVarNameToken } from './utils/placeholder';
|
|
12
|
+
export { isPlaceholderSecret, isNonSecretConnectionString, isSampleJwt, isRedactedTemplateValue, isEnvVarNameToken, isCodeIdentifierReference, isPasswordHash, isPemHeaderWithoutBody } from './utils/placeholder';
|
|
13
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
|
-
export { applyPathAwareSeverity, isTestFilePath } from './utils/path-severity';
|
|
16
|
+
export { applyPathAwareSeverity, isTestFilePath, isLocalePath } from './utils/path-severity';
|
|
17
|
+
export { DEFAULT_FAIL_ON, FAIL_ON_VALUES, isFailOnThreshold, meetsFailThreshold, countBlockingMatches, resolveFailOn, type FailOnThreshold, } from './utils/fail-on';
|
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.readGitIndexFile = exports.getGitStagedFilePaths = exports.isEnvVarNameToken = exports.isRedactedTemplateValue = exports.isSampleJwt = exports.isNonSecretConnectionString = exports.isPlaceholderSecret = exports.DEFAULT_ENTROPY_THRESHOLD = exports.shannonEntropy = exports.fingerprintForMatch = void 0;
|
|
17
|
+
exports.resolveFailOn = exports.countBlockingMatches = exports.meetsFailThreshold = exports.isFailOnThreshold = exports.FAIL_ON_VALUES = exports.DEFAULT_FAIL_ON = exports.isLocalePath = 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.isPemHeaderWithoutBody = exports.isPasswordHash = exports.isCodeIdentifierReference = 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);
|
|
@@ -35,6 +35,9 @@ Object.defineProperty(exports, "isNonSecretConnectionString", { enumerable: true
|
|
|
35
35
|
Object.defineProperty(exports, "isSampleJwt", { enumerable: true, get: function () { return placeholder_1.isSampleJwt; } });
|
|
36
36
|
Object.defineProperty(exports, "isRedactedTemplateValue", { enumerable: true, get: function () { return placeholder_1.isRedactedTemplateValue; } });
|
|
37
37
|
Object.defineProperty(exports, "isEnvVarNameToken", { enumerable: true, get: function () { return placeholder_1.isEnvVarNameToken; } });
|
|
38
|
+
Object.defineProperty(exports, "isCodeIdentifierReference", { enumerable: true, get: function () { return placeholder_1.isCodeIdentifierReference; } });
|
|
39
|
+
Object.defineProperty(exports, "isPasswordHash", { enumerable: true, get: function () { return placeholder_1.isPasswordHash; } });
|
|
40
|
+
Object.defineProperty(exports, "isPemHeaderWithoutBody", { enumerable: true, get: function () { return placeholder_1.isPemHeaderWithoutBody; } });
|
|
38
41
|
var git_utils_1 = require("./utils/git-utils");
|
|
39
42
|
Object.defineProperty(exports, "getGitStagedFilePaths", { enumerable: true, get: function () { return git_utils_1.getGitStagedFilePaths; } });
|
|
40
43
|
Object.defineProperty(exports, "readGitIndexFile", { enumerable: true, get: function () { return git_utils_1.readGitIndexFile; } });
|
|
@@ -53,3 +56,11 @@ Object.defineProperty(exports, "scanTextFileSync", { enumerable: true, get: func
|
|
|
53
56
|
var path_severity_1 = require("./utils/path-severity");
|
|
54
57
|
Object.defineProperty(exports, "applyPathAwareSeverity", { enumerable: true, get: function () { return path_severity_1.applyPathAwareSeverity; } });
|
|
55
58
|
Object.defineProperty(exports, "isTestFilePath", { enumerable: true, get: function () { return path_severity_1.isTestFilePath; } });
|
|
59
|
+
Object.defineProperty(exports, "isLocalePath", { enumerable: true, get: function () { return path_severity_1.isLocalePath; } });
|
|
60
|
+
var fail_on_1 = require("./utils/fail-on");
|
|
61
|
+
Object.defineProperty(exports, "DEFAULT_FAIL_ON", { enumerable: true, get: function () { return fail_on_1.DEFAULT_FAIL_ON; } });
|
|
62
|
+
Object.defineProperty(exports, "FAIL_ON_VALUES", { enumerable: true, get: function () { return fail_on_1.FAIL_ON_VALUES; } });
|
|
63
|
+
Object.defineProperty(exports, "isFailOnThreshold", { enumerable: true, get: function () { return fail_on_1.isFailOnThreshold; } });
|
|
64
|
+
Object.defineProperty(exports, "meetsFailThreshold", { enumerable: true, get: function () { return fail_on_1.meetsFailThreshold; } });
|
|
65
|
+
Object.defineProperty(exports, "countBlockingMatches", { enumerable: true, get: function () { return fail_on_1.countBlockingMatches; } });
|
|
66
|
+
Object.defineProperty(exports, "resolveFailOn", { enumerable: true, get: function () { return fail_on_1.resolveFailOn; } });
|
package/dist/scan-output.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface JsonRunMetadata {
|
|
|
17
17
|
diagnostics_count?: number;
|
|
18
18
|
/** Matches removed because they appeared in `.vault-guard.baseline.json`. */
|
|
19
19
|
baseline_suppressed?: number;
|
|
20
|
+
/** Effective gate threshold for this run (`--fail-on` / `fail_on` / default). */
|
|
21
|
+
fail_on?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Matches at or above {@link fail_on}. This, not the total match count, is
|
|
24
|
+
* what drives the process exit code; integrators gating a build should read
|
|
25
|
+
* this field rather than `summary.secrets`.
|
|
26
|
+
*/
|
|
27
|
+
blocking_matches?: number;
|
|
20
28
|
}
|
|
21
29
|
export interface JsonOutput {
|
|
22
30
|
version: string;
|
|
@@ -16,8 +16,16 @@ const NATIVE_HOOK_SCRIPT = `#!/bin/sh
|
|
|
16
16
|
# vault-guard pre-commit (installed by @vaultcompass/vault-guard)
|
|
17
17
|
set -e
|
|
18
18
|
|
|
19
|
-
# Re-attach stdin for GUI git clients.
|
|
20
|
-
|
|
19
|
+
# Re-attach stdin for GUI git clients when possible.
|
|
20
|
+
# dash (Ubuntu /bin/sh) exits the whole shell on a failed \`exec </dev/tty\`
|
|
21
|
+
# even with \`|| true\` / \`set +e\` — exit status 2. Probe in a subshell first;
|
|
22
|
+
# only \`exec\` in the current shell when that open succeeds. \`[ -r /dev/tty ]\`
|
|
23
|
+
# is not a usable guard: the node can exist and still fail open with ENXIO.
|
|
24
|
+
if [ ! -t 0 ]; then
|
|
25
|
+
if (exec </dev/tty) 2>/dev/null; then
|
|
26
|
+
exec </dev/tty
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
21
29
|
|
|
22
30
|
if ! command -v vault-guard >/dev/null 2>&1; then
|
|
23
31
|
echo "❌ vault-guard: command not found (install: npm i -g @vaultcompass/vault-guard)"
|
|
@@ -47,7 +47,26 @@ const BUILTIN_PATTERNS = new Map([
|
|
|
47
47
|
['openai', { regex: /(?<![A-Za-z0-9_-])sk-[a-zA-Z0-9]{20}T3BlbkFJ[a-zA-Z0-9]{20,}/g, severity: 'critical' }],
|
|
48
48
|
['huggingface', { regex: /hf_[a-zA-Z0-9]{34,}/g, severity: 'critical' }],
|
|
49
49
|
['replicate', { regex: /r8_[a-zA-Z0-9]{32}/g, severity: 'critical' }],
|
|
50
|
+
// Post-2023 AI provider keys. The product's stated wedge is AI-assisted
|
|
51
|
+
// coding, so these carry the same weight as the OpenAI/Anthropic rules.
|
|
52
|
+
// Every one is prefix-anchored with a fixed length, so no entropy gate is
|
|
53
|
+
// needed (same precision profile as `ghp_` / `hf_`).
|
|
54
|
+
['groq', { regex: /(?<![A-Za-z0-9_-])gsk_[a-zA-Z0-9]{52}/g, severity: 'critical' }],
|
|
55
|
+
['openrouter', { regex: /sk-or-v1-[a-f0-9]{64}/g, severity: 'critical' }],
|
|
56
|
+
['xai', { regex: /(?<![A-Za-z0-9_-])xai-[a-zA-Z0-9]{80}/g, severity: 'critical' }],
|
|
57
|
+
['perplexity', { regex: /(?<![A-Za-z0-9_-])pplx-[a-zA-Z0-9]{40,}/g, severity: 'critical' }],
|
|
58
|
+
['mistral', { regex: /(?:mistral_api_key|MISTRAL_API_KEY)\s*[=:]\s*["']?([a-zA-Z0-9]{32})/g, severity: 'critical' }],
|
|
59
|
+
['together-ai', { regex: /(?:together_api_key|TOGETHER_API_KEY)\s*[=:]\s*["']?([a-f0-9]{64})/g, severity: 'critical' }],
|
|
60
|
+
['fireworks-ai', { regex: /(?<![A-Za-z0-9_-])fw_[a-zA-Z0-9]{24,}/g, severity: 'critical' }],
|
|
61
|
+
['langsmith', { regex: /lsv2_(?:pt|sk)_[a-f0-9]{32}_[a-f0-9]{10}/g, severity: 'critical' }],
|
|
62
|
+
['deepseek', { regex: /(?:deepseek_api_key|DEEPSEEK_API_KEY)\s*[=:]\s*["']?(sk-[a-f0-9]{32})/g, severity: 'critical' }],
|
|
50
63
|
// --- Payment processors ---
|
|
64
|
+
// NOTE: `sk_live_` / `sk_test_` are not unique to Stripe — Clerk uses the
|
|
65
|
+
// same prefixes and there is no reliable discriminator in the key body, so a
|
|
66
|
+
// Clerk secret key is reported under the `stripe` rule id. The finding is
|
|
67
|
+
// correct (it IS a live secret key); only the vendor label may be wrong. The
|
|
68
|
+
// id is kept as-is because baseline fingerprints include the rule id and
|
|
69
|
+
// renaming it would silently invalidate every existing baseline entry.
|
|
51
70
|
['stripe', { regex: /sk_live_[a-zA-Z0-9]{24,}/g, severity: 'critical' }],
|
|
52
71
|
['stripe-test', { regex: /sk_test_[a-zA-Z0-9]{24,}/g, severity: 'high' }],
|
|
53
72
|
['paypal', { regex: /access_token\$production\$[a-zA-Z0-9]{20,}/g, severity: 'critical' }],
|
|
@@ -87,12 +106,34 @@ const BUILTIN_PATTERNS = new Map([
|
|
|
87
106
|
['mailgun-api', { regex: /key-[a-zA-Z0-9]{32}/g, severity: 'critical', minEntropy: 3.5 }],
|
|
88
107
|
// --- Package managers ---
|
|
89
108
|
['npm-token', { regex: /npm_[a-zA-Z0-9]{36}/g, severity: 'critical' }],
|
|
109
|
+
// --- Backend / infra platforms ---
|
|
110
|
+
['supabase-token', { regex: /(?<![A-Za-z0-9_-])sbp_[a-f0-9]{40}/g, severity: 'critical' }],
|
|
111
|
+
['supabase-secret', { regex: /(?<![A-Za-z0-9_-])sb_secret_[a-zA-Z0-9_-]{20,}/g, severity: 'critical' }],
|
|
112
|
+
['vercel-blob', { regex: /vercel_blob_rw_[a-zA-Z0-9]{20,}_[a-zA-Z0-9]{20,}/g, severity: 'critical' }],
|
|
113
|
+
['planetscale', { regex: /pscale_(?:tkn|pw)_[a-zA-Z0-9_-]{32,}/g, severity: 'critical' }],
|
|
114
|
+
['doppler-token', { regex: /dp\.(?:pt|st|sa|scim|audit)\.[a-zA-Z0-9]{40,}/g, severity: 'critical' }],
|
|
115
|
+
['databricks-token', { regex: /(?<![A-Za-z0-9_-])dapi[a-f0-9]{32}/g, severity: 'critical' }],
|
|
116
|
+
['cloudflare-token', { regex: /(?:cloudflare_api_token|CLOUDFLARE_API_TOKEN)\s*[=:]\s*["']?([a-zA-Z0-9_-]{40})/g, severity: 'critical' }],
|
|
117
|
+
// --- SaaS / productivity ---
|
|
118
|
+
['notion-token', { regex: /(?<![A-Za-z0-9_-])(?:ntn_[a-zA-Z0-9]{40,}|secret_[a-zA-Z0-9]{43})/g, severity: 'critical' }],
|
|
119
|
+
['airtable-pat', { regex: /(?<![A-Za-z0-9_-])pat[a-zA-Z0-9]{14}\.[a-f0-9]{64}/g, severity: 'critical' }],
|
|
120
|
+
['figma-token', { regex: /(?<![A-Za-z0-9_-])figd_[a-zA-Z0-9_-]{40,}/g, severity: 'critical' }],
|
|
90
121
|
// --- Monitoring ---
|
|
91
122
|
['newrelic-api', { regex: /NRAK-[a-zA-Z0-9]{26}/g, severity: 'critical' }],
|
|
123
|
+
// A Sentry DSN is designed to be embedded in client-side bundles — the
|
|
124
|
+
// public key it carries only permits event ingestion, not data read. Kept at
|
|
125
|
+
// `low` for visibility under the same policy as `gcp-oauth`: real, but not a
|
|
126
|
+
// credential leak worth blocking a commit over.
|
|
127
|
+
['sentry-dsn', { regex: /https:\/\/[a-f0-9]{32}@o\d+\.ingest\.(?:[a-z]{2}\.)?sentry\.io\/\d+/g, severity: 'low' }],
|
|
92
128
|
// --- E-commerce ---
|
|
93
129
|
['shopify-admin', { regex: /shp(?:ss|at|ca)_[a-zA-Z0-9]{32}/g, severity: 'critical' }],
|
|
94
130
|
// --- Keys and auth tokens ---
|
|
95
|
-
|
|
131
|
+
// The algorithm prefix is optional. `-----BEGIN PRIVATE KEY-----` (PKCS#8)
|
|
132
|
+
// has no prefix at all, and it is what modern OpenSSL emits by default and
|
|
133
|
+
// what GCP service-account JSON embeds — i.e. the most common private key
|
|
134
|
+
// form in circulation. Requiring `[A-Z ]+` between BEGIN and PRIVATE meant
|
|
135
|
+
// the scanner printed "No secrets found" on a bare PKCS#8 key file.
|
|
136
|
+
['ssh-private-key', { regex: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g, severity: 'critical' }],
|
|
96
137
|
['jwt-token', { regex: /eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, severity: 'high' }],
|
|
97
138
|
// Generic patterns — entropy-gated AND placeholder-filtered (aggressive) to
|
|
98
139
|
// suppress false positives on documentation samples and unit-test fixtures.
|
|
@@ -292,6 +333,17 @@ class SecretScanner {
|
|
|
292
333
|
if (type === 'jwt-token' && (0, placeholder_1.isSampleJwt)(fullMatch)) {
|
|
293
334
|
continue;
|
|
294
335
|
}
|
|
336
|
+
// A password hash is the safe-at-rest form, not a usable credential.
|
|
337
|
+
// Seed data and fixtures are full of bcrypt/argon2 digests.
|
|
338
|
+
if (type === 'password-in-code' && (0, placeholder_1.isPasswordHash)(rawValue)) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
// A bare PEM header with no key material after it is a UI label or an
|
|
342
|
+
// input placeholder, not a leaked key.
|
|
343
|
+
if (type === 'ssh-private-key' &&
|
|
344
|
+
(0, placeholder_1.isPemHeaderWithoutBody)(content, match.index + fullMatch.length)) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
295
347
|
// Suppress unquoted assignments whose "value" is actually a function
|
|
296
348
|
// call — e.g. `csrf_secret = _add_new_csrf_cookie(request)`. The value
|
|
297
349
|
// capture group stops at `(`, so a `(` immediately following the match
|
|
@@ -303,6 +355,19 @@ class SecretScanner {
|
|
|
303
355
|
content[match.index + fullMatch.length] === '(') {
|
|
304
356
|
continue;
|
|
305
357
|
}
|
|
358
|
+
// Suppress unquoted assignments whose "value" is a bare reference to
|
|
359
|
+
// another identifier — e.g. `'x-api-key': scheduledIngestApiKey`. Only
|
|
360
|
+
// applies when the captured value was NOT wrapped in quotes: a quoted
|
|
361
|
+
// string is a literal, and literals are what we are hunting. Scoped to
|
|
362
|
+
// the low-precision generic assignment patterns.
|
|
363
|
+
if (GENERIC_ASSIGNMENT_IDS.has(type) && match[1] !== undefined) {
|
|
364
|
+
const valueStart = fullMatch.lastIndexOf(rawValue);
|
|
365
|
+
const charBefore = valueStart > 0 ? fullMatch[valueStart - 1] : '';
|
|
366
|
+
const quoted = charBefore === '"' || charBefore === "'";
|
|
367
|
+
if (!quoted && (0, placeholder_1.isCodeIdentifierReference)(rawValue)) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
306
371
|
const line = this.lineFromIndex(lineIndex, match.index);
|
|
307
372
|
const lineContent = this.lineContentAt(content, lineIndex, line);
|
|
308
373
|
if (opts?.filePath &&
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SecretMatch } from '../types';
|
|
2
|
+
import type { FileScanResult } from '../scan-output';
|
|
3
|
+
/**
|
|
4
|
+
* Severity threshold at or above which a finding makes the scan **fail**
|
|
5
|
+
* (non-zero exit). `'none'` never fails the gate — findings are still
|
|
6
|
+
* reported, which is the right mode for an advisory / observe-only rollout.
|
|
7
|
+
*/
|
|
8
|
+
export type FailOnThreshold = SecretMatch['severity'] | 'none';
|
|
9
|
+
/**
|
|
10
|
+
* Default gate threshold.
|
|
11
|
+
*
|
|
12
|
+
* `medium` (not `low`) because the scanner deliberately downgrades findings to
|
|
13
|
+
* `low` in exactly the places where they are not real leaks:
|
|
14
|
+
*
|
|
15
|
+
* - `path-severity.ts` downgrades generic patterns inside test / fixture /
|
|
16
|
+
* docs / `*.example` paths;
|
|
17
|
+
* - public identifiers that are documented as safe to embed (`gcp-oauth`)
|
|
18
|
+
* are `low` by definition.
|
|
19
|
+
*
|
|
20
|
+
* Blocking a commit on those was the dominant real-world complaint: the
|
|
21
|
+
* downgrade existed but bought the user nothing because any match at all
|
|
22
|
+
* returned exit 1. Findings below the threshold are still printed and still
|
|
23
|
+
* appear in JSON / SARIF; they just do not fail the build.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_FAIL_ON: FailOnThreshold;
|
|
26
|
+
/** Accepted `--fail-on` / `fail_on` values, in descending strictness. */
|
|
27
|
+
export declare const FAIL_ON_VALUES: readonly FailOnThreshold[];
|
|
28
|
+
export declare function isFailOnThreshold(v: unknown): v is FailOnThreshold;
|
|
29
|
+
/** True when `severity` is at or above the gate `threshold`. */
|
|
30
|
+
export declare function meetsFailThreshold(severity: SecretMatch['severity'], threshold: FailOnThreshold): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Count findings at or above `threshold` across scan results. A non-zero
|
|
33
|
+
* count is what drives the CLI exit code; the full result set is still what
|
|
34
|
+
* gets displayed and serialized.
|
|
35
|
+
*/
|
|
36
|
+
export declare function countBlockingMatches(results: FileScanResult[], threshold: FailOnThreshold): number;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the effective threshold from (highest priority first) the CLI flag,
|
|
39
|
+
* the repo config, then {@link DEFAULT_FAIL_ON}.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveFailOn(flagValue: string | undefined, configValue: unknown): {
|
|
42
|
+
ok: true;
|
|
43
|
+
threshold: FailOnThreshold;
|
|
44
|
+
} | {
|
|
45
|
+
ok: false;
|
|
46
|
+
invalid: string;
|
|
47
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FAIL_ON_VALUES = exports.DEFAULT_FAIL_ON = void 0;
|
|
4
|
+
exports.isFailOnThreshold = isFailOnThreshold;
|
|
5
|
+
exports.meetsFailThreshold = meetsFailThreshold;
|
|
6
|
+
exports.countBlockingMatches = countBlockingMatches;
|
|
7
|
+
exports.resolveFailOn = resolveFailOn;
|
|
8
|
+
/**
|
|
9
|
+
* Default gate threshold.
|
|
10
|
+
*
|
|
11
|
+
* `medium` (not `low`) because the scanner deliberately downgrades findings to
|
|
12
|
+
* `low` in exactly the places where they are not real leaks:
|
|
13
|
+
*
|
|
14
|
+
* - `path-severity.ts` downgrades generic patterns inside test / fixture /
|
|
15
|
+
* docs / `*.example` paths;
|
|
16
|
+
* - public identifiers that are documented as safe to embed (`gcp-oauth`)
|
|
17
|
+
* are `low` by definition.
|
|
18
|
+
*
|
|
19
|
+
* Blocking a commit on those was the dominant real-world complaint: the
|
|
20
|
+
* downgrade existed but bought the user nothing because any match at all
|
|
21
|
+
* returned exit 1. Findings below the threshold are still printed and still
|
|
22
|
+
* appear in JSON / SARIF; they just do not fail the build.
|
|
23
|
+
*/
|
|
24
|
+
exports.DEFAULT_FAIL_ON = 'medium';
|
|
25
|
+
const SEVERITY_RANK = {
|
|
26
|
+
critical: 4,
|
|
27
|
+
high: 3,
|
|
28
|
+
medium: 2,
|
|
29
|
+
low: 1,
|
|
30
|
+
};
|
|
31
|
+
/** Accepted `--fail-on` / `fail_on` values, in descending strictness. */
|
|
32
|
+
exports.FAIL_ON_VALUES = [
|
|
33
|
+
'low',
|
|
34
|
+
'medium',
|
|
35
|
+
'high',
|
|
36
|
+
'critical',
|
|
37
|
+
'none',
|
|
38
|
+
];
|
|
39
|
+
function isFailOnThreshold(v) {
|
|
40
|
+
return typeof v === 'string' && exports.FAIL_ON_VALUES.includes(v);
|
|
41
|
+
}
|
|
42
|
+
/** True when `severity` is at or above the gate `threshold`. */
|
|
43
|
+
function meetsFailThreshold(severity, threshold) {
|
|
44
|
+
if (threshold === 'none')
|
|
45
|
+
return false;
|
|
46
|
+
return SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Count findings at or above `threshold` across scan results. A non-zero
|
|
50
|
+
* count is what drives the CLI exit code; the full result set is still what
|
|
51
|
+
* gets displayed and serialized.
|
|
52
|
+
*/
|
|
53
|
+
function countBlockingMatches(results, threshold) {
|
|
54
|
+
let n = 0;
|
|
55
|
+
for (const r of results) {
|
|
56
|
+
for (const m of r.matches) {
|
|
57
|
+
if (meetsFailThreshold(m.severity, threshold))
|
|
58
|
+
n++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return n;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve the effective threshold from (highest priority first) the CLI flag,
|
|
65
|
+
* the repo config, then {@link DEFAULT_FAIL_ON}.
|
|
66
|
+
*/
|
|
67
|
+
function resolveFailOn(flagValue, configValue) {
|
|
68
|
+
if (flagValue !== undefined) {
|
|
69
|
+
if (!isFailOnThreshold(flagValue))
|
|
70
|
+
return { ok: false, invalid: flagValue };
|
|
71
|
+
return { ok: true, threshold: flagValue };
|
|
72
|
+
}
|
|
73
|
+
if (configValue !== undefined) {
|
|
74
|
+
if (!isFailOnThreshold(configValue))
|
|
75
|
+
return { ok: false, invalid: String(configValue) };
|
|
76
|
+
return { ok: true, threshold: configValue };
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, threshold: exports.DEFAULT_FAIL_ON };
|
|
79
|
+
}
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import type { SecretMatch } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* True when a file is a translation catalogue.
|
|
4
|
+
*
|
|
5
|
+
* These are entirely natural-language strings keyed by identifiers, so a key
|
|
6
|
+
* such as `tfa_secret` or `api_key_label` puts a translated *label* where the
|
|
7
|
+
* generic assignment patterns expect a value — `tfa_secret: Zwei-Faktor-
|
|
8
|
+
* Authentifizierung` reads as a 29-character high-entropy secret. Translation
|
|
9
|
+
* files never hold real credentials.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isLocalePath(filePath: string): boolean;
|
|
2
12
|
/**
|
|
3
13
|
* Return `true` when `filePath` looks like a test or fixture file.
|
|
4
14
|
*/
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isLocalePath = isLocalePath;
|
|
6
7
|
exports.isTestFilePath = isTestFilePath;
|
|
7
8
|
exports.applyPathAwareSeverity = applyPathAwareSeverity;
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
@@ -37,7 +38,11 @@ const TEST_DIR_SEGMENTS = new Set([
|
|
|
37
38
|
'tests',
|
|
38
39
|
'test',
|
|
39
40
|
'fixtures',
|
|
41
|
+
'fixture',
|
|
40
42
|
'testdata',
|
|
43
|
+
'test-data',
|
|
44
|
+
'test_data',
|
|
45
|
+
'testfixtures',
|
|
41
46
|
'spec',
|
|
42
47
|
'e2e',
|
|
43
48
|
'examples',
|
|
@@ -75,6 +80,35 @@ function isTestDirectorySegment(seg) {
|
|
|
75
80
|
seg.length >= 7 &&
|
|
76
81
|
!NON_TEST_TEST_SUFFIX_DIRS.has(seg));
|
|
77
82
|
}
|
|
83
|
+
/** Directory segments holding UI translation catalogues. */
|
|
84
|
+
const LOCALE_DIR_SEGMENTS = new Set([
|
|
85
|
+
'locales',
|
|
86
|
+
'locale',
|
|
87
|
+
'translations',
|
|
88
|
+
'translation',
|
|
89
|
+
'i18n',
|
|
90
|
+
'lang',
|
|
91
|
+
'langs',
|
|
92
|
+
]);
|
|
93
|
+
/**
|
|
94
|
+
* Locale file basenames: `en.json`, `de-DE.yaml`, `pt_BR.yml`, `zh-Hant.json`.
|
|
95
|
+
*/
|
|
96
|
+
const LOCALE_BASENAME = /^[a-z]{2}(?:[-_][A-Za-z]{2,4})?\.(json|ya?ml|ts|js|po|properties)$/;
|
|
97
|
+
/**
|
|
98
|
+
* True when a file is a translation catalogue.
|
|
99
|
+
*
|
|
100
|
+
* These are entirely natural-language strings keyed by identifiers, so a key
|
|
101
|
+
* such as `tfa_secret` or `api_key_label` puts a translated *label* where the
|
|
102
|
+
* generic assignment patterns expect a value — `tfa_secret: Zwei-Faktor-
|
|
103
|
+
* Authentifizierung` reads as a 29-character high-entropy secret. Translation
|
|
104
|
+
* files never hold real credentials.
|
|
105
|
+
*/
|
|
106
|
+
function isLocalePath(filePath) {
|
|
107
|
+
const parts = (0, path_parts_1.splitPathParts)(filePath);
|
|
108
|
+
if (parts.some(p => LOCALE_DIR_SEGMENTS.has(p.toLowerCase())))
|
|
109
|
+
return true;
|
|
110
|
+
return LOCALE_BASENAME.test(path_1.default.basename(filePath));
|
|
111
|
+
}
|
|
78
112
|
/**
|
|
79
113
|
* Celery / Perl-style test root: `t/unit/…`, `t/integration/…`.
|
|
80
114
|
*/
|
|
@@ -112,7 +146,7 @@ function isTestFilePath(filePath) {
|
|
|
112
146
|
* file is still worth a `critical` alert.
|
|
113
147
|
*/
|
|
114
148
|
function isLowPrecisionContextPath(filePath) {
|
|
115
|
-
return isTestFilePath(filePath) || (0, doc_context_1.isDocumentationPath)(filePath);
|
|
149
|
+
return isTestFilePath(filePath) || (0, doc_context_1.isDocumentationPath)(filePath) || isLocalePath(filePath);
|
|
116
150
|
}
|
|
117
151
|
function applyPathAwareSeverity(matches, filePath) {
|
|
118
152
|
if (matches.length === 0)
|
|
@@ -20,11 +20,61 @@
|
|
|
20
20
|
* by chance.
|
|
21
21
|
*/
|
|
22
22
|
export declare function isRedactedTemplateValue(value: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* A stored password *hash* is the safe-at-rest form of a credential, not a
|
|
25
|
+
* credential. Seed data, fixtures, and migration files are full of them, and
|
|
26
|
+
* flagging them as `password-in-code` is noise: rotating them is meaningless
|
|
27
|
+
* and they cannot be used to authenticate.
|
|
28
|
+
*
|
|
29
|
+
* Covers modular crypt format (bcrypt `$2a/2b/2y$`, sha-crypt `$1/5/6$`,
|
|
30
|
+
* yescrypt `$y$`, `$argon2i/d/id$`, `$pbkdf2-*$`) and the Django/Passlib
|
|
31
|
+
* `algo$iterations$salt$hash` convention.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isPasswordHash(value: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* True when a PEM `-----BEGIN … PRIVATE KEY-----` header is not followed by
|
|
36
|
+
* any key material.
|
|
37
|
+
*
|
|
38
|
+
* UI code and documentation carry the header on its own as a label or an
|
|
39
|
+
* input placeholder (`const privateKeyBeginsWith = '-----BEGIN RSA PRIVATE
|
|
40
|
+
* KEY-----'`). A header with no body leaks nothing. Real PEM files wrap their
|
|
41
|
+
* base64 at 64 characters per line, so requiring a single long base64 run
|
|
42
|
+
* right after the header separates the two cleanly, and still works when the
|
|
43
|
+
* key is embedded in JSON with escaped newlines.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isPemHeaderWithoutBody(content: string, headerEndOffset: number): boolean;
|
|
23
46
|
/**
|
|
24
47
|
* ALL_CAPS identifiers (e.g. `PLAID_TOKEN_ENCRYPTION_KEY`) are env-var names,
|
|
25
48
|
* not secret values — common in GitHub Actions `secret:NAME` checks.
|
|
26
49
|
*/
|
|
27
50
|
export declare function isEnvVarNameToken(value: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* True when an **unquoted** captured value is a reference to a code
|
|
53
|
+
* identifier rather than a literal credential — e.g.
|
|
54
|
+
*
|
|
55
|
+
* headers: { 'x-api-key': scheduledIngestApiKey }
|
|
56
|
+
* api_key = defaultServiceCredential
|
|
57
|
+
*
|
|
58
|
+
* The generic assignment patterns capture whatever follows `:`/`=`, and in
|
|
59
|
+
* real code that is very often a variable, not a secret. An existing check
|
|
60
|
+
* covers the function-call case (`= makeKey(...)`); this covers the far more
|
|
61
|
+
* common bare-reference case.
|
|
62
|
+
*
|
|
63
|
+
* Discriminator: generated credentials are random, so they mix digits into the
|
|
64
|
+
* alphabet and do not decompose into word-shaped segments. We require the
|
|
65
|
+
* value to split (on `_` and camelCase boundaries) into **two or more**
|
|
66
|
+
* segments that are each purely alphabetic and at least two characters long.
|
|
67
|
+
*
|
|
68
|
+
* `scheduledIngestApiKey` → scheduled | Ingest | Api | Key → identifier
|
|
69
|
+
* `default_service_token` → default | service | token → identifier
|
|
70
|
+
* `x7Kf9mQ2pL8vB3nR5wT1` → contains digits → NOT identifier
|
|
71
|
+
* `qwertyuiopasdfghjklz` → one segment → NOT identifier
|
|
72
|
+
*
|
|
73
|
+
* Callers must only apply this to unquoted values on low-precision generic
|
|
74
|
+
* patterns; a quoted string literal is a literal, and vendor-anchored rules
|
|
75
|
+
* must never be weakened by it.
|
|
76
|
+
*/
|
|
77
|
+
export declare function isCodeIdentifierReference(value: string): boolean;
|
|
28
78
|
/**
|
|
29
79
|
* Return `true` when a database/Redis connection string is **not** a real
|
|
30
80
|
* credential leak — i.e. it targets a local/dev/docker/example host, or uses
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.isRedactedTemplateValue = isRedactedTemplateValue;
|
|
25
|
+
exports.isPasswordHash = isPasswordHash;
|
|
26
|
+
exports.isPemHeaderWithoutBody = isPemHeaderWithoutBody;
|
|
25
27
|
exports.isEnvVarNameToken = isEnvVarNameToken;
|
|
28
|
+
exports.isCodeIdentifierReference = isCodeIdentifierReference;
|
|
26
29
|
exports.isSampleJwt = isSampleJwt;
|
|
27
30
|
exports.isNonSecretConnectionString = isNonSecretConnectionString;
|
|
28
31
|
exports.isPlaceholderSecret = isPlaceholderSecret;
|
|
@@ -73,6 +76,7 @@ const AGGRESSIVE_MARKERS = [
|
|
|
73
76
|
'qwerty',
|
|
74
77
|
'letmein',
|
|
75
78
|
'your_', // your_google_places_key, your_api_key_here
|
|
79
|
+
'your-', // your-anthropic-api-key — hyphen form is just as common in docs
|
|
76
80
|
];
|
|
77
81
|
/** Known vendor key prefixes whose remainder is often redacted with X/* in docs. */
|
|
78
82
|
const REDACTED_PREFIXES = [
|
|
@@ -109,6 +113,52 @@ function isRedactedTemplateValue(value) {
|
|
|
109
113
|
}
|
|
110
114
|
return false;
|
|
111
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* A stored password *hash* is the safe-at-rest form of a credential, not a
|
|
118
|
+
* credential. Seed data, fixtures, and migration files are full of them, and
|
|
119
|
+
* flagging them as `password-in-code` is noise: rotating them is meaningless
|
|
120
|
+
* and they cannot be used to authenticate.
|
|
121
|
+
*
|
|
122
|
+
* Covers modular crypt format (bcrypt `$2a/2b/2y$`, sha-crypt `$1/5/6$`,
|
|
123
|
+
* yescrypt `$y$`, `$argon2i/d/id$`, `$pbkdf2-*$`) and the Django/Passlib
|
|
124
|
+
* `algo$iterations$salt$hash` convention.
|
|
125
|
+
*/
|
|
126
|
+
function isPasswordHash(value) {
|
|
127
|
+
if (/^\$(?:2[abxy]?|1|5|6|7|y|gy|argon2(?:i|d|id)?|scrypt|pbkdf2(?:-[a-z0-9]+)?|sha1|md5|apr1|bcrypt)\$/i.test(value)) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
// Django: pbkdf2_sha256$390000$<salt>$<hash>, argon2$..., bcrypt_sha256$...
|
|
131
|
+
return /^(?:pbkdf2_[a-z0-9]+|argon2[a-z]*|bcrypt(?:_sha256)?|scrypt|sha1|md5|crypt|unsalted_[a-z0-9]+)\$\d*\$?/i.test(value);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* True when a PEM `-----BEGIN … PRIVATE KEY-----` header is not followed by
|
|
135
|
+
* any key material.
|
|
136
|
+
*
|
|
137
|
+
* UI code and documentation carry the header on its own as a label or an
|
|
138
|
+
* input placeholder (`const privateKeyBeginsWith = '-----BEGIN RSA PRIVATE
|
|
139
|
+
* KEY-----'`). A header with no body leaks nothing. Real PEM files wrap their
|
|
140
|
+
* base64 at 64 characters per line, so requiring a single long base64 run
|
|
141
|
+
* right after the header separates the two cleanly, and still works when the
|
|
142
|
+
* key is embedded in JSON with escaped newlines.
|
|
143
|
+
*/
|
|
144
|
+
function isPemHeaderWithoutBody(content, headerEndOffset) {
|
|
145
|
+
const window = content.slice(headerEndOffset, headerEndOffset + 400);
|
|
146
|
+
// Split on real newlines and on the escaped `\n` used when a key is embedded
|
|
147
|
+
// in JSON or YAML, then strip the quoting that survives that embedding.
|
|
148
|
+
const lines = window.split(/\r?\n|\\r\\n|\\n/);
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
const token = line.replace(/["'`\\\s]/g, '');
|
|
151
|
+
// A PEM body wraps base64 at 64 characters, so a body line is base64 and
|
|
152
|
+
// nothing else. Requiring the *whole* line to match is what separates it
|
|
153
|
+
// from surrounding code: a long camelCase identifier such as
|
|
154
|
+
// `onUpdateDatasourceSecureJsonDataOption` is a valid base64 substring,
|
|
155
|
+
// but the line it sits on never is.
|
|
156
|
+
if (token.length >= 32 && /^[A-Za-z0-9+/]+={0,2}$/.test(token)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
112
162
|
/**
|
|
113
163
|
* ALL_CAPS identifiers (e.g. `PLAID_TOKEN_ENCRYPTION_KEY`) are env-var names,
|
|
114
164
|
* not secret values — common in GitHub Actions `secret:NAME` checks.
|
|
@@ -116,6 +166,58 @@ function isRedactedTemplateValue(value) {
|
|
|
116
166
|
function isEnvVarNameToken(value) {
|
|
117
167
|
return /^[A-Z][A-Z0-9_]{7,}$/.test(value);
|
|
118
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* True when an **unquoted** captured value is a reference to a code
|
|
171
|
+
* identifier rather than a literal credential — e.g.
|
|
172
|
+
*
|
|
173
|
+
* headers: { 'x-api-key': scheduledIngestApiKey }
|
|
174
|
+
* api_key = defaultServiceCredential
|
|
175
|
+
*
|
|
176
|
+
* The generic assignment patterns capture whatever follows `:`/`=`, and in
|
|
177
|
+
* real code that is very often a variable, not a secret. An existing check
|
|
178
|
+
* covers the function-call case (`= makeKey(...)`); this covers the far more
|
|
179
|
+
* common bare-reference case.
|
|
180
|
+
*
|
|
181
|
+
* Discriminator: generated credentials are random, so they mix digits into the
|
|
182
|
+
* alphabet and do not decompose into word-shaped segments. We require the
|
|
183
|
+
* value to split (on `_` and camelCase boundaries) into **two or more**
|
|
184
|
+
* segments that are each purely alphabetic and at least two characters long.
|
|
185
|
+
*
|
|
186
|
+
* `scheduledIngestApiKey` → scheduled | Ingest | Api | Key → identifier
|
|
187
|
+
* `default_service_token` → default | service | token → identifier
|
|
188
|
+
* `x7Kf9mQ2pL8vB3nR5wT1` → contains digits → NOT identifier
|
|
189
|
+
* `qwertyuiopasdfghjklz` → one segment → NOT identifier
|
|
190
|
+
*
|
|
191
|
+
* Callers must only apply this to unquoted values on low-precision generic
|
|
192
|
+
* patterns; a quoted string literal is a literal, and vendor-anchored rules
|
|
193
|
+
* must never be weakened by it.
|
|
194
|
+
*/
|
|
195
|
+
function isCodeIdentifierReference(value) {
|
|
196
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value))
|
|
197
|
+
return false;
|
|
198
|
+
if (/\d/.test(value))
|
|
199
|
+
return false;
|
|
200
|
+
const segments = value
|
|
201
|
+
.replace(/\$/g, '_')
|
|
202
|
+
.split('_')
|
|
203
|
+
.filter(s => s.length > 0)
|
|
204
|
+
.flatMap(part => part.split(/(?=[A-Z])/));
|
|
205
|
+
if (segments.length < 2)
|
|
206
|
+
return false;
|
|
207
|
+
if (!segments.every(s => s.length >= 2 && /^[A-Za-z]+$/.test(s)))
|
|
208
|
+
return false;
|
|
209
|
+
// Guard against alpha-only random keys with alternating capitals
|
|
210
|
+
// (`PmZkQvXtLdRwNbGhYuJcEaSf` splits into twelve 2-char "segments"). Real
|
|
211
|
+
// identifiers are made of words, so beyond a handful of segments the mean
|
|
212
|
+
// segment length stays word-like. Values of three segments or fewer are
|
|
213
|
+
// exempt: `myApiKey` is a legitimate identifier with short parts.
|
|
214
|
+
if (segments.length > 3) {
|
|
215
|
+
const mean = segments.reduce((n, s) => n + s.length, 0) / segments.length;
|
|
216
|
+
if (mean < 3)
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
119
221
|
/**
|
|
120
222
|
* A value made of one or two distinct characters (e.g. `xxxxxxxx`, `00000000`)
|
|
121
223
|
* is padding, never a real secret.
|
|
@@ -189,7 +291,12 @@ function isSampleJwt(token) {
|
|
|
189
291
|
}
|
|
190
292
|
return (/"sub"\s*:\s*"1234567890"/.test(payload) ||
|
|
191
293
|
/"name"\s*:\s*"John Doe"/.test(payload) ||
|
|
192
|
-
/\b1516239022\b/.test(payload)
|
|
294
|
+
/\b1516239022\b/.test(payload) ||
|
|
295
|
+
// Supabase ships fixed anon / service_role keys for local development.
|
|
296
|
+
// They are printed by `supabase start`, published in Supabase's own docs,
|
|
297
|
+
// and signed with a well-known secret, so they appear verbatim in a large
|
|
298
|
+
// share of Supabase projects. Same category as the jwt.io sample.
|
|
299
|
+
/"iss"\s*:\s*"supabase-demo"/.test(payload));
|
|
193
300
|
}
|
|
194
301
|
function isNonSecretConnectionString(url) {
|
|
195
302
|
const m = /^[a-z][a-z0-9+.-]*:\/\/([^:@/\s]+):([^@/\s]+)@([^:/?\s]+)/i.exec(url);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vaultcompass/vault-guard-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|