guide: read-only guided manual-rotation layer (links + worklist + TUI)

Advances the guided-rotation goal without rotating anything.

- internal/links: offline change-password URL generation — curated per-service
  table + RFC 8615 /.well-known/change-password + parent-domain fallback; derives
  a web host from a credential (git/netrc "user @ host", docker registry, aws)
- internal/worklist: secrets-free rotation worklist (source, identity, auto/manual,
  link) + Markdown renderer
- internal/tui: Bubble Tea walk-through (one credential per screen, open link,
  mark done/skip, progress) — READ-ONLY, enters/stores no secret
- cmd: `incredigo worklist` (offline, stdout/--out) and `incredigo guide` (TUI)
- tests: link derivation, worklist build/markdown + no-secret-column, TUI
  navigation/done/quit/view

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-15 14:58:23 -07:00
parent 6c2bcebc12
commit 71a74e7166
10 changed files with 665 additions and 5 deletions
+59 -1
View File
@@ -23,7 +23,9 @@ import (
"incredigo/internal/policy"
"incredigo/internal/rotate"
"incredigo/internal/sink"
"incredigo/internal/tui"
"incredigo/internal/vault"
"incredigo/internal/worklist"
)
var (
@@ -51,7 +53,7 @@ func main() {
root.PersistentFlags().StringVar(&flagConfig, "config", "", "policy.yaml path")
root.PersistentFlags().StringVar(&flagSealer, "sealer", "age", "backup sealer: age (default, authenticated), hmac (authenticated, openssl-only), or openssl (unauthenticated)")
root.AddCommand(scanCmd(), migrateCmd(), statusCmd(), exportCmd(), importCmd(), rotateCmd())
root.AddCommand(scanCmd(), migrateCmd(), statusCmd(), exportCmd(), importCmd(), rotateCmd(), worklistCmd(), guideCmd())
if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, "incredigo:", err)
@@ -409,6 +411,62 @@ func rotateCmd() *cobra.Command {
return c
}
// worklistCmd emits a rotation worklist (links per credential, no secrets). It is
// read-only, offline, and rotates nothing.
func worklistCmd() *cobra.Command {
var out string
c := &cobra.Command{
Use: "worklist",
Short: "Generate a rotation worklist with per-credential change-password links (no secrets)",
RunE: func(cmd *cobra.Command, args []string) error {
applyPaths()
return withVault(func(ctx context.Context, v *vault.Vault) error {
creds, err := discover.ScanAll(ctx, v, flagSources...)
if err != nil {
return err
}
md := worklist.Markdown(worklist.Build(creds))
if out == "" {
fmt.Print(md)
return nil
}
if err := os.WriteFile(out, []byte(md), 0o644); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "wrote worklist (%d credential(s)) -> %s\n", len(creds), out)
return nil
})
},
}
c.Flags().StringVar(&out, "out", "", "write the worklist to this file (default: stdout)")
c.Flags().StringArrayVar(&flagPaths, "path", nil, "extra file or directory to include")
c.Flags().StringSliceVar(&flagSources, "source", nil, "limit discovery to these scanners")
return c
}
// guideCmd launches the interactive guided-rotation walk-through (TUI). It is
// read-only guidance: it shows each credential and its rotation link and can open
// it, but enters/stores/rotates no secret.
func guideCmd() *cobra.Command {
c := &cobra.Command{
Use: "guide",
Short: "Interactive walk-through of credentials to rotate manually (rotates nothing)",
RunE: func(cmd *cobra.Command, args []string) error {
applyPaths()
return withVault(func(ctx context.Context, v *vault.Vault) error {
creds, err := discover.ScanAll(ctx, v, flagSources...)
if err != nil {
return err
}
return tui.Run(worklist.Build(creds))
})
},
}
c.Flags().StringArrayVar(&flagPaths, "path", nil, "extra file or directory to include")
c.Flags().StringSliceVar(&flagSources, "source", nil, "limit discovery to these scanners")
return c
}
// sealerFor selects the backup sealer. age is the authenticated default; openssl
// is an unauthenticated fallback (see internal/sink/openssl.go).
func sealerFor(name string) (sink.Sealer, error) {