diff --git a/.gitignore b/.gitignore
index c05e3a8..fa50b44 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
noclickops
.envrc
*tfstate*
+*.html
diff --git a/README.md b/README.md
index 5f20dc6..6e3297a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,11 @@
# NoClickOps
+
+
## What it is
-Noclickops intents to be a simple tool to see which and how many of your AWS resources are captured
-in terraform.
+Noclickops is a tool that allows you to see which and how many of your AWS resources are **not**
+captured in terraform.
## What it isn't
@@ -74,6 +76,7 @@ noclickops --s3-bucket example-s3-statefile-bucket --s3-bucket-region eu-west-2
| `--ignore-tags` | `-i` | Can be specified multiple times; each value is a `tagKey=value1,value2` pair - resources carrying any of these tags are excluded from results |
| `--delete-downloaded-state-files` | `-d` | Delete any statefiles downloaded from S3 when done |
| `--force-download` | `-f` | Re-download all files from S3 even if they already exist locally |
+| `--generate-html-report` | `-g` | Generate an HTML report in the current directory |
### Using a config file
@@ -127,6 +130,14 @@ noclickops --statefile ./example.tfstate --regions eu-west-1 --ignore-tags envir
noclickops --statefile ./example.tfstate --regions eu-west-1 --ignore-tags environment=sandbox,staging
```
+## Generating an HTML report
+
+You can provide the `-g` or `--generate-html-report` flag to also generate an html report. This
+allows you to see per-service results and within each service to drill down per resource type and
+per region. For example you can see only security-groups in eu-west-2:
+
+
+
## Output format
`noclickops` prints a JSON object with two top-level keys: `results` (per-service breakdown) and `summary` (account-wide totals).
diff --git a/assets/report_template.html b/assets/report_template.html
new file mode 100644
index 0000000..8913589
--- /dev/null
+++ b/assets/report_template.html
@@ -0,0 +1,840 @@
+
+
+
+
+
+ NoClickOps Report
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Service |
+ Found |
+ Managed |
+ Ignored |
+ Unmanaged |
+ % Unmanaged |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Resource Type | Count |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/config.go b/config.go
index 7700284..c2de7ca 100644
--- a/config.go
+++ b/config.go
@@ -21,6 +21,7 @@ const (
ForceDownload
Regions
IgnoreTags
+ GenerateHTMLReport
)
var configValues = map[ConfigValues]string{
@@ -31,12 +32,14 @@ var configValues = map[ConfigValues]string{
ForceDownload: "force-download",
Regions: "regions",
IgnoreTags: "ignore-tags",
+ GenerateHTMLReport: "generate-html-report",
}
type NoclickopsConfig struct {
stateFile string
deleteDownloadedStatefiles bool
forceDownload bool
+ generateHTMLReport bool
regions string
s3Bucket string
s3BucketRegion string
@@ -52,6 +55,7 @@ func NewConfig(v *viper.Viper) NoclickopsConfig {
config.s3BucketRegion = v.GetString(configValues[S3BucketRegion])
config.deleteDownloadedStatefiles = v.GetBool(configValues[DeleteDownloadedStateFiles])
config.forceDownload = v.GetBool(configValues[ForceDownload])
+ config.generateHTMLReport = v.GetBool(configValues[GenerateHTMLReport])
config.regionsList = v.GetStringSlice(configValues[Regions])
switch t := v.Get(configValues[Regions]).(type) {
@@ -168,6 +172,7 @@ func loadConfig() NoclickopsConfig {
pflag.StringArrayP(configValues[IgnoreTags], "i", []string{}, "Can be used multiple times to provide list of 'tagKey=value1,value2' tags to ignore")
pflag.BoolP(configValues[DeleteDownloadedStateFiles], "d", false, "If specified, any downloaded statefiles will be deleted at the end")
pflag.BoolP(configValues[ForceDownload], "f", false, "If specified, it will download all the files from the bucket even they overwrite existing ones")
+ pflag.BoolP(configValues[GenerateHTMLReport], "g", false, "If specified, it will generate an html report in the current directory")
pflag.Parse()
// use this to pass control to viper
diff --git a/main.go b/main.go
index 23f0889..105d5af 100644
--- a/main.go
+++ b/main.go
@@ -47,4 +47,8 @@ func main() {
summary := calculateSummary(unmanagedResourceIds)
json, _ := json.Marshal(common.Output{Results: unmanagedResourceIds, Summary: summary})
fmt.Println(string(json))
+
+ if config.generateHTMLReport {
+ generateReport(string(json))
+ }
}
diff --git a/report.go b/report.go
new file mode 100644
index 0000000..332893c
--- /dev/null
+++ b/report.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ _ "embed"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+)
+
+//go:embed assets/report_template.html
+var reportTemplate string
+
+const reportDataPlaceholder string = "{{ REPORT_DATA }}"
+const reportFilenamePrefix string = "report"
+const reportFilenameExtension string = "html"
+
+func generateReport(jsonData string) {
+ html := strings.ReplaceAll(reportTemplate, reportDataPlaceholder, jsonData)
+ timestamp := time.Now().UTC().Format("20060102T150405Z")
+ filename := fmt.Sprintf("%v_%v.%v", reportFilenamePrefix, timestamp, reportFilenameExtension)
+
+ println("Saving html report to", filename)
+ f, _ := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0644)
+ defer f.Close()
+ f.WriteString(html)
+}
diff --git a/screenshots/html-report-1.png b/screenshots/html-report-1.png
new file mode 100644
index 0000000..767d931
Binary files /dev/null and b/screenshots/html-report-1.png differ
diff --git a/screenshots/html-report-2.png b/screenshots/html-report-2.png
new file mode 100644
index 0000000..32c331c
Binary files /dev/null and b/screenshots/html-report-2.png differ