177 lines
4.3 KiB
Go
177 lines
4.3 KiB
Go
package output
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"math"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go/aws/awsutil"
|
|
"github.com/cloudskiff/driftctl/pkg/alerter"
|
|
"github.com/cloudskiff/driftctl/pkg/analyser"
|
|
"github.com/cloudskiff/driftctl/pkg/resource"
|
|
"github.com/r3labs/diff/v2"
|
|
)
|
|
|
|
const HTMLOutputType = "html"
|
|
const HTMLOutputExample = "html://PATH/TO/FILE.html"
|
|
|
|
// assets holds our static web content.
|
|
//go:embed assets/*
|
|
var assets embed.FS
|
|
|
|
type HTML struct {
|
|
path string
|
|
}
|
|
|
|
type HTMLTemplateParams struct {
|
|
ScanDate string
|
|
Coverage int
|
|
Summary analyser.Summary
|
|
Unmanaged []resource.Resource
|
|
Differences []analyser.Difference
|
|
Deleted []resource.Resource
|
|
Alerts alerter.Alerts
|
|
Stylesheet template.CSS
|
|
ScanDuration string
|
|
}
|
|
|
|
func NewHTML(path string) *HTML {
|
|
return &HTML{path}
|
|
}
|
|
|
|
func (c *HTML) Write(analysis *analyser.Analysis) error {
|
|
file := os.Stdout
|
|
if !isStdOut(c.path) {
|
|
f, err := os.OpenFile(c.path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
file = f
|
|
}
|
|
|
|
tmplFile, err := assets.ReadFile("assets/index.tmpl")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
styleFile, err := assets.ReadFile("assets/style.css")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
funcMap := template.FuncMap{
|
|
"getResourceTypes": func() []string {
|
|
resources := make([]resource.Resource, 0)
|
|
resources = append(resources, analysis.Unmanaged()...)
|
|
resources = append(resources, analysis.Deleted()...)
|
|
|
|
for _, d := range analysis.Differences() {
|
|
resources = append(resources, d.Res)
|
|
}
|
|
|
|
return distinctResourceTypes(resources)
|
|
},
|
|
"rate": func(count int) float64 {
|
|
if analysis.Summary().TotalResources == 0 {
|
|
return 0
|
|
}
|
|
return math.Round(100 * float64(count) / float64(analysis.Summary().TotalResources))
|
|
},
|
|
"jsonDiff": func(ch analyser.Changelog) template.HTML {
|
|
var buf bytes.Buffer
|
|
|
|
whiteSpace := " "
|
|
for _, change := range ch {
|
|
for i, v := range change.Path {
|
|
if _, err := strconv.Atoi(v); err == nil {
|
|
change.Path[i] = fmt.Sprintf("[%s]", v)
|
|
}
|
|
}
|
|
path := strings.Join(change.Path, ".")
|
|
|
|
switch change.Type {
|
|
case diff.CREATE:
|
|
pref := fmt.Sprintf("%s %s:", "+", path)
|
|
_, _ = fmt.Fprintf(&buf, "%s%s <span class=\"code-box-line-create\">%s</span>", whiteSpace, pref, prettify(change.To))
|
|
case diff.DELETE:
|
|
pref := fmt.Sprintf("%s %s:", "-", path)
|
|
_, _ = fmt.Fprintf(&buf, "%s%s <span class=\"code-box-line-delete\">%s</span>", whiteSpace, pref, prettify(change.From))
|
|
case diff.UPDATE:
|
|
prefix := fmt.Sprintf("%s %s:", "~", path)
|
|
if change.JsonString {
|
|
_, _ = fmt.Fprintf(&buf, "%s%s<br>%s%s<br>", whiteSpace, prefix, whiteSpace, jsonDiff(change.From, change.To, whiteSpace))
|
|
continue
|
|
}
|
|
_, _ = fmt.Fprintf(&buf, "%s%s <span class=\"code-box-line-delete\">%s</span> => <span class=\"code-box-line-create\">%s</span>", whiteSpace, prefix, htmlPrettify(change.From), htmlPrettify(change.To))
|
|
}
|
|
|
|
if change.Computed {
|
|
_, _ = fmt.Fprintf(&buf, " %s", "(computed)")
|
|
}
|
|
_, _ = fmt.Fprintf(&buf, "<br>")
|
|
}
|
|
|
|
return template.HTML(buf.String())
|
|
},
|
|
}
|
|
|
|
tmpl, err := template.New("main").Funcs(funcMap).Parse(string(tmplFile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := &HTMLTemplateParams{
|
|
ScanDate: analysis.Date.Format("Jan 02, 2006"),
|
|
Summary: analysis.Summary(),
|
|
Coverage: analysis.Coverage(),
|
|
Unmanaged: analysis.Unmanaged(),
|
|
Differences: analysis.Differences(),
|
|
Deleted: analysis.Deleted(),
|
|
Alerts: analysis.Alerts(),
|
|
Stylesheet: template.CSS(styleFile),
|
|
ScanDuration: analysis.Duration.Round(time.Second).String(),
|
|
}
|
|
|
|
err = tmpl.Execute(file, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func distinctResourceTypes(resources []resource.Resource) []string {
|
|
types := make([]string, 0)
|
|
|
|
for _, res := range resources {
|
|
found := false
|
|
for _, v := range types {
|
|
if v == res.TerraformType() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
types = append(types, res.TerraformType())
|
|
}
|
|
}
|
|
|
|
return types
|
|
}
|
|
|
|
func htmlPrettify(resource interface{}) string {
|
|
res := reflect.ValueOf(resource)
|
|
if resource == nil || res.Kind() == reflect.Ptr && res.IsNil() {
|
|
return "<null>"
|
|
}
|
|
return awsutil.Prettify(resource)
|
|
}
|