refactor: add filtering to html output

main
sundowndev 2021-04-22 18:21:00 +02:00
parent 5daca2eb46
commit 4b9af2b57b
8 changed files with 523 additions and 82 deletions

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html>
<head>
<title>Driftctl scan report</title>
@ -5,97 +6,136 @@
<style>{{.Stylesheet}}</style>
</head>
<body>
<h1 class="heading title">Driftctl scan report</h1>
<span class="heading subtitle">Coverage {{.Coverage}}%</span>
<span class="heading date">{{.ScanDate}}</span>
<form action="#">
<input type="text">
</form>
<h2>Managed resources ({{ len .Managed }})</h2>
{{ if (eq (len .Managed) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $val := .Managed}}
<div>
<p class="list-item resource-id">{{$val.TerraformId}}</p>
<span class="list-item resource-type">{{$val.TerraformType}}</span>
</div>
{{end}}
<h2>Unmanaged resources ({{ len .Unmanaged }})</h2>
{{ if (eq (len .Unmanaged) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $val := .Unmanaged}}
<div>
<p class="list-item resource-id">{{$val.TerraformId}}</p>
<span class="list-item resource-type">{{$val.TerraformType}}</span>
</div>
{{end}}
<div id="app">
<h1 class="heading title">Driftctl scan report {{ if (eq .Coverage 100) }}✅{{else}}❌{{end}}</h1>
<span class="heading subtitle">Coverage 8%</span>
<span class="heading date">Apr 21, 2021</span>
<hr>
<form id="filter-form" action="#">
<input type="text" name="resource-id-filter" placeholder="Search resources..." onkeyup="refreshState()">
<select name="resource-type-filter" onchange="refreshState()">
<option value="">Resource type</option>
{{ range $type := getResourceTypes }}
<option value="{{$type}}">{{ $type }}</option>
{{end}}
</select>
</form>
<h2>Changed resources ({{ len .Differences }})</h2>
{{ if (eq (len .Differences) 0) }}
<h2>Managed resources <span class="resource-count-managed">({{len .Managed}})</span></h2>
{{ if (eq (len .Managed) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $diff := .Differences}}
<div>
<p class="list-item resource-id">{{$diff.Res.TerraformId}}</p>
<span class="list-item resource-type">{{$diff.Res.TerraformType}}</span>
{{end}}
{{range $res := .Managed}}
<div class="resource-item">
<span class="resource-item-id">{{$res.TerraformId}}</span>
<span class="resource-item-type">{{$res.TerraformType}}</span>
</div>
{{end}}
<h2>Unmanaged resources <span class="resource-count-unmanaged">({{len .Unmanaged}})</span></h2>
{{ if (eq (len .Unmanaged) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $res := .Unmanaged}}
<div class="resource-item">
<span class="resource-item-id">{{$res.TerraformId}}</span>
<span class="resource-item-type">{{$res.TerraformType}}</span>
</div>
{{end}}
<h2>Changed resources <span class="resource-count-changed">({{len .Differences}})</span></h2>
{{ if (eq (len .Differences) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $diff := .Differences}}
<div class="resource-item">
<span class="resource-item-id">{{$diff.Res.TerraformId}}</span>
<span class="resource-item-type">{{$diff.Res.TerraformType}}</span>
<div>
{{range $change := $diff.Changelog}}
<div>{{ formatChange $change }}</div>
<div>{{ formatChange $change }}</div>
{{end}}
</div>
</div>
{{end}}
{{end}}
<h2>Missing resources ({{ len .Deleted }})</h2>
{{ if (eq (len .Deleted) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $val := .Deleted}}
<div>
<p class="list-item resource-id">{{$val.TerraformId}}</p>
<span class="list-item resource-type">{{$val.TerraformType}}</span>
</div>
{{end}}
<h2>Missing resources <span class="resource-count-deleted">({{len .Deleted}})</span></h2>
<h2>Alerts ({{ len .Alerts }})</h2>
{{ if (eq (len .Alerts) 0) }}
{{ if (eq (len .Deleted) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $key, $val := .Alerts}}
<div>
<p class="list-item resource-id">{{$key}}</p>
<span class="list-item resource-type">{{$val}}</span>
{{end}}
{{range $res := .Deleted}}
<div class="resource-item">
<span class="resource-item-id">{{$res.TerraformId}}</span>
<span class="resource-item-type">{{$res.TerraformType}}</span>
</div>
{{end}}
{{end}}
<h2>Alerts <span class="resource-count-alerts">({{len .Alerts}})</span></h2>
{{ if (eq (len .Alerts) 0) }}
<p>There's nothing to see there...</p>
{{end}}
{{range $type, $messages := .Alerts}}
<div class="resource-item">
<span class="resource-item-type">{{ $type }}</span>
<div>
{{range $msg := $messages}}
<div>- {{ $msg }}</div>
{{end}}
</div>
</div>
{{end}}
</div>
</body>
<script>
const managed = {{if .Managed}} {{.Managed}} {{else}} [] {{end}}
const unmanaged = {{if .Unmanaged}} {{.Unmanaged}} {{else}} [] {{end}}
const deleted = {{if .Deleted}} {{.Deleted}} {{else}} [] {{end}}
const allResources = [
...managed,
...unmanaged,
...deleted,
]
const resourceTypes = [
{{range $val := .Managed}}
'{{$val.TerraformType}}',
{{end}}
{{range $val := .Unmanaged}}
'{{$val.TerraformType}}',
{{end}}
{{range $val := .Deleted}}
'{{$val.TerraformType}}',
{{end}}
{{range $val := .Differences}}
'{{$val.Res.TerraformType}}',
{{end}}
].filter((value, index, self) => self.indexOf(value) === index)
<script lang="js">
const resources = document.querySelectorAll('.resource-item')
function search() {
function hideResource(res) {
res.classList.add('hide')
}
function displayResource(res) {
res.classList.remove('hide')
}
function resourceIdContains(res, query) {
const el = res.querySelector('.resource-item-id')
if (!el) {
return
}
return el.innerText.toLowerCase().includes(query.toLowerCase())
}
function resourceTypeEqual(res, type) {
const el = res.querySelector('.resource-item-type')
if (!el) {
return
}
return el.innerText === type
}
function refreshState() {
const queryFilterInput = document.querySelector('input[name=resource-id-filter]').value
const typeFilterInput = document.querySelector('select[name=resource-type-filter]').value
for (const res of resources) {
const matchId = !queryFilterInput.length || resourceIdContains(res, queryFilterInput)
const matchType = !typeFilterInput.length || resourceTypeEqual(res, typeFilterInput)
if (matchId && matchType) {
displayResource(res)
continue
}
hideResource(res)
}
}
refreshState()
</script>
</html>

View File

@ -0,0 +1,189 @@
<!DOCTYPE html>
<html>
<head>
<title>Driftctl scan report</title>
<meta charset="utf-8">
<style>[[.Stylesheet]]</style>
<script>[[.VueScript]]</script>
</head>
<body>
<div id="app">
<h1 class="heading title">Driftctl scan report</h1>
<span class="heading subtitle">Coverage 8%</span>
<span class="heading date">Apr 21, 2021</span>
<hr>
<form action="#">
<input v-model="searchQuery" type="text" placeholder="Search resources...">
<select v-model="resourceTypeQuery">
<option value="">Resource type</option>
<option v-for="type of getResourceTypes" :value="type">{{ type }}</option>
</select>
</form>
<h2>Managed resources ({{Managed.length}})</h2>
<div v-for="res of Managed">
<p class="list-item resource-id">{{ res.id }}</p>
<span class="list-item resource-type">{{ res.type }}</span>
</div>
<p v-if="!Managed.length">There's nothing to see there...</p>
<h2>Unmanaged resources ({{Unmanaged.length}})</h2>
<div v-for="res of Unmanaged">
<p class="list-item resource-id">{{ res.id }}</p>
<span class="list-item resource-type">{{ res.type }}</span>
</div>
<p v-if="!Unmanaged.length">There's nothing to see there...</p>
<h2>Changed resources ({{Changed.length}})</h2>
<div v-for="ch of Changed">
<p class="list-item resource-id">{{ ch.res.id }}</p>
<span class="list-item resource-type">{{ ch.res.type }}</span>
<div v-for="diff of ch.changelog">
<div>{{ formatDifference(diff) }}</div>
</div>
</div>
<p v-if="!Changed.length">There's nothing to see there...</p>
<h2>Missing resources ({{Missing.length}})</h2>
<div v-for="res of Missing">
<p class="list-item resource-id">{{ res.id }}</p>
<span class="list-item resource-type">{{ res.type }}</span>
</div>
<p v-if="!Missing.length">There's nothing to see there...</p>
<h2>Alerts ({{alerts.length}})</h2>
<div v-for="alert of alerts">
<p class="list-item resource-id">{{ alert.type }}</p>
<div v-for="msg of alert.messages">
<div>{{ msg }}</div>
</div>
</div>
<p v-if="!alerts.length">There's nothing to see there...</p>
</div>
</body>
<script lang="js">
Vue.createApp({
data() {
return {
searchQuery: '',
resourceTypeQuery: '',
managed: [
[[range $res := .Managed]]
{
id: [[$res.TerraformId]],
type: [[$res.TerraformType]],
},
[[end]]
],
unmanaged: [
[[range $res := .Unmanaged]]
{
id: [[$res.TerraformId]],
type: [[$res.TerraformType]],
},
[[end]]
],
missing: [
[[range $res := .Deleted]]
{
id: [[$res.TerraformId]],
type: [[$res.TerraformType]],
},
[[end]]
],
changed: [
[[range $ch := .Differences]]
{
res: {
id: [[$ch.Res.TerraformId]],
type: [[$ch.Res.TerraformType]],
},
changelog: [[$ch.Changelog]],
},
[[end]]
],
alerts: [
[[range $type, $alerts := .Alerts]]
{
type: [[$type]],
messages: [
[[range $alert := $alerts]] [[$alert.Message]] [[end]],
]
},
[[end]]
]
}
},
computed: {
Managed() {
return this.filterResources(this.managed)
},
Unmanaged() {
return this.filterResources(this.unmanaged)
},
Missing() {
return this.filterResources(this.missing)
},
Changed() {
return this.filterDifferences(this.changed)
},
getResourceTypes() {
return [...this.managed, ...this.unmanaged, ...this.missing]
.map(res => res.type)
.filter((value, index, self) => self.indexOf(value) === index)
}
},
methods: {
matchSearch(str) {
return str.toLowerCase().includes(this.searchQuery.toLowerCase())
},
filterResources(resources) {
let result = resources.filter(res => this.matchSearch(res.id))
if (this.resourceTypeQuery.length) {
result = result.filter(res => res.type === this.resourceTypeQuery)
}
return result
},
filterDifferences(diffs) {
let result = diffs.filter(d => this.matchSearch(d.res.id))
if (this.resourceTypeQuery.length) {
result = result.filter(d => d.res.type === this.resourceTypeQuery)
}
return result
},
formatDifference(diff) {
let prefix = ""
let suffix = ""
switch (diff.type) {
case "create":
prefix = "+"
case "update":
prefix = "~"
case "delete":
prefix = "-"
}
if (diff.computed) {
suffix = "(computed)"
}
return `${prefix} ${diff.path.join('.')}: ${this.prettifyValue(diff.from)} => ${this.prettifyValue(diff.to)} ${suffix}`
},
prettifyValue(value) {
return value
}
},
}).mount('#app')
</script>
</html>

View File

@ -1,6 +1,11 @@
* {
body {
font-family: sans-serif;
}
div {
display: block;
}
.hide {
display: none;
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,7 @@ import (
"fmt"
"html/template"
"os"
"sort"
"strings"
"time"
@ -63,6 +64,35 @@ func (c *HTML) Write(analysis *analyser.Analysis) error {
}
funcMap := template.FuncMap{
"getResourceTypes": func() []string {
resources := []resource.Resource{}
list := []string{}
resources = append(resources, analysis.Unmanaged()...)
resources = append(resources, analysis.Managed()...)
resources = append(resources, analysis.Deleted()...)
for _, res := range resources {
if i := sort.SearchStrings(list, res.TerraformType()); i <= len(list)-1 {
continue
}
list = append(list, res.TerraformType())
}
for _, d := range analysis.Differences() {
if i := sort.SearchStrings(list, d.Res.TerraformType()); i <= len(list)-1 {
continue
}
list = append(list, d.Res.TerraformType())
}
for kind := range analysis.Alerts() {
if i := sort.SearchStrings(list, kind); i <= len(list)-1 {
continue
}
list = append(list, kind)
}
return list
},
"formatChange": func(ch analyser.Change) string {
prefix := ""
suffix := ""

View File

@ -3,6 +3,7 @@ package output
import (
"io/ioutil"
"path"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -25,7 +26,7 @@ func TestHTML_Write(t *testing.T) {
name: "test html output",
goldenfile: "output.html",
args: args{
analysis: fakeAnalysis(),
analysis: fakeAnalysisWithAlerts(),
},
err: nil,
},
@ -47,14 +48,14 @@ func TestHTML_Write(t *testing.T) {
assert.NoError(t, err)
}
result, err := ioutil.ReadFile(tempFile.Name())
got, err := ioutil.ReadFile(tempFile.Name())
if err != nil {
t.Fatal(err)
}
expectedFilePath := path.Join("./testdata/", tt.goldenfile)
if *goldenfile.Update == tt.goldenfile {
if err := ioutil.WriteFile(expectedFilePath, result, 0600); err != nil {
if err := ioutil.WriteFile(expectedFilePath, got, 0600); err != nil {
t.Fatal(err)
}
}
@ -64,7 +65,10 @@ func TestHTML_Write(t *testing.T) {
t.Fatal(err)
}
assert.Equal(t, string(expected), string(result))
prettifiedExpected := strings.ReplaceAll(string(expected), " ", "")
prettifiedGot := strings.ReplaceAll(string(got), " ", "")
assert.Equal(t, prettifiedExpected, prettifiedGot)
})
}
}

View File

@ -79,6 +79,18 @@ func fakeAnalysis() *analyser.Analysis {
return &a
}
func fakeAnalysisWithAlerts() *analyser.Analysis {
a := fakeAnalysis()
a.SetAlerts(alerter.Alerts{
"": []alerter.Alert{
remote.NewEnumerationAccessDeniedAlert(aws.RemoteAWSTerraform, "aws_vpc", "aws_vpc"),
remote.NewEnumerationAccessDeniedAlert(aws.RemoteAWSTerraform, "aws_sqs", "aws_sqs"),
remote.NewEnumerationAccessDeniedAlert(aws.RemoteAWSTerraform, "aws_sns", "aws_sns"),
},
})
return a
}
func fakeAnalysisNoDrift() *analyser.Analysis {
a := analyser.Analysis{}
for i := 0; i < 5; i++ {

View File

@ -0,0 +1,160 @@
<!DOCTYPE html>
<html>
<head>
<title>Driftctl scan report</title>
<meta charset="utf-8">
<style>body {
font-family: sans-serif;
}
div {
display: block;
}</style>
</head>
<body>
<div id="app">
<h1 class="heading title">Driftctl scan report ❌</h1>
<span class="heading subtitle">Coverage 8%</span>
<span class="heading date">Apr 21, 2021</span>
<hr>
<form id="filter-form" action="#">
<input type="text" name="resource-id-filter" placeholder="Search resources..." onkeyup="refreshState()">
<select name="resource-type-filter" onchange="refreshState()">
<option value="">Resource type</option>
<option value="aws_unmanaged_resource">aws_unmanaged_resource</option>
</select>
</form>
<h2>Managed resources <span class="resource-count-managed">(2)</span></h2>
<div class="resource-item">
<span class="resource-item-id">diff-id-1</span>
<span class="resource-item-type">aws_diff_resource</span>
</div>
<div class="resource-item">
<span class="resource-item-id">no-diff-id-1</span>
<span class="resource-item-type">aws_no_diff_resource</span>
</div>
<h2>Unmanaged resources <span class="resource-count-unmanaged">(2)</span></h2>
<div class="resource-item">
<span class="resource-item-id">unmanaged-id-1</span>
<span class="resource-item-type">aws_unmanaged_resource</span>
</div>
<div class="resource-item">
<span class="resource-item-id">unmanaged-id-2</span>
<span class="resource-item-type">aws_unmanaged_resource</span>
</div>
<h2>Changed resources <span class="resource-count-changed">(1)</span></h2>
<div class="resource-item">
<span class="resource-item-id">diff-id-1</span>
<span class="resource-item-type">aws_diff_resource</span>
<div>
<div>~ updated.field: &#34;foobar&#34; =&gt; &#34;barfoo&#34; </div>
<div>&#43; new.field: &lt;nil&gt; =&gt; &#34;newValue&#34; </div>
<div>- a: &#34;oldValue&#34; =&gt; &lt;nil&gt; </div>
</div>
</div>
<h2>Missing resources <span class="resource-count-deleted">(2)</span></h2>
<div class="resource-item">
<span class="resource-item-id">deleted-id-1</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
<div class="resource-item">
<span class="resource-item-id">deleted-id-2</span>
<span class="resource-item-type">aws_deleted_resource</span>
</div>
<h2>Alerts <span class="resource-count-alerts">(1)</span></h2>
<div class="resource-item">
<span class="resource-item-type"></span>
<div>
<div>- {Ignoring aws_vpc from drift calculation: Listing aws_vpc is forbidden. aws&#43;tf}</div>
<div>- {Ignoring aws_sqs from drift calculation: Listing aws_sqs is forbidden. aws&#43;tf}</div>
<div>- {Ignoring aws_sns from drift calculation: Listing aws_sns is forbidden. aws&#43;tf}</div>
</div>
</div>
</div>
</body>
<script lang="js">
const resources = document.querySelectorAll('.resource-item')
function hideResource(res) {
res.setAttribute('style', 'display:none;')
}
function displayResource(res) {
res.setAttribute('style', '')
}
function resourceIdContains(res, query) {
const el = res.querySelector('.resource-item-id')
if (!el) {
return
}
return el.innerText.toLowerCase().includes(query.toLowerCase())
}
function resourceTypeEqual(res, type) {
const el = res.querySelector('.resource-item-type')
if (!el) {
return
}
return el.innerText === type
}
function refreshState() {
const queryFilterInput = document.querySelector('input[name=resource-id-filter]').value
const typeFilterInput = document.querySelector('select[name=resource-type-filter]').value
for (const res of resources) {
const matchId = !queryFilterInput.length || resourceIdContains(res, queryFilterInput)
const matchType = !typeFilterInput.length || resourceTypeEqual(res, typeFilterInput)
if (matchId && matchType) {
displayResource(res)
continue
}
hideResource(res)
}
}
refreshState()
</script>
</html>