commit
4b071ebbbb
|
@ -42,6 +42,7 @@ type Analysis struct {
|
||||||
summary Summary
|
summary Summary
|
||||||
alerts alerter.Alerts
|
alerts alerter.Alerts
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
|
Date time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type serializableDifference struct {
|
type serializableDifference struct {
|
||||||
|
|
|
@ -111,7 +111,7 @@ func TestDriftctlCmd_Scan(t *testing.T) {
|
||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
"DCTL_OUTPUT": "test",
|
"DCTL_OUTPUT": "test",
|
||||||
},
|
},
|
||||||
err: fmt.Errorf("Unable to parse output flag 'test': \nAccepted formats are: console://,json://PATH/TO/FILE.json"),
|
err: fmt.Errorf("Unable to parse output flag 'test': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
env: map[string]string{
|
env: map[string]string{
|
||||||
|
|
|
@ -367,6 +367,20 @@ func parseOutputFlag(out string) (*output.OutputConfig, error) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
options["path"] = opts[0]
|
options["path"] = opts[0]
|
||||||
|
case output.HTMLOutputType:
|
||||||
|
if len(opts) != 1 || opts[0] == "" {
|
||||||
|
return nil, errors.Wrapf(
|
||||||
|
cmderrors.NewUsageError(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"\nMust be of kind: %s",
|
||||||
|
output.Example(output.HTMLOutputType),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"Invalid html output '%s'",
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
options["path"] = opts[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return &output.OutputConfig{
|
return &output.OutputConfig{
|
||||||
|
|
|
@ -0,0 +1,375 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>driftctl Scan Report</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="shortcut icon"
|
||||||
|
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
|
||||||
|
<style>{{.Stylesheet}}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img src="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/driftctl_light.svg"
|
||||||
|
width="100px" height="81px" alt="driftctl logo"/>
|
||||||
|
<div>
|
||||||
|
<h1>Scan Report</h1>
|
||||||
|
<h2>{{ .ScanDate }}</h2>
|
||||||
|
<p>Scan Duration: {{.ScanDuration}}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<div class="card">
|
||||||
|
<span>Total Resources:</span>
|
||||||
|
<span class="strong">{{.Summary.TotalResources}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Coverage:</span>
|
||||||
|
<span class="strong">{{.Coverage}}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Managed:</span>
|
||||||
|
<span class="strong">{{rate .Summary.TotalManaged}}%</span>
|
||||||
|
<span class="fraction">{{.Summary.TotalManaged}}/{{.Summary.TotalResources}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Unmanaged:</span>
|
||||||
|
<span class="strong">{{rate .Summary.TotalUnmanaged}}%</span>
|
||||||
|
<span class="fraction">{{.Summary.TotalUnmanaged}}/{{.Summary.TotalResources}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Missing:</span>
|
||||||
|
<span class="strong">{{rate .Summary.TotalDeleted}}%</span>
|
||||||
|
<span class="fraction">{{.Summary.TotalDeleted}}/{{.Summary.TotalResources}}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<main>
|
||||||
|
{{ if (lt .Coverage 100) }}
|
||||||
|
<form role="search">
|
||||||
|
<label for="search" class="visuallyhidden">Search Resources:</label>
|
||||||
|
<input type="search" id="search" name="search" placeholder="Search Resources...">
|
||||||
|
<label for="select" class="visuallyhidden">Select Resource Type:</label>
|
||||||
|
<select id="select" name="select">
|
||||||
|
<option value="">Resource Type</option>
|
||||||
|
{{ range $type := getResourceTypes }}
|
||||||
|
<option value="{{$type}}">{{ $type }}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
<input type="reset" value="Reset Filters">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="tabs-wrapper">
|
||||||
|
<div role="tablist" aria-label="List of tabs">
|
||||||
|
{{if (gt (len .Unmanaged) 0)}}
|
||||||
|
<button type="button" role="tab" aria-selected="true" aria-controls="unmanaged-tab" id="unmanaged">
|
||||||
|
Unmanaged Resources (<span data-count="resource-unmanaged">{{len .Unmanaged}}</span>)
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
{{if (gt (len .Differences) 0)}}
|
||||||
|
<button type="button" role="tab" aria-selected="false" aria-controls="changed-tab" id="changed"
|
||||||
|
tabindex="-1">
|
||||||
|
Changed Resources (<span data-count="resource-changed">{{len .Differences}}</span>)
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
{{if (gt (len .Deleted) 0)}}
|
||||||
|
<button type="button" role="tab" aria-selected="false" aria-controls="missing-tab" id="missing"
|
||||||
|
tabindex="-1">
|
||||||
|
Missing Resources (<span data-count="resource-deleted">{{len .Deleted}}</span>)
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
{{if (gt (len .Alerts) 0)}}
|
||||||
|
<button type="button" role="tab" aria-selected="false" aria-controls="alerts-tab" id="alerts"
|
||||||
|
tabindex="-1">
|
||||||
|
Alerts (<span data-count="resource-alerts">0</span>)
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="panels">
|
||||||
|
{{ if (gt (len .Unmanaged) 0) }}
|
||||||
|
<div tabindex="0" role="tabpanel" id="unmanaged-tab" aria-labelledby="unmanaged">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<th>Resource ID</th>
|
||||||
|
<th>Resource Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $res := .Unmanaged}}
|
||||||
|
<tr data-kind="resource-unmanaged" class="resource-item row">
|
||||||
|
<td data-type="resource-id">{{$res.TerraformId}}</td>
|
||||||
|
<td data-type="resource-type">{{$res.TerraformType}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{ if (gt (len .Differences) 0) }}
|
||||||
|
<div class="is-hidden" tabindex="0" role="tabpanel" id="changed-tab" aria-labelledby="changed">
|
||||||
|
<div role="table">
|
||||||
|
<div role="rowgroup">
|
||||||
|
<div role="row" class="table-header">
|
||||||
|
<span role="columnheader">Resource ID</span>
|
||||||
|
<span role="columnheader">Resource Type</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div role="rowgroup" class="table-body">
|
||||||
|
{{range $diff := .Differences}}
|
||||||
|
<div role="row" data-kind="resource-changed" class="resource-item">
|
||||||
|
<div class="row">
|
||||||
|
<span role="cell" data-type="resource-id">{{$diff.Res.TerraformId}}</span>
|
||||||
|
<span role="cell" data-type="resource-type">{{$diff.Res.TerraformType}}</span>
|
||||||
|
</div>
|
||||||
|
<pre class="code-box">
|
||||||
|
<code class="code-box-line">{{ jsonDiff $diff.Changelog }}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{ if (gt (len .Deleted) 0) }}
|
||||||
|
<div class="is-hidden" tabindex="0" role="tabpanel" id="missing-tab" aria-labelledby="missing">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<th>Resource ID</th>
|
||||||
|
<th>Resource Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $res := .Deleted}}
|
||||||
|
<tr data-kind="resource-deleted" class="resource-item row">
|
||||||
|
<td data-type="resource-id">{{$res.TerraformId}}</td>
|
||||||
|
<td data-type="resource-type">{{$res.TerraformType}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{ if (gt (len .Alerts) 0) }}
|
||||||
|
<div class="is-hidden" tabindex="0" role="tabpanel" id="alerts-tab" aria-labelledby="alerts">
|
||||||
|
<ul>
|
||||||
|
{{range $type, $messages := .Alerts}}
|
||||||
|
{{range $el := $messages}}
|
||||||
|
<li data-kind="resource-alerts" class="resource-item">
|
||||||
|
{{ if $type }}
|
||||||
|
<span data-type="resource-type">{{ $type }}</span>
|
||||||
|
{{end}}
|
||||||
|
<span>{{ $el.Message }}</span>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<h1 class="congrats">Congrats! Your infrastructure is in sync</h1>
|
||||||
|
{{end}}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
|
||||||
|
form.addEventListener("submit", (event) => event.preventDefault());
|
||||||
|
|
||||||
|
const resources = document.querySelectorAll("[data-kind^='resource-']");
|
||||||
|
const searchInput = document.querySelector('[type="search"]');
|
||||||
|
const selectBox = document.querySelector("select");
|
||||||
|
const resetButton = document.querySelector('[type="reset"]');
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", filterResources);
|
||||||
|
selectBox.addEventListener("input", filterResources);
|
||||||
|
resetButton.addEventListener("click", resetResources);
|
||||||
|
|
||||||
|
function refreshPanel(count, el) {
|
||||||
|
const panel = document.getElementById(
|
||||||
|
el.parentElement.getAttribute("aria-controls")
|
||||||
|
);
|
||||||
|
if (!panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
panel.firstElementChild.classList.add("is-hidden");
|
||||||
|
panel.children[1].classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
panel.firstElementChild.classList.remove("is-hidden");
|
||||||
|
panel.children[1].classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCounters() {
|
||||||
|
const map = {
|
||||||
|
"[data-kind='resource-unmanaged']": "[data-count='resource-unmanaged']",
|
||||||
|
"[data-kind='resource-changed']": "[data-count='resource-changed']",
|
||||||
|
"[data-kind='resource-deleted']": "[data-count='resource-deleted']",
|
||||||
|
"[data-kind='resource-alerts']": "[data-count='resource-alerts']",
|
||||||
|
};
|
||||||
|
for (const key in map) {
|
||||||
|
const countEl = document.querySelector(map[key]);
|
||||||
|
if (countEl) {
|
||||||
|
const count = Array.from(document.querySelectorAll(key)).filter(
|
||||||
|
(el) => !el.classList.contains("is-hidden")
|
||||||
|
).length;
|
||||||
|
countEl.textContent = count;
|
||||||
|
refreshPanel(count, countEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceIdContains(res, id) {
|
||||||
|
if (id === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-id']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText.toLowerCase().includes(id.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceTypeEquals(res, type) {
|
||||||
|
if (type === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-type']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterResources() {
|
||||||
|
const id = searchInput.value;
|
||||||
|
const type = selectBox.value;
|
||||||
|
for (const res of resources) {
|
||||||
|
const matchId = resourceIdContains(res, id);
|
||||||
|
const matchType = resourceTypeEquals(res, type);
|
||||||
|
if (matchId && matchType) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
res.classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetResources() {
|
||||||
|
for (const res of resources) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetResources()
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
// Enhance accessibility
|
||||||
|
const tablist = document.querySelector('[role="tablist"]')
|
||||||
|
const tabs = document.querySelectorAll('[role="tab"]')
|
||||||
|
const panels = document.querySelectorAll('[role="tabpanel"]')
|
||||||
|
const keys = {left: 37, right: 39}
|
||||||
|
const direction = {37: -1, 39: 1}
|
||||||
|
|
||||||
|
for (let i = 0; i < tabs.length; ++i) {
|
||||||
|
addListeners(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addListeners(index) {
|
||||||
|
tabs[index].addEventListener('click', clickEventListener)
|
||||||
|
tabs[index].addEventListener('keyup', keyupEventListener)
|
||||||
|
tabs[index].index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickEventListener(event) {
|
||||||
|
let tab
|
||||||
|
if (event.target.getAttribute("role") === "tab") {
|
||||||
|
tab = event.target
|
||||||
|
} else {
|
||||||
|
tab = event.target.closest("button")
|
||||||
|
}
|
||||||
|
const selected = tab.getAttribute("aria-selected")
|
||||||
|
if (selected === "false") {
|
||||||
|
activateTab(tab, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyupEventListener(event) {
|
||||||
|
const key = event.keyCode
|
||||||
|
switch (key) {
|
||||||
|
case keys.left:
|
||||||
|
case keys.right:
|
||||||
|
switchTabOnArrowPress(event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTabOnArrowPress(event) {
|
||||||
|
const pressed = event.keyCode
|
||||||
|
for (let x = 0; x < tabs.length; x++) {
|
||||||
|
tabs[x].addEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
if (direction[pressed]) {
|
||||||
|
const target = event.target
|
||||||
|
if (target.index !== undefined) {
|
||||||
|
if (tabs[target.index + direction[pressed]]) {
|
||||||
|
tabs[target.index + direction[pressed]].focus()
|
||||||
|
} else if (pressed === keys.left) {
|
||||||
|
tabs[tabs.length - 1].focus()
|
||||||
|
} else if (pressed === keys.right) {
|
||||||
|
tabs[0].focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(tab, setFocus) {
|
||||||
|
setFocus = setFocus || true
|
||||||
|
deactivateTabs()
|
||||||
|
tab.removeAttribute('tabindex')
|
||||||
|
tab.setAttribute('aria-selected', 'true')
|
||||||
|
const controls = tab.getAttribute('aria-controls')
|
||||||
|
document.getElementById(controls).classList.remove('is-hidden')
|
||||||
|
if (setFocus) {
|
||||||
|
tab.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateTabs() {
|
||||||
|
for (let t = 0; t < tabs.length; t++) {
|
||||||
|
tabs[t].setAttribute('tabindex', '-1')
|
||||||
|
tabs[t].setAttribute('aria-selected', 'false')
|
||||||
|
tabs[t].removeEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
for (let p = 0; p < panels.length; p++) {
|
||||||
|
panels[p].classList.add('is-hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusEventHandler(event) {
|
||||||
|
const target = event.target
|
||||||
|
if (target === document.activeElement) {
|
||||||
|
activateTab(target, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,329 @@
|
||||||
|
html, body, div, span, h1, h2, p, pre, a, code, img, ul, li, form, label, table, tbody, thead, tr, th, td, header, section, button {
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f7f7f9;
|
||||||
|
color: #1c1e21;
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 130px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 3px solid #71b2c3;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0Ljk1IDEwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6IzQ0NDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPmFycm93czwvdGl0bGU+PHJlY3QgY2xhc3M9ImNscy0xIiB3aWR0aD0iNC45NSIgaGVpZ2h0PSIxMCIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSIxLjQxIDQuNjcgMi40OCAzLjE4IDMuNTQgNC42NyAxLjQxIDQuNjciLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMy41NCA1LjMzIDIuNDggNi44MiAxLjQxIDUuMzMgMy41NCA1LjMzIi8+PC9zdmc+) no-repeat 97% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody, ul, .table-body {
|
||||||
|
border-left: 1px solid #ececec;
|
||||||
|
border-right: 1px solid #ececec;
|
||||||
|
border-top: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"]:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"][aria-selected="true"] {
|
||||||
|
background: #71b2c3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"] {
|
||||||
|
-webkit-animation: fadein .8s;
|
||||||
|
animation: fadein .8s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"].is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"] {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #5faabd;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 34px;
|
||||||
|
margin: 5px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"], select {
|
||||||
|
border: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 36px;
|
||||||
|
margin: 5px;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line {
|
||||||
|
line-height: 30px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-create {
|
||||||
|
background-color: #22863a1a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #22863a;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-delete {
|
||||||
|
background-color: #bf404a17;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #bf404a;
|
||||||
|
padding: 3px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.congrats {
|
||||||
|
color: #4d9221;
|
||||||
|
text-align: center;
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-panel {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fraction {
|
||||||
|
background: #e8e8e8;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 4px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
border-bottom: 1px solid #ececec;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visuallyhidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
form {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 0 50px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,234 @@
|
||||||
|
package output
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudskiff/driftctl/pkg/resource"
|
||||||
|
testresource "github.com/cloudskiff/driftctl/test/resource"
|
||||||
|
"github.com/r3labs/diff/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/cloudskiff/driftctl/pkg/analyser"
|
||||||
|
"github.com/cloudskiff/driftctl/test/goldenfile"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHTML_Write(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
goldenfile string
|
||||||
|
analysis func() *analyser.Analysis
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test html output when there's no resources",
|
||||||
|
goldenfile: "output_empty.html",
|
||||||
|
analysis: func() *analyser.Analysis {
|
||||||
|
a := &analyser.Analysis{}
|
||||||
|
a.Date = time.Date(2021, 06, 10, 0, 0, 0, 0, &time.Location{})
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test html output when infrastructure is in sync",
|
||||||
|
goldenfile: "output_sync.html",
|
||||||
|
analysis: func() *analyser.Analysis {
|
||||||
|
a := &analyser.Analysis{}
|
||||||
|
a.Date = time.Date(2021, 06, 10, 0, 0, 0, 0, &time.Location{})
|
||||||
|
a.Duration = 72 * time.Second
|
||||||
|
a.AddManaged(
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "deleted-id-3",
|
||||||
|
Type: "aws_deleted_resource",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test html output",
|
||||||
|
goldenfile: "output.html",
|
||||||
|
|
||||||
|
analysis: func() *analyser.Analysis {
|
||||||
|
a := fakeAnalysisWithAlerts()
|
||||||
|
a.Date = time.Date(2021, 06, 10, 0, 0, 0, 0, &time.Location{})
|
||||||
|
a.Duration = 91 * time.Second
|
||||||
|
a.AddDeleted(
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "deleted-id-3",
|
||||||
|
Type: "aws_deleted_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "deleted-id-4",
|
||||||
|
Type: "aws_deleted_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "deleted-id-5",
|
||||||
|
Type: "aws_deleted_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "deleted-id-6",
|
||||||
|
Type: "aws_deleted_resource",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
a.AddUnmanaged(
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "unmanaged-id-3",
|
||||||
|
Type: "aws_unmanaged_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "unmanaged-id-4",
|
||||||
|
Type: "aws_unmanaged_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "unmanaged-id-5",
|
||||||
|
Type: "aws_unmanaged_resource",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
a.AddDifference(analyser.Difference{Res: &testresource.FakeResource{
|
||||||
|
Id: "diff-id-2",
|
||||||
|
Type: "aws_diff_resource",
|
||||||
|
}, Changelog: []analyser.Change{
|
||||||
|
{
|
||||||
|
Change: diff.Change{
|
||||||
|
Type: diff.DELETE,
|
||||||
|
Path: []string{"path", "to", "fields", "0"},
|
||||||
|
From: "value",
|
||||||
|
To: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Change: diff.Change{
|
||||||
|
Type: diff.UPDATE,
|
||||||
|
Path: []string{"path", "to", "fields", "1"},
|
||||||
|
From: 12,
|
||||||
|
To: "12",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Change: diff.Change{
|
||||||
|
Type: diff.DELETE,
|
||||||
|
Path: []string{"group_ids"},
|
||||||
|
From: []string{"a071314398026"},
|
||||||
|
To: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Change: diff.Change{
|
||||||
|
Type: diff.UPDATE,
|
||||||
|
Path: []string{"Policies", "0"},
|
||||||
|
From: testresource.FakeResource{},
|
||||||
|
To: testresource.FakeResource{Id: "093cd6ba-cf6d-4800-b252-6a50ca8903cd", Type: "aws_iam_policy"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Change: diff.Change{
|
||||||
|
Type: diff.CREATE,
|
||||||
|
Path: []string{"Tags", "0", "Name"},
|
||||||
|
From: nil,
|
||||||
|
To: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Change: diff.Change{
|
||||||
|
Type: diff.UPDATE,
|
||||||
|
Path: []string{"InstanceInitiatedShutdownBehavior"},
|
||||||
|
From: "",
|
||||||
|
To: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
tempFile, err := ioutil.TempFile(tempDir, "result")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
c := NewHTML(tempFile.Name())
|
||||||
|
|
||||||
|
err = c.Write(tt.analysis())
|
||||||
|
if tt.err != nil {
|
||||||
|
assert.EqualError(t, err, tt.err.Error())
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, got, 0600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected, err := ioutil.ReadFile(expectedFilePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, string(expected), string(got))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTML_DistinctResourceTypes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
resources []resource.Resource
|
||||||
|
value []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test empty array",
|
||||||
|
resources: []resource.Resource{},
|
||||||
|
value: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test empty array",
|
||||||
|
resources: []resource.Resource{
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "deleted-id-1",
|
||||||
|
Type: "aws_deleted_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "unmanaged-id-1",
|
||||||
|
Type: "aws_unmanaged_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "unmanaged-id-2",
|
||||||
|
Type: "aws_unmanaged_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "diff-id-1",
|
||||||
|
Type: "aws_diff_resource",
|
||||||
|
},
|
||||||
|
&testresource.FakeResource{
|
||||||
|
Id: "deleted-id-2",
|
||||||
|
Type: "aws_deleted_resource",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
value: []string{"aws_deleted_resource", "aws_unmanaged_resource", "aws_diff_resource"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := distinctResourceTypes(tt.resources)
|
||||||
|
assert.Equal(t, tt.value, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,11 +14,13 @@ type Output interface {
|
||||||
var supportedOutputTypes = []string{
|
var supportedOutputTypes = []string{
|
||||||
ConsoleOutputType,
|
ConsoleOutputType,
|
||||||
JSONOutputType,
|
JSONOutputType,
|
||||||
|
HTMLOutputType,
|
||||||
}
|
}
|
||||||
|
|
||||||
var supportedOutputExample = map[string]string{
|
var supportedOutputExample = map[string]string{
|
||||||
ConsoleOutputType: ConsoleOutputExample,
|
ConsoleOutputType: ConsoleOutputExample,
|
||||||
JSONOutputType: JSONOutputExample,
|
JSONOutputType: JSONOutputExample,
|
||||||
|
HTMLOutputType: HTMLOutputExample,
|
||||||
}
|
}
|
||||||
|
|
||||||
func SupportedOutputs() []string {
|
func SupportedOutputs() []string {
|
||||||
|
@ -53,6 +55,8 @@ func GetOutput(config OutputConfig, quiet bool) Output {
|
||||||
switch config.Key {
|
switch config.Key {
|
||||||
case JSONOutputType:
|
case JSONOutputType:
|
||||||
return NewJSON(config.Options["path"])
|
return NewJSON(config.Options["path"])
|
||||||
|
case HTMLOutputType:
|
||||||
|
return NewHTML(config.Options["path"])
|
||||||
case ConsoleOutputType:
|
case ConsoleOutputType:
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -79,6 +79,18 @@ func fakeAnalysis() *analyser.Analysis {
|
||||||
return &a
|
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 {
|
func fakeAnalysisNoDrift() *analyser.Analysis {
|
||||||
a := analyser.Analysis{}
|
a := analyser.Analysis{}
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
|
|
|
@ -0,0 +1,775 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>driftctl Scan Report</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="shortcut icon"
|
||||||
|
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
|
||||||
|
<style>html, body, div, span, h1, h2, p, pre, a, code, img, ul, li, form, label, table, tbody, thead, tr, th, td, header, section, button {
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f7f7f9;
|
||||||
|
color: #1c1e21;
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 130px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 3px solid #71b2c3;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0Ljk1IDEwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6IzQ0NDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPmFycm93czwvdGl0bGU+PHJlY3QgY2xhc3M9ImNscy0xIiB3aWR0aD0iNC45NSIgaGVpZ2h0PSIxMCIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSIxLjQxIDQuNjcgMi40OCAzLjE4IDMuNTQgNC42NyAxLjQxIDQuNjciLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMy41NCA1LjMzIDIuNDggNi44MiAxLjQxIDUuMzMgMy41NCA1LjMzIi8+PC9zdmc+) no-repeat 97% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody, ul, .table-body {
|
||||||
|
border-left: 1px solid #ececec;
|
||||||
|
border-right: 1px solid #ececec;
|
||||||
|
border-top: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"]:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"][aria-selected="true"] {
|
||||||
|
background: #71b2c3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"] {
|
||||||
|
-webkit-animation: fadein .8s;
|
||||||
|
animation: fadein .8s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"].is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"] {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #5faabd;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 34px;
|
||||||
|
margin: 5px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"], select {
|
||||||
|
border: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 36px;
|
||||||
|
margin: 5px;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line {
|
||||||
|
line-height: 30px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-create {
|
||||||
|
background-color: #22863a1a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #22863a;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-delete {
|
||||||
|
background-color: #bf404a17;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #bf404a;
|
||||||
|
padding: 3px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.congrats {
|
||||||
|
color: #4d9221;
|
||||||
|
text-align: center;
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-panel {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fraction {
|
||||||
|
background: #e8e8e8;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 4px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
border-bottom: 1px solid #ececec;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visuallyhidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
form {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 0 50px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img src="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/driftctl_light.svg"
|
||||||
|
width="100px" height="81px" alt="driftctl logo"/>
|
||||||
|
<div>
|
||||||
|
<h1>Scan Report</h1>
|
||||||
|
<h2>Jun 10, 2021</h2>
|
||||||
|
<p>Scan Duration: 1m31s</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<div class="card">
|
||||||
|
<span>Total Resources:</span>
|
||||||
|
<span class="strong">13</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Coverage:</span>
|
||||||
|
<span class="strong">15%</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Managed:</span>
|
||||||
|
<span class="strong">15%</span>
|
||||||
|
<span class="fraction">2/13</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Unmanaged:</span>
|
||||||
|
<span class="strong">38%</span>
|
||||||
|
<span class="fraction">5/13</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Missing:</span>
|
||||||
|
<span class="strong">46%</span>
|
||||||
|
<span class="fraction">6/13</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<form role="search">
|
||||||
|
<label for="search" class="visuallyhidden">Search Resources:</label>
|
||||||
|
<input type="search" id="search" name="search" placeholder="Search Resources...">
|
||||||
|
<label for="select" class="visuallyhidden">Select Resource Type:</label>
|
||||||
|
<select id="select" name="select">
|
||||||
|
<option value="">Resource Type</option>
|
||||||
|
|
||||||
|
<option value="aws_unmanaged_resource">aws_unmanaged_resource</option>
|
||||||
|
|
||||||
|
<option value="aws_deleted_resource">aws_deleted_resource</option>
|
||||||
|
|
||||||
|
<option value="aws_diff_resource">aws_diff_resource</option>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<input type="reset" value="Reset Filters">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="tabs-wrapper">
|
||||||
|
<div role="tablist" aria-label="List of tabs">
|
||||||
|
|
||||||
|
<button type="button" role="tab" aria-selected="true" aria-controls="unmanaged-tab" id="unmanaged">
|
||||||
|
Unmanaged Resources (<span data-count="resource-unmanaged">5</span>)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="button" role="tab" aria-selected="false" aria-controls="changed-tab" id="changed"
|
||||||
|
tabindex="-1">
|
||||||
|
Changed Resources (<span data-count="resource-changed">2</span>)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="button" role="tab" aria-selected="false" aria-controls="missing-tab" id="missing"
|
||||||
|
tabindex="-1">
|
||||||
|
Missing Resources (<span data-count="resource-deleted">6</span>)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button type="button" role="tab" aria-selected="false" aria-controls="alerts-tab" id="alerts"
|
||||||
|
tabindex="-1">
|
||||||
|
Alerts (<span data-count="resource-alerts">0</span>)
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="panels">
|
||||||
|
|
||||||
|
<div tabindex="0" role="tabpanel" id="unmanaged-tab" aria-labelledby="unmanaged">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<th>Resource ID</th>
|
||||||
|
<th>Resource Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr data-kind="resource-unmanaged" class="resource-item row">
|
||||||
|
<td data-type="resource-id">unmanaged-id-1</td>
|
||||||
|
<td data-type="resource-type">aws_unmanaged_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-unmanaged" class="resource-item row">
|
||||||
|
<td data-type="resource-id">unmanaged-id-2</td>
|
||||||
|
<td data-type="resource-type">aws_unmanaged_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-unmanaged" class="resource-item row">
|
||||||
|
<td data-type="resource-id">unmanaged-id-3</td>
|
||||||
|
<td data-type="resource-type">aws_unmanaged_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-unmanaged" class="resource-item row">
|
||||||
|
<td data-type="resource-id">unmanaged-id-4</td>
|
||||||
|
<td data-type="resource-type">aws_unmanaged_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-unmanaged" class="resource-item row">
|
||||||
|
<td data-type="resource-id">unmanaged-id-5</td>
|
||||||
|
<td data-type="resource-type">aws_unmanaged_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="is-hidden" tabindex="0" role="tabpanel" id="changed-tab" aria-labelledby="changed">
|
||||||
|
<div role="table">
|
||||||
|
<div role="rowgroup">
|
||||||
|
<div role="row" class="table-header">
|
||||||
|
<span role="columnheader">Resource ID</span>
|
||||||
|
<span role="columnheader">Resource Type</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div role="rowgroup" class="table-body">
|
||||||
|
|
||||||
|
<div role="row" data-kind="resource-changed" class="resource-item">
|
||||||
|
<div class="row">
|
||||||
|
<span role="cell" data-type="resource-id">diff-id-1</span>
|
||||||
|
<span role="cell" data-type="resource-type">aws_diff_resource</span>
|
||||||
|
</div>
|
||||||
|
<pre class="code-box">
|
||||||
|
<code class="code-box-line"> ~ updated.field: <span class="code-box-line-delete">"foobar"</span> => <span class="code-box-line-create">"barfoo"</span><br> + new.field: <span class="code-box-line-create">"newValue"</span><br> - a: <span class="code-box-line-delete">"oldValue"</span><br></code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div role="row" data-kind="resource-changed" class="resource-item">
|
||||||
|
<div class="row">
|
||||||
|
<span role="cell" data-type="resource-id">diff-id-2</span>
|
||||||
|
<span role="cell" data-type="resource-type">aws_diff_resource</span>
|
||||||
|
</div>
|
||||||
|
<pre class="code-box">
|
||||||
|
<code class="code-box-line"> - path.to.fields.[0]: <span class="code-box-line-delete">"value"</span><br> ~ path.to.fields.[1]: <span class="code-box-line-delete">12</span> => <span class="code-box-line-create">"12"</span><br> - group_ids: <span class="code-box-line-delete">["a071314398026"]</span><br> ~ Policies.[0]: <span class="code-box-line-delete">{
|
||||||
|
Id: "",
|
||||||
|
Type: ""
|
||||||
|
}</span> => <span class="code-box-line-create">{
|
||||||
|
Id: "093cd6ba-cf6d-4800-b252-6a50ca8903cd",
|
||||||
|
Type: "aws_iam_policy"
|
||||||
|
}</span><br> + Tags.[0].Name: <span class="code-box-line-create">"test"</span><br> ~ InstanceInitiatedShutdownBehavior: <span class="code-box-line-delete">""</span> => <span class="code-box-line-create">null</span><br></code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="is-hidden" tabindex="0" role="tabpanel" id="missing-tab" aria-labelledby="missing">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr class="table-header">
|
||||||
|
<th>Resource ID</th>
|
||||||
|
<th>Resource Type</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr data-kind="resource-deleted" class="resource-item row">
|
||||||
|
<td data-type="resource-id">deleted-id-1</td>
|
||||||
|
<td data-type="resource-type">aws_deleted_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-deleted" class="resource-item row">
|
||||||
|
<td data-type="resource-id">deleted-id-2</td>
|
||||||
|
<td data-type="resource-type">aws_deleted_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-deleted" class="resource-item row">
|
||||||
|
<td data-type="resource-id">deleted-id-3</td>
|
||||||
|
<td data-type="resource-type">aws_deleted_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-deleted" class="resource-item row">
|
||||||
|
<td data-type="resource-id">deleted-id-4</td>
|
||||||
|
<td data-type="resource-type">aws_deleted_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-deleted" class="resource-item row">
|
||||||
|
<td data-type="resource-id">deleted-id-5</td>
|
||||||
|
<td data-type="resource-type">aws_deleted_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr data-kind="resource-deleted" class="resource-item row">
|
||||||
|
<td data-type="resource-id">deleted-id-6</td>
|
||||||
|
<td data-type="resource-type">aws_deleted_resource</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="is-hidden" tabindex="0" role="tabpanel" id="alerts-tab" aria-labelledby="alerts">
|
||||||
|
<ul>
|
||||||
|
|
||||||
|
|
||||||
|
<li data-kind="resource-alerts" class="resource-item">
|
||||||
|
|
||||||
|
<span>Ignoring aws_vpc from drift calculation: Listing aws_vpc is forbidden.</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li data-kind="resource-alerts" class="resource-item">
|
||||||
|
|
||||||
|
<span>Ignoring aws_sqs from drift calculation: Listing aws_sqs is forbidden.</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li data-kind="resource-alerts" class="resource-item">
|
||||||
|
|
||||||
|
<span>Ignoring aws_sns from drift calculation: Listing aws_sns is forbidden.</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<div class="empty-panel is-hidden">
|
||||||
|
<p>No results matched your filters</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
|
||||||
|
form.addEventListener("submit", (event) => event.preventDefault());
|
||||||
|
|
||||||
|
const resources = document.querySelectorAll("[data-kind^='resource-']");
|
||||||
|
const searchInput = document.querySelector('[type="search"]');
|
||||||
|
const selectBox = document.querySelector("select");
|
||||||
|
const resetButton = document.querySelector('[type="reset"]');
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", filterResources);
|
||||||
|
selectBox.addEventListener("input", filterResources);
|
||||||
|
resetButton.addEventListener("click", resetResources);
|
||||||
|
|
||||||
|
function refreshPanel(count, el) {
|
||||||
|
const panel = document.getElementById(
|
||||||
|
el.parentElement.getAttribute("aria-controls")
|
||||||
|
);
|
||||||
|
if (!panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
panel.firstElementChild.classList.add("is-hidden");
|
||||||
|
panel.children[1].classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
panel.firstElementChild.classList.remove("is-hidden");
|
||||||
|
panel.children[1].classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCounters() {
|
||||||
|
const map = {
|
||||||
|
"[data-kind='resource-unmanaged']": "[data-count='resource-unmanaged']",
|
||||||
|
"[data-kind='resource-changed']": "[data-count='resource-changed']",
|
||||||
|
"[data-kind='resource-deleted']": "[data-count='resource-deleted']",
|
||||||
|
"[data-kind='resource-alerts']": "[data-count='resource-alerts']",
|
||||||
|
};
|
||||||
|
for (const key in map) {
|
||||||
|
const countEl = document.querySelector(map[key]);
|
||||||
|
if (countEl) {
|
||||||
|
const count = Array.from(document.querySelectorAll(key)).filter(
|
||||||
|
(el) => !el.classList.contains("is-hidden")
|
||||||
|
).length;
|
||||||
|
countEl.textContent = count;
|
||||||
|
refreshPanel(count, countEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceIdContains(res, id) {
|
||||||
|
if (id === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-id']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText.toLowerCase().includes(id.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceTypeEquals(res, type) {
|
||||||
|
if (type === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-type']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterResources() {
|
||||||
|
const id = searchInput.value;
|
||||||
|
const type = selectBox.value;
|
||||||
|
for (const res of resources) {
|
||||||
|
const matchId = resourceIdContains(res, id);
|
||||||
|
const matchType = resourceTypeEquals(res, type);
|
||||||
|
if (matchId && matchType) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
res.classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetResources() {
|
||||||
|
for (const res of resources) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetResources()
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const tablist = document.querySelector('[role="tablist"]')
|
||||||
|
const tabs = document.querySelectorAll('[role="tab"]')
|
||||||
|
const panels = document.querySelectorAll('[role="tabpanel"]')
|
||||||
|
const keys = {left: 37, right: 39}
|
||||||
|
const direction = {37: -1, 39: 1}
|
||||||
|
|
||||||
|
for (let i = 0; i < tabs.length; ++i) {
|
||||||
|
addListeners(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addListeners(index) {
|
||||||
|
tabs[index].addEventListener('click', clickEventListener)
|
||||||
|
tabs[index].addEventListener('keyup', keyupEventListener)
|
||||||
|
tabs[index].index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickEventListener(event) {
|
||||||
|
let tab
|
||||||
|
if (event.target.getAttribute("role") === "tab") {
|
||||||
|
tab = event.target
|
||||||
|
} else {
|
||||||
|
tab = event.target.closest("button")
|
||||||
|
}
|
||||||
|
const selected = tab.getAttribute("aria-selected")
|
||||||
|
if (selected === "false") {
|
||||||
|
activateTab(tab, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyupEventListener(event) {
|
||||||
|
const key = event.keyCode
|
||||||
|
switch (key) {
|
||||||
|
case keys.left:
|
||||||
|
case keys.right:
|
||||||
|
switchTabOnArrowPress(event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTabOnArrowPress(event) {
|
||||||
|
const pressed = event.keyCode
|
||||||
|
for (let x = 0; x < tabs.length; x++) {
|
||||||
|
tabs[x].addEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
if (direction[pressed]) {
|
||||||
|
const target = event.target
|
||||||
|
if (target.index !== undefined) {
|
||||||
|
if (tabs[target.index + direction[pressed]]) {
|
||||||
|
tabs[target.index + direction[pressed]].focus()
|
||||||
|
} else if (pressed === keys.left) {
|
||||||
|
tabs[tabs.length - 1].focus()
|
||||||
|
} else if (pressed === keys.right) {
|
||||||
|
tabs[0].focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(tab, setFocus) {
|
||||||
|
setFocus = setFocus || true
|
||||||
|
deactivateTabs()
|
||||||
|
tab.removeAttribute('tabindex')
|
||||||
|
tab.setAttribute('aria-selected', 'true')
|
||||||
|
const controls = tab.getAttribute('aria-controls')
|
||||||
|
document.getElementById(controls).classList.remove('is-hidden')
|
||||||
|
if (setFocus) {
|
||||||
|
tab.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateTabs() {
|
||||||
|
for (let t = 0; t < tabs.length; t++) {
|
||||||
|
tabs[t].setAttribute('tabindex', '-1')
|
||||||
|
tabs[t].setAttribute('aria-selected', 'false')
|
||||||
|
tabs[t].removeEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
for (let p = 0; p < panels.length; p++) {
|
||||||
|
panels[p].classList.add('is-hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusEventHandler(event) {
|
||||||
|
const target = event.target
|
||||||
|
if (target === document.activeElement) {
|
||||||
|
activateTab(target, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,592 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>driftctl Scan Report</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="shortcut icon"
|
||||||
|
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
|
||||||
|
<style>html, body, div, span, h1, h2, p, pre, a, code, img, ul, li, form, label, table, tbody, thead, tr, th, td, header, section, button {
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f7f7f9;
|
||||||
|
color: #1c1e21;
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 130px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 3px solid #71b2c3;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0Ljk1IDEwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6IzQ0NDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPmFycm93czwvdGl0bGU+PHJlY3QgY2xhc3M9ImNscy0xIiB3aWR0aD0iNC45NSIgaGVpZ2h0PSIxMCIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSIxLjQxIDQuNjcgMi40OCAzLjE4IDMuNTQgNC42NyAxLjQxIDQuNjciLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMy41NCA1LjMzIDIuNDggNi44MiAxLjQxIDUuMzMgMy41NCA1LjMzIi8+PC9zdmc+) no-repeat 97% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody, ul, .table-body {
|
||||||
|
border-left: 1px solid #ececec;
|
||||||
|
border-right: 1px solid #ececec;
|
||||||
|
border-top: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"]:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"][aria-selected="true"] {
|
||||||
|
background: #71b2c3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"] {
|
||||||
|
-webkit-animation: fadein .8s;
|
||||||
|
animation: fadein .8s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"].is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"] {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #5faabd;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 34px;
|
||||||
|
margin: 5px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"], select {
|
||||||
|
border: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 36px;
|
||||||
|
margin: 5px;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line {
|
||||||
|
line-height: 30px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-create {
|
||||||
|
background-color: #22863a1a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #22863a;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-delete {
|
||||||
|
background-color: #bf404a17;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #bf404a;
|
||||||
|
padding: 3px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.congrats {
|
||||||
|
color: #4d9221;
|
||||||
|
text-align: center;
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-panel {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fraction {
|
||||||
|
background: #e8e8e8;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 4px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
border-bottom: 1px solid #ececec;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visuallyhidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
form {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 0 50px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img src="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/driftctl_light.svg"
|
||||||
|
width="100px" height="81px" alt="driftctl logo"/>
|
||||||
|
<div>
|
||||||
|
<h1>Scan Report</h1>
|
||||||
|
<h2>Jun 10, 2021</h2>
|
||||||
|
<p>Scan Duration: 0s</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<div class="card">
|
||||||
|
<span>Total Resources:</span>
|
||||||
|
<span class="strong">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Coverage:</span>
|
||||||
|
<span class="strong">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Managed:</span>
|
||||||
|
<span class="strong">0%</span>
|
||||||
|
<span class="fraction">0/0</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Unmanaged:</span>
|
||||||
|
<span class="strong">0%</span>
|
||||||
|
<span class="fraction">0/0</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Missing:</span>
|
||||||
|
<span class="strong">0%</span>
|
||||||
|
<span class="fraction">0/0</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<form role="search">
|
||||||
|
<label for="search" class="visuallyhidden">Search Resources:</label>
|
||||||
|
<input type="search" id="search" name="search" placeholder="Search Resources...">
|
||||||
|
<label for="select" class="visuallyhidden">Select Resource Type:</label>
|
||||||
|
<select id="select" name="select">
|
||||||
|
<option value="">Resource Type</option>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
<input type="reset" value="Reset Filters">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="tabs-wrapper">
|
||||||
|
<div role="tablist" aria-label="List of tabs">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="panels">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
|
||||||
|
form.addEventListener("submit", (event) => event.preventDefault());
|
||||||
|
|
||||||
|
const resources = document.querySelectorAll("[data-kind^='resource-']");
|
||||||
|
const searchInput = document.querySelector('[type="search"]');
|
||||||
|
const selectBox = document.querySelector("select");
|
||||||
|
const resetButton = document.querySelector('[type="reset"]');
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", filterResources);
|
||||||
|
selectBox.addEventListener("input", filterResources);
|
||||||
|
resetButton.addEventListener("click", resetResources);
|
||||||
|
|
||||||
|
function refreshPanel(count, el) {
|
||||||
|
const panel = document.getElementById(
|
||||||
|
el.parentElement.getAttribute("aria-controls")
|
||||||
|
);
|
||||||
|
if (!panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
panel.firstElementChild.classList.add("is-hidden");
|
||||||
|
panel.children[1].classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
panel.firstElementChild.classList.remove("is-hidden");
|
||||||
|
panel.children[1].classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCounters() {
|
||||||
|
const map = {
|
||||||
|
"[data-kind='resource-unmanaged']": "[data-count='resource-unmanaged']",
|
||||||
|
"[data-kind='resource-changed']": "[data-count='resource-changed']",
|
||||||
|
"[data-kind='resource-deleted']": "[data-count='resource-deleted']",
|
||||||
|
"[data-kind='resource-alerts']": "[data-count='resource-alerts']",
|
||||||
|
};
|
||||||
|
for (const key in map) {
|
||||||
|
const countEl = document.querySelector(map[key]);
|
||||||
|
if (countEl) {
|
||||||
|
const count = Array.from(document.querySelectorAll(key)).filter(
|
||||||
|
(el) => !el.classList.contains("is-hidden")
|
||||||
|
).length;
|
||||||
|
countEl.textContent = count;
|
||||||
|
refreshPanel(count, countEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceIdContains(res, id) {
|
||||||
|
if (id === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-id']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText.toLowerCase().includes(id.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceTypeEquals(res, type) {
|
||||||
|
if (type === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-type']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterResources() {
|
||||||
|
const id = searchInput.value;
|
||||||
|
const type = selectBox.value;
|
||||||
|
for (const res of resources) {
|
||||||
|
const matchId = resourceIdContains(res, id);
|
||||||
|
const matchType = resourceTypeEquals(res, type);
|
||||||
|
if (matchId && matchType) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
res.classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetResources() {
|
||||||
|
for (const res of resources) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetResources()
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const tablist = document.querySelector('[role="tablist"]')
|
||||||
|
const tabs = document.querySelectorAll('[role="tab"]')
|
||||||
|
const panels = document.querySelectorAll('[role="tabpanel"]')
|
||||||
|
const keys = {left: 37, right: 39}
|
||||||
|
const direction = {37: -1, 39: 1}
|
||||||
|
|
||||||
|
for (let i = 0; i < tabs.length; ++i) {
|
||||||
|
addListeners(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addListeners(index) {
|
||||||
|
tabs[index].addEventListener('click', clickEventListener)
|
||||||
|
tabs[index].addEventListener('keyup', keyupEventListener)
|
||||||
|
tabs[index].index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickEventListener(event) {
|
||||||
|
let tab
|
||||||
|
if (event.target.getAttribute("role") === "tab") {
|
||||||
|
tab = event.target
|
||||||
|
} else {
|
||||||
|
tab = event.target.closest("button")
|
||||||
|
}
|
||||||
|
const selected = tab.getAttribute("aria-selected")
|
||||||
|
if (selected === "false") {
|
||||||
|
activateTab(tab, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyupEventListener(event) {
|
||||||
|
const key = event.keyCode
|
||||||
|
switch (key) {
|
||||||
|
case keys.left:
|
||||||
|
case keys.right:
|
||||||
|
switchTabOnArrowPress(event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTabOnArrowPress(event) {
|
||||||
|
const pressed = event.keyCode
|
||||||
|
for (let x = 0; x < tabs.length; x++) {
|
||||||
|
tabs[x].addEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
if (direction[pressed]) {
|
||||||
|
const target = event.target
|
||||||
|
if (target.index !== undefined) {
|
||||||
|
if (tabs[target.index + direction[pressed]]) {
|
||||||
|
tabs[target.index + direction[pressed]].focus()
|
||||||
|
} else if (pressed === keys.left) {
|
||||||
|
tabs[tabs.length - 1].focus()
|
||||||
|
} else if (pressed === keys.right) {
|
||||||
|
tabs[0].focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(tab, setFocus) {
|
||||||
|
setFocus = setFocus || true
|
||||||
|
deactivateTabs()
|
||||||
|
tab.removeAttribute('tabindex')
|
||||||
|
tab.setAttribute('aria-selected', 'true')
|
||||||
|
const controls = tab.getAttribute('aria-controls')
|
||||||
|
document.getElementById(controls).classList.remove('is-hidden')
|
||||||
|
if (setFocus) {
|
||||||
|
tab.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateTabs() {
|
||||||
|
for (let t = 0; t < tabs.length; t++) {
|
||||||
|
tabs[t].setAttribute('tabindex', '-1')
|
||||||
|
tabs[t].setAttribute('aria-selected', 'false')
|
||||||
|
tabs[t].removeEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
for (let p = 0; p < panels.length; p++) {
|
||||||
|
panels[p].classList.add('is-hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusEventHandler(event) {
|
||||||
|
const target = event.target
|
||||||
|
if (target === document.activeElement) {
|
||||||
|
activateTab(target, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,568 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>driftctl Scan Report</title>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<link rel="shortcut icon"
|
||||||
|
href="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/favicon.ico"/>
|
||||||
|
<style>html, body, div, span, h1, h2, p, pre, a, code, img, ul, li, form, label, table, tbody, thead, tr, th, td, header, section, button {
|
||||||
|
border: 0;
|
||||||
|
font: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #f7f7f9;
|
||||||
|
color: #1c1e21;
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 130px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #ccc;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
background-color: #fff;
|
||||||
|
border-top: 3px solid #71b2c3;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 5px #0000000a;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0Ljk1IDEwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6IzQ0NDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPmFycm93czwvdGl0bGU+PHJlY3QgY2xhc3M9ImNscy0xIiB3aWR0aD0iNC45NSIgaGVpZ2h0PSIxMCIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSIxLjQxIDQuNjcgMi40OCAzLjE4IDMuNTQgNC42NyAxLjQxIDQuNjciLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMy41NCA1LjMzIDIuNDggNi44MiAxLjQxIDUuMzMgMy41NCA1LjMzIi8+PC9zdmc+) no-repeat 97% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody, ul, .table-body {
|
||||||
|
border-left: 1px solid #ececec;
|
||||||
|
border-right: 1px solid #ececec;
|
||||||
|
border-top: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 4px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"]:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"][aria-selected="true"] {
|
||||||
|
background: #71b2c3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"] {
|
||||||
|
-webkit-animation: fadein .8s;
|
||||||
|
animation: fadein .8s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tabpanel"].is-hidden {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="reset"] {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #5faabd;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 34px;
|
||||||
|
margin: 5px;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"], select {
|
||||||
|
border: 1px solid #ececec;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 36px;
|
||||||
|
margin: 5px;
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line {
|
||||||
|
line-height: 30px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-create {
|
||||||
|
background-color: #22863a1a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #22863a;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box-line-delete {
|
||||||
|
background-color: #bf404a17;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #bf404a;
|
||||||
|
padding: 3px;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.congrats {
|
||||||
|
color: #4d9221;
|
||||||
|
text-align: center;
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
width: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-panel {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fraction {
|
||||||
|
background: #e8e8e8;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #555;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 4px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item {
|
||||||
|
border-bottom: 1px solid #ececec;
|
||||||
|
color: #6e7071;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-item:hover {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-header {
|
||||||
|
color: #747578;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visuallyhidden {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadein {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
form {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 0 50px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tab"] {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<img src="https://raw.githubusercontent.com/cloudskiff/driftctl-docs/main/static/img/driftctl_light.svg"
|
||||||
|
width="100px" height="81px" alt="driftctl logo"/>
|
||||||
|
<div>
|
||||||
|
<h1>Scan Report</h1>
|
||||||
|
<h2>Jun 10, 2021</h2>
|
||||||
|
<p>Scan Duration: 1m12s</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<div class="card">
|
||||||
|
<span>Total Resources:</span>
|
||||||
|
<span class="strong">1</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Coverage:</span>
|
||||||
|
<span class="strong">100%</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Managed:</span>
|
||||||
|
<span class="strong">100%</span>
|
||||||
|
<span class="fraction">1/1</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Unmanaged:</span>
|
||||||
|
<span class="strong">0%</span>
|
||||||
|
<span class="fraction">0/1</span>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<span>Missing:</span>
|
||||||
|
<span class="strong">0%</span>
|
||||||
|
<span class="fraction">0/1</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<main>
|
||||||
|
|
||||||
|
<h1 class="congrats">Congrats! Your infrastructure is in sync</h1>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const form = document.querySelector("form");
|
||||||
|
|
||||||
|
form.addEventListener("submit", (event) => event.preventDefault());
|
||||||
|
|
||||||
|
const resources = document.querySelectorAll("[data-kind^='resource-']");
|
||||||
|
const searchInput = document.querySelector('[type="search"]');
|
||||||
|
const selectBox = document.querySelector("select");
|
||||||
|
const resetButton = document.querySelector('[type="reset"]');
|
||||||
|
|
||||||
|
searchInput.addEventListener("input", filterResources);
|
||||||
|
selectBox.addEventListener("input", filterResources);
|
||||||
|
resetButton.addEventListener("click", resetResources);
|
||||||
|
|
||||||
|
function refreshPanel(count, el) {
|
||||||
|
const panel = document.getElementById(
|
||||||
|
el.parentElement.getAttribute("aria-controls")
|
||||||
|
);
|
||||||
|
if (!panel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
panel.firstElementChild.classList.add("is-hidden");
|
||||||
|
panel.children[1].classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
panel.firstElementChild.classList.remove("is-hidden");
|
||||||
|
panel.children[1].classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCounters() {
|
||||||
|
const map = {
|
||||||
|
"[data-kind='resource-unmanaged']": "[data-count='resource-unmanaged']",
|
||||||
|
"[data-kind='resource-changed']": "[data-count='resource-changed']",
|
||||||
|
"[data-kind='resource-deleted']": "[data-count='resource-deleted']",
|
||||||
|
"[data-kind='resource-alerts']": "[data-count='resource-alerts']",
|
||||||
|
};
|
||||||
|
for (const key in map) {
|
||||||
|
const countEl = document.querySelector(map[key]);
|
||||||
|
if (countEl) {
|
||||||
|
const count = Array.from(document.querySelectorAll(key)).filter(
|
||||||
|
(el) => !el.classList.contains("is-hidden")
|
||||||
|
).length;
|
||||||
|
countEl.textContent = count;
|
||||||
|
refreshPanel(count, countEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceIdContains(res, id) {
|
||||||
|
if (id === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-id']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText.toLowerCase().includes(id.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
function resourceTypeEquals(res, type) {
|
||||||
|
if (type === "") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const el = res.querySelector("[data-type='resource-type']");
|
||||||
|
if (!el) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return el.innerText === type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterResources() {
|
||||||
|
const id = searchInput.value;
|
||||||
|
const type = selectBox.value;
|
||||||
|
for (const res of resources) {
|
||||||
|
const matchId = resourceIdContains(res, id);
|
||||||
|
const matchType = resourceTypeEquals(res, type);
|
||||||
|
if (matchId && matchType) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
} else {
|
||||||
|
res.classList.add("is-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetResources() {
|
||||||
|
for (const res of resources) {
|
||||||
|
res.classList.remove("is-hidden");
|
||||||
|
}
|
||||||
|
refreshCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetResources()
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
const tablist = document.querySelector('[role="tablist"]')
|
||||||
|
const tabs = document.querySelectorAll('[role="tab"]')
|
||||||
|
const panels = document.querySelectorAll('[role="tabpanel"]')
|
||||||
|
const keys = {left: 37, right: 39}
|
||||||
|
const direction = {37: -1, 39: 1}
|
||||||
|
|
||||||
|
for (let i = 0; i < tabs.length; ++i) {
|
||||||
|
addListeners(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addListeners(index) {
|
||||||
|
tabs[index].addEventListener('click', clickEventListener)
|
||||||
|
tabs[index].addEventListener('keyup', keyupEventListener)
|
||||||
|
tabs[index].index = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickEventListener(event) {
|
||||||
|
let tab
|
||||||
|
if (event.target.getAttribute("role") === "tab") {
|
||||||
|
tab = event.target
|
||||||
|
} else {
|
||||||
|
tab = event.target.closest("button")
|
||||||
|
}
|
||||||
|
const selected = tab.getAttribute("aria-selected")
|
||||||
|
if (selected === "false") {
|
||||||
|
activateTab(tab, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyupEventListener(event) {
|
||||||
|
const key = event.keyCode
|
||||||
|
switch (key) {
|
||||||
|
case keys.left:
|
||||||
|
case keys.right:
|
||||||
|
switchTabOnArrowPress(event)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTabOnArrowPress(event) {
|
||||||
|
const pressed = event.keyCode
|
||||||
|
for (let x = 0; x < tabs.length; x++) {
|
||||||
|
tabs[x].addEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
if (direction[pressed]) {
|
||||||
|
const target = event.target
|
||||||
|
if (target.index !== undefined) {
|
||||||
|
if (tabs[target.index + direction[pressed]]) {
|
||||||
|
tabs[target.index + direction[pressed]].focus()
|
||||||
|
} else if (pressed === keys.left) {
|
||||||
|
tabs[tabs.length - 1].focus()
|
||||||
|
} else if (pressed === keys.right) {
|
||||||
|
tabs[0].focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTab(tab, setFocus) {
|
||||||
|
setFocus = setFocus || true
|
||||||
|
deactivateTabs()
|
||||||
|
tab.removeAttribute('tabindex')
|
||||||
|
tab.setAttribute('aria-selected', 'true')
|
||||||
|
const controls = tab.getAttribute('aria-controls')
|
||||||
|
document.getElementById(controls).classList.remove('is-hidden')
|
||||||
|
if (setFocus) {
|
||||||
|
tab.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateTabs() {
|
||||||
|
for (let t = 0; t < tabs.length; t++) {
|
||||||
|
tabs[t].setAttribute('tabindex', '-1')
|
||||||
|
tabs[t].setAttribute('aria-selected', 'false')
|
||||||
|
tabs[t].removeEventListener('focus', focusEventHandler)
|
||||||
|
}
|
||||||
|
for (let p = 0; p < panels.length; p++) {
|
||||||
|
panels[p].classList.add('is-hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusEventHandler(event) {
|
||||||
|
const target = event.target
|
||||||
|
if (target === document.activeElement) {
|
||||||
|
activateTab(target, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -177,7 +177,7 @@ func Test_parseOutputFlag(t *testing.T) {
|
||||||
out: "",
|
out: "",
|
||||||
},
|
},
|
||||||
want: nil,
|
want: nil,
|
||||||
err: fmt.Errorf("Unable to parse output flag '': \nAccepted formats are: console://,json://PATH/TO/FILE.json"),
|
err: fmt.Errorf("Unable to parse output flag '': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test invalid",
|
name: "test invalid",
|
||||||
|
@ -185,7 +185,7 @@ func Test_parseOutputFlag(t *testing.T) {
|
||||||
out: "sdgjsdgjsdg",
|
out: "sdgjsdgjsdg",
|
||||||
},
|
},
|
||||||
want: nil,
|
want: nil,
|
||||||
err: fmt.Errorf("Unable to parse output flag 'sdgjsdgjsdg': \nAccepted formats are: console://,json://PATH/TO/FILE.json"),
|
err: fmt.Errorf("Unable to parse output flag 'sdgjsdgjsdg': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test invalid",
|
name: "test invalid",
|
||||||
|
@ -193,7 +193,7 @@ func Test_parseOutputFlag(t *testing.T) {
|
||||||
out: "://",
|
out: "://",
|
||||||
},
|
},
|
||||||
want: nil,
|
want: nil,
|
||||||
err: fmt.Errorf("Unable to parse output flag '://': \nAccepted formats are: console://,json://PATH/TO/FILE.json"),
|
err: fmt.Errorf("Unable to parse output flag '://': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test unsupported",
|
name: "test unsupported",
|
||||||
|
@ -201,7 +201,7 @@ func Test_parseOutputFlag(t *testing.T) {
|
||||||
out: "foobar://",
|
out: "foobar://",
|
||||||
},
|
},
|
||||||
want: nil,
|
want: nil,
|
||||||
err: fmt.Errorf("Unsupported output 'foobar': \nValid formats are: console://,json://PATH/TO/FILE.json"),
|
err: fmt.Errorf("Unsupported output 'foobar': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test empty json",
|
name: "test empty json",
|
||||||
|
|
|
@ -128,12 +128,13 @@ func (d DriftCTL) Run() (*analyser.Analysis, error) {
|
||||||
driftIgnore := filter.NewDriftIgnore(d.opts.DriftignorePath)
|
driftIgnore := filter.NewDriftIgnore(d.opts.DriftignorePath)
|
||||||
|
|
||||||
analysis, err := d.analyzer.Analyze(remoteResources, resourcesFromState, driftIgnore)
|
analysis, err := d.analyzer.Analyze(remoteResources, resourcesFromState, driftIgnore)
|
||||||
analysis.Duration = time.Since(start)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
analysis.Duration = time.Since(start)
|
||||||
|
analysis.Date = time.Now()
|
||||||
|
|
||||||
return &analysis, nil
|
return &analysis, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue