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:
+59
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user