The report truncated each line to 120 characters and redacted nothing, so a
credential shorter than the budget was printed whole -- into the terminal
scrollback, the CI log, and wherever that log is shipped. It applied to every
mode, which meant a real leak caught by the pre-commit hook was also a real leak
printed to a terminal. The comment above it claimed the match was never echoed
in full; it was corrected to describe the behaviour in the previous commit, and
this changes the behaviour instead.
The match is now masked before truncation. \001 is the substitution delimiter,
as a real control byte rather than the literal backslash-zero-zero-one a
double-quoted "\001" produces -- that first attempt made sed take `\` as its
delimiter and silently substitute nothing, which looked exactly like working
code. These patterns contain both / and |, so either would end the expression
early.
Widening the JWT pattern was part of the same fix, not a separate improvement.
Masking removes exactly what the pattern matched, so `eyJ[A-Za-z0-9_-]{10,}`
redacted the header and printed the payload and signature next to it -- and
those are the token. It now matches all three segments. A pattern that
under-matches is a pattern that half-prints the secret.
Verified in --built and --staged: a planted JWT and a user:pass@host URL are
each reported with file and line, and neither planted value appears anywhere in
the output.
closes#11
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--staged and --tracked scan what is in git. Neither sees the bundle, which is
the only artifact a user receives -- and a key reaches it without ever being
committed, inlined from an environment variable at build time. An auditor of
applications of this kind reported hardcoded credentials in the frontend bundle
of seven of eight in a single week.
Two tiers, because one would have been useless:
findings (exit 1) eyJ, service_role, apikey=, plus every pattern the other
modes already use
noted (exit 0) NEXT_PUBLIC_, VITE_, REACT_APP_, anon
The second tier is printed and fails nothing. Those prefixes mean "deliberately
shipped to the browser", so failing on them would be a permanently red gate, and
a gate that is always red is one everybody has learned to ignore. But a Supabase
anon key is safe exactly as far as row-level security makes it safe, and knowing
it is out there is the input to that judgement rather than a substitute for it.
eyJ is confined to --built on purpose: it is the base64 of the `{"` every JWT
header starts with, and against source it matches ordinary base64 constantly.
Verified: a planted JWT and service_role in a scratch dist/ are found and exit
1; removing them exits 0 with the public references still listed; a directory
that does not exist exits 2, because nothing scanned is not a pass. Findings are
reported relative to the build directory -- an absolute path consumed the whole
truncation budget and left findings that named a file and showed nothing.
One correction shipped with it: the comment above the report claimed the match
is never echoed in full. It is not redacted at all, only truncated at 120
characters, so a short credential is printed whole. The comment now says what
the code does. Masking the matched span is the real fix and is filed separately.
closes#8
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.
This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.
secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>