@vaultcompass/vault-guard-core 1.0.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/LICENSE +21 -0
- package/dist/baseline.d.ts +24 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +87 -0
- package/dist/baseline.js.map +1 -0
- package/dist/config-validate.d.ts +13 -0
- package/dist/config-validate.d.ts.map +1 -0
- package/dist/config-validate.js +111 -0
- package/dist/config-validate.js.map +1 -0
- package/dist/config.d.ts +69 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +106 -0
- package/dist/config.js.map +1 -0
- package/dist/diagnostics.d.ts +64 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +59 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/errors.d.ts +63 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +98 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/match-fingerprint.d.ts +7 -0
- package/dist/match-fingerprint.d.ts.map +1 -0
- package/dist/match-fingerprint.js +28 -0
- package/dist/match-fingerprint.js.map +1 -0
- package/dist/scan-output.d.ts +65 -0
- package/dist/scan-output.d.ts.map +1 -0
- package/dist/scan-output.js +140 -0
- package/dist/scan-output.js.map +1 -0
- package/dist/scanners/index.d.ts +5 -0
- package/dist/scanners/index.d.ts.map +1 -0
- package/dist/scanners/index.js +21 -0
- package/dist/scanners/index.js.map +1 -0
- package/dist/scanners/pre-commit-hook.d.ts +41 -0
- package/dist/scanners/pre-commit-hook.d.ts.map +1 -0
- package/dist/scanners/pre-commit-hook.js +389 -0
- package/dist/scanners/pre-commit-hook.js.map +1 -0
- package/dist/scanners/secret-scanner.d.ts +99 -0
- package/dist/scanners/secret-scanner.d.ts.map +1 -0
- package/dist/scanners/secret-scanner.js +422 -0
- package/dist/scanners/secret-scanner.js.map +1 -0
- package/dist/scanners/token-counter.d.ts +27 -0
- package/dist/scanners/token-counter.d.ts.map +1 -0
- package/dist/scanners/token-counter.js +121 -0
- package/dist/scanners/token-counter.js.map +1 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/entropy.d.ts +17 -0
- package/dist/utils/entropy.d.ts.map +1 -0
- package/dist/utils/entropy.js +35 -0
- package/dist/utils/entropy.js.map +1 -0
- package/dist/utils/file-utils.d.ts +39 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +442 -0
- package/dist/utils/file-utils.js.map +1 -0
- package/dist/utils/git-utils.d.ts +12 -0
- package/dist/utils/git-utils.d.ts.map +1 -0
- package/dist/utils/git-utils.js +55 -0
- package/dist/utils/git-utils.js.map +1 -0
- package/dist/utils/path-severity.d.ts +17 -0
- package/dist/utils/path-severity.d.ts.map +1 -0
- package/dist/utils/path-severity.js +96 -0
- package/dist/utils/path-severity.js.map +1 -0
- package/dist/utils/placeholder.d.ts +53 -0
- package/dist/utils/placeholder.d.ts.map +1 -0
- package/dist/utils/placeholder.js +198 -0
- package/dist/utils/placeholder.js.map +1 -0
- package/dist/utils/regex-safety.d.ts +102 -0
- package/dist/utils/regex-safety.d.ts.map +1 -0
- package/dist/utils/regex-safety.js +193 -0
- package/dist/utils/regex-safety.js.map +1 -0
- package/dist/utils/scan-file.d.ts +29 -0
- package/dist/utils/scan-file.d.ts.map +1 -0
- package/dist/utils/scan-file.js +125 -0
- package/dist/utils/scan-file.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isTestFilePath = isTestFilePath;
|
|
7
|
+
exports.applyPathAwareSeverity = applyPathAwareSeverity;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/**
|
|
10
|
+
* Pattern IDs whose severity is downgraded to `low` in obvious test / fixture
|
|
11
|
+
* paths. Two groups:
|
|
12
|
+
*
|
|
13
|
+
* - Low-precision generic patterns (`password-in-code`, …) — common in test
|
|
14
|
+
* scaffolding and rarely real leaks there.
|
|
15
|
+
* - Connection strings and key/token shapes (`postgresql-url`,
|
|
16
|
+
* `ssh-private-key`, `jwt-token`, …) — test suites are full of throwaway
|
|
17
|
+
* DSNs, fixture PEMs, and sample tokens. Downgrading (not suppressing)
|
|
18
|
+
* keeps them visible at `low` without drowning real criticals.
|
|
19
|
+
*
|
|
20
|
+
* Hard vendor-anchored API-key patterns (anthropic, aws-access, stripe,
|
|
21
|
+
* github-token, …) are intentionally **absent**: a real provider key is a real
|
|
22
|
+
* key even in a test file, and those patterns have near-zero false positives.
|
|
23
|
+
*/
|
|
24
|
+
const TEST_PATH_DOWNGRADE_IDS = new Set([
|
|
25
|
+
// generic / assignment
|
|
26
|
+
'password-in-code',
|
|
27
|
+
'api-key-generic',
|
|
28
|
+
'secret-generic',
|
|
29
|
+
'bearer-token',
|
|
30
|
+
// connection strings
|
|
31
|
+
'postgresql-url',
|
|
32
|
+
'mysql-url',
|
|
33
|
+
'mongodb-url',
|
|
34
|
+
'redis-url',
|
|
35
|
+
// key / token shapes
|
|
36
|
+
'ssh-private-key',
|
|
37
|
+
'jwt-token',
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Segments that indicate a file lives in a test / fixture tree.
|
|
41
|
+
* Matched against every directory component in the file path.
|
|
42
|
+
*/
|
|
43
|
+
const TEST_DIR_SEGMENTS = new Set([
|
|
44
|
+
'__tests__',
|
|
45
|
+
'__mocks__',
|
|
46
|
+
'tests',
|
|
47
|
+
'test',
|
|
48
|
+
'fixtures',
|
|
49
|
+
'testdata',
|
|
50
|
+
'spec',
|
|
51
|
+
'e2e',
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* File name suffixes / extensions that mark test or fixture files.
|
|
55
|
+
* Checked against `path.basename(filePath)`.
|
|
56
|
+
*/
|
|
57
|
+
const TEST_FILE_PATTERNS = [
|
|
58
|
+
/\.test\.[jt]sx?$/,
|
|
59
|
+
/\.spec\.[jt]sx?$/,
|
|
60
|
+
/\.test\.api\.[jt]sx?$/,
|
|
61
|
+
/\.fixture\.[jt]sx?$/,
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* Return `true` when `filePath` looks like a test or fixture file.
|
|
65
|
+
*/
|
|
66
|
+
function isTestFilePath(filePath) {
|
|
67
|
+
const parts = filePath.split(path_1.default.sep).flatMap(p => p.split('/'));
|
|
68
|
+
if (parts.some(seg => TEST_DIR_SEGMENTS.has(seg)))
|
|
69
|
+
return true;
|
|
70
|
+
const basename = path_1.default.basename(filePath);
|
|
71
|
+
return TEST_FILE_PATTERNS.some(re => re.test(basename));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Downgrade low-precision generic pattern findings to `'low'` severity when
|
|
75
|
+
* they appear inside a test / fixture file.
|
|
76
|
+
*
|
|
77
|
+
* Rationale: password assignments, bearer tokens, and generic api-key patterns
|
|
78
|
+
* are common in test scaffolding (`const password = 'Admin1234!'`) and are
|
|
79
|
+
* rarely real leaked credentials in that context. Vendor-anchored patterns
|
|
80
|
+
* (aws-access, anthropic, stripe, …) are unaffected — a real key in a test
|
|
81
|
+
* file is still worth a `critical` alert.
|
|
82
|
+
*/
|
|
83
|
+
function applyPathAwareSeverity(matches, filePath) {
|
|
84
|
+
if (matches.length === 0)
|
|
85
|
+
return matches;
|
|
86
|
+
if (!isTestFilePath(filePath))
|
|
87
|
+
return matches;
|
|
88
|
+
return matches.map(m => {
|
|
89
|
+
if (!TEST_PATH_DOWNGRADE_IDS.has(m.type))
|
|
90
|
+
return m;
|
|
91
|
+
if (m.severity === 'low')
|
|
92
|
+
return m;
|
|
93
|
+
return { ...m, severity: 'low' };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=path-severity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-severity.js","sourceRoot":"","sources":["../../src/utils/path-severity.ts"],"names":[],"mappings":";;;;;AA+DA,wCAKC;AAYD,wDAYC;AA5FD,gDAAwB;AAGxB;;;;;;;;;;;;;;GAcG;AACH,MAAM,uBAAuB,GAAG,IAAI,GAAG,CAAC;IACtC,uBAAuB;IACvB,kBAAkB;IAClB,iBAAiB;IACjB,gBAAgB;IAChB,cAAc;IACd,qBAAqB;IACrB,gBAAgB;IAChB,WAAW;IACX,aAAa;IACb,WAAW;IACX,qBAAqB;IACrB,iBAAiB;IACjB,WAAW;CACZ,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,WAAW;IACX,WAAW;IACX,OAAO;IACP,MAAM;IACN,UAAU;IACV,UAAU;IACV,MAAM;IACN,KAAK;CACN,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,kBAAkB,GAAG;IACzB,kBAAkB;IAClB,kBAAkB;IAClB,uBAAuB;IACvB,qBAAqB;CACtB,CAAC;AAEF;;GAEG;AACH,SAAgB,cAAc,CAAC,QAAgB;IAC7C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,cAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAClE,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,QAAQ,GAAG,cAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,sBAAsB,CACpC,OAAsB,EACtB,QAAgB;IAEhB,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACzC,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC;QAAE,OAAO,OAAO,CAAC;IAE9C,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACrB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;QACnD,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK;YAAE,OAAO,CAAC,CAAC;QACnC,OAAO,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,KAAc,EAAE,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognise obviously non-secret placeholder / example / test values so that
|
|
3
|
+
* broad patterns stop firing on documentation samples and unit-test fixtures —
|
|
4
|
+
* empirically the dominant real-world false-positive source (e.g. AWS's own
|
|
5
|
+
* documented `AKIAIOSFODNN7EXAMPLE` key, or `const password = 'testPass1234'`).
|
|
6
|
+
*
|
|
7
|
+
* Two tiers, by precision cost:
|
|
8
|
+
*
|
|
9
|
+
* - `standard` (safe for every pattern, including vendor-anchored keys):
|
|
10
|
+
* unambiguous markers that effectively never occur inside a real generated
|
|
11
|
+
* credential — `EXAMPLE`, `changeme`, `your_token_here`, all-`x` padding, …
|
|
12
|
+
*
|
|
13
|
+
* - `aggressive` (opt-in, used only by the low-precision generic / password
|
|
14
|
+
* assignment patterns): additionally treats common test-fixture words
|
|
15
|
+
* (`test`, `sample`, `password`, …) as placeholders. Scoped to those
|
|
16
|
+
* patterns so vendor-anchored keys keep full recall.
|
|
17
|
+
*
|
|
18
|
+
* Matching is substring-based on the lower-cased value. Markers are chosen to
|
|
19
|
+
* be long/specific enough that a real high-entropy secret will not contain them
|
|
20
|
+
* by chance.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Return `true` when a database/Redis connection string is **not** a real
|
|
24
|
+
* credential leak — i.e. it targets a local/dev/docker/example host, or uses
|
|
25
|
+
* obvious placeholder/default credentials.
|
|
26
|
+
*
|
|
27
|
+
* The exploitable secret in a DSN is the password against a *reachable* host.
|
|
28
|
+
* We suppress when either:
|
|
29
|
+
* 1. the host is local, a bare docker-compose service name, or a reserved
|
|
30
|
+
* TLD (`localhost`, `mysql`, `db.local`, …) — not remotely reachable; or
|
|
31
|
+
* 2. the password is a placeholder/default (`pass`, `PASSWORD`, `root:root`,
|
|
32
|
+
* `${DB_PASS}`, `<your-password>`, …).
|
|
33
|
+
*
|
|
34
|
+
* A real remote host with a real password (e.g.
|
|
35
|
+
* `postgres://app:8Fk2$mQ9z@db.prod.example-corp.com/main`) is **not**
|
|
36
|
+
* suppressed.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Recognise the canonical jwt.io / RFC 7519 sample token that is pasted into
|
|
40
|
+
* countless READMEs, OpenAPI specs, and tutorials. Its decoded payload carries
|
|
41
|
+
* the well-known sample claims (`sub: "1234567890"`, `name: "John Doe"`,
|
|
42
|
+
* `iat: 1516239022`). These are never real credentials.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isSampleJwt(token: string): boolean;
|
|
45
|
+
export declare function isNonSecretConnectionString(url: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* @returns `true` when `value` looks like a placeholder / example / test
|
|
48
|
+
* credential and should be suppressed.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isPlaceholderSecret(value: string, opts?: {
|
|
51
|
+
aggressive?: boolean;
|
|
52
|
+
}): boolean;
|
|
53
|
+
//# sourceMappingURL=placeholder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholder.d.ts","sourceRoot":"","sources":["../../src/utils/placeholder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA0FH;;;;;;;;;;;;;;;GAeG;AACH;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAclD;AAED,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAwBhE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,OAAO,CAiBT"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Recognise obviously non-secret placeholder / example / test values so that
|
|
4
|
+
* broad patterns stop firing on documentation samples and unit-test fixtures —
|
|
5
|
+
* empirically the dominant real-world false-positive source (e.g. AWS's own
|
|
6
|
+
* documented `AKIAIOSFODNN7EXAMPLE` key, or `const password = 'testPass1234'`).
|
|
7
|
+
*
|
|
8
|
+
* Two tiers, by precision cost:
|
|
9
|
+
*
|
|
10
|
+
* - `standard` (safe for every pattern, including vendor-anchored keys):
|
|
11
|
+
* unambiguous markers that effectively never occur inside a real generated
|
|
12
|
+
* credential — `EXAMPLE`, `changeme`, `your_token_here`, all-`x` padding, …
|
|
13
|
+
*
|
|
14
|
+
* - `aggressive` (opt-in, used only by the low-precision generic / password
|
|
15
|
+
* assignment patterns): additionally treats common test-fixture words
|
|
16
|
+
* (`test`, `sample`, `password`, …) as placeholders. Scoped to those
|
|
17
|
+
* patterns so vendor-anchored keys keep full recall.
|
|
18
|
+
*
|
|
19
|
+
* Matching is substring-based on the lower-cased value. Markers are chosen to
|
|
20
|
+
* be long/specific enough that a real high-entropy secret will not contain them
|
|
21
|
+
* by chance.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.isSampleJwt = isSampleJwt;
|
|
25
|
+
exports.isNonSecretConnectionString = isNonSecretConnectionString;
|
|
26
|
+
exports.isPlaceholderSecret = isPlaceholderSecret;
|
|
27
|
+
/** Unambiguous placeholder markers — applied to all patterns. */
|
|
28
|
+
const STANDARD_MARKERS = [
|
|
29
|
+
'example',
|
|
30
|
+
'changeme',
|
|
31
|
+
'change-me',
|
|
32
|
+
'change_me',
|
|
33
|
+
'placeholder',
|
|
34
|
+
'redacted',
|
|
35
|
+
'notreal',
|
|
36
|
+
'not-a-real',
|
|
37
|
+
'dummy',
|
|
38
|
+
'yourtoken',
|
|
39
|
+
'yourkey',
|
|
40
|
+
'yourapikey',
|
|
41
|
+
'your_token',
|
|
42
|
+
'your-token',
|
|
43
|
+
'your_key',
|
|
44
|
+
'your-key',
|
|
45
|
+
'your_api_key',
|
|
46
|
+
'your-api-key',
|
|
47
|
+
'insertyour',
|
|
48
|
+
'insert_your',
|
|
49
|
+
'replace_me',
|
|
50
|
+
'replaceme',
|
|
51
|
+
'loremipsum',
|
|
52
|
+
// Pure character repetition (e.g. `xxxxxxxx`, `00000000`) is handled by the
|
|
53
|
+
// low-variety check below rather than literal markers, so it does not clash
|
|
54
|
+
// with real keys that merely contain a short repeated run.
|
|
55
|
+
];
|
|
56
|
+
/** Common test / fixture markers — applied only to generic assignment patterns. */
|
|
57
|
+
const AGGRESSIVE_MARKERS = [
|
|
58
|
+
'test',
|
|
59
|
+
'sample',
|
|
60
|
+
'demo',
|
|
61
|
+
'fake',
|
|
62
|
+
'mock',
|
|
63
|
+
'foobar',
|
|
64
|
+
'password',
|
|
65
|
+
'passw0rd',
|
|
66
|
+
'secret',
|
|
67
|
+
'hunter2',
|
|
68
|
+
'qwerty',
|
|
69
|
+
'letmein',
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* A value made of one or two distinct characters (e.g. `xxxxxxxx`, `00000000`)
|
|
73
|
+
* is padding, never a real secret.
|
|
74
|
+
*/
|
|
75
|
+
function isLowVariety(value) {
|
|
76
|
+
return value.length >= 8 && new Set(value).size <= 2;
|
|
77
|
+
}
|
|
78
|
+
/** Hosts that are never a remotely-exploitable credential leak. */
|
|
79
|
+
const LOCAL_HOSTS = new Set([
|
|
80
|
+
'localhost',
|
|
81
|
+
'127.0.0.1',
|
|
82
|
+
'0.0.0.0',
|
|
83
|
+
'::1',
|
|
84
|
+
'[::1]',
|
|
85
|
+
'host.docker.internal',
|
|
86
|
+
]);
|
|
87
|
+
/** Reserved / non-routable TLD suffixes (RFC 6761 + docker/dev conventions). */
|
|
88
|
+
const LOCAL_TLD_SUFFIXES = [
|
|
89
|
+
'.local',
|
|
90
|
+
'.localhost',
|
|
91
|
+
'.test',
|
|
92
|
+
'.example',
|
|
93
|
+
'.invalid',
|
|
94
|
+
];
|
|
95
|
+
/**
|
|
96
|
+
* Password tokens that are obviously defaults / placeholders rather than a
|
|
97
|
+
* real secret. Matched case-insensitively against the password component of a
|
|
98
|
+
* connection string. Deliberately scoped to the *password* — usernames like
|
|
99
|
+
* `admin` / `root` / `postgres` are extremely common in genuine leaks, so we
|
|
100
|
+
* never suppress based on the username alone.
|
|
101
|
+
*/
|
|
102
|
+
const PLACEHOLDER_PASSWORDS = new Set([
|
|
103
|
+
'password', 'passwd', 'pass', 'pwd', 'secret',
|
|
104
|
+
'changeme', 'example', 'test', 'user', 'username',
|
|
105
|
+
'root', 'admin', 'postgres', 'mysql', 'mongo', 'mongodb', 'redis',
|
|
106
|
+
'db', 'database', 'prisma', 'identifier', 'key', 'token', 'name',
|
|
107
|
+
'randompassword', 'yourpassword', 'mypassword',
|
|
108
|
+
]);
|
|
109
|
+
/**
|
|
110
|
+
* Return `true` when a database/Redis connection string is **not** a real
|
|
111
|
+
* credential leak — i.e. it targets a local/dev/docker/example host, or uses
|
|
112
|
+
* obvious placeholder/default credentials.
|
|
113
|
+
*
|
|
114
|
+
* The exploitable secret in a DSN is the password against a *reachable* host.
|
|
115
|
+
* We suppress when either:
|
|
116
|
+
* 1. the host is local, a bare docker-compose service name, or a reserved
|
|
117
|
+
* TLD (`localhost`, `mysql`, `db.local`, …) — not remotely reachable; or
|
|
118
|
+
* 2. the password is a placeholder/default (`pass`, `PASSWORD`, `root:root`,
|
|
119
|
+
* `${DB_PASS}`, `<your-password>`, …).
|
|
120
|
+
*
|
|
121
|
+
* A real remote host with a real password (e.g.
|
|
122
|
+
* `postgres://app:8Fk2$mQ9z@db.prod.example-corp.com/main`) is **not**
|
|
123
|
+
* suppressed.
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* Recognise the canonical jwt.io / RFC 7519 sample token that is pasted into
|
|
127
|
+
* countless READMEs, OpenAPI specs, and tutorials. Its decoded payload carries
|
|
128
|
+
* the well-known sample claims (`sub: "1234567890"`, `name: "John Doe"`,
|
|
129
|
+
* `iat: 1516239022`). These are never real credentials.
|
|
130
|
+
*/
|
|
131
|
+
function isSampleJwt(token) {
|
|
132
|
+
const parts = token.split('.');
|
|
133
|
+
if (parts.length < 2 || !parts[1])
|
|
134
|
+
return false;
|
|
135
|
+
let payload;
|
|
136
|
+
try {
|
|
137
|
+
payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return (/"sub"\s*:\s*"1234567890"/.test(payload) ||
|
|
143
|
+
/"name"\s*:\s*"John Doe"/.test(payload) ||
|
|
144
|
+
/\b1516239022\b/.test(payload));
|
|
145
|
+
}
|
|
146
|
+
function isNonSecretConnectionString(url) {
|
|
147
|
+
const m = /^[a-z][a-z0-9+.-]*:\/\/([^:@/\s]+):([^@/\s]+)@([^:/?\s]+)/i.exec(url);
|
|
148
|
+
if (!m)
|
|
149
|
+
return false;
|
|
150
|
+
const user = m[1];
|
|
151
|
+
const pass = m[2];
|
|
152
|
+
const host = m[3].toLowerCase();
|
|
153
|
+
// 1. Non-routable / local / docker-service / reserved-TLD host.
|
|
154
|
+
if (LOCAL_HOSTS.has(host))
|
|
155
|
+
return true;
|
|
156
|
+
if (LOCAL_TLD_SUFFIXES.some(suffix => host.endsWith(suffix)))
|
|
157
|
+
return true;
|
|
158
|
+
// Bare single-token host with no dot (and not a raw IPv4) is a docker-compose
|
|
159
|
+
// service name (`mysql`, `db`, `postgres`) — local to a compose network.
|
|
160
|
+
if (!host.includes('.') && !host.includes(':') && !/^\d+$/.test(host))
|
|
161
|
+
return true;
|
|
162
|
+
// 2. Placeholder / default password.
|
|
163
|
+
const p = pass.toLowerCase();
|
|
164
|
+
if (PLACEHOLDER_PASSWORDS.has(p))
|
|
165
|
+
return true;
|
|
166
|
+
if (user.toLowerCase() === p)
|
|
167
|
+
return true; // root:root, prisma:prisma
|
|
168
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(pass))
|
|
169
|
+
return true; // USER:PASSWORD, DBPASS
|
|
170
|
+
if (pass.startsWith('$') || pass.startsWith('<') || pass.startsWith('{'))
|
|
171
|
+
return true; // ${DB_PASS}, <pw>
|
|
172
|
+
if (isPlaceholderSecret(pass, { aggressive: true }))
|
|
173
|
+
return true;
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* @returns `true` when `value` looks like a placeholder / example / test
|
|
178
|
+
* credential and should be suppressed.
|
|
179
|
+
*/
|
|
180
|
+
function isPlaceholderSecret(value, opts = {}) {
|
|
181
|
+
if (!value)
|
|
182
|
+
return false;
|
|
183
|
+
const v = value.toLowerCase();
|
|
184
|
+
for (const marker of STANDARD_MARKERS) {
|
|
185
|
+
if (v.includes(marker))
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (isLowVariety(value))
|
|
189
|
+
return true;
|
|
190
|
+
if (opts.aggressive) {
|
|
191
|
+
for (const marker of AGGRESSIVE_MARKERS) {
|
|
192
|
+
if (v.includes(marker))
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=placeholder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"placeholder.js","sourceRoot":"","sources":["../../src/utils/placeholder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { DiagnosticCode } from '../diagnostics';
|
|
2
|
+
/**
|
|
3
|
+
* Heuristic safety checks for user-supplied regex patterns.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ---------------
|
|
7
|
+
* `VaultGuardConfig.extra_patterns[].regex` is compiled with `new RegExp(src, 'g')`
|
|
8
|
+
* and run against every line of every scanned file. A malicious or careless
|
|
9
|
+
* `.vault-guard.json` (anywhere in the repo, after the `loadConfig` git-boundary
|
|
10
|
+
* fix) can therefore implant a catastrophic-backtracking regex that pins the
|
|
11
|
+
* scanner CPU on the first file with the right shape.
|
|
12
|
+
*
|
|
13
|
+
* What this does (and does not) catch
|
|
14
|
+
* -----------------------------------
|
|
15
|
+
* This is a **conservative, dependency-free** static check:
|
|
16
|
+
*
|
|
17
|
+
* 1. Hard length cap (256 chars): real-world secret patterns are <100 chars;
|
|
18
|
+
* anything longer is suspicious and also bounds compile-time RAM use.
|
|
19
|
+
* 2. Quantifier-density cap (>25 of `* + ? {`): the academic ReDoS literature
|
|
20
|
+
* shows pathological patterns concentrate quantifiers; this is the same
|
|
21
|
+
* threshold `safe-regex` uses.
|
|
22
|
+
* 3. Nested-quantifier shape: `(…[*+]…)[*+]` — catches `(a+)+`, `(\d+)*`.
|
|
23
|
+
* 4. Alternation-quantifier shape: `(.|.)[*+]` — catches `(a|a)+`, `(\d|\d)*`.
|
|
24
|
+
*
|
|
25
|
+
* What this does NOT catch
|
|
26
|
+
* ------------------------
|
|
27
|
+
* - Cross-group backtracking like `(.*x)(y.*)` chains.
|
|
28
|
+
* - Pathological lookaheads.
|
|
29
|
+
* - Anything a determined attacker can hide behind character classes.
|
|
30
|
+
*
|
|
31
|
+
* Real defence in depth requires execution-time bounds (e.g. `re2`). That is
|
|
32
|
+
* tracked as a Phase 8 follow-up; this module is the pre-launch backstop.
|
|
33
|
+
*
|
|
34
|
+
* Users who need to bypass the heuristic for a known-safe pattern can set
|
|
35
|
+
* `extra_patterns_unsafe: true` in `.vault-guard.json`. The length cap still
|
|
36
|
+
* applies as a memory-use backstop even in unsafe mode.
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Maximum allowed source length for user-provided regex strings.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* This cap is both a safety and performance bound. Real-world secret patterns
|
|
43
|
+
* are usually well under 100 characters; 256 leaves margin for legitimate
|
|
44
|
+
* patterns while rejecting pathological or accidental megaregex input.
|
|
45
|
+
*/
|
|
46
|
+
export declare const REGEX_MAX_LENGTH = 256;
|
|
47
|
+
/**
|
|
48
|
+
* Maximum number of quantifier tokens (`* + ? {`) allowed in a user pattern.
|
|
49
|
+
*
|
|
50
|
+
* @remarks
|
|
51
|
+
* High quantifier density correlates strongly with catastrophic backtracking.
|
|
52
|
+
* The threshold intentionally matches common static-check heuristics.
|
|
53
|
+
*/
|
|
54
|
+
export declare const REGEX_MAX_QUANTIFIERS = 25;
|
|
55
|
+
export type RegexSafetyReason = 'too_long' | 'too_many_quantifiers' | 'nested_quantifier' | 'alternation_quantifier' | 'invalid_syntax';
|
|
56
|
+
/**
|
|
57
|
+
* Canonical mapping from regex-safety rejections to diagnostics codes.
|
|
58
|
+
*
|
|
59
|
+
* Keep this mapping in one place so scan/reporting surfaces don't drift.
|
|
60
|
+
*/
|
|
61
|
+
export declare const REGEX_REASON_TO_DIAGNOSTIC_CODE: Record<RegexSafetyReason, DiagnosticCode>;
|
|
62
|
+
export interface RegexSafetyResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
reason?: RegexSafetyReason;
|
|
65
|
+
detail?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Validate a user-supplied regex source string with heuristic ReDoS guards.
|
|
69
|
+
*
|
|
70
|
+
* @param source - Raw regex source from `.vault-guard.json` (without delimiters).
|
|
71
|
+
* @returns `ok: true` when accepted, otherwise `ok: false` with machine-readable
|
|
72
|
+
* `reason` and human-readable `detail` for diagnostics output.
|
|
73
|
+
*/
|
|
74
|
+
export declare function validateRegexSafety(source: string): RegexSafetyResult;
|
|
75
|
+
/**
|
|
76
|
+
* Perform a length-only safety check.
|
|
77
|
+
*
|
|
78
|
+
* @param source - Raw regex source from `.vault-guard.json`.
|
|
79
|
+
* @returns `ok: false` with `reason: 'too_long'` when the source exceeds
|
|
80
|
+
* `REGEX_MAX_LENGTH`, otherwise `ok: true`.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* This is intentionally used even when `extra_patterns_unsafe: true` so there
|
|
84
|
+
* is always a hard memory backstop.
|
|
85
|
+
*/
|
|
86
|
+
export declare function validateRegexLength(source: string): RegexSafetyResult;
|
|
87
|
+
/**
|
|
88
|
+
* Convert a regex safety rejection reason to the canonical diagnostic code.
|
|
89
|
+
*
|
|
90
|
+
* @param reason - Safety rejection identifier.
|
|
91
|
+
* @returns Stable diagnostics code for JSON/SARIF/text reporting.
|
|
92
|
+
*/
|
|
93
|
+
export declare function mapRegexSafetyReasonToDiagnosticCode(reason: RegexSafetyReason): DiagnosticCode;
|
|
94
|
+
/**
|
|
95
|
+
* Safely map an untyped rejection reason string to a diagnostics code.
|
|
96
|
+
*
|
|
97
|
+
* @param reason - Rejection reason coming from runtime validation paths.
|
|
98
|
+
* @returns Canonical diagnostics code, defaulting to `pattern.redos_unsafe`
|
|
99
|
+
* when the reason is unknown.
|
|
100
|
+
*/
|
|
101
|
+
export declare function mapPatternRejectionReasonToDiagnosticCode(reason: string): DiagnosticCode;
|
|
102
|
+
//# sourceMappingURL=regex-safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regex-safety.d.ts","sourceRoot":"","sources":["../../src/utils/regex-safety.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAKxC,MAAM,MAAM,iBAAiB,GACzB,UAAU,GACV,sBAAsB,GACtB,mBAAmB,GACnB,wBAAwB,GACxB,gBAAgB,CAAC;AAErB;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,iBAAiB,EAAE,cAAc,CAMrF,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAmCrE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CASrE;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAAC,MAAM,EAAE,iBAAiB,GAAG,cAAc,CAE9F;AAED;;;;;;GAMG;AACH,wBAAgB,yCAAyC,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAKxF"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REGEX_REASON_TO_DIAGNOSTIC_CODE = exports.REGEX_MAX_QUANTIFIERS = exports.REGEX_MAX_LENGTH = void 0;
|
|
4
|
+
exports.validateRegexSafety = validateRegexSafety;
|
|
5
|
+
exports.validateRegexLength = validateRegexLength;
|
|
6
|
+
exports.mapRegexSafetyReasonToDiagnosticCode = mapRegexSafetyReasonToDiagnosticCode;
|
|
7
|
+
exports.mapPatternRejectionReasonToDiagnosticCode = mapPatternRejectionReasonToDiagnosticCode;
|
|
8
|
+
/**
|
|
9
|
+
* Heuristic safety checks for user-supplied regex patterns.
|
|
10
|
+
*
|
|
11
|
+
* Why this exists
|
|
12
|
+
* ---------------
|
|
13
|
+
* `VaultGuardConfig.extra_patterns[].regex` is compiled with `new RegExp(src, 'g')`
|
|
14
|
+
* and run against every line of every scanned file. A malicious or careless
|
|
15
|
+
* `.vault-guard.json` (anywhere in the repo, after the `loadConfig` git-boundary
|
|
16
|
+
* fix) can therefore implant a catastrophic-backtracking regex that pins the
|
|
17
|
+
* scanner CPU on the first file with the right shape.
|
|
18
|
+
*
|
|
19
|
+
* What this does (and does not) catch
|
|
20
|
+
* -----------------------------------
|
|
21
|
+
* This is a **conservative, dependency-free** static check:
|
|
22
|
+
*
|
|
23
|
+
* 1. Hard length cap (256 chars): real-world secret patterns are <100 chars;
|
|
24
|
+
* anything longer is suspicious and also bounds compile-time RAM use.
|
|
25
|
+
* 2. Quantifier-density cap (>25 of `* + ? {`): the academic ReDoS literature
|
|
26
|
+
* shows pathological patterns concentrate quantifiers; this is the same
|
|
27
|
+
* threshold `safe-regex` uses.
|
|
28
|
+
* 3. Nested-quantifier shape: `(…[*+]…)[*+]` — catches `(a+)+`, `(\d+)*`.
|
|
29
|
+
* 4. Alternation-quantifier shape: `(.|.)[*+]` — catches `(a|a)+`, `(\d|\d)*`.
|
|
30
|
+
*
|
|
31
|
+
* What this does NOT catch
|
|
32
|
+
* ------------------------
|
|
33
|
+
* - Cross-group backtracking like `(.*x)(y.*)` chains.
|
|
34
|
+
* - Pathological lookaheads.
|
|
35
|
+
* - Anything a determined attacker can hide behind character classes.
|
|
36
|
+
*
|
|
37
|
+
* Real defence in depth requires execution-time bounds (e.g. `re2`). That is
|
|
38
|
+
* tracked as a Phase 8 follow-up; this module is the pre-launch backstop.
|
|
39
|
+
*
|
|
40
|
+
* Users who need to bypass the heuristic for a known-safe pattern can set
|
|
41
|
+
* `extra_patterns_unsafe: true` in `.vault-guard.json`. The length cap still
|
|
42
|
+
* applies as a memory-use backstop even in unsafe mode.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Maximum allowed source length for user-provided regex strings.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* This cap is both a safety and performance bound. Real-world secret patterns
|
|
49
|
+
* are usually well under 100 characters; 256 leaves margin for legitimate
|
|
50
|
+
* patterns while rejecting pathological or accidental megaregex input.
|
|
51
|
+
*/
|
|
52
|
+
exports.REGEX_MAX_LENGTH = 256;
|
|
53
|
+
/**
|
|
54
|
+
* Maximum number of quantifier tokens (`* + ? {`) allowed in a user pattern.
|
|
55
|
+
*
|
|
56
|
+
* @remarks
|
|
57
|
+
* High quantifier density correlates strongly with catastrophic backtracking.
|
|
58
|
+
* The threshold intentionally matches common static-check heuristics.
|
|
59
|
+
*/
|
|
60
|
+
exports.REGEX_MAX_QUANTIFIERS = 25;
|
|
61
|
+
const NESTED_QUANTIFIER = /\([^()]*[*+][^()]*\)[*+]/;
|
|
62
|
+
const ALT_QUANTIFIER = /\([^()|]*\|[^()|]*\)[*+]/;
|
|
63
|
+
/**
|
|
64
|
+
* Canonical mapping from regex-safety rejections to diagnostics codes.
|
|
65
|
+
*
|
|
66
|
+
* Keep this mapping in one place so scan/reporting surfaces don't drift.
|
|
67
|
+
*/
|
|
68
|
+
exports.REGEX_REASON_TO_DIAGNOSTIC_CODE = {
|
|
69
|
+
invalid_syntax: 'pattern.invalid',
|
|
70
|
+
too_long: 'pattern.too_long',
|
|
71
|
+
too_many_quantifiers: 'pattern.redos_unsafe',
|
|
72
|
+
nested_quantifier: 'pattern.redos_unsafe',
|
|
73
|
+
alternation_quantifier: 'pattern.redos_unsafe',
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Validate a user-supplied regex source string with heuristic ReDoS guards.
|
|
77
|
+
*
|
|
78
|
+
* @param source - Raw regex source from `.vault-guard.json` (without delimiters).
|
|
79
|
+
* @returns `ok: true` when accepted, otherwise `ok: false` with machine-readable
|
|
80
|
+
* `reason` and human-readable `detail` for diagnostics output.
|
|
81
|
+
*/
|
|
82
|
+
function validateRegexSafety(source) {
|
|
83
|
+
if (source.length > exports.REGEX_MAX_LENGTH) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: 'too_long',
|
|
87
|
+
detail: `regex source is ${source.length} chars (max ${exports.REGEX_MAX_LENGTH})`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const quantifierCount = countQuantifiers(source);
|
|
91
|
+
if (quantifierCount > exports.REGEX_MAX_QUANTIFIERS) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
reason: 'too_many_quantifiers',
|
|
95
|
+
detail: `regex contains ${quantifierCount} quantifiers (max ${exports.REGEX_MAX_QUANTIFIERS})`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (NESTED_QUANTIFIER.test(source)) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
reason: 'nested_quantifier',
|
|
102
|
+
detail: 'nested quantifier of the form `(…[*+]…)[*+]` detected (catastrophic backtracking)',
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (ALT_QUANTIFIER.test(source)) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: 'alternation_quantifier',
|
|
109
|
+
detail: 'alternation under quantifier `(a|b)[*+]` detected (potential exponential backtracking)',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { ok: true };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Perform a length-only safety check.
|
|
116
|
+
*
|
|
117
|
+
* @param source - Raw regex source from `.vault-guard.json`.
|
|
118
|
+
* @returns `ok: false` with `reason: 'too_long'` when the source exceeds
|
|
119
|
+
* `REGEX_MAX_LENGTH`, otherwise `ok: true`.
|
|
120
|
+
*
|
|
121
|
+
* @remarks
|
|
122
|
+
* This is intentionally used even when `extra_patterns_unsafe: true` so there
|
|
123
|
+
* is always a hard memory backstop.
|
|
124
|
+
*/
|
|
125
|
+
function validateRegexLength(source) {
|
|
126
|
+
if (source.length > exports.REGEX_MAX_LENGTH) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: 'too_long',
|
|
130
|
+
detail: `regex source is ${source.length} chars (max ${exports.REGEX_MAX_LENGTH})`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Convert a regex safety rejection reason to the canonical diagnostic code.
|
|
137
|
+
*
|
|
138
|
+
* @param reason - Safety rejection identifier.
|
|
139
|
+
* @returns Stable diagnostics code for JSON/SARIF/text reporting.
|
|
140
|
+
*/
|
|
141
|
+
function mapRegexSafetyReasonToDiagnosticCode(reason) {
|
|
142
|
+
return exports.REGEX_REASON_TO_DIAGNOSTIC_CODE[reason];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Safely map an untyped rejection reason string to a diagnostics code.
|
|
146
|
+
*
|
|
147
|
+
* @param reason - Rejection reason coming from runtime validation paths.
|
|
148
|
+
* @returns Canonical diagnostics code, defaulting to `pattern.redos_unsafe`
|
|
149
|
+
* when the reason is unknown.
|
|
150
|
+
*/
|
|
151
|
+
function mapPatternRejectionReasonToDiagnosticCode(reason) {
|
|
152
|
+
if (reason in exports.REGEX_REASON_TO_DIAGNOSTIC_CODE) {
|
|
153
|
+
return exports.REGEX_REASON_TO_DIAGNOSTIC_CODE[reason];
|
|
154
|
+
}
|
|
155
|
+
return 'pattern.redos_unsafe';
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Count `* + ? {` quantifier characters outside character classes.
|
|
159
|
+
*
|
|
160
|
+
* Approximation only — doesn't fully tokenise the regex, just skips `[…]`
|
|
161
|
+
* blocks where these characters are literal. Good enough to flag pathological
|
|
162
|
+
* patterns without false-positiving on `[a-z?]` or `\?`.
|
|
163
|
+
*/
|
|
164
|
+
function countQuantifiers(source) {
|
|
165
|
+
let count = 0;
|
|
166
|
+
let inClass = false;
|
|
167
|
+
let escape = false;
|
|
168
|
+
for (let i = 0; i < source.length; i++) {
|
|
169
|
+
const c = source[i];
|
|
170
|
+
if (escape) {
|
|
171
|
+
escape = false;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (c === '\\') {
|
|
175
|
+
escape = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (c === '[' && !inClass) {
|
|
179
|
+
inClass = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (c === ']' && inClass) {
|
|
183
|
+
inClass = false;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (inClass)
|
|
187
|
+
continue;
|
|
188
|
+
if (c === '*' || c === '+' || c === '?' || c === '{')
|
|
189
|
+
count++;
|
|
190
|
+
}
|
|
191
|
+
return count;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=regex-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"regex-safety.js","sourceRoot":"","sources":["../../src/utils/regex-safety.ts"],"names":[],"mappings":"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"}
|