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 +![html-report](screenshots/html-report-1.png) + ## 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: + +![html-report-2](screenshots/html-report-2.png) + ## 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 + + + + +
+ + +
+
+

NoClickOps Report

+
+
+ +
+ + +
+ + +
+
+
+ Services +
+ + + + + + + + + + + + +
ServiceFoundManagedIgnoredUnmanaged% Unmanaged
+
+ +
+
+ Unmanaged by service +
+
+ +
+
+
+ + + + +
+ + + 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