blast+cli: read-only blast-radius map + wire rotate --execute / --blast

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>
This commit is contained in:
leetcrypt
2026-06-18 14:48:42 -07:00
parent 09cd7208d6
commit 12192a8423
4 changed files with 528 additions and 18 deletions
+157 -12
View File
@@ -19,6 +19,7 @@ import (
"golang.org/x/term"
"incredigo/internal/audit"
"incredigo/internal/blast"
"incredigo/internal/discover"
"incredigo/internal/policy"
"incredigo/internal/rotate"
@@ -79,6 +80,42 @@ func applyPaths() {
flagSources = append(flagSources, "file")
}
// gopassCredsForExecute sources the credentials to rotate from gopass (Mode A).
// It lists the entries under prefix, loads each secret into the vault, and rebuilds
// a Credential whose Source is the path's first segment after the prefix
// (e.g. imported/postgres/labapp -> Source "postgres") so the right driver Detects
// it, and whose Location is the exact gopass path the new secret must overwrite.
func gopassCredsForExecute(ctx context.Context, gp *sink.Gopass, v *vault.Vault, prefix string) ([]discover.Credential, error) {
paths, err := gp.ListPaths(ctx, prefix)
if err != nil {
return nil, err
}
var creds []discover.Credential
for _, p := range paths {
h, err := gp.Show(ctx, v, p)
if err != nil {
return nil, err
}
creds = append(creds, discover.Credential{
Source: sourceFromStorePath(p, prefix),
Identity: p,
Location: p,
Secret: h,
})
}
return creds, nil
}
// sourceFromStorePath recovers the scanner source from a gopass entry path:
// "<prefix>/<source>/<slug...>" -> "<source>".
func sourceFromStorePath(p, prefix string) string {
rest := strings.TrimPrefix(p, strings.TrimSuffix(prefix, "/")+"/")
if i := strings.IndexByte(rest, '/'); i >= 0 {
return rest[:i]
}
return rest
}
// withVault sets up the RAM vault and guarantees it is purged on exit.
func withVault(fn func(ctx context.Context, v *vault.Vault) error) error {
v := vault.New()
@@ -333,16 +370,29 @@ func importCmd() *cobra.Command {
// are zero registered drivers, and --execute is refused.
func rotateCmd() *cobra.Command {
var prefix, backupOut string
var execute bool
var execute, dryRun, blastScan bool
var blastRoots []string
c := &cobra.Command{
Use: "rotate",
Short: "Plan rotation + take a verified backup (DESIGN PHASE — rotates nothing)",
Long: "Rotation is not implemented yet (see docs/ROTATION.md). This command runs the\n" +
"MANDATORY backup gate (sealed, verified snapshot of the gopass prefix) and prints\n" +
"the rotation plan. It never changes a credential.",
Short: "Plan/execute rotation behind a MANDATORY verified backup gate",
Long: "Runs the MANDATORY backup gate (sealed, verified snapshot of the gopass prefix)\n" +
"first, then EITHER plans (default) or executes rotation.\n\n" +
"Default: prints the rotation plan and changes nothing. With --dry-run it walks the\n" +
"full spine (rotate→verify→revoke) using the noop driver, touching no service.\n\n" +
"--execute performs REAL, destructive rotation of the gopass entries under --prefix\n" +
"(rotate→verify→store→re-read→revoke). It is refused unless INCREDIGO_ALLOW_EXECUTE=1\n" +
"is set and at least one driver is registered. See docs/ROTATION.md §16.",
RunE: func(cmd *cobra.Command, args []string) error {
if execute {
return fmt.Errorf("rotate --execute refused: no rotation drivers are implemented (design phase) — see docs/ROTATION.md")
// Real, destructive rotation. Gated by an explicit env authorization
// on top of the flag (ROTATION.md §16), and refused if no driver can
// actually perform a rotation.
if os.Getenv("INCREDIGO_ALLOW_EXECUTE") != "1" {
return fmt.Errorf("rotate --execute refused: set INCREDIGO_ALLOW_EXECUTE=1 to authorize real, destructive rotation (see docs/ROTATION.md §16)")
}
if len(rotate.Drivers()) == 0 {
return fmt.Errorf("rotate --execute refused: no rotation drivers registered")
}
}
applyPaths()
gp := &sink.Gopass{Prefix: prefix}
@@ -378,26 +428,118 @@ func rotateCmd() *cobra.Command {
}
fmt.Fprintf(os.Stderr, "✓ backup gate: %d entr(ies) sealed + verified -> %s\n\n", n, backupOut)
log, _ := audit.Open(flagAuditLog, time.Now)
defer log.Close()
if execute {
// Mode A — rotate what's already in gopass. Source the stored
// entries under the prefix (the backup gate just proved coverage
// of exactly these), then run the real rotate→verify→store→
// re-read→revoke spine. Backup has already succeeded above.
ecreds, err := gopassCredsForExecute(ctx, gp, v, prefix)
if err != nil {
return err
}
results := rotate.Execute(ctx, gp, ecreds, v)
ew := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
fmt.Fprintln(ew, "DRIVER\tENTRY\tROTATED\tVERIFIED\tSTORED\tREVOKED\tERROR")
var rotated, failed int
for _, r := range results {
errStr := ""
if r.Err != nil {
errStr = r.Err.Error()
failed++
} else {
rotated++
}
fmt.Fprintf(ew, "%s\t%s\t%t\t%t\t%t\t%t\t%s\n",
r.Driver, r.StorePath, r.Rotated, r.Verified, r.Stored, r.Revoked, errStr)
out := "ok"
if r.Err != nil {
out = r.Err.Error()
}
log.Write(audit.Entry{Action: "rotate-execute", Source: r.Credential.Source,
Identity: r.StorePath, Location: backupOut, Outcome: out})
}
ew.Flush()
fmt.Fprintf(os.Stderr, "\nROTATED %d credential(s), %d failed. Backup: %s\n", rotated, failed, backupOut)
if failed > 0 {
return fmt.Errorf("rotation completed with %d failure(s) — old credential(s) left intact; restore from backup if needed", failed)
}
return nil
}
// Non-destructive plan over discoverable credentials.
creds, err := discover.ScanAll(ctx, v, flagSources...)
if err != nil {
return err
}
// Optional read-only blast-radius map: which files consume each
// credential (searched by non-secret markers only — never the secret).
var radius []blast.CredConsumers
if blastScan {
radius, err = blast.Map(creds, blast.Options{Roots: blastRoots})
if err != nil {
return err
}
}
w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
fmt.Fprintln(w, "DRIVER\tSOURCE\tIDENTITY")
for _, p := range rotate.PlanAll(creds) {
if blastScan {
fmt.Fprintln(w, "DRIVER\tSOURCE\tIDENTITY\tCONSUMERS")
} else {
fmt.Fprintln(w, "DRIVER\tSOURCE\tIDENTITY")
}
for i, p := range rotate.PlanAll(creds) {
drv := p.Driver
if drv == "" {
drv = "(none)"
}
fmt.Fprintf(w, "%s\t%s\t%s\n", drv, p.Credential.Source, p.Credential.Identity)
if blastScan {
fmt.Fprintf(w, "%s\t%s\t%s\t%d file(s)\n",
drv, p.Credential.Source, p.Credential.Identity, len(radius[i].Files()))
} else {
fmt.Fprintf(w, "%s\t%s\t%s\n", drv, p.Credential.Source, p.Credential.Identity)
}
}
w.Flush()
log, _ := audit.Open(flagAuditLog, time.Now)
defer log.Close()
if blastScan {
fmt.Fprintln(os.Stderr, "\nblast radius (read-only; matched by non-secret markers):")
for _, cc := range radius {
files := cc.Files()
if len(files) == 0 {
continue
}
fmt.Fprintf(os.Stderr, " %s -> %s\n", cc.Credential.Identity, strings.Join(files, ", "))
}
}
log.Write(audit.Entry{Action: "rotate-plan", Outcome: "ok", Location: backupOut})
fmt.Fprintf(os.Stderr, "\nDESIGN PHASE: %d rotation driver(s) registered — nothing rotated. See docs/ROTATION.md\n",
if dryRun {
// Exercise the FULL rotation spine with the noop driver: it mints
// an in-vault secret, verifies it, "revokes", and persists nothing.
// No real credential at any service is touched.
fmt.Fprintln(os.Stderr, "\n--dry-run: walking rotate→verify→revoke with the noop driver (touches no service)")
results := rotate.DryRun(ctx, &rotate.NoopRotator{}, creds, v)
dw := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
fmt.Fprintln(dw, "DRIVER\tIDENTITY\tROTATED\tVERIFIED\tREVOKED\tERROR")
for _, r := range results {
errStr := ""
if r.Err != nil {
errStr = r.Err.Error()
}
fmt.Fprintf(dw, "%s\t%s\t%t\t%t\t%t\t%s\n",
r.Driver, r.Credential.Identity, r.Rotated, r.Verified, r.Revoked, errStr)
}
dw.Flush()
log.Write(audit.Entry{Action: "rotate-dryrun", Outcome: "ok", Location: backupOut})
fmt.Fprintf(os.Stderr, "\nDRY RUN complete: %d credential(s) walked, nothing persisted, no service touched.\n", len(results))
return nil
}
fmt.Fprintf(os.Stderr, "\nPLAN ONLY: %d rotation driver(s) registered — nothing rotated. Add --execute (with INCREDIGO_ALLOW_EXECUTE=1) to rotate. See docs/ROTATION.md\n",
len(rotate.Drivers()))
return nil
})
@@ -406,6 +548,9 @@ func rotateCmd() *cobra.Command {
c.Flags().StringVar(&prefix, "prefix", "imported/", "gopass prefix to back up and plan")
c.Flags().StringVar(&backupOut, "backup-out", "", "backup bundle path (default ~/.incredigo/rotation-backups/<ts>.age)")
c.Flags().BoolVar(&execute, "execute", false, "(reserved) perform rotation — refused in design phase")
c.Flags().BoolVar(&dryRun, "dry-run", false, "walk the full rotation spine with the noop driver (touches no service, persists nothing)")
c.Flags().BoolVar(&blastScan, "blast", false, "map each credential's blast radius — which files consume it (read-only, non-secret markers)")
c.Flags().StringArrayVar(&blastRoots, "blast-root", nil, "directories to search for consumers (default: current directory)")
c.Flags().StringArrayVar(&flagPaths, "path", nil, "extra file or directory to include in the plan")
c.Flags().StringSliceVar(&flagSources, "source", nil, "limit discovery to these scanners")
return c