Add ProofLiveReal (highest level, no driver yet) and enforce it at
execute time: rotate --execute only runs drivers with proof >= LIVE-VM
(MinExecuteProof). MOCK-ONLY/UNPROVEN drivers are skipped before any
provider call unless --allow-mock-proven is passed. ExecuteResult gains
Skipped/SkipReason; the CLI reports and audit-logs skips separately from
failures. Closes the last structural gap where an emulator-only driver
could touch a real credential unattended. Covered by execute_gate_test.go.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
policy 66% -> 94.7%: TestLoad exercises the previously-untested Load path —
empty/missing path fall back to Default, valid YAML parses onto the Default base
(absent fields keep their default; overrides apply), malformed YAML and a
non-regular file (directory) both error. Documents that a map override entry is
replaced wholesale by yaml.v3 (Evaluate re-merges defaults, so it's harmless).
tui 57% -> 87.8%: cover Init, the "o" open branches (URL vs none), "s" skip and
prev/next boundary guards, non-key messages, enter-at-last-item, esc/ctrl+c
quit, openURL's command, and Run's headless empty-list path. The residual is the
TTY-only tea.Program.Run() launch, not unit-testable headless.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cmd/incredigo 4.3% -> 34.5%. Extract newRootCmd() as a testability seam so tests
drive the real wired CLI (persistent flags + every subcommand) via SetArgs.
main_test.go: a runCLI harness that captures os.Stdout/os.Stderr around Execute,
plus a fake gopass on PATH + fixture HOME so no command touches the user's real
store or host dotfiles (discovery isolated to --source file). Covers the safety-
critical contracts: rotate --execute refused without INCREDIGO_ALLOW_EXECUTE=1;
backup gate aborts BEFORE any driver runs when the snapshot fails; empty
passphrase rejected with no sealed content left behind; export refuses to
overwrite; unknown sealer rejected. Plus scan/status/worklist happy paths,
migrate --dedupe, and an export -> import round-trip proving byte-exact restore
with no plaintext in the sealed bundle.
discover: add ClearPaths() to reset the file scanner's process-global --path
targets between CLI invocations (a test-binary need; the one-shot CLI never hits
the stale-target accumulation).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The backup gate is only as trustworthy as the sealers; internal/sink was the
safety-critical coverage gap blocking all real rotation work.
sealer_test.go: round-trip per sealer (age/hmac/openssl) across ascii/binary/
unicode/empty/large payloads, asserting plaintext never leaks into sealed
output; tamper fails closed (age & hmac error with zero bytes emitted on a
flipped ciphertext or MAC byte); openssl's unauthenticated failure mode pinned
as an executable contract (no exact-plaintext recovery only); wrong-passphrase
clean-error; short-bundle, bad-binary, and empty-passphrase error paths.
framing_test.go: bundle framing (zero-record, clean-EOF, truncated mid-path/
len/secret, oversized path); gopass wrapper Insert/InsertAt/Show/Exists/
Available; secret-never-in-argv leak assertion (Hard Rule #3); gopass-failure
paths surface errors instead of silently short backups; end-to-end export ->
seal -> open -> count -> restore proving completeness + authenticity together.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GitHub PATs and Stripe secret keys have no scriptable rotation API (GitHub's
create-token API was removed in 2020; Stripe has no create-key endpoint), so
they belong in the guided change-password layer, not as Rotators. But they
arrive from the env/file scanner as generic Source="env" tokens with no host,
so the worklist showed them as "manual — no web page".
Recognise them by their well-known PUBLIC value prefixes (ghp_/gho_/ghs_/
github_pat_/…, sk_live_/sk_test_/rk_live_/…) at scan time and record a
non-secret Meta["service"] hint — a fixed service NAME, never the secret bytes.
discover.ServiceForSecret does the detection; env.go attaches it for generic
tokens, file.go before Store wipes the buffer. links.HostFor consults the hint
and maps github→github.com / stripe→stripe.com, so the curated change-password
URLs (already in the table) now light up for these credentials.
Leak-safe (service name is derived from a public prefix, not the secret) and
verified by the existing assertNoLeak checks. go build/vet clean; full suite
179 tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two more provider-API rotation drivers for the vibe-coder credential surface,
both following the create-new → verify → revoke-old pattern with the
self-contained blob form <provider>://<host>/?id=&secret=.
- vercel: POST /v3/user/tokens mints a new bearer; Verify lists tokens and
asserts the new id is present; RevokeOld DELETEs the old token by id.
- sendgrid: clones the old key's scopes (GET /v3/api_keys/<id>) onto the new
key so rotation is a faithful replacement, not a privilege downgrade; Verify
is a GET /v3/scopes auth proof; RevokeOld DELETEs the old key by id.
Both ship Bearer-enforcing httptest emulators that prove a real cutover (old
credential dead, new alive after RevokeOld) plus a leak check. Recorded as
MOCK-ONLY in proofs.go + docs/ROTATION-PROOFS.md (no self-host). go build/vet
clean; 77 rotate tests pass, -race clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
First of the vibe-coder API-rotation batch. Resend mints a new API key
authenticated by the old one (POST /api-keys), verifies it by listing keys
with the new Bearer token and asserting its id is present, then deletes the
old key by id (DELETE /api-keys/<id>). Self-contained blob
resend://host/?id=&secret= keeps the token off Identity/Meta/logs/argv.
httptest emulator enforces Bearer validity (key authenticates only while it
exists) so the test proves a real cutover: old dead, new alive, plus a leak
check. Recorded MOCK-ONLY in proofs.go + ROTATION-PROOFS.md (no self-host).
Note: GitHub PAT is intentionally NOT an API Rotator — GitHub has no
create-token API (classic Authorizations API removed 2020; fine-grained PATs
are UI-only), so a PAT belongs in the guided/worklist path, not here.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Group 2 of the manager wire-up — two CLI ItemUpdaters:
pass/gopass: the user's own password-store as a rotation source. Reads
line-1=password + key:value metadata via `<bin> show`; updates in place by
piping the full body to `<bin> insert -m -f` (off-argv), preserving metadata
across the password swap. gopass enumerates via `ls --flat`, pass by walking
the store dir; --pass-prefix scopes a subtree.
keychain: macOS internet passwords via the `security` CLI. Pure-exec (no
build tag) so it unit-tests cross-platform via a fake bin; Available() is
false off darwin. Read off-argv (find-internet-password -w); write is
delete+add with -w on argv (CLI limitation, documented like 1password).
Both MOCK-ONLY (fake-binary unit tests + leak checks); recorded as DATA in
BROWSER-ROTATION.md §8. 153 tests green, vet clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
One data-driven csvManager (layout = header + record func) covers
edge/brave (chrome layout), safari, lastpass, dashlane, protonpass,
nordpass, roboform, and read/guide-only enpass — replacing 8 near-identical
files. Export is header-name driven (parseLoginCSV + new aliases); import
stages a per-manager-layout CSV on tmpfs and shreds it, gated by --allow-csv.
Notes now round-trip through Meta. Reimporter exposes per-manager re-import
hints. Column layouts are MOCK-ONLY (unit-validated) on the same LIVE-VM
staging machinery; recorded as DATA in BROWSER-ROTATION.md §8.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Proven end-to-end (scan→plan→commit→restore-from-backup) against the real
keepassxc-cli 2.7.6 in the incredigo-sbx sandbox VM. The live run surfaced an
adapter bug the fake CSV masked: KeePassXC's CSV "Group" column is the full path
including the root group name (default root is "Passwords"), but keepassxc-cli
addresses entries relative to root (/GitHub, not /Passwords/GitHub). kpEntryPath
now drops the leading root segment; the unit test that encoded the buggy
/Root/GitHub form is corrected and a nested-group table test added.
Records the proof as data: docs/BROWSER-ROTATION.md promotes keepassxc
MOCK-ONLY -> LIVE-VM (the others stay MOCK-ONLY).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the secondary-persona path from docs/BROWSER-ROTATION.md: ingest a password
manager / browser store, take a mandatory verified sealed backup, generate fresh
strong passwords, hand the human a ready-to-finish task at the MFA wall, and commit
the new value into the manager only after the site change is confirmed.
- internal/pwgen: crypto/rand generator (rejection sampling, per-class guarantee,
vault-native — never returns plaintext as a Go string).
- internal/pwstore: Manager/ItemUpdater/BulkImporter contracts, secrets-free
RedactIdentity, and five adapters — bitwarden (bw, stdin), keepassxc
(keepassxc-cli, stdin), 1password (op, argv assignment with documented caveat),
chrome/firefox (tmpfs CSV ingest-and-shred). All real code; validation status is
data (MOCK-ONLY against fake binaries/CSVs, recorded in the design doc).
- internal/pwstore/stage.go: age-sealed staged-list (WriteStaged/ReadStaged) +
StagedImporter — the only artifacts crossing the plan->commit gap, never plaintext.
- cmd/incredigo: passwords scan|plan|guide|commit. Backup gate seals + round-trip
verifies before any commit; guide is interactive verify-before-commit; commit is
headless over a sealed stage. Browser CSV writes are gated behind --allow-csv.
Hard rules honored: backup-before-commit, verify-before-commit, no plaintext on
disk (browser CSV is the flag-gated tmpfs+shred exception), MFA always a human
handoff. go vet + -race clean; end-to-end verified against a fake bw.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ran live cutover POCs in the sandbox VM against the real target software and
promoted both drivers from MOCK-ONLY to LIVE-VM in the proof table (data, not
driver behaviour):
- k8s: proven against real k3s v1.35 kube-apiserver + token controller
(lab-provision-k8s.sh). The real apiserver serves a self-signed cert, so the
driver gained CA-pinned TLS — blob params ca= (base64 PEM, pinned as the only
trusted root) and insecure=1 (lab-only escape hatch), routed through a new
clientFor(cr). Neither is test-awareness; a real deployment configures the same
CA. Added TestK8sTLSTrust covering CA-pinned / insecure / untrusted-rejection
and the blob round-trip.
- mongo: proven against real mongod 8.0 (lab-provision-mongo.sh).
npm stays MOCK-ONLY by decision (registry.npmjs.org not self-hostable; real
create-token requires the account password in the body) — documented as a
contract gap rather than faking a LIVE-VM. gcp/twilio/flyio remain MOCK-ONLY.
Live POCs surfaced real-target-only behaviour now recorded in the docs: the
apiserver's ~10s successful-auth cache (old token kept working ~7s after Secret
deletion) and real mongosh's stdout prompt. Full suite 104 green, -race clean.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each driver is real-service code only (no test awareness; tests inject just an
HTTPClient/Bin), following the established patterns:
- mongo: in-place changeUserPassword over the old connection; no secret on argv
(host/db on the URI, both passwords on stdin, output scrubbed).
- npm: create token -> /-/whoami -> delete-by-key; blob records the key.
- gcp: service-account KEY rotation; mints an RS256 JWT signed with the SA's
own private key (crypto/rsa, stdlib) to self-auth the IAM create/verify/delete.
- twilio: API Key create -> get -> delete, Basic auth (KeySid/secret).
- flyio: GraphQL — a managing token mints/deletes the rotated deploy token.
- k8s: create a service-account-token Secret, await the populated token,
verify against the SA, delete the old Secret (invalidates the old token).
All 6 register their honest proof level (MOCK-ONLY) in proofs.go +
ROTATION-PROOFS.md; mongo/npm/k8s are LIVE-VM candidates pending real-target VM
POCs. Emulators enforce credential validity (gcp verifies the JWT signature),
tests assert Verify(old) fails after revoke + a secret-leak canary.
Full suite: 103 green, -race clean on rotate/sink/vault.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add gitlab, cloudflare, ghactions (MOCK-ONLY) and appsecret (LIVE-VM) one-file
Rotators, each with a table-driven cutover-proof + leak-check test. gitlab/
cloudflare/ghactions are in-place SaaS-token rolls (self/rotate, value-roll,
sealed-secret overwrite) so RevokeOld is a no-op; ghactions seals via
nacl/box.SealAnonymous (no new go.mod dep). appsecret regenerates a local app
signing secret and rewrites it in place across every target file (atomic, mode-
preserving, redacted errors), discoverable via exact-match app-signing key names
in env.go.
Keep real-rotation code distinct from mock code: how each driver's cutover was
validated lives as DATA in internal/rotate/proofs.go (single source of truth) +
docs/ROTATION-PROOFS.md, surfaced as a PROOF column in `incredigo rotate` so a
MOCK-ONLY driver can never be mistaken for a LIVE-VM one. appsecret proven LIVE-VM
against real local files in the sandbox VM.
82 tests green, -race clean on rotate/sink/vault.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
internal/blast builds a read-only consumer map (which other files appear to
use each credential) from NON-secret metadata only — env-var names, hostnames,
identifiers — never the vault secret; rotation needs it to know what to
redeploy after a change. cmd/incredigo wires Mode A `rotate --execute`
(reconstructs Source from the gopass path segment, runs the execute spine) and
surfaces the blast-radius map. Adjusts a worklist test accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
An env credential's Identity (".env / API_TOKEN") produced StorePath
imported/env/.env/api_token; gopass hides dot-prefixed segments from
`ls --flat`, so those entries were invisible to bundle.ListPaths and the
mandatory backup gate silently under-covered them (Hard Rule #1 violation).
slug() now strips leading dots per /-separated segment so incredigo can no
longer write a path that `ls --flat` hides. Locked in by
TestStorePathNoDotSegment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per the standing directive (dynamic discovery per credential type as each
driver lands):
- env.go: tag DB connection URLs by scheme (postgres/postgresql, mysql/mariadb,
redis/rediss) with a user:password authority, routing DATABASE_URL/REDIS_URL/
MYSQL_URL straight to the in-place DB drivers, kept regardless of the
secret-name heuristic.
- aws.go: emit the driver-ready single-line blob (was the bare secret) and
resolve region from env / ~/.aws/config.
- ssh.go: content-based key detection (not just id_*) + ~/.ssh/config
IdentityFile parsing.
- tea.go: new scanner mapping Gitea `tea` CLI config to a driver-ready PAT blob.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements the Rotator interface across all four rotation patterns, each a
self-contained one-file driver self-registering via init():
in-place DB password postgres, mysql (3-stmt unprivileged fallback), redis
local keypair + propagate sshkey (ed25519), wireguard (clamped curve25519)
provider-API token gitea PAT, mullvad device key
cloud-key self-identifying aws IAM access key (hand-rolled SigV4, no SDK dep)
Spine (execute.go) enforces Hard Rule #2: backup -> rotate -> verify(new) ->
store -> re-read+verify -> revoke-old; dryrun.go keeps --execute gated. Each
driver ships a table-driven test proving real cutover (old secret stops
authenticating) and asserting no secret substring leaks to argv/Meta/errors.
Promotes golang.org/x/crypto to a direct dependency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Design-phase foundation for v2 rotation. Changes NO credential at any service.
- internal/rotate: Rotator interface + registry + PlanAll (zero drivers
registered, so every credential plans as "(none)")
- internal/rotate.Snapshot: the MANDATORY backup gate — seals the gopass
prefix into an authenticated bundle, re-opens it, and confirms the entry
count matches before returning; any failure blocks rotation
- internal/sink.CountBundleRecords: read-only bundle completeness check
- cmd: `incredigo rotate` runs the backup gate + prints the plan; it is
dry-run only and refuses --execute (design phase)
- tests: backup gate happy path, no-overwrite, tamper detection; race-clean
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>