diff --git a/internal/command/arguments/show.go b/internal/command/arguments/show.go index cf9bfd8106b9..21c36386f411 100644 --- a/internal/command/arguments/show.go +++ b/internal/command/arguments/show.go @@ -27,8 +27,10 @@ func ParseShow(args []string) (*Show, tfdiags.Diagnostics) { } var jsonOutput bool + var redactedOutput bool cmdFlags := defaultFlagSet("show") cmdFlags.BoolVar(&jsonOutput, "json", false, "json") + cmdFlags.BoolVar(&redactedOutput, "redacted", false, "redacted") if err := cmdFlags.Parse(args); err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -52,8 +54,12 @@ func ParseShow(args []string) (*Show, tfdiags.Diagnostics) { } switch { - case jsonOutput: + case jsonOutput && redactedOutput: + show.ViewType = ViewJSONRedacted + case jsonOutput && !redactedOutput: show.ViewType = ViewJSON + case redactedOutput: + show.ViewType = ViewHumanRedacted default: show.ViewType = ViewHuman } diff --git a/internal/command/arguments/types.go b/internal/command/arguments/types.go index 4e9065359b36..708b17cb161e 100644 --- a/internal/command/arguments/types.go +++ b/internal/command/arguments/types.go @@ -9,10 +9,12 @@ package arguments type ViewType rune const ( - ViewNone ViewType = 0 - ViewHuman ViewType = 'H' - ViewJSON ViewType = 'J' - ViewRaw ViewType = 'R' + ViewNone ViewType = 0 + ViewHuman ViewType = 'H' + ViewHumanRedacted ViewType = 'I' + ViewJSON ViewType = 'J' + ViewJSONRedacted ViewType = 'K' + ViewRaw ViewType = 'R' ) func (vt ViewType) String() string { @@ -21,8 +23,12 @@ func (vt ViewType) String() string { return "none" case ViewHuman: return "human" + case ViewHumanRedacted: + return "humanredacted" case ViewJSON: return "json" + case ViewJSONRedacted: + return "jsonredacted" case ViewRaw: return "raw" default: diff --git a/internal/command/jsonformat/plan.go b/internal/command/jsonformat/plan.go index 7eb54899d1a1..eb0d705daced 100644 --- a/internal/command/jsonformat/plan.go +++ b/internal/command/jsonformat/plan.go @@ -36,6 +36,56 @@ type Plan struct { ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas,omitempty"` } +func (plan Plan) Redacted() (Plan, error) { + outputChanges := make(map[string]jsonplan.Change, len(plan.OutputChanges)) + for key, change := range plan.OutputChanges { + redacted, err := change.Redacted() + if err != nil { + return Plan{}, err + } + outputChanges[key] = redacted + } + + resourceChanges := make([]jsonplan.ResourceChange, len(plan.ResourceChanges)) + for i, change := range plan.ResourceChanges { + redacted, err := change.Redacted() + if err != nil { + return Plan{}, err + } + resourceChanges[i] = redacted + } + + resourceDrift := make([]jsonplan.ResourceChange, len(plan.ResourceDrift)) + for i, change := range plan.ResourceDrift { + redacted, err := change.Redacted() + if err != nil { + return Plan{}, err + } + resourceDrift[i] = redacted + } + + deferredChanges := make([]jsonplan.DeferredResourceChange, len(plan.DeferredChanges)) + for i, change := range plan.DeferredChanges { + deferredChanges[i] = change + redacted, err := change.ResourceChange.Redacted() + if err != nil { + return Plan{}, err + } + deferredChanges[i].ResourceChange = redacted + } + + return Plan{ + PlanFormatVersion: plan.PlanFormatVersion, + OutputChanges: outputChanges, + ResourceChanges: resourceChanges, + ResourceDrift: resourceDrift, + RelevantAttributes: plan.RelevantAttributes, + DeferredChanges: deferredChanges, + ProviderFormatVersion: plan.ProviderFormatVersion, + ProviderSchemas: plan.ProviderSchemas, + }, nil +} + func (plan Plan) getSchema(change jsonplan.ResourceChange) *jsonprovider.Schema { switch change.Mode { case jsonstate.ManagedResourceMode: diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index 16bd9d07e76e..d16a638c24b7 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -163,6 +163,56 @@ type Change struct { AfterIdentity json.RawMessage `json:"after_identity,omitempty"` } +func redactedSensitiveFromObject(obj json.RawMessage) (json.RawMessage, error) { + var unmarshaled any + if err := json.Unmarshal(obj, &unmarshaled); err != nil { + return nil, err + } + + var sensitive any + if s, ok := unmarshaled.([]any); ok { + tmp := make([]bool, len(s)) + for i, _ := range s { + tmp[i] = true + } + sensitive = tmp + } else if m, ok := unmarshaled.(map[string]any); ok { + tmp := make(map[string]any, len(m)) + for key, _ := range m { + tmp[key] = true + } + sensitive = tmp + } else if unmarshaled == nil { + sensitive = map[string]any{} + } else { + sensitive = true + } + + marshaled, err := json.Marshal(sensitive) + if err != nil { + return nil, err + } + return marshaled, nil +} + +func (change Change) Redacted() (Change, error) { + output := change + + beforeSensitive, err := redactedSensitiveFromObject(change.Before) + if err != nil { + return Change{}, err + } + output.BeforeSensitive = beforeSensitive + + afterSensitive, err := redactedSensitiveFromObject(change.After) + if err != nil { + return Change{}, err + } + output.AfterSensitive = afterSensitive + + return output, nil +} + // Importing is a nested object for the resource import metadata. type Importing struct { // The original ID of this resource used to target it as part of planned diff --git a/internal/command/jsonplan/resource.go b/internal/command/jsonplan/resource.go index f5e6bff61784..84935da64095 100644 --- a/internal/command/jsonplan/resource.go +++ b/internal/command/jsonplan/resource.go @@ -105,6 +105,30 @@ type ResourceChange struct { ActionReason string `json:"action_reason,omitempty"` } +func (change ResourceChange) Redacted() (ResourceChange, error) { + output := change + + output.Name = "(sensitive value)" + output.Address = "(sensitive value)" + if len(output.PreviousAddress) > 0 { + output.PreviousAddress = "(sensitive value)" + } + if len(output.ModuleAddress) > 0 { + output.ModuleAddress = "(sensitive value)" + } + if len(output.Index) > 0 { + output.Index = json.RawMessage(`"(sensitive value)"`) + } + + redacted, err := change.Change.Redacted() + if err != nil { + return ResourceChange{}, err + } + output.Change = redacted + + return output, nil +} + // DeferredResourceChange is a description of a resource change that has been // deferred for some reason. type DeferredResourceChange struct { diff --git a/internal/command/show.go b/internal/command/show.go index 9e624a915d1c..581ae66799e0 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -98,6 +98,8 @@ Options: -no-color If specified, output won't contain any color. -json If specified, output the Terraform plan or state in a machine-readable form. + -redacted If specified, output the Terraform plan or state in + a redacted form. ` return strings.TrimSpace(helpText) diff --git a/internal/command/views/show.go b/internal/command/views/show.go index e5ab06d09735..b8796a52b3cb 100644 --- a/internal/command/views/show.go +++ b/internal/command/views/show.go @@ -33,15 +33,20 @@ func NewShow(vt arguments.ViewType, view *View) Show { switch vt { case arguments.ViewJSON: return &ShowJSON{view: view} + case arguments.ViewJSONRedacted: + return &ShowJSON{view: view, redacted: true} case arguments.ViewHuman: return &ShowHuman{view: view} + case arguments.ViewHumanRedacted: + return &ShowHuman{view: view, redacted: true} default: panic(fmt.Sprintf("unknown view type %v", vt)) } } type ShowHuman struct { - view *View + view *View + redacted bool } var _ Show = (*ShowHuman)(nil) @@ -67,6 +72,15 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON * v.view.streams.Eprintf("Couldn't decode renderable JSON plan format: %s", err) } + if v.redacted { + redacted, err := p.Redacted() + if err != nil { + v.view.streams.Eprintf("Failed to redact plan: %s", err) + return 1 + } + p = redacted + } + v.view.streams.Print(v.view.colorize.Color(planJSON.RunHeader + "\n")) renderer.RenderHumanPlan(p, planJSON.Mode, planJSON.Qualities...) v.view.streams.Print(v.view.colorize.Color("\n" + planJSON.RunFooter + "\n")) @@ -87,6 +101,15 @@ func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON * RelevantAttributes: attrs, } + if v.redacted { + redacted, err := jplan.Redacted() + if err != nil { + v.view.streams.Eprintf("Failed to redact plan: %s", err) + return 1 + } + jplan = redacted + } + var opts []plans.Quality if plan.Errored { opts = append(opts, plans.Errored) @@ -125,7 +148,8 @@ func (v *ShowHuman) Diagnostics(diags tfdiags.Diagnostics) { } type ShowJSON struct { - view *View + view *View + redacted bool } var _ Show = (*ShowJSON)(nil) diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index 5b30d65317fe..9a580b98d6bb 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -55,10 +55,6 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { return nil, fmt.Errorf("unsupported plan file format version %d; only version %d is supported", rawPlan.Version, tfplanFormatVersion) } - if rawPlan.TerraformVersion != version.String() { - return nil, fmt.Errorf("plan file was created by Terraform %s, but this is %s; plan files cannot be transferred between different Terraform versions", rawPlan.TerraformVersion, version.String()) - } - plan := &plans.Plan{ VariableValues: map[string]plans.DynamicValue{}, Changes: &plans.ChangesSrc{