Skip to content

Commit 6faec08

Browse files
authored
Enrich diff markdown and SARIF outputs
1 parent 1bc7ded commit 6faec08

10 files changed

Lines changed: 387 additions & 78 deletions

File tree

.github/workflows/bomly-review.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ permissions:
77
contents: read
88
pull-requests: write
99
issues: write
10+
security-events: write
1011

1112
jobs:
1213
review:

docs/OUTPUT_FORMATS.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ bomly scan --enrich --audit --fail-on high --format sarif > bomly.sarif
7878

7979
GitHub Code Scanning, Azure DevOps, and most IDE extensions ingest SARIF directly. See [CI integration](CI_INTEGRATION.md) for upload recipes.
8080

81-
## SBOM output: `-o`
81+
## Additional output: `-o`
8282

83-
Independent of `--format`. You can write any number of SBOM artifacts alongside the reporting output:
83+
Independent of `--format`. You can write review reports and SBOM artifacts alongside the primary output:
8484

8585
```bash
8686
bomly scan --format json \
87+
-o markdown=summary.md \
88+
-o sarif=bomly.sarif \
8789
-o spdx=sbom.spdx.json \
8890
-o cyclonedx=sbom.cdx.json
8991
```
@@ -92,10 +94,12 @@ Supported targets:
9294

9395
| `-o` value | Format |
9496
| --- | --- |
97+
| `markdown` | GitHub-flavored Markdown report |
98+
| `sarif` | SARIF 2.1.0 report; requires `--audit` |
9599
| `spdx` | SPDX 2.3 JSON |
96100
| `cyclonedx` | CycloneDX 1.6 JSON |
97101

98-
See [SBOM formats](SBOM.md) for the comparison and writing rules.
102+
`spdx` and `cyclonedx` are supported by `scan`. `markdown` and `sarif` are supported by report-producing commands. See [SBOM formats](SBOM.md) for the SBOM comparison and writing rules.
99103

100104
## Combining outputs
101105

@@ -110,10 +114,12 @@ Example:
110114

111115
```bash
112116
bomly scan --enrich --audit --fail-on high \
113-
--format sarif \
117+
--format json \
118+
-o markdown=summary.md \
119+
-o sarif=bomly.sarif \
114120
-o spdx=sbom.spdx.json \
115121
-o cyclonedx=sbom.cdx.json \
116-
> bomly.sarif
122+
> bomly.json
117123
```
118124

119125
Detector and matcher work runs once. All outputs derive from the same in-memory graph.

internal/cli/diff_cmd.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,12 @@ func newDiffCmd() *cobra.Command {
8787
if err != nil {
8888
return exit.InvalidInputError("%v", err)
8989
}
90-
if err := validateMarkdownOnlyOutputs(outputSpecs); err != nil {
90+
if err := validateReportOutputs(outputSpecs); err != nil {
9191
return exit.InvalidInputError("%v", err)
9292
}
93+
if hasOutputFormat(outputSpecs, render.OutputFormatSARIF) && !current.Audit {
94+
return exit.InvalidInputError("-o sarif requires --audit")
95+
}
9396
if current.Interactive && len(outputSpecs) > 0 {
9497
return exit.InvalidInputError("--output cannot be combined with --interactive")
9598
}
@@ -159,11 +162,23 @@ func newDiffCmd() *cobra.Command {
159162
markdownRenderer := func(w io.Writer) error {
160163
return render.DiffMarkdown(w, payload)
161164
}
165+
sarifRenderer := func(w io.Writer) error {
166+
return output.WriteSARIF(w, diffResult.Findings, "bomly", cmd.Root().Version)
167+
}
162168
if len(outputSpecs) > 0 {
163169
prog.Advance("Writing additional output")
164170
for _, spec := range outputSpecs {
165-
if err := writeRenderedOutput(streams.reportWriter(), spec, markdownRenderer); err != nil {
166-
return err
171+
switch spec.Format {
172+
case render.OutputFormatMarkdown:
173+
if err := writeRenderedOutput(streams.reportWriter(), spec, markdownRenderer); err != nil {
174+
return err
175+
}
176+
case render.OutputFormatSARIF:
177+
if err := writeRenderedOutput(streams.reportWriter(), spec, sarifRenderer); err != nil {
178+
return err
179+
}
180+
default:
181+
return exit.InvalidInputError("output format %q is only supported by scan", spec.Label)
167182
}
168183
}
169184
}

internal/cli/diff_cmd_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,18 @@ func TestRenderDiffMarkdownIncludesPatchedVersionsByDefault(t *testing.T) {
5252
Added: []output.DiffPackageChange{{Package: output.PackageRef{Name: "react", Version: "18.2.0"}}},
5353
Changed: []output.DiffChangedPackage{{After: output.PackageRef{Name: "zod", Version: "3.23.0"}, Before: output.PackageRef{Name: "zod", Version: "3.22.0"}}},
5454
},
55+
Vulnerabilities: output.DiffVulnerabilityResults{
56+
Added: []output.DiffVulnerabilityChange{{
57+
Package: output.PackageRef{Name: "react", Version: "18.2.0"},
58+
Vulnerability: output.VulnerabilityRef{
59+
ID: "OSV-123",
60+
Severity: "high",
61+
Source: "osv",
62+
Title: "Prototype pollution in react",
63+
FixedIn: "18.2.1",
64+
},
65+
}},
66+
},
5567
},
5668
Audit: &output.DiffAudit{
5769
Introduced: []output.AuditFinding{{
@@ -75,10 +87,13 @@ func TestRenderDiffMarkdownIncludesPatchedVersionsByDefault(t *testing.T) {
7587
for _, want := range []string{
7688
"# Bomly Diff Summary",
7789
"Compared `main` to `feature`.",
78-
"- Added: 1",
79-
"- Changed: 1",
90+
"**Summary:** 1 added, 1 changed, 0 removed.",
91+
"| added | react@18.2.0 | 18.2.0 | unknown | - | - |",
92+
"| changed | zod | 3.22.0 → 3.23.0 | unknown | - | - |",
8093
"## Vulnerabilities",
81-
"- [fail] `react@18.2.0`: Prototype pollution in react (patched in `18.2.1`)",
94+
"| ❌ | introduced | HIGH | OSV-123 | react@18.2.0 | 18.2.1 | osv | Prototype pollution in react |",
95+
"## Policy Findings",
96+
"| ❌ | introduced | vulnerability | HIGH | fail | OSV-123 | react@18.2.0 | 18.2.1 | Prototype pollution in react |",
8297
} {
8398
if !strings.Contains(report, want) {
8499
t.Fatalf("expected Markdown report to contain %q, got:\n%s", want, report)

internal/cli/explain_cmd.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,12 @@ func newExplainCmd() *cobra.Command {
5959
if err != nil {
6060
return exit.InvalidInputError("%v", err)
6161
}
62-
if err := validateMarkdownOnlyOutputs(outputSpecs); err != nil {
62+
if err := validateReportOutputs(outputSpecs); err != nil {
6363
return exit.InvalidInputError("%v", err)
6464
}
65+
if hasOutputFormat(outputSpecs, render.OutputFormatSARIF) && !context.ResolvedConfig.Audit {
66+
return exit.InvalidInputError("-o sarif requires --audit")
67+
}
6568
if context.ResolvedConfig.Interactive && len(outputSpecs) > 0 {
6669
return exit.InvalidInputError("--output cannot be combined with --interactive")
6770
}
@@ -112,8 +115,19 @@ func newExplainCmd() *cobra.Command {
112115
if len(outputSpecs) > 0 {
113116
prog.Advance("Writing additional output")
114117
for _, spec := range outputSpecs {
115-
if err := writeRenderedOutput(streams.reportWriter(), spec, markdownRenderer); err != nil {
116-
return err
118+
switch spec.Format {
119+
case render.OutputFormatMarkdown:
120+
if err := writeRenderedOutput(streams.reportWriter(), spec, markdownRenderer); err != nil {
121+
return err
122+
}
123+
case render.OutputFormatSARIF:
124+
if err := writeRenderedOutput(streams.reportWriter(), spec, func(w io.Writer) error {
125+
return output.WriteSARIF(w, explainResult.Findings, "bomly", cmd.Root().Version)
126+
}); err != nil {
127+
return err
128+
}
129+
default:
130+
return exit.InvalidInputError("output format %q is only supported by scan", spec.Label)
117131
}
118132
}
119133
}

internal/cli/output_artifacts.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ func hasStdoutOutput(specs []render.OutputSpec) bool {
2424
return false
2525
}
2626

27+
func hasOutputFormat(specs []render.OutputSpec, format render.OutputFormat) bool {
28+
for _, spec := range specs {
29+
if spec.Format == format {
30+
return true
31+
}
32+
}
33+
return false
34+
}
35+
2736
func allOutputsAreSBOM(specs []render.OutputSpec) bool {
2837
if len(specs) == 0 {
2938
return false
@@ -45,6 +54,17 @@ func validateMarkdownOnlyOutputs(specs []render.OutputSpec) error {
4554
return nil
4655
}
4756

57+
func validateReportOutputs(specs []render.OutputSpec) error {
58+
for _, spec := range specs {
59+
switch spec.Format {
60+
case render.OutputFormatMarkdown, render.OutputFormatSARIF:
61+
default:
62+
return fmt.Errorf("output format %q is only supported by scan", spec.Label)
63+
}
64+
}
65+
return nil
66+
}
67+
4868
func writeRenderedOutput(stdout io.Writer, spec render.OutputSpec, renderer func(io.Writer) error) error {
4969
var buf bytes.Buffer
5070
if err := renderer(&buf); err != nil {

0 commit comments

Comments
 (0)