rotate: LIVE-REAL proof level + execute-time proof gate (M2)
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>
This commit is contained in:
+22
-12
@@ -378,7 +378,7 @@ func importCmd() *cobra.Command {
|
||||
// are zero registered drivers, and --execute is refused.
|
||||
func rotateCmd() *cobra.Command {
|
||||
var prefix, backupOut string
|
||||
var execute, dryRun, blastScan bool
|
||||
var execute, dryRun, blastScan, allowMock bool
|
||||
var blastRoots []string
|
||||
c := &cobra.Command{
|
||||
Use: "rotate",
|
||||
@@ -389,7 +389,9 @@ func rotateCmd() *cobra.Command {
|
||||
"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.",
|
||||
"is set and at least one driver is registered. Only drivers proven against real\n" +
|
||||
"target software (proof ≥ LIVE-VM) run; MOCK-ONLY drivers are skipped unless\n" +
|
||||
"--allow-mock-proven is passed. See docs/ROTATION.md §16.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if execute {
|
||||
// Real, destructive rotation. Gated by an explicit env authorization
|
||||
@@ -448,29 +450,36 @@ func rotateCmd() *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
results := rotate.Execute(ctx, gp, ecreds, v)
|
||||
results := rotate.Execute(ctx, gp, ecreds, v, allowMock)
|
||||
ew := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
|
||||
fmt.Fprintln(ew, "DRIVER\tPROOF\tENTRY\tROTATED\tVERIFIED\tSTORED\tREVOKED\tERROR")
|
||||
var rotated, failed int
|
||||
var rotated, failed, skipped int
|
||||
for _, r := range results {
|
||||
errStr := ""
|
||||
if r.Err != nil {
|
||||
outcome := "ok"
|
||||
switch {
|
||||
case r.Skipped:
|
||||
errStr = "SKIPPED: " + r.SkipReason
|
||||
outcome = errStr
|
||||
skipped++
|
||||
case r.Err != nil:
|
||||
errStr = r.Err.Error()
|
||||
outcome = errStr
|
||||
failed++
|
||||
} else {
|
||||
default:
|
||||
rotated++
|
||||
}
|
||||
fmt.Fprintf(ew, "%s\t%s\t%s\t%t\t%t\t%t\t%t\t%s\n",
|
||||
r.Driver, rotate.ProofLevelOf(r.Driver).String(), 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})
|
||||
Identity: r.StorePath, Location: backupOut, Outcome: outcome})
|
||||
}
|
||||
ew.Flush()
|
||||
fmt.Fprintf(os.Stderr, "\nROTATED %d credential(s), %d failed. Backup: %s\n", rotated, failed, backupOut)
|
||||
fmt.Fprintf(os.Stderr, "\nROTATED %d credential(s), %d failed, %d skipped (proof gate). Backup: %s\n", rotated, failed, skipped, backupOut)
|
||||
if skipped > 0 {
|
||||
fmt.Fprintf(os.Stderr, "%d credential(s) skipped: driver proof below %s. Re-run with --allow-mock-proven to rotate them anyway.\n",
|
||||
skipped, rotate.MinExecuteProof)
|
||||
}
|
||||
if failed > 0 {
|
||||
return fmt.Errorf("rotation completed with %d failure(s) — old credential(s) left intact; restore from backup if needed", failed)
|
||||
}
|
||||
@@ -563,6 +572,7 @@ func rotateCmd() *cobra.Command {
|
||||
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(&allowMock, "allow-mock-proven", false, "also execute drivers proven only against a mock/emulator (MOCK-ONLY); default skips them")
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user