Skip to content

Replace hand-rolled vendor secret patterns with secretlint - #865

Merged
aviggiano merged 1 commit into
mainfrom
819-third-party-secret-scanning
Aug 25, 2026
Merged

aviggiano merged 1 commit into
mainfrom
819-third-party-secret-scanning

Conversation

@aviggiano

@aviggiano aviggiano commented Aug 25, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #819. Closes #822. Supersedes #820 and #823, applying the maintainer instruction from their review comments: "We want to avoid manually scanning secrets, we should use a third party library" (#820) and "This sensitive redaction is giving us too much headache, we want preferably to use a 3rd party library instead of reinventing the wheel" (#823).

Library chosen

secretlint 13.0.4 — @secretlint/core + @secretlint/secretlint-rule-preset-recommend (pinned exact, MIT, pure JS, no Go binary). Its programmatic API returns findings with match ranges, which the redaction callers need to rewrite text. The API is Promise-based while every caller of this module is synchronous — including the generated Smithers workflow verifier, whose emitted code is pinned by contract tests — so the library runs fully in-process on a dedicated worker thread behind a synchronous shared-memory bridge (Atomics.wait + length-prefixed payloads in a growable SharedArrayBuffer). Steady-state cost is ~0.2ms per scan, flat over thousands of calls; an early MessagePort-based bridge degraded linearly with message count (95ms/scan after 2000 messages) and was replaced, which is also why the modal/evals suites now run faster than before.

The preset's filter-comments rule is deliberately excluded: scanned content is agent-controlled, and an in-content secretlint-disable comment must not be able to suppress the fail-closed gate's findings (regression-tested).

The tier model

tier owner publication gate display redaction
exact configured credential values (byte-for-byte) app knowledge, unchanged ✅ ✅
vendor formats secretlint findings + documented supplemental gap-fillers ✅ ✅
BIP39 mnemonics, context-labeled private keys, URL credentials, Bearer tokens app patterns (no secretlint rule exists) ✅ ✅
speculative heuristics (high entropy, unlabeled 40-hex, key-name assignment) retired from the gate (#819's positive-only intent) ⛔ ✅

SecretScanMode defaults to "all", so every existing redaction caller is unchanged; only the fail-closed publication gate opts into "positive-only".

True-positive proof table

Every credential format the old hand-rolled patterns could name is asserted still rejected by the gate (packages/artifacts/test/artifacts.test.ts, k01–k15):

case caught by
ghp_… (36), github_pat_… (82), gho_… secretlint github
glpat-… secretlint gitlab
xoxb-…, Slack webhook URLs secretlint slack
npm_… (36) secretlint npm
hf_… (34) secretlint huggingface
full-length PEM private keys secretlint privatekey
https://user:pass@host secretlint basicauth
postgres://user:pass@host/db secretlint database-connection-string
plus Stripe, SendGrid, Shopify, OpenAI, Anthropic, Groq, Linear, Notion, 1Password, Vault, Vercel, Databricks, Docker, Figma, Cloudflare, Tailscale new coverage, free
truncated/short PEM blocks (preset needs a 100+ char body) supplemental pattern
sk-ant-… short/legacy (preset needs 90–128 chars ending AA) supplemental pattern
bare sk-… (OpenRouter/DeepSeek; preset needs the T3BlbkFJ marker) supplemental pattern
AKIA/ASIA IDs (preset's ID scan is opt-in, bundles an account-ID keyword heuristic of exactly the #819-retired class, and builtin-ignores the docs example key) supplemental pattern
AIza…, ya29.…, Modal ak-/as-, JWTs, Alchemy/Infura URL keys, Bearer <token> supplemental patterns (no preset rule)
BIP39 mnemonics (checksum-validated via @scure/bip39), context-labeled 32-byte hex keys kept app checks (no preset rule)
URL credentials on allowlisted hosts (localhost) and non-HTTP schemes kept app rewrite (preset's basicauth skips them)

Each supplemental pattern carries a comment naming the concrete gap it fills — documented gap-fillers, not a parallel scanner.

False positives eliminated (ported as regression tests)

Test updates with justification

  • Two npm-token fixtures went from 32 to 36 chars — the real npm token format that secretlint encodes.
  • The gate fixture asserting an opaque high-entropy blob is rejected now asserts the adjacent mnemonic rejects instead (mirrors Scan artifact publications for positively identified secrets only #820: the tradeoff stays visible in the suite rather than implied).
  • Two runtime fixtures that relied on the retired token= keyword heuristic now use a positively identified ghp_ credential.

Residual exposure, stated plainly: an opaque secret in no recognized format that is also not a configured value no longer fails publication. Same trade #820 made: artifacts are local run output, configured credentials remain byte-matched, and the speculative rules destroyed five consecutive runs on false positives while contributing nothing for any nameable format.

Dependency policy-gate evidence

  • node scripts/validate-audit-profile-package.mjs — pass
  • pnpm -w security:dependency-advisories — "0 High/Critical production advisories; 0 active exceptions"
  • pnpm -w test:ci-scripts — 109/109 (bun policy-script suite)
  • Deps pinned exact in packages/security: @secretlint/core@13.0.4, @secretlint/secretlint-rule-preset-recommend@13.0.4 (+ @secretlint/types@13.0.4 dev). All MIT; transitive additions are @secretlint/{types,profiler}, debug, ms, structured-source, boundary. Runs fully offline, in-process.

Verification

  • security 23/23 · artifacts 321/322 (sole failure is the known pre-existing flake a runtime document read rejects an atomic path replacement during its snapshot, untouched here) · modal 629/629 · evals 426/426 · config 72/72 · runtime secret-behavior files (generated-workflow-verifier, verified-output, workspace-handoff) 223/223
  • pnpm -w typecheck, pnpm lint, pnpm format:check clean
  • The five VALIDATOR_BUILD_IDENTITY modules are untouched.

🤖 Generated with Claude Code

Greptile Summary

The PR replaces hand-maintained vendor credential patterns with a pinned secretlint scanner while retaining supplemental checks for unsupported formats. It also introduces a synchronous worker-thread bridge and separates positive-only publication gating from broader display redaction.

  • Adds a reusable secretlint worker backed by a growable shared-memory request/response channel.
  • Preserves synchronous redaction APIs while propagating scanner failures fail-closed.
  • Uses positive-only detection for immutable artifact publication and keeps speculative heuristics for display redaction.
  • Expands regression coverage for supported credentials and previously observed false positives.
  • Pins the new secretlint dependencies and updates TypeScript shared-memory libraries.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete changed-code failure remaining after reviewing the scanner bridge, packaging assumptions, publication gate, and redaction behavior.

The supported Node runtime provides the required worker and shared-memory features, the worker source is included in the package build, configured credentials remain byte-matched, and no realistic recognized credential was shown to bypass the revised detection paths.

Important Files Changed

Filename Overview
packages/security/src/secretlint-scanner.ts Introduces the synchronous scanner session, timeout handling, worker lifecycle, and shared-memory request protocol; no actionable defect was established.
packages/security/src/secretlint-scan-worker.ts Runs secretlint asynchronously on a dedicated worker and returns normalized finding ranges while excluding content-controlled suppression comments.
packages/security/src/secretlint-scan-channel.ts Defines bounded growable shared-memory payload encoding and sequence signaling for scanner requests and responses.
packages/security/src/sensitive-redaction.ts Delegates vendor detection to secretlint, documents supplemental formats, and separates positive detections from speculative display heuristics.
packages/artifacts/src/secret-gate.ts Changes immutable publication checks to positive-only scanning while preserving byte-exact configured-credential rejection.
packages/security/package.json Pins compatible secretlint dependencies for the repository’s supported Node runtime.
packages/security/test/sensitive-redaction.test.ts Adds coverage for delegated vendor formats, suppression resistance, positive-only behavior, and false-positive regressions.
packages/artifacts/test/artifacts.test.ts Verifies publication rejection for named credential formats and acceptance of documented non-secret technical prose.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Text or artifact bytes] --> B[Exact configured-value check]
    B --> C[Synchronous scanner API]
    C --> D[SharedArrayBuffer request]
    D --> E[Secretlint worker]
    E --> F[Finding ranges]
    F --> G[Supplemental positive-format checks]
    G --> H{Caller mode}
    H -->|positive-only| I[Publication gate accepts or rejects unchanged bytes]
    H -->|all| J[Apply speculative heuristics]
    J --> K[Redacted display or persisted diagnostic]
Loading

Reviews (1): Last reviewed commit: "feat(security): replace hand-rolled vend..." | Re-trigger Greptile

Context used (3)

…tlint

Maintainer instruction from the review comments on #820 ("We want to avoid
manually scanning secrets, we should use a third party library") and #823
("This sensitive redaction is giving us too much headache, we want preferably
to use a 3rd party library instead of reinventing the wheel"). This PR
supersedes both.

Root cause across #819 and #822: a hand-rolled pattern set has to be
recalibrated shape by shape every time credential text collides with ordinary
technical prose, and each recalibration (entropy exemptions, Bearer token
shape, the ak-/as- suffix class) only unblocked one run before the next prose
shape failed the fail-closed publication gate.

Detection now has three tiers:

- Exact configured credential values stay matched byte-for-byte; the library
  cannot know the run's configuration, so that tier remains app knowledge.
- Vendor-format detection is delegated to secretlint 13.0.4 (@secretlint/core
  plus @secretlint/secretlint-rule-preset-recommend, MIT, pure JS), run fully
  in-process on a dedicated worker thread behind a synchronous shared-memory
  bridge so every existing sync caller, including the generated Smithers
  verifier, keeps its API. A documented supplemental pattern remains only for
  each concrete true positive the preset cannot see (short/truncated PEM
  blocks, non-canonical sk-ant-/sk- prefixes, AKIA/ASIA IDs, AIza keys, Modal
  ak-/as- keys with the #822 hyphen fix, ya29 tokens, JWTs, Alchemy/Infura
  URLs, Bearer tokens with the #820 token-shape fix), plus the BIP39 mnemonic
  and context-labeled private-key checks, which no secretlint rule covers.
- The speculative heuristics (high entropy, unlabeled 40-hex, key-name
  assignment) are retired from the publication gate, which now scans
  positive-only; they remain for default display-redaction callers, where
  over-redaction is cheap.

The preset's filter-comments rule is excluded: scanned content is
agent-controlled, and in-content secretlint-disable comments must not be able
to suppress the gate's findings (covered by a regression test).

The gate keeps failing closed: a hit still rejects publication without
rewriting bytes, exact values plus every positively identified format from
the #819/#820/#822/#823 examples are still rejected (asserted one by one in
the artifacts suite), and the five prose shapes that each killed a campaign
run are asserted publishable.

Closes #819
Closes #822

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​secretlint/​types@​13.0.41001008493100
Added@​secretlint/​core@​13.0.41001008693100
Added@​secretlint/​secretlint-rule-preset-recommend@​13.0.49910010093100

View full report

@aviggiano
aviggiano merged commit 25c8add into main Aug 25, 2026
5 checks passed
@aviggiano
aviggiano deleted the 819-third-party-secret-scanning branch August 25, 2026 12:58
aviggiano added a commit that referenced this pull request Aug 25, 2026
The three fail-on-hit secret gates in packages/modal — recovery
lifecycle, public eval diagnostics, and public benchmark bundle files —
ran redactSecretsInText in default "all" mode, so the speculative
high-entropy pass flagged the pipeline's own identifiers
(ci-<run_id>-… logical run ids, ultrafuzz-ci-… volume names) as
secrets. Inside the sandbox that kills the worker as
dependency-unreachable before any node runs; on the runner it crashes
collect with "Modal recovery lifecycle contains secret-like content".
Every benchmark lane has been broken this way since the gates landed in
#558, unexercised while the workflow was disabled.

Align them with the artifacts secret gate (#865, #819): keep the exact
injected-secret-value check byte-for-byte and run the generic scan as
containsSensitiveSecrets(text, [], "positive-only"). All positive
detections — secretlint findings, supplemental vendor formats,
mnemonics, labeled private keys, URL and Bearer credentials — still
fail the gates; existing ghp_/sk- test fixtures still throw. Rewriting
sanitizers stay in "all" mode.

Closes #883

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant