node lib/jev-secrets.ts --history checks every commit on branches and tags, and the earlier review revisions under refs/forge/keep, which review pages show. A push's check only reads commits the repo doesn't have yet, so it can't vet a repo that is about to be made public. Same exit codes: 3 means a finding. The wording no longer says everything here is public: a public repo shows a pushed secret to everyone, and a private one keeps it in its history for whoever reads it.
- commit
- 07e5022d80f8999bf738e68cef85becd6432f41f
- parent
- f97fd9844137ebdb7d4126e3cbe528c33da004df
- author
- cat <admin@dev.site>
- kind
- feat risk score 2.0 of 3, Jev's estimate
| M | lib/ | 56 | +++++++++++++++++++++--------- |
@@ -1,5 +1,7 @@
1-// The pre-receive secret check. Everything on the forge is public, so a push2-// that adds a credential is refused before it lands, not rotated after. Each1+// The pre-receive secret check. A public repo shows everyone what is pushed2+// to it, and a private one keeps it for its readers, and for everyone once it3+// is made public, so a push that adds a credential is refused before it4+// lands, not rotated after. Private repos are checked like public ones. Each35 // new commit's added lines are scanned, since history keeps a secret even46 // after a later commit removes it:57 //@@ -18,7 +20,12 @@
1820 // the API timed out or was unreachable (see ask()), only the patterns run.1921 // Imports (-o noreview) and `git push -o allow-secret` skip the check.2022 //23+// Making a private repo public (ssh <host> public) runs it over the repo's24+// whole history first: a push's check only reads commits the repo doesn't25+// have yet, which there are none of then.26+//2127 // node lib/jev-secrets.ts <updates pre-receive's "old new ref" lines, GIT_DIR set28+// node lib/jev-secrets.ts --history every commit in the repo (historyCommits), GIT_DIR set2229 //2330 // Exit 3 means a finding, printed on stderr; anything else accepts the push.2431 // Not 1, which node also uses when a module fails to load: a broken import@@ -83,6 +90,13 @@ export function newCommits(gitDir: string, updates: string): string[] {
8390 return tips.length ? revList(gitDir, [...tips, '--not', '--all']) : [];8491 }8592 93+// Every commit that going public shows: those on branches and tags, as the94+// push check reads them, and the earlier revisions of reviews95+// (refs/forge/keep), which review pages show.96+export function historyCommits(gitDir: string): string[] {97+ return revList(gitDir, ['--branches', '--tags', '--glob=refs/forge/keep/*']);98+}99+86100 const unquote = (s: string): string => {87101 s = s.replace(/\t$/, '');88102 if (!s.startsWith('"')) return s;@@ -260,10 +274,10 @@ export function state(batch: Part[]): unknown {
260274 export function questions(paths: string[]) {261275 return {262276 secret: noul(263- 'These are the lines a git push adds to a public repository, grouped by file, each starting with its line number. ' +277+ 'These are the lines commits add to a git repository that is public or may be made public, grouped by file, each starting with its line number. ' +264278 'Do the added lines put a real credential into the repository: a password, API key, access token, private key, or a connection string or URL with a password or token in it?',265279 {266- true: 'At least one added line contains a literal secret value that looks real and would let anyone who reads this public repository log in or call an API: a hardcoded password, a token or key string, a database URL with a password, a webhook URL with its secret token, or a .env or config entry with a real secret value.',280+ true: 'At least one added line contains a literal secret value that looks real and would let anyone who reads this repository log in or call an API: a hardcoded password, a token or key string, a database URL with a password, a webhook URL with its secret token, or a .env or config entry with a real secret value.',267281 false: 'No added line contains a real secret value. These are not real: placeholders and examples (changeme, <your-password>, xxx, your-api-key, example.com), test fixtures and mock values, environment variable names and reads (process.env.X, os.environ["X"], ${X}), empty values, redacted or masked values (****, REDACTED), hashes and checksums, public keys, and code that handles credentials without containing one.',268282 },269283 ),@@ -314,35 +328,45 @@ export async function jevFindings(all: Part[][], min: number, ms: number): Promi
314328 return { findings, failed };315329 }316330 317-function report(found: Finding[]): void {331+function report(found: Finding[], history: boolean): void {318332 const seen = new Set<string>();319333 const rows = found.map((f) => ` ${f.path}:${f.line} in ${f.commit.slice(0, 7)}: ${f.by}`).filter((r) => !seen.has(r) && seen.add(r));320334 process.stderr.write([321- 'forge: secret check: this push adds what looks like a credential:',335+ history ? "forge: secret check: this repo's history has what looks like a credential:" : 'forge: secret check: this push adds what looks like a credential:',322336 ...rows.slice(0, 20),323337 ...(rows.length > 20 ? [` and ${rows.length - 20} more`] : []),324- 'Everything on this forge is public. Take the secret out of every commit that adds it',325- '(git commit --amend for the last commit, git rebase -i for older ones), read it from',326- 'the environment or an untracked file instead, push again, and rotate the secret.',327- 'If it is not a real secret: git push -o allow-secret <remote> <branch>',338+ ...(history339+ ? [340+ 'Making the repo public would show it to everyone. Take the secret out of every commit',341+ 'that adds it (git rebase -i, or git filter-repo), force-push, and rotate the secret.',342+ 'If it is not a real secret: ssh <host> public <owner>/<repo> --force',343+ ]344+ : [345+ 'A public repo shows it to everyone, and a private one keeps it in its history. Take the',346+ 'secret out of every commit that adds it (git commit --amend for the last commit,',347+ 'git rebase -i for older ones), read it from the environment or an untracked file',348+ 'instead, push again, and rotate the secret.',349+ 'If it is not a real secret: git push -o allow-secret <remote> <branch>',350+ ]),328351 ].join('\n') + '\n');329352 }330353 331-export async function check(gitDir: string, updates: string, env: NodeJS.ProcessEnv = process.env): Promise<number> {354+// updates: pre-receive's lines; null checks the whole history (--history).355+export async function check(gitDir: string, updates: string | null, env: NodeJS.ProcessEnv = process.env): Promise<number> {332356 const t0 = Date.now();333- const opts = pushOptions(env);357+ const opts = updates == null ? [] : pushOptions(env);334358 if (opts.includes('noreview')) return 0;335359 if (opts.includes('allow-secret')) {336360 warn('skipped (-o allow-secret)');337361 return 0;338362 }339- const commits = newCommits(gitDir, updates);363+ const commits = updates == null ? historyCommits(gitDir) : newCommits(gitDir, updates);340364 const added = await addedLines(gitDir, commits);341365 const found = patternFindings(added);342366 let warning: string | null = null;343367 const todo = enabled(env) ? batches(added) : [];344368 if (todo.length && commits.length > MAX_COMMITS) {345- warning = `${commits.length} new commits; checked known key formats only`;369+ warning = `${commits.length} ${updates == null ? '' : 'new '}commits; checked known key formats only`;346370 } else if (todo.length) {347371 const left = DEADLINE - (Date.now() - t0);348372 if (left < 1000) warning = 'no time left for Jev; checked known key formats only';@@ -353,7 +377,7 @@ export async function check(gitDir: string, updates: string, env: NodeJS.Process
353377 }354378 }355379 if (found.length) {356- report(found);380+ report(found, updates == null);357381 return FOUND;358382 }359383 if (warning) warn(warning);@@ -364,7 +388,7 @@ export async function check(gitDir: string, updates: string, env: NodeJS.Process
364388 if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {365389 let code = 0;366390 try {367- code = await check(process.env.GIT_DIR || '.', fs.readFileSync(0, 'utf8'));391+ code = await check(process.env.GIT_DIR || '.', process.argv[2] === '--history' ? null : fs.readFileSync(0, 'utf8'));368392 } catch (err) {369393 warn(`${err instanceof Error ? err.message : String(err)}; push not checked`);370394 }