Skip to content

Commit

Permalink
Added multiple output options (#233)
Browse files Browse the repository at this point in the history
* Added multiple output options
* Renamed renderer attribute, fixed openai enrichment detection
* Added test for number of renderers
* Fixes for PR

---------

Signed-off-by: AndriyDmytrenko <[email protected]>
  • Loading branch information
AndriyDmytrenko authored Nov 15, 2024
1 parent acc6bd2 commit 4b0fc36
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 41 deletions.
16 changes: 15 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -30,7 +44,7 @@
"./_TESTDATA_/sbom/jena-kafka-1.4.0-SNAPSHOT-bom.json"
]
},
{
{
"name": "Debug File (osv)",
"type": "go",
"request": "launch",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"log"
"os"
"slices"
"strings"

"github.com/devops-kung-fu/common/util"
"github.com/gookit/color"
Expand All @@ -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)
Expand All @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions lib/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down
6 changes: 3 additions & 3 deletions lib/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
}
2 changes: 1 addition & 1 deletion lib/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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$`

Expand Down
6 changes: 3 additions & 3 deletions renderers/ai/ai.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion renderers/html/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 9 additions & 2 deletions renderers/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
29 changes: 16 additions & 13 deletions renderers/rendererfactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package renderers

import (
"fmt"
"strings"

"github.com/devops-kung-fu/bomber/models"
"github.com/devops-kung-fu/bomber/renderers/ai"
Expand All @@ -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
}
27 changes: 17 additions & 10 deletions renderers/rendererfactory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 4b0fc36

Please sign in to comment.