Skip to content

Commit e5da6de

Browse files
authored
fix(report): set correct sarif ROOTPATH uri when scanning a git repository (aquasecurity#10366)
1 parent d7fb355 commit e5da6de

7 files changed

Lines changed: 138 additions & 79 deletions

File tree

integration/testdata/alpine-310.sarif.golden

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,6 @@
186186
}
187187
],
188188
"columnKind": "utf16CodeUnits",
189-
"originalUriBaseIds": {
190-
"ROOTPATH": {
191-
"uri": "file:///"
192-
}
193-
},
194189
"properties": {
195190
"imageID": "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
196191
"imageName": "testdata/fixtures/images/alpine-310.tar.gz",

pkg/report/export_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,9 @@ var ToProperties = toProperties
1010

1111
// ToUri exports toUri for testing.
1212
var ToUri = toUri
13+
14+
// ToSarifErrorLevel exports toSarifErrorLevel for testing.
15+
var ToSarifErrorLevel = toSarifErrorLevel
16+
17+
// PathToFileURI exports pathToFileURI for testing.
18+
var PathToFileURI = pathToFileURI

pkg/report/sarif.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ const (
3737
)
3838

3939
var (
40-
rootPath = "file:///"
41-
4240
// pathRegex to extract file path in case string includes (distro:version)
4341
pathRegex = regexp.MustCompile(`(?P<path>.+?)(?:\s*\((?:.*?)\).*?)?$`)
4442
)
@@ -116,6 +114,17 @@ func getRuleIndex(id string, indexes map[string]int) int {
116114
return l
117115
}
118116

117+
// pathToFileURI converts a filesystem path to a file URI.
118+
// On Windows, backslashes are converted to forward slashes and a leading slash is prepended.
119+
func pathToFileURI(path string) string {
120+
absPath, _ := filepath.Abs(path)
121+
slashPath := filepath.ToSlash(absPath)
122+
if !strings.HasPrefix(slashPath, "/") {
123+
slashPath = "/" + slashPath
124+
}
125+
return fmt.Sprintf("file://%s/", slashPath)
126+
}
127+
119128
func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
120129
sarifReport, err := sarif.New(sarif.Version210)
121130
if err != nil {
@@ -133,10 +142,6 @@ func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
133142
"imageID": report.Metadata.ImageID,
134143
}
135144
}
136-
if sw.Target != "" {
137-
absPath, _ := filepath.Abs(sw.Target)
138-
rootPath = fmt.Sprintf("file://%s/", absPath)
139-
}
140145

141146
ruleIndexes := make(map[string]int)
142147
for _, res := range report.Results {
@@ -254,8 +259,14 @@ func (sw *SarifWriter) Write(_ context.Context, report types.Report) error {
254259

255260
}
256261
sw.run.ColumnKind = columnKind
257-
sw.run.OriginalUriBaseIDs = map[string]*sarif.ArtifactLocation{
258-
"ROOTPATH": {URI: &rootPath},
262+
263+
if sw.Target != "" {
264+
rootPath := pathToFileURI(sw.Target)
265+
sw.run.OriginalUriBaseIDs = map[string]*sarif.ArtifactLocation{
266+
"ROOTPATH": {
267+
URI: &rootPath,
268+
},
269+
}
259270
}
260271
sarifReport.AddRun(sw.run)
261272
return sarifReport.PrettyWrite(sw.Output)

pkg/report/sarif_test.go

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,22 @@ import (
1717
"github.com/aquasecurity/trivy/pkg/types"
1818
)
1919

20+
// TestReportWriter_Sarif reuses report.PathToFileURI to compute expected URIs.
21+
// The correctness of PathToFileURI itself (including Windows path handling) is verified in TestWrite_Sarif.
2022
func TestReportWriter_Sarif(t *testing.T) {
23+
tmpScanURI := report.PathToFileURI("/tmp/scan")
24+
2125
tests := []struct {
22-
name string
23-
input types.Report
24-
want *sarif.Report
26+
name string
27+
target string
28+
input types.Report
29+
want *sarif.Report
2530
}{
2631
{
2732
name: "report with vulnerabilities",
33+
// Container images don't have a local filesystem path, so target is empty
34+
// and OriginalUriBaseIDs is omitted from the SARIF output.
35+
target: "",
2836
input: types.Report{
2937
ArtifactName: "debian:9",
3038
ArtifactType: ftypes.TypeContainerImage,
@@ -170,11 +178,6 @@ func TestReportWriter_Sarif(t *testing.T) {
170178
},
171179
},
172180
ColumnKind: "utf16CodeUnits",
173-
OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{
174-
"ROOTPATH": {
175-
URI: lo.ToPtr("file:///"),
176-
},
177-
},
178181
PropertyBag: sarif.PropertyBag{
179182
Properties: map[string]any{
180183
"imageName": "debian:9",
@@ -188,7 +191,8 @@ func TestReportWriter_Sarif(t *testing.T) {
188191
},
189192
},
190193
{
191-
name: "report with misconfigurations",
194+
name: "report with misconfigurations",
195+
target: "/tmp/scan",
192196
input: types.Report{
193197
Results: types.Results{
194198
{
@@ -329,15 +333,16 @@ func TestReportWriter_Sarif(t *testing.T) {
329333
ColumnKind: "utf16CodeUnits",
330334
OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{
331335
"ROOTPATH": {
332-
URI: lo.ToPtr("file:///"),
336+
URI: lo.ToPtr(tmpScanURI),
333337
},
334338
},
335339
},
336340
},
337341
},
338342
},
339343
{
340-
name: "report with secrets",
344+
name: "report with secrets",
345+
target: "/tmp/scan",
341346
input: types.Report{
342347
Results: types.Results{
343348
{
@@ -423,15 +428,16 @@ func TestReportWriter_Sarif(t *testing.T) {
423428
ColumnKind: "utf16CodeUnits",
424429
OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{
425430
"ROOTPATH": {
426-
URI: lo.ToPtr("file:///"),
431+
URI: lo.ToPtr(tmpScanURI),
427432
},
428433
},
429434
},
430435
},
431436
},
432437
},
433438
{
434-
name: "report with licenses",
439+
name: "report with licenses",
440+
target: "/tmp/scan",
435441
input: types.Report{
436442
Results: types.Results{
437443
{
@@ -512,15 +518,16 @@ func TestReportWriter_Sarif(t *testing.T) {
512518
ColumnKind: "utf16CodeUnits",
513519
OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{
514520
"ROOTPATH": {
515-
URI: lo.ToPtr("file:///"),
521+
URI: lo.ToPtr(tmpScanURI),
516522
},
517523
},
518524
},
519525
},
520526
},
521527
},
522528
{
523-
name: "no vulns",
529+
name: "no vulns",
530+
target: "/tmp/scan",
524531
want: &sarif.Report{
525532
Version: "2.1.0",
526533
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
@@ -539,7 +546,7 @@ func TestReportWriter_Sarif(t *testing.T) {
539546
ColumnKind: "utf16CodeUnits",
540547
OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{
541548
"ROOTPATH": {
542-
URI: lo.ToPtr("file:///"),
549+
URI: lo.ToPtr(tmpScanURI),
543550
},
544551
},
545552
},
@@ -721,11 +728,6 @@ func TestReportWriter_Sarif(t *testing.T) {
721728
},
722729
},
723730
ColumnKind: "utf16CodeUnits",
724-
OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{
725-
"ROOTPATH": {
726-
URI: lo.ToPtr("file:///"),
727-
},
728-
},
729731
},
730732
},
731733
},
@@ -737,6 +739,7 @@ func TestReportWriter_Sarif(t *testing.T) {
737739
sarifWritten := bytes.NewBuffer(nil)
738740
w := report.SarifWriter{
739741
Output: sarifWritten,
742+
Target: tt.target,
740743
}
741744
err := w.Write(t.Context(), tt.input)
742745
require.NoError(t, err)
@@ -749,6 +752,25 @@ func TestReportWriter_Sarif(t *testing.T) {
749752
}
750753
}
751754

755+
func TestReportWriter_toSarifErrorLevel(t *testing.T) {
756+
tests := []struct {
757+
severity string
758+
sarifErrorLevel string
759+
}{
760+
{severity: "CRITICAL", sarifErrorLevel: "error"},
761+
{severity: "HIGH", sarifErrorLevel: "error"},
762+
{severity: "MEDIUM", sarifErrorLevel: "warning"},
763+
{severity: "LOW", sarifErrorLevel: "note"},
764+
{severity: "UNKNOWN", sarifErrorLevel: "note"},
765+
{severity: "OTHER", sarifErrorLevel: "none"},
766+
}
767+
for _, tc := range tests {
768+
t.Run(tc.severity, func(t *testing.T) {
769+
assert.Equal(t, tc.sarifErrorLevel, report.ToSarifErrorLevel(tc.severity), tc.severity)
770+
})
771+
}
772+
}
773+
752774
func TestToPathUri(t *testing.T) {
753775
tests := []struct {
754776
input string

pkg/report/writer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func Write(ctx context.Context, report types.Report, option flag.Options) (err e
9494
}
9595
case types.FormatSarif:
9696
target := ""
97-
if report.ArtifactType == ftypes.TypeFilesystem {
97+
if report.ArtifactType == ftypes.TypeFilesystem || report.ArtifactType == ftypes.TypeRepository {
9898
target = option.Target
9999
}
100100
writer = &SarifWriter{

pkg/report/writer_internal_test.go

Lines changed: 0 additions & 44 deletions
This file was deleted.

pkg/report/writer_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ package report_test
22

33
import (
44
"bytes"
5+
"encoding/json"
6+
"regexp"
57
"testing"
68

79
"github.com/stretchr/testify/assert"
810
"github.com/stretchr/testify/require"
911

1012
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
1113
"github.com/aquasecurity/trivy/internal/hooktest"
14+
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
1215
"github.com/aquasecurity/trivy/pkg/flag"
1316
"github.com/aquasecurity/trivy/pkg/report"
1417
"github.com/aquasecurity/trivy/pkg/types"
@@ -178,3 +181,69 @@ func TestWrite(t *testing.T) {
178181
})
179182
}
180183
}
184+
185+
func TestWrite_Sarif(t *testing.T) {
186+
// On Unix: file:///tmp/foo/, on Windows: file:///D:/tmp/foo/
187+
tmpFooRootPath := regexp.MustCompile(`^file:///([A-Z]:/)?tmp/foo/$`)
188+
189+
tests := []struct {
190+
name string
191+
artifactType ftypes.ArtifactType
192+
target string
193+
wantRootPath *regexp.Regexp
194+
}{
195+
{
196+
name: "TypeFilesystem sets ROOTPATH to target path",
197+
artifactType: ftypes.TypeFilesystem,
198+
target: "/tmp/foo",
199+
wantRootPath: tmpFooRootPath,
200+
},
201+
{
202+
name: "TypeRepository sets ROOTPATH to target path",
203+
artifactType: ftypes.TypeRepository,
204+
target: "/tmp/foo",
205+
wantRootPath: tmpFooRootPath,
206+
},
207+
{
208+
name: "TypeContainerImage does not set ROOTPATH",
209+
artifactType: ftypes.TypeContainerImage,
210+
target: "/tmp/foo",
211+
wantRootPath: nil,
212+
},
213+
}
214+
215+
for _, tt := range tests {
216+
t.Run(tt.name, func(t *testing.T) {
217+
output := new(bytes.Buffer)
218+
opts := flag.Options{
219+
ScanOptions: flag.ScanOptions{
220+
Target: tt.target,
221+
},
222+
ReportOptions: flag.ReportOptions{
223+
Format: types.FormatSarif,
224+
},
225+
}
226+
opts.SetOutputWriter(output)
227+
228+
err := report.Write(t.Context(), types.Report{ArtifactType: tt.artifactType}, opts)
229+
require.NoError(t, err)
230+
231+
var result struct {
232+
Runs []struct {
233+
OriginalUriBaseIDs map[string]struct {
234+
URI string `json:"uri"`
235+
} `json:"originalUriBaseIds"`
236+
} `json:"runs"`
237+
}
238+
err = json.Unmarshal(output.Bytes(), &result)
239+
require.NoError(t, err)
240+
require.Len(t, result.Runs, 1)
241+
242+
if tt.wantRootPath != nil {
243+
assert.Regexp(t, tt.wantRootPath, result.Runs[0].OriginalUriBaseIDs["ROOTPATH"].URI)
244+
} else {
245+
assert.Empty(t, result.Runs[0].OriginalUriBaseIDs["ROOTPATH"].URI)
246+
}
247+
})
248+
}
249+
}

0 commit comments

Comments
 (0)