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) }