driftctl/pkg/cmd/scan/output/html.go

242 lines
5.9 KiB
Go

package output
import (
"bytes"
"embed"
"encoding/base64"
"fmt"
"html/template"
"math"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/r3labs/diff/v2"
"github.com/snyk/driftctl/pkg/alerter"
"github.com/snyk/driftctl/pkg/analyser"
"github.com/snyk/driftctl/pkg/resource"
)
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 {
IsSync bool
ScanDate string
Coverage int
Summary analyser.Summary
Unmanaged []*resource.Resource
Differences []analyser.Difference
Deleted []*resource.Resource
Alerts alerter.Alerts
Stylesheet template.CSS
ScanDuration string
ProviderName string
ProviderVersion string
LogoSvg template.HTML
FaviconBase64 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
}
logoSvgFile, err := assets.ReadFile("assets/driftctl_light.svg")
if err != nil {
return err
}
faviconFile, err := assets.ReadFile("assets/favicon.ico")
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)
},
"getIaCSources": func() []string {
resources := make([]*resource.Resource, 0)
resources = append(resources, analysis.Deleted()...)
resources = append(resources, analysis.Managed()...)
return distinctIaCSources(resources)
},
"rate": func(count int) float64 {
if analysis.Summary().TotalResources == 0 {
return 0
}
rate := 100 * float64(count) / float64(analysis.Summary().TotalResources)
return math.Floor(rate*100) / 100
},
"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, jsonDiffHTML(change.From, change.To))
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{
IsSync: analysis.IsSync(),
ScanDate: analysis.Date.Format("Jan 02, 2006"),
Coverage: analysis.Coverage(),
Summary: analysis.Summary(),
Unmanaged: analysis.Unmanaged(),
Differences: analysis.Differences(),
Deleted: analysis.Deleted(),
Alerts: analysis.Alerts(),
Stylesheet: template.CSS(styleFile),
ScanDuration: analysis.Duration.Round(time.Second).String(),
ProviderName: analysis.ProviderName,
ProviderVersion: analysis.ProviderVersion,
LogoSvg: template.HTML(logoSvgFile),
FaviconBase64: base64.StdEncoding.EncodeToString(faviconFile),
}
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.ResourceType() {
found = true
break
}
}
if !found {
types = append(types, res.ResourceType())
}
}
return types
}
func distinctIaCSources(resources []*resource.Resource) []string {
types := make([]string, 0)
for _, res := range resources {
if res.Src() == nil {
continue
}
found := false
for _, v := range types {
if v == res.Src().Source() {
found = true
break
}
}
if !found {
types = append(types, res.Src().Source())
}
}
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)
}
func jsonDiffHTML(a, b interface{}) string {
diffStr := jsonDiff(a, b, false)
re := regexp.MustCompile(`(?m)^(?P<value>(\-)(.*))$`)
diffStr = re.ReplaceAllString(diffStr, `<span class="code-box-line-delete">$value</span>`)
re = regexp.MustCompile(`(?m)^(?P<value>(\+)(.*))$`)
diffStr = re.ReplaceAllString(diffStr, `<span class="code-box-line-create">$value</span>`)
return diffStr
}