From bdbae6c276e3cca1d067dc2c4e48727457356f58 Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 11:59:00 +1200
Subject: [PATCH 1/8] add md to flag description
---
cmd/root.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/root.go b/cmd/root.go
index 6550c76..0a8eb2d 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -59,5 +59,5 @@ func Execute() {
func init() {
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "displays debug level log messages.")
- rootCmd.PersistentFlags().StringVar(&output, "output", "stdout", "how bomber should output findings (json, html, ai, stdout)")
+ rootCmd.PersistentFlags().StringVar(&output, "output", "stdout", "how bomber should output findings (json, html, ai, md, stdout)")
}
From 642c79eb9dac0e568fb0b3247ab43ce005b6197f Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 11:59:19 +1200
Subject: [PATCH 2/8] add md to rendererfactory
---
renderers/rendererfactory.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/renderers/rendererfactory.go b/renderers/rendererfactory.go
index b93498d..9fd0711 100644
--- a/renderers/rendererfactory.go
+++ b/renderers/rendererfactory.go
@@ -9,6 +9,7 @@ import (
"github.com/devops-kung-fu/bomber/renderers/html"
"github.com/devops-kung-fu/bomber/renderers/json"
"github.com/devops-kung-fu/bomber/renderers/stdout"
+ "github.com/devops-kung-fu/bomber/renderers/md"
)
// NewRenderer will return a Renderer interface for the requested output
@@ -22,6 +23,8 @@ func NewRenderer(output string) (renderer models.Renderer, err error) {
renderer = html.Renderer{}
case "ai":
renderer = ai.Renderer{}
+ case "md":
+ renderer = md.Renderer{}
default:
err = fmt.Errorf("%s is not a valid output type", output)
}
From b627793bf7ec5a318458567674ed7facc6498383 Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 11:59:26 +1200
Subject: [PATCH 3/8] add md renderer
---
renderers/md/md.go | 196 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 196 insertions(+)
create mode 100644 renderers/md/md.go
diff --git a/renderers/md/md.go b/renderers/md/md.go
new file mode 100644
index 0000000..c382eaa
--- /dev/null
+++ b/renderers/md/md.go
@@ -0,0 +1,196 @@
+package md
+
+import (
+ "fmt"
+ "log"
+ "math"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "text/template"
+ "time"
+
+ "github.com/devops-kung-fu/common/util"
+ "github.com/spf13/afero"
+
+ "github.com/devops-kung-fu/bomber/models"
+)
+
+// Renderer contains methods to render results to a Markdown file
+type Renderer struct{}
+
+// Render renders results to a Markdown file
+func (Renderer) Render(results models.Results) error {
+ var afs *afero.Afero
+
+ if results.Meta.Provider == "test" {
+ afs = &afero.Afero{Fs: afero.NewMemMapFs()}
+ } else {
+ afs = &afero.Afero{Fs: afero.NewOsFs()}
+ }
+
+ filename := generateFilename()
+ util.PrintInfo("Writing filename:", filename)
+
+ err := writeTemplate(afs, filename, results)
+
+ return err
+}
+
+// generateFilename generates a unique filename based on the current timestamp
+// in the format "2006-01-02 15:04:05" and replaces certain characters to
+// create a valid filename. The resulting filename is a combination of the
+// timestamp and a fixed suffix.
+func generateFilename() string {
+ t := time.Now()
+ r := strings.NewReplacer("-", "", " ", "-", ":", "-")
+ return filepath.Join(".", fmt.Sprintf("%s-bomber-results.md", r.Replace(t.Format("2006-01-02 15:04:05"))))
+}
+
+// writeTemplate writes the results to a file with the specified filename,
+// using the given Afero filesystem interface. It creates the file, processes
+// percentiles in the results, converts Markdown to HTML, and writes the
+// templated results to the file. It also sets file permissions to 0777.
+func writeTemplate(afs *afero.Afero, filename string, results models.Results) error {
+ processPercentiles(results)
+
+ file, err := afs.Create(filename)
+ if err != nil {
+ log.Println(err)
+ return err
+ }
+
+ // markdownToHTML(results)
+
+ template := genTemplate("output")
+ err = template.ExecuteTemplate(file, "output", results)
+ if err != nil {
+ log.Println(err)
+ return err
+ }
+
+ err = afs.Fs.Chmod(filename, 0777)
+
+ return err
+}
+
+// processPercentiles calculates and updates the percentile values for
+// vulnerabilities in the given results. It converts the percentile from
+// a decimal to a percentage and updates the results in place.
+func processPercentiles(results models.Results) {
+ for i, p := range results.Packages {
+ for vi, v := range p.Vulnerabilities {
+ per, err := strconv.ParseFloat(v.Epss.Percentile, 64)
+ if err != nil {
+ log.Println(err)
+ } else {
+ percentage := math.Round(per * 100)
+ if percentage > 0 {
+ results.Packages[i].Vulnerabilities[vi].Epss.Percentile = fmt.Sprintf("%d%%", uint64(percentage))
+ } else {
+ results.Packages[i].Vulnerabilities[vi].Epss.Percentile = "N/A"
+ }
+ }
+ }
+ }
+}
+
+// // markdownToHTML converts the Markdown descriptions of vulnerabilities in
+// // the given results to HTML. It uses the Blackfriday library to perform the
+// // conversion and sanitizes the HTML using Bluemonday.
+// func markdownToHTML(results models.Results) {
+// for i := range results.Packages {
+// for ii := range results.Packages[i].Vulnerabilities {
+// md := []byte(results.Packages[i].Vulnerabilities[ii].Description)
+// html := markdown.ToHTML(md, nil, nil)
+// results.Packages[i].Vulnerabilities[ii].Description = string(bluemonday.UGCPolicy().SanitizeBytes(html))
+// }
+// }
+// }
+//
+func genTemplate(output string) (t *template.Template) {
+
+ content := `
+![IMG](https://raw.githubusercontent.com/devops-kung-fu/bomber/main/img/bomber-readme-logo.png)
+
+The following results were detected by `+ "`{{.Meta.Generator}} {{.Meta.Version}}`" + ` on {{.Meta.Date}} using the {{.Meta.Provider}} provider.
+{{ if ne (len .Packages) 0 }}
+
+Vulnerabilities displayed may differ from provider to provider. This list may not contain all possible vulnerabilities. Please try the other providers that ` + "`bomber`" + ` supports (osv, ossindex, snyk). There is no guarantee that the next time you scan for vulnerabilities that there won't be more, or less of them. Threats are continuous.
+
+EPSS Percentage indicates the % chance that the vulnerability will be exploited. This value will assist in prioritizing remediation. For more information on EPSS, refer to [https://www.first.org/epss/](https://www.first.org/epss/)
+{{ else }}
+No vulnerabilities found!
+{{ end }}
+
+{{ if ne (len .Files) 0 }}
+## Scanned Files
+{{ range .Files }}
+**{{ .Name }}** (sha256:{{ .SHA256 }})
+{{ end }}
+{{end}}
+
+{{ if ne (len .Licenses) 0 }}
+## Licenses
+
+The following licenses were found by ` + "`bomber`" + `:
+
+{{ range $license := .Licenses }}
+- {{ $license }}
+{{ end }}
+{{ else }}
+**No license information detected.**
+{{ end }}
+
+{{ if ne (len .Packages) 0 }}
+## Vulnerability Summary
+
+{{ if ne (len .Meta.SeverityFilter) 0 }}
+Only showing vulnerabilities with a severity of ***{{ .Meta.SeverityFilter }}*** or higher.
+
+{{ end }}
+| Severity | Count |
+| --- | --- |{{ if gt .Summary.Critical 0 }}
+| Critical | {{ .Summary.Critical }} |{{ end }}{{ if gt .Summary.High 0 }}
+| High | {{ .Summary.High }} |{{ end }}{{ if gt .Summary.Moderate 0 }}
+| Moderate | {{ .Summary.Moderate }} |{{ end }}{{ if gt .Summary.Low 0 }}
+| Low | {{ .Summary.Low }} |{{ end }}{{ if gt .Summary.Unspecified 0 }}
+| Unspecified | {{ .Summary.Unspecified }} |{{ end }}
+
+## Vulnerability Details
+
+{{ range .Packages }}
+### {{ .Purl }}
+
+{{ .Description }}
+
+#### Vulnerabilities
+
+{{ range .Vulnerabilities }}
+{{ if .Title }}
+##### {{ .Title }}
+
+{{ end }}
+Severity: **{{ .Severity }}**
+
+{{ if ne (len .Epss.Percentile) 0 }}
+EPSS: {{ .Epss.Percentile }}
+
+{{ end }}
+
+[Reference Documentation]({{ .Reference }})
+
+{{ .Description }}
+
+{{ end }}
+
+{{ end }}
+{{ end }}
+
+
+
+Powered by the [DevOps Kung Fu Mafia](https://github.com/devops-kung-fu)
+`
+ return template.Must(template.New(output).Parse(content))
+}
+
From fcd00f8bc2fc4bd345da471a25a5ff923fae480a Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 12:09:09 +1200
Subject: [PATCH 4/8] formatting changes for better readability
---
renderers/md/md.go | 26 ++++----------------------
1 file changed, 4 insertions(+), 22 deletions(-)
diff --git a/renderers/md/md.go b/renderers/md/md.go
index c382eaa..3768d70 100644
--- a/renderers/md/md.go
+++ b/renderers/md/md.go
@@ -60,8 +60,6 @@ func writeTemplate(afs *afero.Afero, filename string, results models.Results) er
return err
}
- // markdownToHTML(results)
-
template := genTemplate("output")
err = template.ExecuteTemplate(file, "output", results)
if err != nil {
@@ -95,19 +93,6 @@ func processPercentiles(results models.Results) {
}
}
-// // markdownToHTML converts the Markdown descriptions of vulnerabilities in
-// // the given results to HTML. It uses the Blackfriday library to perform the
-// // conversion and sanitizes the HTML using Bluemonday.
-// func markdownToHTML(results models.Results) {
-// for i := range results.Packages {
-// for ii := range results.Packages[i].Vulnerabilities {
-// md := []byte(results.Packages[i].Vulnerabilities[ii].Description)
-// html := markdown.ToHTML(md, nil, nil)
-// results.Packages[i].Vulnerabilities[ii].Description = string(bluemonday.UGCPolicy().SanitizeBytes(html))
-// }
-// }
-// }
-//
func genTemplate(output string) (t *template.Template) {
content := `
@@ -134,10 +119,8 @@ No vulnerabilities found!
## Licenses
The following licenses were found by ` + "`bomber`" + `:
-
{{ range $license := .Licenses }}
-- {{ $license }}
-{{ end }}
+- {{ $license }}{{ end }}
{{ else }}
**No license information detected.**
{{ end }}
@@ -182,15 +165,14 @@ EPSS: {{ .Epss.Percentile }}
{{ .Description }}
+
+
{{ end }}
{{ end }}
{{ end }}
-
-
-Powered by the [DevOps Kung Fu Mafia](https://github.com/devops-kung-fu)
+Powered by the [DevOps Kung Fu Mafia](https://github.com/devops-kung-fu)
`
return template.Must(template.New(output).Parse(content))
}
-
From 7f400110e7e6ef60c4e7ace089edd8127a727d60 Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 13:52:27 +1200
Subject: [PATCH 5/8] more formatting tweaks
---
renderers/md/md.go | 24 ++++++------------------
1 file changed, 6 insertions(+), 18 deletions(-)
diff --git a/renderers/md/md.go b/renderers/md/md.go
index 3768d70..d504888 100644
--- a/renderers/md/md.go
+++ b/renderers/md/md.go
@@ -110,11 +110,9 @@ No vulnerabilities found!
{{ if ne (len .Files) 0 }}
## Scanned Files
-{{ range .Files }}
-**{{ .Name }}** (sha256:{{ .SHA256 }})
-{{ end }}
-{{end}}
+{{ range .Files }}**{{ .Name }}** (sha256:{{ .SHA256 }}){{ end }}
+{{end}}
{{ if ne (len .Licenses) 0 }}
## Licenses
@@ -144,23 +142,13 @@ Only showing vulnerabilities with a severity of ***{{ .Meta.SeverityFilter }}***
{{ range .Packages }}
### {{ .Purl }}
-
-{{ .Description }}
-
+{{if .Description }}{{ .Description }}{{ end }}
#### Vulnerabilities
{{ range .Vulnerabilities }}
-{{ if .Title }}
-##### {{ .Title }}
-
-{{ end }}
-Severity: **{{ .Severity }}**
-
-{{ if ne (len .Epss.Percentile) 0 }}
-EPSS: {{ .Epss.Percentile }}
-
-{{ end }}
-
+{{ if .Title }}Title: **{{ .Title }}**
{{ end }}
+Severity: **{{ .Severity }}**
+{{ if ne (len .Epss.Percentile) 0 }} EPSS: {{ .Epss.Percentile }}
{{ end }}
[Reference Documentation]({{ .Reference }})
{{ .Description }}
From db05c1a08ea7560020649ac7541c3a3e25b248df Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 13:52:37 +1200
Subject: [PATCH 6/8] add tests
---
renderers/md/md_test.go | 73 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 73 insertions(+)
create mode 100644 renderers/md/md_test.go
diff --git a/renderers/md/md_test.go b/renderers/md/md_test.go
new file mode 100644
index 0000000..c7a72ec
--- /dev/null
+++ b/renderers/md/md_test.go
@@ -0,0 +1,73 @@
+package md
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/devops-kung-fu/common/util"
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/devops-kung-fu/bomber/models"
+)
+
+func Test_writeTemplate(t *testing.T) {
+ afs := &afero.Afero{Fs: afero.NewMemMapFs()}
+
+ err := writeTemplate(afs, "test.md", models.NewResults([]models.Package{}, models.Summary{}, []models.ScannedFile{}, []string{"GPL"}, "0.0.0", "test", "low"))
+ assert.NoError(t, err)
+
+ b, err := afs.ReadFile("test.md")
+ assert.NotNil(t, b)
+ assert.NoError(t, err)
+
+ info, err := afs.Stat("test.md")
+ assert.NoError(t, err)
+ assert.Equal(t, os.FileMode(0777), info.Mode().Perm())
+}
+
+func Test_genTemplate(t *testing.T) {
+ template := genTemplate("test")
+
+ assert.NotNil(t, template)
+ assert.Len(t, template.Root.Nodes, 17)
+}
+
+func TestRenderer_Render(t *testing.T) {
+ output := util.CaptureOutput(func() {
+ renderer := Renderer{}
+ err := renderer.Render(models.NewResults([]models.Package{}, models.Summary{}, []models.ScannedFile{}, []string{"GPL"}, "0.0.0", "test", ""))
+ if err != nil {
+ fmt.Println(err)
+ }
+ })
+ assert.NotNil(t, output)
+}
+
+func Test_processPercentiles(t *testing.T) {
+ // Create a sample Results struct for testing
+ results := models.Results{
+ Packages: []models.Package{
+ {
+ Vulnerabilities: []models.Vulnerability{
+ {
+ Epss: models.EpssScore{Percentile: "0.75"},
+ },
+ {
+ Epss: models.EpssScore{Percentile: "invalid"}, // Simulate an invalid percentile
+ },
+ {
+ Epss: models.EpssScore{Percentile: "0"}, // Simulate a zero percentile
+ },
+ },
+ },
+ },
+ }
+
+ processPercentiles(results)
+
+ assert.Equal(t, "75%", results.Packages[0].Vulnerabilities[0].Epss.Percentile, "Expected 75% percentile")
+ assert.Equal(t, "invalid", results.Packages[0].Vulnerabilities[1].Epss.Percentile, "Expected invalid for invalid percentile")
+ assert.Equal(t, "N/A", results.Packages[0].Vulnerabilities[2].Epss.Percentile, "Expected N/A for zero percentile")
+}
From 53109e4a2cfc32e6212c3fb168a32e80498a367c Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 14:08:51 +1200
Subject: [PATCH 7/8] add Markdown section to readme with example
---
README.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/README.md b/README.md
index e15925d..bbab544 100644
--- a/README.md
+++ b/README.md
@@ -148,6 +148,16 @@ Example command:
bomber scan bad-bom.json --output=json > filename.json
```
+### Markdown Output
+
+`bomber` also supports output in Markdown format. This is very similar to the HTML output, but offloads styling to the Markdown renderer, such as GitHub. The output is saved to a file in the format "YYYY-MM-DD-HH-MM-SS-bomber-results.md".
+
+Example command:
+
+```bash
+bomber scan bad-bom.json --output=md
+```
+
## Ignoring Vulnerabilities
If needed, you can use the ```--ignore-file``` flag to load a list of CVEs to ignore in the vulnerability output. This list needs to be in a specific format where each CVE to ignore is entered on a separate line similar to the following:
From fe5832928edd85ccec474c536e7ed4a7a01bf1bb Mon Sep 17 00:00:00 2001
From: Scott McKendry <39483124+scottmckendry@users.noreply.github.com>
Date: Fri, 7 Jun 2024 14:18:33 +1200
Subject: [PATCH 8/8] add renderer_factory test for md format
---
renderers/rendererfactory_test.go | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/renderers/rendererfactory_test.go b/renderers/rendererfactory_test.go
index 6b27450..01407b7 100644
--- a/renderers/rendererfactory_test.go
+++ b/renderers/rendererfactory_test.go
@@ -8,6 +8,7 @@ import (
"github.com/devops-kung-fu/bomber/renderers/ai"
"github.com/devops-kung-fu/bomber/renderers/html"
"github.com/devops-kung-fu/bomber/renderers/json"
+ "github.com/devops-kung-fu/bomber/renderers/md"
"github.com/devops-kung-fu/bomber/renderers/stdout"
)
@@ -28,6 +29,10 @@ func TestNewRenderer(t *testing.T) {
assert.NoError(t, err)
assert.IsType(t, ai.Renderer{}, renderer)
+ renderer, err = NewRenderer("md")
+ assert.NoError(t, err)
+ assert.IsType(t, md.Renderer{}, renderer)
+
_, err = NewRenderer("test")
assert.Error(t, err)
}