From 9510bddea2bc29f97f48e9572a529b5e10bb6fa3 Mon Sep 17 00:00:00 2001 From: Zack Whitson Date: Sat, 8 Aug 2026 15:21:03 -0500 Subject: [PATCH] Release CodeTruss CLI v0.2.57 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Syncs the 0.2.57 source from the private monorepo. Two false-positive fixes reach users here: Firebase web API keys stop being reported as committed credentials (0.2.56), and an error-code enum stops being reported as three leaked passwords (0.2.57). The mirror's own .gitignore would have silently swallowed the new `.env.production.fixture` — `.env.*` matches it — which would have shipped the 0.2.56 acceptance assertions with no fixture to run against. The monorepo hit the same trap and carries the same negation; this adds it here. Co-Authored-By: Claude Opus 5 --- .gitignore | 5 + CHANGELOG.md | 170 ++- packages/analyzer-engine/src/secrets.ts | 199 ++++ packages/cli/CHANGELOG.md | 168 +++ packages/cli/package.json | 2 +- .../acceptance/.env.production.fixture | 21 + .../acceptance/src/firebase.ts.fixture | 14 + packages/cli/scripts/test-acceptance.mjs | 146 ++- packages/cli/src/cli.ts | 8 +- packages/cli/src/config.ts | 89 +- packages/cli/src/receipt-markdown.ts | 590 ++++++++++ packages/cli/src/receipt-store.ts | 198 ++++ packages/cli/src/receipt-verify.ts | 265 +++++ packages/cli/src/receipt.ts | 1042 +---------------- packages/cli/test/command-e2e.test.ts | 18 + packages/cli/test/config.test.ts | 79 ++ .../codetruss-cli-0.2.57.sbom.cdx.json | 170 +++ public/downloads/codetruss-cli-0.2.57.tgz | Bin 0 -> 979549 bytes .../downloads/codetruss-cli-0.2.57.tgz.sha256 | 1 + public/downloads/codetruss-cli-latest.json | 14 +- .../codetruss-cli-latest.sbom.cdx.json | 10 +- public/downloads/codetruss-cli-latest.tgz | Bin 962141 -> 979549 bytes .../downloads/codetruss-cli-latest.tgz.sha256 | 2 +- release-reference.json | 10 +- 24 files changed, 2151 insertions(+), 1070 deletions(-) create mode 100644 packages/cli/scripts/fixtures/acceptance/.env.production.fixture create mode 100644 packages/cli/scripts/fixtures/acceptance/src/firebase.ts.fixture create mode 100644 packages/cli/src/receipt-markdown.ts create mode 100644 packages/cli/src/receipt-store.ts create mode 100644 packages/cli/src/receipt-verify.ts create mode 100644 public/downloads/codetruss-cli-0.2.57.sbom.cdx.json create mode 100644 public/downloads/codetruss-cli-0.2.57.tgz create mode 100644 public/downloads/codetruss-cli-0.2.57.tgz.sha256 diff --git a/.gitignore b/.gitignore index 83a1ca6..1a55d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ coverage/ .env .env.* !.env.example +# The CLI acceptance fixtures. `secrets` escalates on a path matching `/\.env/`, +# so a fixture that reproduces that escalation has to be named like one — and +# `.env.*` above silently swallowed it, which would have shipped a check that +# never reached CI. Only this directory is exempt, and it holds fabricated keys. +!packages/cli/scripts/fixtures/acceptance/.env*.fixture *.pem *.key *.p12 diff --git a/CHANGELOG.md b/CHANGELOG.md index aeb6379..ffe03fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ CodeTruss CLI follows semantic versioning. Release artifacts and their SHA-256 checksums are published at . -The current public release is [v0.2.55 on GitHub](https://github.com/CodeTruss/codetruss-cli/releases/tag/v0.2.55), +The current public release is [v0.2.57 on GitHub](https://github.com/CodeTruss/codetruss-cli/releases/tag/v0.2.57), distributed from . The npm `latest` tag is still [`@codetruss/cli@0.2.50`](https://www.npmjs.com/package/@codetruss/cli/v/0.2.50): @@ -16,6 +16,174 @@ were superseded before distribution. No unreleased changes. +## 0.2.57 — 2026-08-08 + +- **An error-code enum is no longer three leaked passwords.** A value that + merely spells out its own key — `IncorrectEmailPassword = + "incorrect-email-password"` — carries nothing the identifier beside it did not + already carry. That is what an enum member, an error code, or an action + constant looks like; a credential is the opposite, a value the key cannot + predict. The SCREAMING_SNAKE exemption already covered `UPDATE_PASSWORD = + 'UPDATE_PASSWORD'`, so a repository writing the same constants in camelCase or + kebab-case got HIGH "treat as compromised, rotate immediately" findings over a + file of error codes. The exemption is now the RELATION between value and key + rather than the shape of the value, which is the whole point: `password = + "correct-horse-battery-staple"` is a real passphrase in kebab case and is + still reported. It is one-directional — words dropped from the key are fine + (`UserMissingPassword = "missing-password"`), words added to it are not, + because added words are where entropy would live — and a one-word restatement + (`password = "password"`) stays reported, because that is a worthless + credential rather than an enum. Gated on recall rather than on examples: 400 + random secrets across base64 and hex on six credential key names, and 200 + kebab-case passphrases sharing no word with their key, all still reported. + Re-measured on the ten-repository cross-tool corpus before and after: **904 + findings both ways, nothing removed and nothing added**, every analyzer + unchanged. State that for what it is — the corpus proves this narrowing costs + no recall there, not that it helps there. The shape it removes does not occur + in those ten repositories; it was found by adjudicating a scan of + calcom/cal.com, which is not one of them. +- **Repository-supplied scope globs are bounded before they reach the matcher.** + `allow`, `deny` and `exclude` come from the scanned repository's + `.codetruss.yml`, and `minimatch`'s brace expansion is combinatorial in the + pattern: a 7.5 KB glob of repeated `{a,b}` groups ended 0.2.45 in an + out-of-memory abort — exit 134, no verdict, no receipt, nothing a `catch` + could see. 0.2.46 fixed that by taking a `brace-expansion` release that bounds + its own output, which left the fix living entirely in a dependency, for a + matcher shape that recurs. The untrusted input is now bounded too: 512 + characters, 16 brace groups, and 1,024 potential expansions, checked before + any pattern reaches the matcher and applied identically to a typed `--allow` + flag. Oversized patterns are rejected by name rather than truncated, because a + truncated glob silently enforces a policy nobody wrote. This repository's own + longest policy glob is under 50 characters with two brace groups. +- **`receipt.ts` is four modules instead of one 1,042-line file.** Nothing it + does changes. The file had grown to hold three unrelated jobs — rendering the + Markdown, keeping receipts on disk, and checking one against its signature — + and its own `size` analyzer had been flagging it. It is now + `receipt-markdown.ts`, `receipt-store.ts` and `receipt-verify.ts` behind a + `receipt.ts` facade that re-exports every name that was exported before; no + call site changed. The frozen renderers were the reason to be careful rather + than a reason not to move them: their bytes sit inside receipts that are + already signed, so each of `local-registry-v1` … `v5` was re-rendered and + checked against the SHA-256 its signature covers, and all five are unchanged. + +## 0.2.56 — 2026-08-08 + +Two CRITICAL findings survived 0.2.53 on the cross-tool corpus. Hand adjudication +judged both wrong. They were the loudest thing this CLI said about anyone's code, and +what they said was "treat this as compromised" about a value Google publishes on +purpose. + +- **A Firebase web API key is a public client identifier, and we reported it CRITICAL.** + `excalidraw/excalidraw` commits a `VITE_APP_FIREBASE_CONFIG` in `.env.development:17` + and `.env.production:17`. Each holds a Firebase **web** `apiKey`, which is compiled + into the browser bundle by design, which Google documents as an identifier rather than + a secret, and which is protected by Security Rules, App Check and per-key restrictions + — never by keeping it out of a repository. + + The escalation did not come from the credential type. It came from the **path**: a file + matching `/(^|\/)\.env/` reports CRITICAL, so an identifier the framework publishes to + every visitor was reported as a leak with "Committed credentials should be treated as + compromised" attached. That is not a severity that was slightly too high. It is wrong + advice about the class. + + The engine now has a notion of **publishable client identifier**: a credential whose + exposure is by design, recognised from the credential TYPE plus its surroundings, never + from either alone. Only `Google API key` is eligible today, so no surroundings can + downgrade a Stripe live key, a GitHub token or a PEM block. Eligibility alone downgrades + nothing either, because a Google key is just as often a server key. Two recognisers sit + behind it: + + - **A publishable variable name.** `VITE_`, `NEXT_PUBLIC_`, `REACT_APP_` and `PUBLIC_` + are the documented conventions by which Vite, Next.js, Create React App, SvelteKit and + Astro inline a value into the client bundle. The prefix is not a hint about intent, it + is the build tool's guarantee that the value is already public. Reported **LOW**, as a + restriction question: confirm it is a client key and restrict it, and if a server key + was pasted under a published name then it is already exposed and must be rotated. + - **The Firebase web config object.** `apiKey` bound to Google's `AIza` + 35-character + web format, with at least three of `authDomain`, `projectId`, `storageBucket`, + `messagingSenderId`, `appId`, `databaseURL`, `measurementId` as object keys within + eight lines. This is what catches `initializeApp({ … })` written straight into client + source, where there is no environment variable to read. Reported **INFO**. + + What it deliberately does **not** match: a Google API key under any other name; an + `apiKey` whose value is not `AIza…`; an `AIza…` key bound to its own name beside a + Firebase block; or a Firebase **admin** service account, whose keys are snake_case and + whose actual secret is a PEM block the private-key pattern still reports at full + severity. Exempting on the key name `apiKey` would have hidden real API keys in every + codebase, which is a worse defect than the one this fixes, so the name is one conjunct + of three and never sufficient alone. + + **Measured on the ten-repository corpus, not on our own code.** Published 0.2.55 against + this build, same pinned commits, same harness + (`docs/benchmarks/cross-tool-2026-08/lib/run-codetruss.sh`): **904 findings before, 904 + after.** Exactly four findings moved, all four in excalidraw — the two CRITICALs became + the two INFOs, at the same files and the same lines. Every other repository's count, + every other rule's count, and the LOW/MEDIUM/HIGH mix are unchanged. CRITICAL on this + corpus goes 2 → 0. All 30 hand-adjudicated findings in + `docs/benchmarks/cross-tool-2026-08/adjudication/verdicts.md` were re-checked + mechanically: **zero correct findings lost**, and sample #18 moves from CRITICAL to INFO. + + `packages/cli/scripts/test-acceptance.mjs` pins the whole truth table rather than the one + case that was wrong — a Firebase config in a published `.env` variable (INFO), the same + config as an `initializeApp` argument (INFO), a published variable with no Firebase config + around it (LOW), and, in the same committed `.env`, the same key format under a name the + build tool does not publish (**CRITICAL**). That last row is the reason the other three + are safe, and it fails loudly if the exemption is ever widened into "an `apiKey` is never + a secret". Verified in both directions: without this change the check fails with nine + differences, with it the four fixtures reproduce six adjudicated verdicts. + +- **0.2.55 overstated its own test coverage.** That entry said the acceptance check "runs + in all nine compatibility contexts and in the release job". It runs in **six of nine**, + and in the release job. + + The mirror's `.github/workflows/ci.yml` gates its `Test` step on + `if: matrix.node != '20.9.0'`, so the three Node 20.9 contexts run + `test-deterministic-package.mjs` and `test-release-verifier.mjs` and never reach + `pnpm test`. Mirror CI run `31269228613`, which gated the 0.2.55 release, records + `Test=skipped` on ubuntu, macOS and Windows at Node 20.9.0 and `Test=success` on the + other six. The claim was written from the intent of the change and never checked against + a run. + + **0.2.55's published bytes are unchanged.** They are tagged, attested and served; the + correction ships here, the way 0.2.53's stale README profile id was corrected in 0.2.54. + + We think the honest fix is to make the claim true rather than keep softening it, and the + gate does not appear to have a reason that covers this script. It exists because + `pnpm test` begins with `vitest run`, and `vite@8` declares + `node: ^20.19.0 || >=22.12.0` — which is also why the 20.9 job installs with + `--config.engine-strict=false`. `test-acceptance.mjs` is plain Node using only built-ins + available since 20.1, and it spawns `dist/cli.cjs`, whose own floor is the 20.9 this + package promises. Those contexts already build the artifact and already run the shipped + binary through `pnpm test:install`, so the runtime is proven there; what is missing is + the behaviour check, on the oldest Node we support, for about 45 seconds. That change + belongs to `CodeTruss/codetruss-cli`, which this repository does not contain, so it is a + recommendation here and not a diff. It is recorded on `CodeTruss/codetruss-cli#47`. + +- **…and it named a `CONTRIBUTING.md` the mirror does not carry.** 0.2.55 said + "`CONTRIBUTING.md` now says what mirror CI verifies". That section was added to the + **monorepo's** `CONTRIBUTING.md`. The mirror has its own `CONTRIBUTING.md` — a different + document, addressed to people filing public issues, which states that outside source + changes are not accepted — and a reader of the mirror cannot see the section the entry + pointed them at. + + We are not syncing the section across. The mirror's file is authored in the mirror and + written for an audience that cannot open a pull request against the build it describes; + moving maintainer CI guidance into it would put advice there that nobody there can act + on, and nothing in this repository generates that file, so claiming the sync would carry + it would be a second false claim. The correction is this entry, and the fact itself is + stated here rather than pointed at, because the CHANGELOG is the one document that does + reach the mirror and the archive: + + > The nine mirror CI contexts verify packaging — a reproducible archive, a verifier that + > rejects tampering, a changelog chain, a README whose profile id matches the code. Six + > of them, plus the tag-triggered release job, additionally run + > `packages/cli/scripts/test-acceptance.mjs`, which executes the built `dist/cli.cjs` + > over committed fixtures and asserts the findings the release must and must not + > produce. The rule-level SAST suites live in the monorepo and reach neither. + + The monorepo's `CONTRIBUTING.md` carried the same "all nine contexts" error and is + corrected in this change. + ## 0.2.55 — 2026-08-08 Nine green CI contexts gated every release before this one, and none of them could diff --git a/packages/analyzer-engine/src/secrets.ts b/packages/analyzer-engine/src/secrets.ts index 679626b..50bae10 100644 --- a/packages/analyzer-engine/src/secrets.ts +++ b/packages/analyzer-engine/src/secrets.ts @@ -110,6 +110,110 @@ export const FAKE_LITERAL_VALUE = new RegExp( 'i', ) +/** + * PUBLISHABLE CLIENT IDENTIFIERS — credentials whose exposure is by design. + * + * Some credential formats are not secrets at all. A Firebase **web** `apiKey`, a + * Google Maps browser key: these are compiled into the client bundle, Google + * documents them as public identifiers, and what protects the project is + * Security Rules, App Check and per-key restrictions — never keeping the value + * out of the repository. Reporting one as a committed credential is wrong, and + * "treat as compromised, rotate immediately" is wrong advice about it. + * + * A publishable identifier is recognised from the credential TYPE plus its + * surroundings, never from either alone. Only types listed here are eligible, + * so the widest imaginable surroundings can never downgrade a Stripe live key or + * a PEM block — and eligibility alone downgrades nothing, because a Google API + * key is just as often a server key. + */ +const PUBLISHABLE_CREDENTIAL_TYPES = new Set(['Google API key']) + +/** + * Build-time variable prefixes that mean "this value ships to the browser". + * + * Vite, Next.js, Create React App, SvelteKit and Astro each inline variables + * carrying these prefixes into the client bundle. The prefix is not a hint about + * intent — it is the framework's guarantee that the value is already public to + * every visitor, which is precisely why a committed one is not the leak. + * + * Four prefixes, each a documented convention of a major framework. Adding a + * fifth widens what this analyzer will decline to escalate, so it is a + * deliberate act and not a matter of being generous with synonyms. + */ +const PUBLISHABLE_ENV_BINDING = /\b((?:VITE_|NEXT_PUBLIC_|REACT_APP_|PUBLIC_)[A-Z0-9_]*)\s*[:=]/ + +/** + * The keys a Firebase **web** app config carries beside `apiKey`, as object + * keys (`authDomain:` or `"authDomain":`). Exact case, because that is how + * Firebase documents and how its console emits them; a looser spelling buys + * nothing and widens an exemption. + */ +const FIREBASE_WEB_CONFIG_KEYS = [ + 'authDomain', + 'projectId', + 'storageBucket', + 'messagingSenderId', + 'appId', + 'databaseURL', + 'measurementId', +].map((key) => new RegExp(`['"]?${key}['"]?\\s*:`)) + +/** + * How many of {@link FIREBASE_WEB_CONFIG_KEYS} must sit beside the key before + * the object is a Firebase web config rather than something that merely holds + * an `apiKey`. Three, because Firebase's console emits six or seven and the + * threshold is the only thing standing between this exemption and a real key. + */ +const FIREBASE_WEB_CONFIG_SIBLINGS = 3 + +/** + * Lines either side of the match that count as "beside" it. A web config is at + * most eight keys, so its siblings are within eight lines of `apiKey` whether it + * is written as one JSON line in a `.env` or spread down an `initializeApp({…})` + * argument. + */ +const FIREBASE_WEB_CONFIG_WINDOW = 8 + +/** + * The binding itself: `apiKey` assigned a value in Google's documented web-key + * format, on ONE line. Both halves are load-bearing and neither is sufficient. + */ +const FIREBASE_WEB_API_KEY_BINDING = /['"]?apiKey['"]?\s*:\s*['"]AIza[0-9A-Za-z_-]{35}['"]/ + +/** + * Whether this line's Google API key is a Firebase **web** config key. + * + * The second recogniser, and the one that does not need an environment file: the + * config object itself. `initializeApp({ apiKey, authDomain, projectId, … })` + * written straight into client source carries no `VITE_`-style name, and is the + * same public identifier as the `.env` spelling. + * + * The predicate is a CONJUNCTION of three things, because the obvious shortcut — + * exempting the key name `apiKey` — would hide real API keys in every codebase, + * which is a far worse defect than the one it fixes: + * + * 1. the line binds the literal key `apiKey` (not `API_KEY`, not `googleKey`); + * 2. its value is in Google's `AIza` + 35-character web-key format; + * 3. at least {@link FIREBASE_WEB_CONFIG_SIBLINGS} of the Firebase config's + * other keys appear as object keys within + * {@link FIREBASE_WEB_CONFIG_WINDOW} lines. + * + * So it does NOT match: a Google API key under any other name; an `apiKey` whose + * value is not `AIza…` (Stripe, OpenAI, an internal key); an `AIza…` Maps or + * YouTube key that happens to sit near a Firebase block but is bound to its own + * name; or a Firebase **admin** service-account JSON, whose keys are snake_case + * and whose actual secret is a PEM block the private-key pattern still reports. + */ +export function isFirebaseWebApiKey(lines: readonly string[], index: number): boolean { + if (!FIREBASE_WEB_API_KEY_BINDING.test(lines[index])) return false + const context = lines + .slice(Math.max(0, index - FIREBASE_WEB_CONFIG_WINDOW), index + FIREBASE_WEB_CONFIG_WINDOW + 1) + .join('\n') + let siblings = 0 + for (const key of FIREBASE_WEB_CONFIG_KEYS) if (key.test(context)) siblings += 1 + return siblings >= FIREBASE_WEB_CONFIG_SIBLINGS +} + /** * The literal a match assigns, for placeholder judgement. * @@ -131,6 +235,63 @@ function placeholderSubject(credentialType: string, matched: string): string | n * `UPDATE_PASSWORD = 'UPDATE_PASSWORD'` is an enum member. */ const CONSTANT_IDENTIFIER_VALUE = /^[A-Z0-9]+(_[A-Z0-9]+)+$/ +/** + * An identifier or slug broken into its lowercase words, so the two casings a + * codebase uses for the same name compare equal: `IncorrectEmailPassword` and + * `incorrect-email-password` both yield `[incorrect, email, password]`. + */ +function identifierWords(text: string): string[] { + return text + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase -> camel Case + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // HTTPServer -> HTTP Server + .split(/[^A-Za-z0-9]+/) + .filter(Boolean) + .map((word) => word.toLowerCase()) +} + +/** True when `needle` appears as a contiguous run of words inside `haystack`. */ +function isWordRun(needle: string[], haystack: string[]): boolean { + if (needle.length === 0 || needle.length > haystack.length) return false + for (let start = 0; start + needle.length <= haystack.length; start++) { + if (needle.every((word, offset) => haystack[start + offset] === word)) return true + } + return false +} + +/** The full identifier the pattern matched on, which spans further left than + * the match: `password` is where `IncorrectEmailPassword` ends, not begins. */ +function assignedKey(line: string, matchIndex: number): string { + const identifierChar = /[A-Za-z0-9_$]/ + let start = matchIndex + while (start > 0 && identifierChar.test(line[start - 1])) start-- + let end = matchIndex + while (end < line.length && identifierChar.test(line[end])) end++ + return line.slice(start, end) +} + +/** + * A value that merely spells out its own KEY — `IncorrectPassword = + * "incorrect-password"`. Its words are a contiguous run of the key's words, so + * it carries no information the identifier beside it did not already carry. + * That is what an enum member, an error code, or an action constant looks like; + * a credential is the opposite, a value the key cannot predict. + * + * The narrow shape matters. `password: "correct-horse-battery-staple"` is a + * real passphrase in kebab case, so exempting kebab-case values wholesale would + * suppress genuine leaks — the exemption has to be the RELATION to the key, not + * the shape of the value. The relation is deliberately one-directional: words + * dropped from the key are fine (`UserMissingPassword = "missing-password"`), + * words ADDED to it are not, because added words are where entropy would live. + * + * A one-word restatement (`password = "password"`) stays reported. It is not an + * enum member — it is a credential that happens to be worthless, and the two + * are indistinguishable from the key alone. + */ +function restatesItsKey(line: string, matchIndex: number, value: string): boolean { + const valueWords = identifierWords(value) + return valueWords.length >= 2 && isWordRun(valueWords, identifierWords(assignedKey(line, matchIndex))) +} + /** Dev/CI dummy hosts — credentials pointing here are not real secrets. */ const DUMMY_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'host.docker.internal']) @@ -328,6 +489,43 @@ export const secretsAnalyzer: Analyzer = { })) break } + // Publishable client identifiers, before any severity is assigned — + // including the `.env` escalation below, which is what made these + // CRITICAL. The file being a committed `.env` is not evidence about a + // value the framework publishes to every visitor anyway. + if (PUBLISHABLE_CREDENTIAL_TYPES.has(name)) { + if (isFirebaseWebApiKey(lines, i)) { + report(() => ({ + category: 'SECURITY_HYGIENE', + severity: 'INFO', + title: `Firebase web API key (public by design) in ${file.path.split('/').pop()}`, + description: `Line ${i + 1} of ${file.path} binds \`apiKey\` to a Google web-format key surrounded by a Firebase web app config (authDomain, projectId, storageBucket, messagingSenderId, appId). That config is compiled into the browser bundle by design and Google documents this key as a public client identifier, not a secret, so it is NOT reported as a leak. A Google API key under any other name, or one whose neighbours are not this config, is still reported.`, + filePath: file.path, + line: i + 1, + suggestion: 'No action needed for the key itself — it is already public in every client that loads this app. What protects the project is Firebase Security Rules, App Check, and an application restriction on the key in the Google Cloud console; confirm those are in place.', + impactScore: 5, + effort: 'low', + metadata: { credentialType: name, publishableClientKey: true, firebaseWebConfig: true }, + })) + break + } + const published = PUBLISHABLE_ENV_BINDING.exec(line) + if (published) { + report(() => ({ + category: 'SECURITY_HYGIENE', + severity: 'LOW', + title: `Browser-published ${name} under ${published[1]} in ${file.path.split('/').pop()}`, + description: `Line ${i + 1} of ${file.path} assigns a ${name} to \`${published[1]}\`, whose prefix tells the build tool to inline the value into the client bundle. It is therefore already public to every visitor, so committing it is not the leak — but a browser key is only as safe as its restrictions. Reported for confirmation rather than as a committed credential.`, + filePath: file.path, + line: i + 1, + suggestion: `Confirm this is a client key and restrict it in the Google Cloud console — by referrer or app, and by API. If a SERVER key was pasted under a public-prefixed name it is already exposed to every visitor: rotate it and move it to a variable the build tool does not publish.`, + impactScore: 20, + effort: 'low', + metadata: { credentialType: name, publishableClientKey: true, publicEnvPrefix: published[1] }, + })) + break + } + } // A credential committed to source is a single token. Internal // whitespace means the value is display text — a validation message, // an i18n string, a UI label — which nearly every repo has under a @@ -337,6 +535,7 @@ export const secretsAnalyzer: Analyzer = { if (name === 'Generic password assignment') { const value = /['"]([^'"]+)['"]/.exec(match[0])?.[1] if (value && CONSTANT_IDENTIFIER_VALUE.test(value)) continue + if (value && restatesItsKey(line, match.index ?? 0, value)) continue if (value && /\s/.test(value)) messageString = true } if (name === 'Database URL with credentials') { diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index a618a1a..310890c 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -5,6 +5,174 @@ checksums are published at