package output import ( "encoding/json" "fmt" "os" "reflect" "sort" "strings" "github.com/aws/aws-sdk-go/aws/awsutil" "github.com/fatih/color" "github.com/mattn/go-isatty" "github.com/r3labs/diff/v2" "github.com/snyk/driftctl/pkg/remote/alerts" "github.com/yudai/gojsondiff" "github.com/yudai/gojsondiff/formatter" "github.com/snyk/driftctl/pkg/analyser" "github.com/snyk/driftctl/pkg/resource" ) const ConsoleOutputType = "console" const ConsoleOutputExample = "console://" type Console struct { summary string } func NewConsole() *Console { return &Console{ `Total coverage is {{ analysis.Coverage }}`, } } func (c *Console) Write(analysis *analyser.Analysis) error { if analysis.Summary().TotalDeleted > 0 { var sources []string groupedBySource := make(map[string][]*resource.Resource) for _, deletedResource := range analysis.Deleted() { key := "" if deletedResource.Source != nil { key = deletedResource.Source.Source() } if _, exist := groupedBySource[key]; !exist { groupedBySource[key] = []*resource.Resource{deletedResource} continue } groupedBySource[key] = append(groupedBySource[key], deletedResource) } for s := range groupedBySource { sources = append(sources, s) } sort.Strings(sources) fmt.Println("Found missing resources:") for _, source := range sources { indentBase := " " if source != "" { fmt.Print(color.BlueString("%sFrom %s\n", indentBase, source)) indentBase += indentBase } for _, deletedResource := range groupedBySource[source] { humanStringSource := deletedResource.ResourceType() if deletedResource.SourceString() != "" { humanStringSource = deletedResource.SourceString() } humanString := fmt.Sprintf("%s- %s (%s)", indentBase, deletedResource.ResourceId(), humanStringSource) if humanAttrs := formatResourceAttributes(deletedResource); humanAttrs != "" { humanString += fmt.Sprintf("\n%s %s", indentBase, humanAttrs) } fmt.Println(humanString) } } } if analysis.Summary().TotalUnmanaged > 0 { fmt.Println("Found resources not covered by IaC:") unmanagedByType, keys := groupByType(analysis.Unmanaged()) for _, ty := range keys { fmt.Printf(" %s:\n", ty) for _, res := range unmanagedByType[ty] { humanString := fmt.Sprintf(" - %s", res.ResourceId()) if humanAttrs := formatResourceAttributes(res); humanAttrs != "" { humanString += fmt.Sprintf("\n %s", humanAttrs) } fmt.Println(humanString) } } } if analysis.Summary().TotalDrifted > 0 { var sources []string groupedBySource := make(map[string][]analyser.Difference) for _, difference := range analysis.Differences() { key := "" if difference.Res.Source != nil { key = difference.Res.Source.Source() } if _, exist := groupedBySource[key]; !exist { groupedBySource[key] = []analyser.Difference{difference} continue } groupedBySource[key] = append(groupedBySource[key], difference) } for s := range groupedBySource { sources = append(sources, s) } sort.Strings(sources) fmt.Println("Found changed resources:") for _, source := range sources { indentBase := " " if source != "" { fmt.Print(color.BlueString("%sFrom %s\n", indentBase, source)) indentBase += indentBase } for _, difference := range groupedBySource[source] { humanStringSource := difference.Res.ResourceType() if difference.Res.SourceString() != "" { humanStringSource = difference.Res.SourceString() } humanString := fmt.Sprintf("%s- %s (%s):", indentBase, difference.Res.ResourceId(), humanStringSource) whiteSpace := indentBase + " " if humanAttrs := formatResourceAttributes(difference.Res); humanAttrs != "" { humanString += fmt.Sprintf("\n%s%s", whiteSpace, humanAttrs) whiteSpace += " " } fmt.Println(humanString) for _, change := range difference.Changelog { path := strings.Join(change.Path, ".") pref := fmt.Sprintf("%s %s:", color.YellowString("~"), path) if change.Type == diff.CREATE { pref = fmt.Sprintf("%s %s:", color.GreenString("+"), path) } else if change.Type == diff.DELETE { pref = fmt.Sprintf("%s %s:", color.RedString("-"), path) } if change.Type == diff.UPDATE { if change.JsonString { prefix := " " fmt.Printf("%s%s\n%s%s\n", whiteSpace, pref, prefix, jsonDiff(change.From, change.To, isatty.IsTerminal(os.Stdout.Fd()))) continue } } fmt.Printf("%s%s %s => %s", whiteSpace, pref, prettify(change.From), prettify(change.To)) if change.Computed { fmt.Printf(" %s", color.YellowString("(computed)")) } fmt.Printf("\n") } } } } c.writeSummary(analysis) enumerationErrorMessage := "" for _, a := range analysis.Alerts() { for _, alert := range a { fmt.Println(color.YellowString(alert.Message())) if alert, ok := alert.(*alerts.RemoteAccessDeniedAlert); ok && enumerationErrorMessage == "" { enumerationErrorMessage = alert.GetProviderMessage() } } } if enumerationErrorMessage != "" { _, _ = fmt.Fprintf(os.Stderr, "\n%s\n", color.YellowString(enumerationErrorMessage)) } return nil } func (c Console) writeSummary(analysis *analyser.Analysis) { boldWriter := color.New(color.Bold) successWriter := color.New(color.Bold, color.FgGreen) warningWriter := color.New(color.Bold, color.FgYellow) errorWriter := color.New(color.Bold, color.FgRed) total := boldWriter.Sprintf("%d", analysis.Summary().TotalResources) fmt.Printf( "Found %s resource(s)\n", total, ) fmt.Printf( " - %s%% coverage\n", boldWriter.Sprintf( "%d", analysis.Coverage(), ), ) if !analysis.IsSync() { managed := successWriter.Sprintf("0") if analysis.Summary().TotalManaged > 0 { managed = warningWriter.Sprintf("%d", analysis.Summary().TotalManaged) } fmt.Printf(" - %s resource(s) managed by terraform\n", managed) drifted := successWriter.Sprintf("0") if analysis.Summary().TotalDrifted > 0 { drifted = errorWriter.Sprintf("%d", analysis.Summary().TotalDrifted) } if analysis.Options().Deep { fmt.Printf(" - %s resource(s) out of sync with Terraform state\n", boldWriter.Sprintf("%s/%d", drifted, analysis.Summary().TotalManaged)) } unmanaged := successWriter.Sprintf("0") if analysis.Summary().TotalUnmanaged > 0 { unmanaged = warningWriter.Sprintf("%d", analysis.Summary().TotalUnmanaged) } fmt.Printf(" - %s resource(s) not managed by Terraform\n", unmanaged) deleted := successWriter.Sprintf("0") if analysis.Summary().TotalDeleted > 0 { deleted = errorWriter.Sprintf("%d", analysis.Summary().TotalDeleted) } fmt.Printf(" - %s resource(s) found in a Terraform state but missing on the cloud provider\n", deleted) } if analysis.IsSync() { fmt.Println(color.GreenString("Congrats! Your infrastructure is fully in sync.")) } } func prettify(resource interface{}) string { res := reflect.ValueOf(resource) if resource == nil || res.Kind() == reflect.Ptr && res.IsNil() { return "" } return awsutil.Prettify(resource) } func groupByType(resources []*resource.Resource) (map[string][]*resource.Resource, []string) { result := map[string][]*resource.Resource{} for _, res := range resources { if result[res.ResourceType()] == nil { result[res.ResourceType()] = []*resource.Resource{res} continue } result[res.ResourceType()] = append(result[res.ResourceType()], res) } keys := make([]string, 0, len(result)) for k := range result { keys = append(keys, k) } sort.Strings(keys) return result, keys } func jsonDiff(a, b interface{}, coloring bool) string { aStr := fmt.Sprintf("%s", a) bStr := fmt.Sprintf("%s", b) d := gojsondiff.New() var aJson map[string]interface{} _ = json.Unmarshal([]byte(aStr), &aJson) result, _ := d.Compare([]byte(aStr), []byte(bStr)) f := formatter.NewAsciiFormatter(aJson, formatter.AsciiFormatterConfig{ Coloring: coloring, }) // Set foreground green color for added lines and red color for deleted lines formatter.AsciiStyles = map[string]string{ "+": "32", "-": "31", } diffStr, _ := f.Format(result) return diffStr } func formatResourceAttributes(res *resource.Resource) string { if res.Schema() == nil || res.Schema().HumanReadableAttributesFunc == nil { return "" } attributes := res.Schema().HumanReadableAttributesFunc(res) if len(attributes) <= 0 { return "" } // sort attributes keys := make([]string, 0, len(attributes)) for k := range attributes { keys = append(keys, k) } sort.Strings(keys) // retrieve stringer attrString := "" for _, k := range keys { if attrString != "" { attrString += ", " } attrString += fmt.Sprintf("%s: %s", k, attributes[k]) } return attrString }