easyvibegate 0.6.2 → 0.6.9
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/README.en.md +10 -2
- package/README.md +11 -2
- package/dist/cli/index.js +27 -6
- package/dist/engine/checkers/deep/deps.js +198 -54
- package/dist/engine/checkers/index.js +2 -0
- package/dist/engine/checkers/static/client-exposure.js +16 -3
- package/dist/engine/checkers/static/config-risks.js +12 -2
- package/dist/engine/checkers/static/next-image-ssrf.js +137 -0
- package/dist/engine/checkers/static/rls-migrations.js +112 -44
- package/dist/engine/checkers/static/secrets.js +372 -291
- package/dist/engine/detect.js +17 -3
- package/dist/engine/util/git-exposure.js +41 -0
- package/dist/engine/util/text.js +25 -1
- package/dist/engine/walk.js +22 -12
- package/package.json +1 -1
package/dist/engine/detect.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { DNS_LABEL } from './util/text.js';
|
|
3
|
+
import { DNS_LABEL, looksLikeTestOrDocPath } from './util/text.js';
|
|
4
|
+
import { maskCode } from './util/mask.js';
|
|
4
5
|
const SUPABASE_URL_FALLBACK = new RegExp(`https?://${DNS_LABEL}\\.supabase\\.co`, 'i');
|
|
5
6
|
/** Infer the project's frameworks, backends, languages and package managers. */
|
|
6
7
|
export function detect(root, files) {
|
|
@@ -104,10 +105,23 @@ export function detect(root, files) {
|
|
|
104
105
|
// false positives from code that merely mentions a backend by name. Uses the
|
|
105
106
|
// shared bounded DNS_LABEL (see util/text.ts) so this cannot go quadratic on
|
|
106
107
|
// a long run of matching characters in a large file.
|
|
107
|
-
|
|
108
|
+
//
|
|
109
|
+
// Two independent guards against a project's own code making itself look
|
|
110
|
+
// like it uses a backend it only mentions: test/fixture files are excluded
|
|
111
|
+
// (a test suite for code that talks to Supabase routinely contains
|
|
112
|
+
// synthetic mock URLs shaped like a real project's own), and comments are
|
|
113
|
+
// masked before matching (an explanatory comment describing that exact URL
|
|
114
|
+
// shape is otherwise itself a match). Both were needed: this fallback once
|
|
115
|
+
// made EasyVibeGate's own test suite, and separately its own source
|
|
116
|
+
// comments, detect EasyVibeGate itself as a Supabase project — a security
|
|
117
|
+
// scanner flagging itself is exactly what dogfooding exists to catch.
|
|
118
|
+
// Strings are NOT masked: a real URL/call normally appears in one.
|
|
119
|
+
const nonTestFiles = files.filter((f) => !looksLikeTestOrDocPath(f.rel));
|
|
120
|
+
const codeOnly = (f) => maskCode(f.content, { file: f.rel });
|
|
121
|
+
if (!backends.has('supabase') && nonTestFiles.some((f) => SUPABASE_URL_FALLBACK.test(codeOnly(f)))) {
|
|
108
122
|
backends.add('supabase');
|
|
109
123
|
}
|
|
110
|
-
if (!backends.has('firebase') &&
|
|
124
|
+
if (!backends.has('firebase') && nonTestFiles.some((f) => { const c = codeOnly(f); return /\binitializeApp\s*\(/.test(c) && /firebase/i.test(c); })) {
|
|
111
125
|
backends.add('firebase');
|
|
112
126
|
}
|
|
113
127
|
return {
|
|
@@ -31,3 +31,44 @@ export function createExposure(root) {
|
|
|
31
31
|
return ex;
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* For a file that is NOT committed now (ignored or untracked): was THIS
|
|
36
|
+
* value ever in a version of it that WAS committed? Tells apart "moved to
|
|
37
|
+
* .gitignore before the real value was ever written here — never leaked"
|
|
38
|
+
* from "was committed with this exact credential, then removed — a real
|
|
39
|
+
* incident, still in history forever." Advice like "rotate it" is sound for
|
|
40
|
+
* the second and needless noise for the first, especially for a production
|
|
41
|
+
* key whose rotation itself carries real operational risk.
|
|
42
|
+
*
|
|
43
|
+
* Bounded to the file's last `maxCommits` commits touching it (a `git log`,
|
|
44
|
+
* then up to that many `git show` blob reads) — deliberate for a file with
|
|
45
|
+
* unusually long history; the answer degrades to "unknown", never "safe".
|
|
46
|
+
*
|
|
47
|
+
* Returns `true` (found it committed), `false` (checked history, never
|
|
48
|
+
* found), or `null` (no history to check, or git failed) — `null` must be
|
|
49
|
+
* treated as "no evidence either way", never as "confirmed safe".
|
|
50
|
+
*/
|
|
51
|
+
export function createHistoryChecker(root) {
|
|
52
|
+
const commitsCache = new Map();
|
|
53
|
+
const commitsFor = (rel, maxCommits) => {
|
|
54
|
+
const key = `${rel}\u0000${maxCommits}`;
|
|
55
|
+
const hit = commitsCache.get(key);
|
|
56
|
+
if (hit !== undefined)
|
|
57
|
+
return hit;
|
|
58
|
+
const log = git(root, ['log', '--all', '-n', String(maxCommits), '--format=%H', '--', rel]);
|
|
59
|
+
const commits = log === null ? null : log.split('\n').filter(Boolean);
|
|
60
|
+
commitsCache.set(key, commits);
|
|
61
|
+
return commits;
|
|
62
|
+
};
|
|
63
|
+
return (rel, value, maxCommits = 20) => {
|
|
64
|
+
const commits = commitsFor(rel, maxCommits);
|
|
65
|
+
if (commits === null || commits.length === 0)
|
|
66
|
+
return null; // never tracked at all, or git failed
|
|
67
|
+
for (const sha of commits) {
|
|
68
|
+
const blob = git(root, ['show', `${sha}:${rel}`]);
|
|
69
|
+
if (blob !== null && blob.includes(value))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
}
|
package/dist/engine/util/text.js
CHANGED
|
@@ -29,7 +29,17 @@ export function lineAt(content, index) {
|
|
|
29
29
|
}
|
|
30
30
|
return line;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* A value that IS a reference to a secret, not the secret itself: `${NAME}`
|
|
34
|
+
* (shell/JS template), `env(NAME)` (Supabase CLI's config.toml substitution —
|
|
35
|
+
* the exact shape `supabase init` writes into every generated config, so this
|
|
36
|
+
* fired on every Supabase project's default file, not one repo's mistake),
|
|
37
|
+
* `{{ NAME }}` (Jinja/Helm/GitHub Actions expression), or a bare `$NAME`
|
|
38
|
+
* shell variable with nothing else in the value. The last one is anchored to
|
|
39
|
+
* the WHOLE value (`^...$`) — a real secret that merely contains a `$`
|
|
40
|
+
* somewhere must not be excluded, only a value that IS just a var reference.
|
|
41
|
+
*/
|
|
42
|
+
const PLACEHOLDER = /(x{3,}|your[_-]?|<[^>]+>|\$\{|process\.env|import\.meta\.env|example|placeholder|changeme|dummy|test[_-]?key|xxxxx|\.\.\.|env\(\s*[A-Z][A-Z0-9_]*\s*\)|\{\{\s*[\w.]+\s*\}\}|^\$[A-Z][A-Z0-9_]*$)/i;
|
|
33
43
|
/** True if a captured value looks like a template/placeholder, not a real secret. */
|
|
34
44
|
export function looksLikePlaceholder(value) {
|
|
35
45
|
return PLACEHOLDER.test(value);
|
|
@@ -60,3 +70,17 @@ export function decodeJwtPayload(token) {
|
|
|
60
70
|
* milliseconds — and no real hostname label is longer than this anyway.
|
|
61
71
|
*/
|
|
62
72
|
export const DNS_LABEL = '[a-z0-9-]{1,63}';
|
|
73
|
+
/**
|
|
74
|
+
* Documentation, examples and test/fixture paths — where a sample config
|
|
75
|
+
* value (a fake Supabase URL, a placeholder key) legitimately lives without
|
|
76
|
+
* describing anything about the real project. A test suite for code that
|
|
77
|
+
* TALKS to a backend routinely contains literal-looking URLs/keys for that
|
|
78
|
+
* backend as mock data; without this, scanning a project's own tests can
|
|
79
|
+
* make the project appear to use a backend it merely tests against.
|
|
80
|
+
*/
|
|
81
|
+
export function looksLikeTestOrDocPath(rel) {
|
|
82
|
+
return /\.(md|txt|mdx|rst)$/i.test(rel)
|
|
83
|
+
|| /\.(example|sample|template|dist)$/i.test(rel)
|
|
84
|
+
|| /(^|\/)(docs?|examples?|fixtures?|__fixtures__|__tests__|test|tests|spec|__mocks__)(\/|$)/i.test(rel)
|
|
85
|
+
|| /\.(test|spec)\.[a-z]+$/i.test(rel);
|
|
86
|
+
}
|
package/dist/engine/walk.js
CHANGED
|
@@ -15,22 +15,32 @@ const SKIP_DIRS = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
/**
|
|
17
17
|
* Names ambiguous enough that they are sometimes real source (a module named
|
|
18
|
-
* "cache", a package called "tmp") and sometimes pure data.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
18
|
+
* "cache", a package called "tmp") and sometimes pure data. Path depth alone
|
|
19
|
+
* does not tell them apart: `src/cache` and `data/cache` are both 2 segments,
|
|
20
|
+
* but the first is exactly as likely to be a project's own source directory
|
|
21
|
+
* (a caching module) as the second is to be scan output. What DOES tell them
|
|
22
|
+
* apart is the parent: a data-ish parent name (`data`, `var`, `storage`) means
|
|
23
|
+
* generated/fetched content; an ordinary source-tree parent does not. Skipped
|
|
24
|
+
* only when the ambiguous name is the project root's own child (`cache/`,
|
|
25
|
+
* `tmp/` at the top level), or its immediate parent is one of those data-ish
|
|
26
|
+
* names (`data/cache`, `var/tmp`). The real-world evidence for this was
|
|
27
|
+
* `data/cache/*.json` full of API pagination tokens, 559 of 569 "generic
|
|
28
|
+
* secrets" in one real project; blanket name-matching at any depth once made
|
|
29
|
+
* a source directory invisible to every check with no visible coverage gap,
|
|
30
|
+
* and matching by depth alone reopened that exact gap for `src/cache`.
|
|
27
31
|
*/
|
|
28
32
|
const AMBIGUOUS_DATA_DIRS = new Set(['cache', 'caches', 'tmp', 'temp', '.tmp']);
|
|
29
|
-
const
|
|
33
|
+
const DATA_ISH_PARENTS = new Set(['data', 'var', 'storage']);
|
|
34
|
+
const isShallowDataDir = (name, relDir) => {
|
|
35
|
+
if (!AMBIGUOUS_DATA_DIRS.has(name))
|
|
36
|
+
return false;
|
|
37
|
+
const segs = relDir.split('/');
|
|
38
|
+
return segs.length === 1 || (segs.length === 2 && DATA_ISH_PARENTS.has((segs[0] ?? '').toLowerCase()));
|
|
39
|
+
};
|
|
30
40
|
const TEXT_EXT = new Set([
|
|
31
|
-
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte',
|
|
41
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts', '.vue', '.svelte',
|
|
32
42
|
'.py', '.rb', '.php', '.go', '.rs', '.java', '.kt', '.cs',
|
|
33
|
-
'.html', '.css', '.scss', '.json', '.yml', '.yaml', '.toml',
|
|
43
|
+
'.html', '.css', '.scss', '.json', '.jsonc', '.json5', '.yml', '.yaml', '.toml',
|
|
34
44
|
'.env', '.sh', '.sql', '.md', '.txt', '.ini', '.conf', '.tf',
|
|
35
45
|
'.pem', '.key', '.crt', '.cert', '.pkcs8',
|
|
36
46
|
'.astro', '.properties', '.plist', '.swift', '.dart', '.ipynb', '.bash', '.zsh', '.mdx',
|
package/package.json
CHANGED