shipped-secrets 1.0.1 → 1.0.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.
- package/README.md +11 -2
- package/bin/shipped-secrets.js +5 -3
- package/package.json +1 -1
- package/src/index.d.ts +4 -4
- package/src/scan.js +58 -10
- package/src/url.js +67 -67
- package/src/vite.d.ts +1 -1
- package/src/vite.js +2 -2
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Secrets are always masked in the output. Nothing is sent anywhere unless you ask
|
|
|
22
22
|
|
|
23
23
|
## What it checks
|
|
24
24
|
|
|
25
|
-
1. **Your real secret values.** It reads your `.env*` files (not `.env.example`), Cloudflare's `.dev.vars`, and in CI the environment variables whose names look secret. Then it searches every file the browser gets, source maps included, for each server value. This catches any secret, whatever it looks like, including your own tokens that no pattern knows. It also looks for each value
|
|
25
|
+
1. **Your real secret values.** It reads your `.env*` files (not `.env.example`), Cloudflare's `.dev.vars`, and in CI the environment variables whose names look secret. Then it searches every file the browser gets, source maps included, for each server value. This catches any secret, whatever it looks like, including your own tokens that no pattern knows. It also looks for each value the common ways it gets encoded on its way into a bundle: base64 (standard and URL-safe, with or without padding), hex, URL-encoded, and with `/` escaped as `\/` or `/` the way some JSON and RSC payloads write it. Each of these is an exact transform of your real value, so none of them can raise a false alarm.
|
|
26
26
|
2. **Known key shapes**, for keys that are not in your local files: Stripe secret and restricted keys, Supabase service_role keys and `sb_secret_` keys, OpenAI, Anthropic, AWS, GitHub, SendGrid, Mailgun, Resend, Twilio, Slack, private keys, and database URLs with a password. Google API keys are a warning, because Maps and Firebase browser keys are meant to be public.
|
|
27
27
|
3. **Supabase keys are decoded.** The anon key and the service_role key look the same. `shipped-secrets` reads the role inside the key, so the anon key is never reported and the service_role key always is.
|
|
28
28
|
4. **Risky names.** A public prefix on a name that sounds secret (`VITE_STRIPE_SECRET`, `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY`) is a warning. Names meant for browsers (anon key, publishable key, client ID, client token, site key) are not.
|
|
@@ -113,9 +113,18 @@ Checked: Stripe (`/v1/balance`), OpenAI and Anthropic (list models), GitHub (`/u
|
|
|
113
113
|
|
|
114
114
|
`--all` is `--history` and `--verify` together.
|
|
115
115
|
|
|
116
|
+
## A secret split into pieces
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
npx shipped-secrets --deep
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
A plain split like `"sk_live_" + "..."` is usually caught already, because minifiers join string pieces back together. `--deep` is for a split the minifier cannot undo, such as pieces in an array joined when the code runs. It cuts each `.env` value into pieces of 10 characters, keeps only the ones that look random (a digit, a letter, eight different characters, so never a common start like `sk_live_` or `https://`), and reports a file that holds two or more of them but never the whole value. It is off by default to keep the default run free of false alarms. The Vite plugin takes it too: `shippedSecrets({ deep: true })`.
|
|
123
|
+
|
|
116
124
|
## What it does not do
|
|
117
125
|
|
|
118
|
-
- It does not find
|
|
126
|
+
- It does not find an encrypted secret, or one encoded in a way not listed above. If the browser can decrypt it, so can anyone, so it belongs on the server anyway.
|
|
127
|
+
- `--deep` does not find a secret cut into pieces shorter than 10 characters, and it is not used on git history.
|
|
119
128
|
- It does not fix anything. A shipped or committed secret must be rotated at the provider: deleting it from the code, or rewriting history, does not take it back from anyone who already has it.
|
|
120
129
|
- `--verify` does not check AWS, Twilio, private keys or database URLs: checking those safely needs more than one harmless call.
|
|
121
130
|
|
package/bin/shipped-secrets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// shipped-secrets [folder or URL] [--dir <path>] [--allow NAME] [--history] [--verify] [--all] [--strict] [--json]
|
|
2
|
+
// shipped-secrets [folder or URL] [--dir <path>] [--allow NAME] [--history] [--verify] [--deep] [--all] [--strict] [--json]
|
|
3
3
|
//
|
|
4
4
|
// With no argument: finds the browser output in this project (dist, .next/static,
|
|
5
5
|
// build, out, ...), reads this project's .env files, and looks for your real
|
|
@@ -25,6 +25,7 @@ names that look secret, and source maps left public.
|
|
|
25
25
|
|
|
26
26
|
--history also search every commit in git history, and flag any .env file ever committed
|
|
27
27
|
--verify ask each found key's own provider whether it still works (sends the key to that provider only)
|
|
28
|
+
--deep also look for a secret split into pieces (two or more long random pieces of it in one file)
|
|
28
29
|
--all --history and --verify
|
|
29
30
|
--dir <path> scan this folder instead of finding the build output (repeatable)
|
|
30
31
|
--allow NAME a variable that is fine to ship (repeatable). Or add "# shipped-ok" to its .env line
|
|
@@ -37,14 +38,15 @@ const target = positional[0] ?? '.'
|
|
|
37
38
|
const allow = values('--allow')
|
|
38
39
|
const history = flag('--history') || flag('--all')
|
|
39
40
|
const verify = flag('--verify') || flag('--all')
|
|
41
|
+
const deep = flag('--deep')
|
|
40
42
|
let result
|
|
41
43
|
if (/^https?:\/\//.test(target)) {
|
|
42
44
|
const { secrets } = loadSecrets(process.cwd(), { env: process.env, allow })
|
|
43
|
-
result = await scanUrl(target, secrets)
|
|
45
|
+
result = await scanUrl(target, secrets, { deep })
|
|
44
46
|
} else {
|
|
45
47
|
const root = resolve(target)
|
|
46
48
|
const dirs = values('--dir')
|
|
47
|
-
result = scanProject(root, { dirs: dirs.length ? dirs.map((d) => resolve(root, d)) : undefined, allow })
|
|
49
|
+
result = scanProject(root, { dirs: dirs.length ? dirs.map((d) => resolve(root, d)) : undefined, allow, deep })
|
|
48
50
|
if (!dirs.length && !result.dirs.length && !history) {
|
|
49
51
|
console.error('No build output found (dist, build, out, .next/static, ...). Build first, pass --dir <path>, or use --history.')
|
|
50
52
|
process.exit(2)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shipped-secrets",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Find secrets that ship to the browser or sit in git history, and check whether they still work: your real .env values, known key shapes and risky public env names, in any framework's build output, source maps included.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type Finding = {
|
|
2
2
|
level: 'error' | 'warn'
|
|
3
|
-
kind: 'value' | 'shape' | 'name' | 'sourcemap' | 'history-value' | 'history-shape' | 'history-env'
|
|
3
|
+
kind: 'value' | 'pieces' | 'shape' | 'name' | 'sourcemap' | 'history-value' | 'history-shape' | 'history-env'
|
|
4
4
|
file: string
|
|
5
5
|
line: number
|
|
6
6
|
what: string
|
|
@@ -15,13 +15,13 @@ export type Finding = {
|
|
|
15
15
|
export type Result = { findings: Finding[]; files: number; dirs?: string[]; secretsChecked?: number; commits?: number; verified?: boolean }
|
|
16
16
|
export type Secret = { name: string; value: string; source: string; line: number; ok: boolean }
|
|
17
17
|
|
|
18
|
-
export declare function scanProject(root: string, opts?: { dirs?: string[]; allow?: string[]; env?: Record<string, string | undefined>; envFiles?: string[] }): Result
|
|
19
|
-
export declare function scanUrl(url: string, secrets?: Secret[]): Promise<Result>
|
|
18
|
+
export declare function scanProject(root: string, opts?: { dirs?: string[]; allow?: string[]; env?: Record<string, string | undefined>; envFiles?: string[]; deep?: boolean }): Result
|
|
19
|
+
export declare function scanUrl(url: string, secrets?: Secret[], opts?: { deep?: boolean }): Promise<Result>
|
|
20
20
|
export declare function scanHistory(root: string, secrets?: Secret[]): Promise<{ findings: Finding[]; commits: number } | null>
|
|
21
21
|
export declare function verifyKey(shapeId: string, key: string, fetch?: typeof globalThis.fetch): Promise<'live' | 'dead' | 'unknown'>
|
|
22
22
|
export declare function verifyFindings(findings: Finding[], fetch?: typeof globalThis.fetch): Promise<Finding[]>
|
|
23
23
|
export declare const VERIFIABLE: string[]
|
|
24
|
-
export declare function scanText(text: string, secrets: Secret[]): Omit<Finding, 'file'>[]
|
|
24
|
+
export declare function scanText(text: string, secrets: Secret[], opts?: { deep?: boolean }): Omit<Finding, 'file'>[]
|
|
25
25
|
export declare function findOutputDirs(root: string): string[]
|
|
26
26
|
export declare function loadSecrets(dir: string, opts?: { env?: Record<string, string | undefined>; files?: string[]; allow?: string[] }): { secrets: Secret[]; all: Secret[] }
|
|
27
27
|
export declare function report(result: Result): string
|
package/src/scan.js
CHANGED
|
@@ -74,27 +74,75 @@ export function keyOf(finding, value) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* The value as written
|
|
78
|
-
*
|
|
79
|
-
*
|
|
77
|
+
* The value as written and the common ways it gets encoded on its way into a
|
|
78
|
+
* bundle: base64 (padding dropped, so it matches with or without the "="), hex,
|
|
79
|
+
* URL-encoded, and with "/" escaped the way some JSON and RSC payloads write it.
|
|
80
|
+
* Each is an exact transform of the real value, so none of them can raise a
|
|
81
|
+
* false alarm.
|
|
80
82
|
*/
|
|
81
83
|
export function forms(value) {
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
|
|
84
|
+
const out = [{ text: value, how: '' }]
|
|
85
|
+
const add = (text, how) => { if (text !== value && !out.some((f) => f.text === text)) out.push({ text, how }) }
|
|
86
|
+
const buf = Buffer.from(value, 'utf8')
|
|
87
|
+
const b64 = buf.toString('base64').replace(/=+$/, '')
|
|
88
|
+
add(b64, ', base64')
|
|
89
|
+
add(b64.replace(/\+/g, '-').replace(/\//g, '_'), ', base64url')
|
|
90
|
+
add(buf.toString('hex'), ', hex')
|
|
91
|
+
add(buf.toString('hex').toUpperCase(), ', hex')
|
|
92
|
+
add(encodeURIComponent(value), ', URL-encoded')
|
|
93
|
+
if (value.includes('/')) {
|
|
94
|
+
add(value.replace(/\//g, '\\/'), ', escaped')
|
|
95
|
+
add(value.replace(/\//g, '\\u002f'), ', escaped')
|
|
96
|
+
}
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const PIECE = 10
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* For --deep: pieces of the value, to find a secret split up so that the whole
|
|
104
|
+
* of it never appears. Only from values long enough to have a random part, and
|
|
105
|
+
* only pieces that look random (a digit, a letter, eight different characters),
|
|
106
|
+
* so a common start like "sk_live_" or "https://" is never one of them.
|
|
107
|
+
*/
|
|
108
|
+
export function pieces(value) {
|
|
109
|
+
if (value.length < 24) return []
|
|
110
|
+
const out = []
|
|
111
|
+
for (let i = 0; i + PIECE <= value.length; i += PIECE) {
|
|
112
|
+
const p = value.slice(i, i + PIECE)
|
|
113
|
+
if (/\d/.test(p) && /[A-Za-z]/.test(p) && new Set(p).size >= 8) out.push(p)
|
|
114
|
+
}
|
|
115
|
+
return out
|
|
85
116
|
}
|
|
86
117
|
|
|
87
|
-
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} text
|
|
120
|
+
* @param {{ name: string, value: string, source: string }[]} secrets
|
|
121
|
+
* @param {{ deep?: boolean }} [opts]
|
|
122
|
+
*/
|
|
123
|
+
export function scanText(text, secrets, opts = {}) {
|
|
88
124
|
const found = []
|
|
89
125
|
for (const s of secrets) {
|
|
126
|
+
const from = s.source.split(/[\\/]/).pop()
|
|
127
|
+
let whole = false
|
|
90
128
|
for (const { text: form, how } of forms(s.value)) {
|
|
91
129
|
let at = text.indexOf(form)
|
|
92
130
|
while (at !== -1) {
|
|
93
|
-
|
|
131
|
+
whole = true
|
|
132
|
+
found.push(keyOf({ level: 'error', kind: 'value', what: `${s.name} from ${from}${how}`, sample: mask(s.value), line: lineAt(text, at) }, s.value))
|
|
94
133
|
at = text.indexOf(form, at + form.length)
|
|
95
134
|
if (found.length > 50) break
|
|
96
135
|
}
|
|
97
136
|
}
|
|
137
|
+
// Two or more random pieces of one secret in the same file, and never the
|
|
138
|
+
// whole of it: split on purpose. A single piece is left alone.
|
|
139
|
+
if (opts.deep && !whole) {
|
|
140
|
+
const all = pieces(s.value)
|
|
141
|
+
const hits = all.map((p) => text.indexOf(p)).filter((at) => at !== -1)
|
|
142
|
+
if (hits.length >= 2) {
|
|
143
|
+
found.push(keyOf({ level: 'error', kind: 'pieces', what: `${s.name} from ${from}, in pieces (${hits.length} of ${all.length} found)`, sample: mask(s.value), line: lineAt(text, Math.min(...hits)) }, s.value))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
98
146
|
}
|
|
99
147
|
for (const f of findShapes(text)) {
|
|
100
148
|
// A value already reported by name is not reported twice as a shape.
|
|
@@ -108,7 +156,7 @@ export function scanText(text, secrets) {
|
|
|
108
156
|
* Scans a project folder: its env files for risky names, and its browser output
|
|
109
157
|
* for real secret values and known key shapes.
|
|
110
158
|
* @param {string} root
|
|
111
|
-
* @param {{ dirs?: string[], allow?: string[], env?: Record<string, string | undefined>, envFiles?: string[] }} [opts]
|
|
159
|
+
* @param {{ dirs?: string[], allow?: string[], env?: Record<string, string | undefined>, envFiles?: string[], deep?: boolean }} [opts]
|
|
112
160
|
*/
|
|
113
161
|
export function scanProject(root, opts = {}) {
|
|
114
162
|
const { secrets, all } = loadSecrets(root, { env: opts.env ?? process.env, files: opts.envFiles, allow: opts.allow })
|
|
@@ -129,7 +177,7 @@ export function scanProject(root, opts = {}) {
|
|
|
129
177
|
files++
|
|
130
178
|
if (file.endsWith('.map')) maps++
|
|
131
179
|
const text = readFileSync(file, 'utf8')
|
|
132
|
-
for (const f of scanText(text, secrets)) findings.push(Object.assign(f, { file: relative(root, file) }))
|
|
180
|
+
for (const f of scanText(text, secrets, { deep: opts.deep })) findings.push(Object.assign(f, { file: relative(root, file) }))
|
|
133
181
|
}
|
|
134
182
|
}
|
|
135
183
|
if (maps) {
|
package/src/url.js
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
// Scans a live site the way a stranger would: the page, the scripts it loads,
|
|
2
|
-
// the chunks those scripts load, and any source maps they point to.
|
|
3
|
-
|
|
4
|
-
import { scanText } from './scan.js'
|
|
5
|
-
|
|
6
|
-
const MAX_FILES = 200
|
|
7
|
-
|
|
8
|
-
async function get(url) {
|
|
9
|
-
try {
|
|
10
|
-
const res = await fetch(url, { redirect: 'follow', headers: { 'user-agent': 'shipped-secrets' } })
|
|
11
|
-
if (!res.ok) return null
|
|
12
|
-
const type = res.headers.get('content-type') || ''
|
|
13
|
-
if (!/text|javascript|json|html|xml|svg/.test(type) && !/\.(m?js|map|json|css|html?)(\?|$)/.test(url)) return null
|
|
14
|
-
return await res.text()
|
|
15
|
-
} catch {
|
|
16
|
-
return null
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function assetsIn(text, base, isHtml) {
|
|
21
|
-
const urls = new Set()
|
|
22
|
-
const add = (u) => {
|
|
23
|
-
try {
|
|
24
|
-
const abs = new URL(u, base)
|
|
25
|
-
if (abs.origin === new URL(base).origin) urls.add(abs.href.split('#')[0])
|
|
26
|
-
} catch {
|
|
27
|
-
/* not a URL */
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (isHtml) {
|
|
31
|
-
for (const m of text.matchAll(/<script[^>]*\bsrc=["']([^"']+)["']/gi)) add(m[1])
|
|
32
|
-
for (const m of text.matchAll(/<link[^>]*\brel=["'](?:modulepreload|preload|stylesheet)["'][^>]*\bhref=["']([^"']+)["']/gi)) add(m[1])
|
|
33
|
-
for (const m of text.matchAll(/<link[^>]*\bhref=["']([^"']+)["'][^>]*\brel=["'](?:modulepreload|preload|stylesheet)["']/gi)) add(m[1])
|
|
34
|
-
} else {
|
|
35
|
-
// Chunks a bundle loads later, and its source map.
|
|
36
|
-
for (const m of text.matchAll(/["'`]((?:\.{0,2}\/)?[\w./-]+\.(?:m?js|css))["'`]/g)) add(m[1])
|
|
37
|
-
for (const m of text.matchAll(/\/[/*]# sourceMappingURL=([^\s*]+)/g)) if (!m[1].startsWith('data:')) add(m[1])
|
|
38
|
-
}
|
|
39
|
-
return urls
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Scans a deployed site by URL. Only files on the same site are fetched.
|
|
44
|
-
* @param {string} url
|
|
45
|
-
* @param {{ name: string, value: string, source: string }[]} [secrets] local values to look for too
|
|
46
|
-
*/
|
|
47
|
-
export async function scanUrl(url, secrets = []) {
|
|
48
|
-
const findings = []
|
|
49
|
-
const seen = new Set()
|
|
50
|
-
const queue = [url]
|
|
51
|
-
let maps = 0
|
|
52
|
-
while (queue.length && seen.size < MAX_FILES) {
|
|
53
|
-
const u = queue.shift()
|
|
54
|
-
if (seen.has(u)) continue
|
|
55
|
-
seen.add(u)
|
|
56
|
-
const text = await get(u)
|
|
57
|
-
if (text === null) continue
|
|
58
|
-
const isHtml = u === url || /<html|<!doctype/i.test(text.slice(0, 500))
|
|
59
|
-
if (/\.map(\?|$)/.test(u)) maps++
|
|
60
|
-
for (const f of scanText(text, secrets)) findings.push(Object.assign(f, { file: u }))
|
|
61
|
-
for (const next of assetsIn(text, u, isHtml)) if (!seen.has(next)) queue.push(next)
|
|
62
|
-
}
|
|
63
|
-
if (maps) {
|
|
64
|
-
findings.push({ level: 'warn', kind: 'sourcemap', file: url, line: 0, what: `${maps} source map${maps === 1 ? '' : 's'} can be downloaded from this site. Anyone can read your original source code from them.` })
|
|
65
|
-
}
|
|
66
|
-
return { findings, files: seen.size }
|
|
67
|
-
}
|
|
1
|
+
// Scans a live site the way a stranger would: the page, the scripts it loads,
|
|
2
|
+
// the chunks those scripts load, and any source maps they point to.
|
|
3
|
+
|
|
4
|
+
import { scanText } from './scan.js'
|
|
5
|
+
|
|
6
|
+
const MAX_FILES = 200
|
|
7
|
+
|
|
8
|
+
async function get(url) {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch(url, { redirect: 'follow', headers: { 'user-agent': 'shipped-secrets' } })
|
|
11
|
+
if (!res.ok) return null
|
|
12
|
+
const type = res.headers.get('content-type') || ''
|
|
13
|
+
if (!/text|javascript|json|html|xml|svg/.test(type) && !/\.(m?js|map|json|css|html?)(\?|$)/.test(url)) return null
|
|
14
|
+
return await res.text()
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assetsIn(text, base, isHtml) {
|
|
21
|
+
const urls = new Set()
|
|
22
|
+
const add = (u) => {
|
|
23
|
+
try {
|
|
24
|
+
const abs = new URL(u, base)
|
|
25
|
+
if (abs.origin === new URL(base).origin) urls.add(abs.href.split('#')[0])
|
|
26
|
+
} catch {
|
|
27
|
+
/* not a URL */
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (isHtml) {
|
|
31
|
+
for (const m of text.matchAll(/<script[^>]*\bsrc=["']([^"']+)["']/gi)) add(m[1])
|
|
32
|
+
for (const m of text.matchAll(/<link[^>]*\brel=["'](?:modulepreload|preload|stylesheet)["'][^>]*\bhref=["']([^"']+)["']/gi)) add(m[1])
|
|
33
|
+
for (const m of text.matchAll(/<link[^>]*\bhref=["']([^"']+)["'][^>]*\brel=["'](?:modulepreload|preload|stylesheet)["']/gi)) add(m[1])
|
|
34
|
+
} else {
|
|
35
|
+
// Chunks a bundle loads later, and its source map.
|
|
36
|
+
for (const m of text.matchAll(/["'`]((?:\.{0,2}\/)?[\w./-]+\.(?:m?js|css))["'`]/g)) add(m[1])
|
|
37
|
+
for (const m of text.matchAll(/\/[/*]# sourceMappingURL=([^\s*]+)/g)) if (!m[1].startsWith('data:')) add(m[1])
|
|
38
|
+
}
|
|
39
|
+
return urls
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Scans a deployed site by URL. Only files on the same site are fetched.
|
|
44
|
+
* @param {string} url
|
|
45
|
+
* @param {{ name: string, value: string, source: string }[]} [secrets] local values to look for too
|
|
46
|
+
*/
|
|
47
|
+
export async function scanUrl(url, secrets = [], opts = {}) {
|
|
48
|
+
const findings = []
|
|
49
|
+
const seen = new Set()
|
|
50
|
+
const queue = [url]
|
|
51
|
+
let maps = 0
|
|
52
|
+
while (queue.length && seen.size < MAX_FILES) {
|
|
53
|
+
const u = queue.shift()
|
|
54
|
+
if (seen.has(u)) continue
|
|
55
|
+
seen.add(u)
|
|
56
|
+
const text = await get(u)
|
|
57
|
+
if (text === null) continue
|
|
58
|
+
const isHtml = u === url || /<html|<!doctype/i.test(text.slice(0, 500))
|
|
59
|
+
if (/\.map(\?|$)/.test(u)) maps++
|
|
60
|
+
for (const f of scanText(text, secrets, { deep: opts.deep })) findings.push(Object.assign(f, { file: u }))
|
|
61
|
+
for (const next of assetsIn(text, u, isHtml)) if (!seen.has(next)) queue.push(next)
|
|
62
|
+
}
|
|
63
|
+
if (maps) {
|
|
64
|
+
findings.push({ level: 'warn', kind: 'sourcemap', file: url, line: 0, what: `${maps} source map${maps === 1 ? '' : 's'} can be downloaded from this site. Anyone can read your original source code from them.` })
|
|
65
|
+
}
|
|
66
|
+
return { findings, files: seen.size }
|
|
67
|
+
}
|
package/src/vite.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default function shippedSecrets(opts?: { allow?: string[]; failOnWarn?: boolean }): { name: string; apply: 'build' }
|
|
1
|
+
export default function shippedSecrets(opts?: { allow?: string[]; failOnWarn?: boolean; deep?: boolean }): { name: string; apply: 'build' }
|
package/src/vite.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { report, scanProject } from './index.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* @param {{ allow?: string[], failOnWarn?: boolean }} [opts]
|
|
10
|
+
* @param {{ allow?: string[], failOnWarn?: boolean, deep?: boolean }} [opts]
|
|
11
11
|
*/
|
|
12
12
|
export default function shippedSecrets(opts = {}) {
|
|
13
13
|
let root = process.cwd()
|
|
@@ -23,7 +23,7 @@ export default function shippedSecrets(opts = {}) {
|
|
|
23
23
|
},
|
|
24
24
|
closeBundle() {
|
|
25
25
|
if (!isBuild) return
|
|
26
|
-
const result = scanProject(root, { dirs: [outDir], allow: opts.allow })
|
|
26
|
+
const result = scanProject(root, { dirs: [outDir], allow: opts.allow, deep: opts.deep })
|
|
27
27
|
if (!result.findings.length) return
|
|
28
28
|
const text = report(result)
|
|
29
29
|
const fail = result.findings.some((f) => f.level === 'error') || (opts.failOnWarn && result.findings.length)
|