driftctl/pkg/analyser/analysis.go

325 lines
8.6 KiB
Go

package analyser
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"github.com/snyk/driftctl/enumeration/alerter"
"github.com/r3labs/diff/v2"
"github.com/snyk/driftctl/enumeration/resource"
)
type Change struct {
diff.Change
Computed bool `json:"computed"`
JsonString bool `json:"-"`
}
type Changelog []Change
type Difference struct {
Res *resource.Resource
Changelog Changelog
}
type Summary struct {
TotalResources int `json:"total_resources"`
TotalDrifted int `json:"total_changed"`
TotalUnmanaged int `json:"total_unmanaged"`
TotalDeleted int `json:"total_missing"`
TotalManaged int `json:"total_managed"`
TotalIaCSourceCount uint `json:"total_iac_source_count"`
}
type Analysis struct {
unmanaged []*resource.Resource
managed []*resource.Resource
deleted []*resource.Resource
differences []Difference
options AnalyzerOptions
summary Summary
alerts alerter.Alerts
Duration time.Duration
Date time.Time
ProviderName string
ProviderVersion string
}
type serializableDifference struct {
Res resource.SerializableResource `json:"res"`
Changelog Changelog `json:"changelog"`
}
type serializableAnalysis struct {
Options AnalyzerOptions `json:"options"`
Summary Summary `json:"summary"`
Managed []resource.SerializableResource `json:"managed"`
Unmanaged []resource.SerializableResource `json:"unmanaged"`
Deleted []resource.SerializableResource `json:"missing"`
Differences []serializableDifference `json:"differences"`
Coverage int `json:"coverage"`
Alerts map[string][]alerter.SerializableAlert `json:"alerts"`
ProviderName string `json:"provider_name"`
ProviderVersion string `json:"provider_version"`
ScanDuration uint `json:"scan_duration,omitempty"`
Date time.Time `json:"date"`
}
type GenDriftIgnoreOptions struct {
ExcludeUnmanaged bool
ExcludeDeleted bool
ExcludeDrifted bool
InputPath string
OutputPath string
}
func NewAnalysis(options AnalyzerOptions) *Analysis {
return &Analysis{options: options}
}
func (a Analysis) MarshalJSON() ([]byte, error) {
bla := serializableAnalysis{}
for _, m := range a.managed {
bla.Managed = append(bla.Managed, *resource.NewSerializableResource(m))
}
for _, u := range a.unmanaged {
bla.Unmanaged = append(bla.Unmanaged, *resource.NewSerializableResource(u))
}
for _, d := range a.deleted {
bla.Deleted = append(bla.Deleted, *resource.NewSerializableResource(d))
}
for _, di := range a.differences {
bla.Differences = append(bla.Differences, serializableDifference{
Res: *resource.NewSerializableResource(di.Res),
Changelog: di.Changelog,
})
}
if len(a.alerts) > 0 {
bla.Alerts = make(map[string][]alerter.SerializableAlert)
for k, v := range a.alerts {
for _, al := range v {
bla.Alerts[k] = append(bla.Alerts[k], alerter.SerializableAlert{Alert: al})
}
}
}
bla.Summary = a.summary
bla.Coverage = a.Coverage()
bla.ProviderName = a.ProviderName
bla.ProviderVersion = a.ProviderVersion
bla.ScanDuration = uint(a.Duration.Seconds())
bla.Options = a.Options()
bla.Date = a.Date
return json.Marshal(bla)
}
func (a *Analysis) UnmarshalJSON(bytes []byte) error {
bla := serializableAnalysis{}
if err := json.Unmarshal(bytes, &bla); err != nil {
return err
}
for _, u := range bla.Unmanaged {
a.AddUnmanaged(&resource.Resource{
Id: u.Id,
Type: u.Type,
})
}
for _, d := range bla.Deleted {
a.AddDeleted(&resource.Resource{
Id: d.Id,
Type: d.Type,
})
}
for _, m := range bla.Managed {
res := &resource.Resource{
Id: m.Id,
Type: m.Type,
}
if m.Source != nil {
// We loose the source type in the serialization process, for now everything is serialized back to a
// TerraformStateSource.
// TODO: Add a discriminator field to be able to serialize back to the right type
// when we'll introduce a new source type
res.Source = &resource.TerraformStateSource{
State: m.Source.S,
Module: m.Source.Ns,
Name: m.Source.Name,
}
}
a.AddManaged(res)
}
for _, di := range bla.Differences {
a.AddDifference(Difference{
Res: &resource.Resource{
Id: di.Res.Id,
Type: di.Res.Type,
},
Changelog: di.Changelog,
})
}
if len(bla.Alerts) > 0 {
a.alerts = make(alerter.Alerts)
for k, v := range bla.Alerts {
for _, al := range v {
a.alerts[k] = append(a.alerts[k], &alerter.SerializedAlert{
Msg: al.Message(),
})
}
}
}
a.ProviderName = bla.ProviderName
a.ProviderVersion = bla.ProviderVersion
a.SetIaCSourceCount(bla.Summary.TotalIaCSourceCount)
a.Duration = time.Duration(bla.ScanDuration) * time.Second
a.options = bla.Options
a.Date = bla.Date
return nil
}
func (a *Analysis) IsSync() bool {
return a.summary.TotalDrifted == 0 && a.summary.TotalUnmanaged == 0 && a.summary.TotalDeleted == 0
}
func (a *Analysis) Options() AnalyzerOptions {
return a.options
}
func (a *Analysis) AddDeleted(resources ...*resource.Resource) {
a.deleted = append(a.deleted, resources...)
a.summary.TotalResources += len(resources)
a.summary.TotalDeleted += len(resources)
}
func (a *Analysis) AddUnmanaged(resources ...*resource.Resource) {
a.unmanaged = append(a.unmanaged, resources...)
a.summary.TotalResources += len(resources)
a.summary.TotalUnmanaged += len(resources)
}
func (a *Analysis) AddManaged(resources ...*resource.Resource) {
a.managed = append(a.managed, resources...)
a.summary.TotalResources += len(resources)
a.summary.TotalManaged += len(resources)
}
func (a *Analysis) AddDifference(diffs ...Difference) {
a.differences = append(a.differences, diffs...)
a.summary.TotalDrifted += len(diffs)
}
func (a *Analysis) SetAlerts(alerts alerter.Alerts) {
a.alerts = alerts
}
func (a *Analysis) SetOptions(options AnalyzerOptions) {
a.options = options
}
func (a *Analysis) SetIaCSourceCount(i uint) {
a.summary.TotalIaCSourceCount = i
}
func (a *Analysis) Coverage() int {
if a.summary.TotalResources > 0 {
return int((float32(a.summary.TotalManaged) / float32(a.summary.TotalResources)) * 100.0)
}
return 0
}
func (a *Analysis) Managed() []*resource.Resource {
return a.managed
}
func (a *Analysis) Unmanaged() []*resource.Resource {
return a.unmanaged
}
func (a *Analysis) Deleted() []*resource.Resource {
return a.deleted
}
func (a *Analysis) Differences() []Difference {
return a.differences
}
func (a *Analysis) Summary() Summary {
return a.summary
}
func (a *Analysis) Alerts() alerter.Alerts {
return a.alerts
}
func (a *Analysis) SortResources() {
a.unmanaged = resource.Sort(a.unmanaged)
a.deleted = resource.Sort(a.deleted)
a.differences = SortDifferences(a.differences)
}
func (a *Analysis) DriftIgnoreList(opts GenDriftIgnoreOptions) (int, string) {
var list []string
resourceCount := 0
addResources := func(res ...*resource.Resource) {
for _, r := range res {
list = append(list, fmt.Sprintf("%s.%s", r.ResourceType(), escapeKey(r.ResourceId())))
}
resourceCount += len(res)
}
addDifferences := func(diff ...Difference) {
for _, d := range diff {
addResources(d.Res)
}
resourceCount += len(diff)
}
if !opts.ExcludeUnmanaged && a.Summary().TotalUnmanaged > 0 {
list = append(list, "# Resources not covered by IaC")
addResources(a.Unmanaged()...)
}
if !opts.ExcludeDeleted && a.Summary().TotalDeleted > 0 {
list = append(list, "# Missing resources")
addResources(a.Deleted()...)
}
if !opts.ExcludeDrifted && a.Summary().TotalDrifted > 0 {
list = append(list, "# Changed resources")
addDifferences(a.Differences()...)
}
return resourceCount, strings.Join(list, "\n")
}
func SortDifferences(diffs []Difference) []Difference {
sort.SliceStable(diffs, func(i, j int) bool {
if diffs[i].Res.ResourceType() != diffs[j].Res.ResourceType() {
return diffs[i].Res.ResourceType() < diffs[j].Res.ResourceType()
}
return diffs[i].Res.ResourceId() < diffs[j].Res.ResourceId()
})
for _, d := range diffs {
SortChanges(d.Changelog)
}
return diffs
}
func SortChanges(changes []Change) []Change {
sort.SliceStable(changes, func(i, j int) bool {
return strings.Join(changes[i].Path, ".") < strings.Join(changes[j].Path, ".")
})
return changes
}
func escapeKey(line string) string {
line = strings.ReplaceAll(line, `\`, `\\`)
line = strings.ReplaceAll(line, `.`, `\.`)
return line
}