diff --git a/.vscode/launch.json b/.vscode/launch.json index 3fe4f45..cb4832c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,20 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Debug File (no provider - railsgoat - Multiple Outputs)", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "args": [ + "--debug=true", + "--output=json,stdout,html", + "--severity=high", + "scan", + "./_TESTDATA_/sbom/railsgoat.cyclonedx.json" + ] + }, { "name": "Debug File (github provider)", "type": "go", @@ -30,7 +44,7 @@ "./_TESTDATA_/sbom/jena-kafka-1.4.0-SNAPSHOT-bom.json" ] }, - { + { "name": "Debug File (osv)", "type": "go", "request": "launch", diff --git a/README.md b/README.md index cf1655a..0b4db96 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ You'll see a similar result to what a Single SBOM scan will provide. ## Output Formats -`bomber` outputs data into three useful formats. By default, output is rendered to the command line. For enhanced reporting, you can output to HTML using the `--output=html` flag. To output to JSON, utilize the `--output=json` flag. +`bomber` outputs data into three useful formats. By default, output is rendered to the command line. For enhanced reporting, you can output to HTML using the `--output=html` flag. To output to JSON, utilize the `--output=json` flag. Use comma separated output specification to get output in multiple formats `--output=html,stdout,json`. ### HTML Output diff --git a/cmd/scan.go b/cmd/scan.go index 23d4128..895d0be 100644 --- a/cmd/scan.go +++ b/cmd/scan.go @@ -4,6 +4,7 @@ import ( "log" "os" "slices" + "strings" "github.com/devops-kung-fu/common/util" "github.com/gookit/color" @@ -22,7 +23,7 @@ var ( Use: "scan", Short: "Scans a provided SBOM file or folder containing SBOMs for vulnerabilities.", PreRun: func(cmd *cobra.Command, args []string) { - if output == "ai" && !slices.Contains(scanner.Enrichment, "openai") { + if slices.Contains(strings.Split(output, ","), "ai") && !slices.Contains(scanner.Enrichment, "openai") { scanner.Enrichment = append(scanner.Enrichment, "openai") } r, err := renderers.NewRenderer(output) @@ -31,7 +32,7 @@ var ( _ = cmd.Help() os.Exit(1) } - scanner.Renderer = r + scanner.Renderers = r p, err := providers.NewProvider(scanner.ProviderName) if err != nil { color.Red.Printf("%v\n\n", err) diff --git a/lib/scanner.go b/lib/scanner.go index e01af22..2899c53 100644 --- a/lib/scanner.go +++ b/lib/scanner.go @@ -22,7 +22,7 @@ import ( type Scanner struct { SeveritySummary models.Summary Credentials models.Credentials - Renderer models.Renderer + Renderers []models.Renderer Provider models.Provider Enrichment []string IgnoreFile string @@ -222,9 +222,9 @@ func (s *Scanner) processResults(scanned []models.ScannedFile, licenses []string // Create results object results := models.NewResults(response, s.SeveritySummary, scanned, licenses, s.Version, s.ProviderName, s.Severity) - // Render results using the specified renderer - if s.Renderer != nil { - if err := s.Renderer.Render(results); err != nil { + // Render results using the specified renderer(s) + for _, r := range s.Renderers { + if err := r.Render(results); err != nil { log.Println(err) } } diff --git a/lib/util.go b/lib/util.go index b972a39..5bbc87a 100644 --- a/lib/util.go +++ b/lib/util.go @@ -118,7 +118,7 @@ func MarkdownToHTML(results models.Results) { 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)) - + md = []byte(results.Packages[i].Vulnerabilities[ii].Explanation) html = markdown.ToHTML(md, nil, nil) results.Packages[i].Vulnerabilities[ii].Explanation = string(bluemonday.UGCPolicy().SanitizeBytes(html)) @@ -131,8 +131,8 @@ func MarkdownToHTML(results models.Results) { // create a valid filename. The resulting filename is a combination of the // timestamp and a fixed suffix. // TODO: Need to make this generic. It's only being used for HTML Renderers -func GenerateFilename() string { +func GenerateFilename(format string) string { t := time.Now() r := strings.NewReplacer("-", "", " ", "-", ":", "-") - return filepath.Join(".", fmt.Sprintf("%s-bomber-results.html", r.Replace(t.Format("2006-01-02 15:04:05")))) + return filepath.Join(".", fmt.Sprintf("%s-bomber-results.%s", r.Replace(t.Format("2006-01-02 15:04:05")), format)) } diff --git a/lib/util_test.go b/lib/util_test.go index f844f39..e7f87a3 100644 --- a/lib/util_test.go +++ b/lib/util_test.go @@ -176,7 +176,7 @@ func Test_MarkdownToHTML(t *testing.T) { } func TestGenerateFilename(t *testing.T) { - filename := GenerateFilename() + filename := GenerateFilename("html") pattern := `^\d{8}-\d{2}-\d{2}-\d{2}-bomber-results\.html$` diff --git a/renderers/ai/ai.go b/renderers/ai/ai.go index f996c29..808ee7d 100644 --- a/renderers/ai/ai.go +++ b/renderers/ai/ai.go @@ -20,15 +20,15 @@ type Renderer struct{} func (Renderer) Render(results models.Results) error { var afs *afero.Afero - lib.MarkdownToHTML(results) + lib.MarkdownToHTML(results) - if results.Meta.Provider == "test" { + if results.Meta.Provider == "test" { afs = &afero.Afero{Fs: afero.NewMemMapFs()} } else { afs = &afero.Afero{Fs: afero.NewOsFs()} } - filename := lib.GenerateFilename() + filename := lib.GenerateFilename("html") util.PrintInfo("Writing AI Enriched HTML report:", filename) resultString, err := generateTemplateResult(templateString(), results) diff --git a/renderers/html/html.go b/renderers/html/html.go index be38fa1..3305afc 100644 --- a/renderers/html/html.go +++ b/renderers/html/html.go @@ -28,7 +28,7 @@ func (Renderer) Render(results models.Results) error { afs = &afero.Afero{Fs: afero.NewOsFs()} } - filename := lib.GenerateFilename() + filename := lib.GenerateFilename("html") util.PrintInfo("Writing HTML output:", filename) err := writeTemplate(afs, filename, results) diff --git a/renderers/json/json.go b/renderers/json/json.go index 123ff90..d703308 100644 --- a/renderers/json/json.go +++ b/renderers/json/json.go @@ -3,9 +3,12 @@ package json import ( "encoding/json" - "fmt" + "log" + "os" + "github.com/devops-kung-fu/bomber/lib" "github.com/devops-kung-fu/bomber/models" + "github.com/devops-kung-fu/common/util" ) // Renderer contains methods to render to JSON format @@ -14,6 +17,10 @@ type Renderer struct{} // Render outputs json to STDOUT func (Renderer) Render(results models.Results) error { b, _ := json.MarshalIndent(results, "", "\t") - fmt.Println(string(b)) + filename := lib.GenerateFilename("json") + util.PrintInfo("Writing JSON output:", filename) + if err := os.WriteFile(filename, b, 0666); err != nil { + log.Fatal(err) + } return nil } diff --git a/renderers/rendererfactory.go b/renderers/rendererfactory.go index 9fd0711..07fafa5 100644 --- a/renderers/rendererfactory.go +++ b/renderers/rendererfactory.go @@ -3,6 +3,7 @@ package renderers import ( "fmt" + "strings" "github.com/devops-kung-fu/bomber/models" "github.com/devops-kung-fu/bomber/renderers/ai" @@ -13,20 +14,22 @@ import ( ) // NewRenderer will return a Renderer interface for the requested output -func NewRenderer(output string) (renderer models.Renderer, err error) { - switch output { - case "stdout": - renderer = stdout.Renderer{} - case "json": - renderer = json.Renderer{} - case "html": - renderer = html.Renderer{} - case "ai": - renderer = ai.Renderer{} +func NewRenderer(output string) (renderers []models.Renderer, err error) { + for _, s := range strings.Split(output, ",") { + switch s { + case "stdout": + renderers = append(renderers, stdout.Renderer{}) + case "json": + renderers = append(renderers, json.Renderer{}) + case "html": + renderers = append(renderers, html.Renderer{}) + case "ai": + renderers = append(renderers, ai.Renderer{}) case "md": - renderer = md.Renderer{} - default: - err = fmt.Errorf("%s is not a valid output type", output) + renderers = append(renderers, md.Renderer{}) + default: + err = fmt.Errorf("%s is not a valid output type", s) + } } return } diff --git a/renderers/rendererfactory_test.go b/renderers/rendererfactory_test.go index 01407b7..41df83e 100644 --- a/renderers/rendererfactory_test.go +++ b/renderers/rendererfactory_test.go @@ -13,25 +13,32 @@ import ( ) func TestNewRenderer(t *testing.T) { - renderer, err := NewRenderer("stdout") + renderers, err := NewRenderer("stdout") assert.NoError(t, err) - assert.IsType(t, stdout.Renderer{}, renderer) + assert.IsType(t, stdout.Renderer{}, renderers[0]) - renderer, err = NewRenderer("json") + renderers, err = NewRenderer("json") assert.NoError(t, err) - assert.IsType(t, json.Renderer{}, renderer) + assert.IsType(t, json.Renderer{}, renderers[0]) - renderer, err = NewRenderer("html") + renderers, err = NewRenderer("html") assert.NoError(t, err) - assert.IsType(t, html.Renderer{}, renderer) + assert.IsType(t, html.Renderer{}, renderers[0]) - renderer, err = NewRenderer("ai") + renderers, err = NewRenderer("ai") assert.NoError(t, err) - assert.IsType(t, ai.Renderer{}, renderer) + assert.IsType(t, ai.Renderer{}, renderers[0]) - renderer, err = NewRenderer("md") + renderers, err = NewRenderer("stdout,json,html") assert.NoError(t, err) - assert.IsType(t, md.Renderer{}, renderer) + assert.IsType(t, stdout.Renderer{}, renderers[0]) + assert.IsType(t, json.Renderer{}, renderers[1]) + assert.IsType(t, html.Renderer{}, renderers[2]) + assert.Len(t, renderers, 3) + + renderers, err = NewRenderer("md") + assert.NoError(t, err) + assert.IsType(t, md.Renderer{}, renderers[0]) _, err = NewRenderer("test") assert.Error(t, err)