Skip to content

Commit a6900f5

Browse files
Fix SARIF validation errors for GitHub CodeQL upload (#384)
* Initial plan * Fix SARIF validation errors for GitHub CodeQL upload - Set index property on ToolComponentReference to 0 (pointing to first taxonomy) - Set guid property to avoid null serialization - Initialize Rules field in ToolComponent as empty array instead of null - Add comprehensive test case to validate SARIF structure Co-authored-by: fproulx-boostsecurity <76956526+fproulx-boostsecurity@users.noreply.github.com> * Fix lint error and add test for issue #384 - Replace require.Equal with require.InDelta for float comparison to fix testifylint error - Add test fixture with exact YAML from issue #384 - Add comprehensive test case TestSarifFormatIssue384 to validate the fix Co-authored-by: fproulx-boostsecurity <76956526+fproulx-boostsecurity@users.noreply.github.com> * Update test to actually scan and use the issue-384 workflow fixture - Modified TestSarifFormatIssue384 to use InventoryScanner to scan the testdata directory - Test now parses the actual workflow file and runs OPA analysis to generate findings - Added imports for scanner and opa packages - Test validates that issue-384.yml workflow is found and scanned - SARIF output is generated from real scan results instead of mock data Co-authored-by: fproulx-boostsecurity <76956526+fproulx-boostsecurity@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fproulx-boostsecurity <76956526+fproulx-boostsecurity@users.noreply.github.com>
1 parent 5760250 commit a6900f5

3 files changed

Lines changed: 238 additions & 1 deletion

File tree

formatters/sarif/sarif.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,13 @@ func (f *Format) Format(ctx context.Context, packages []*models.PackageInsights)
7070
Name: "boost/sast",
7171
Version: &version,
7272
Organization: &organization,
73+
Rules: []*sarif.ReportingDescriptor{},
7374
}
7475

75-
taxonomyRef := sarif.NewToolComponentReference().WithName("boost/sast")
76+
taxonomyRef := sarif.NewToolComponentReference().
77+
WithName("boost/sast").
78+
WithIndex(0).
79+
WithGuid("00000000-0000-0000-0000-000000000000")
7680
run.Tool.Driver.WithSupportedTaxonomies([]*sarif.ToolComponentReference{taxonomyRef})
7781

7882
run.WithTaxonomies([]*sarif.ToolComponent{taxonomy})

formatters/sarif/sarif_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,108 @@
11
package sarif
22

33
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
47
"testing"
58

9+
"github.com/boostsecurityio/poutine/models"
10+
"github.com/boostsecurityio/poutine/opa"
11+
"github.com/boostsecurityio/poutine/results"
12+
. "github.com/boostsecurityio/poutine/scanner"
613
"github.com/stretchr/testify/require"
714
)
815

16+
func TestSarifFormat(t *testing.T) {
17+
// Create a test package with findings
18+
pkg := &models.PackageInsights{
19+
Purl: "pkg:github/test/repo@1.0.0",
20+
SourceGitRepo: "test/repo",
21+
SourceGitCommitSha: "abc123",
22+
SourceGitRef: "main",
23+
SourceScmType: "github",
24+
FindingsResults: results.FindingsResult{
25+
Findings: []results.Finding{
26+
{
27+
RuleId: "test_rule",
28+
Purl: "pkg:github/test/repo@1.0.0",
29+
Meta: results.FindingMeta{
30+
Path: ".github/workflows/test.yml",
31+
Line: 10,
32+
},
33+
},
34+
},
35+
Rules: map[string]results.Rule{
36+
"test_rule": {
37+
Id: "test_rule",
38+
Title: "Test Rule",
39+
Description: "This is a test rule",
40+
Level: "warning",
41+
},
42+
},
43+
},
44+
}
45+
46+
var buf bytes.Buffer
47+
formatter := NewFormat(&buf, "1.0.0")
48+
err := formatter.Format(context.Background(), []*models.PackageInsights{pkg})
49+
require.NoError(t, err)
50+
51+
// Parse the generated SARIF
52+
var sarifOutput map[string]interface{}
53+
err = json.Unmarshal(buf.Bytes(), &sarifOutput)
54+
require.NoError(t, err)
55+
56+
// Validate the structure
57+
runs, ok := sarifOutput["runs"].([]interface{})
58+
require.True(t, ok, "runs should be an array")
59+
require.Len(t, runs, 1)
60+
61+
run, ok := runs[0].(map[string]interface{})
62+
require.True(t, ok)
63+
64+
// Check tool.driver.supportedTaxonomies
65+
tool, ok := run["tool"].(map[string]interface{})
66+
require.True(t, ok)
67+
driver, ok := tool["driver"].(map[string]interface{})
68+
require.True(t, ok)
69+
70+
supportedTaxonomies, ok := driver["supportedTaxonomies"].([]interface{})
71+
require.True(t, ok, "supportedTaxonomies should be an array")
72+
require.Len(t, supportedTaxonomies, 1)
73+
74+
taxonomy, ok := supportedTaxonomies[0].(map[string]interface{})
75+
require.True(t, ok)
76+
77+
// Validate that index is an integer (not null)
78+
index, exists := taxonomy["index"]
79+
require.True(t, exists, "index field should exist")
80+
require.NotNil(t, index, "index should not be null")
81+
indexFloat, ok := index.(float64)
82+
require.True(t, ok, "index should be a number")
83+
require.InDelta(t, float64(0), indexFloat, 0.0001, "index should be 0 for the first taxonomy")
84+
85+
// Validate that guid is either a string or omitted (not null)
86+
if guid, exists := taxonomy["guid"]; exists {
87+
require.NotNil(t, guid, "guid should not be null if present")
88+
}
89+
90+
// Check taxonomies array
91+
taxonomies, ok := run["taxonomies"].([]interface{})
92+
require.True(t, ok, "taxonomies should be an array")
93+
require.Len(t, taxonomies, 1)
94+
95+
taxonomyItem, ok := taxonomies[0].(map[string]interface{})
96+
require.True(t, ok)
97+
98+
// Validate that rules is an array (not null)
99+
rulesField, exists := taxonomyItem["rules"]
100+
require.True(t, exists, "rules field should exist in taxonomy")
101+
require.NotNil(t, rulesField, "rules should not be null")
102+
_, ok = rulesField.([]interface{})
103+
require.True(t, ok, "rules should be an array")
104+
}
105+
9106
func TestIsValidGitURL(t *testing.T) {
10107
tests := []struct {
11108
name string
@@ -45,3 +142,106 @@ func TestIsValidGitURL(t *testing.T) {
45142
})
46143
}
47144
}
145+
146+
// TestSarifFormatIssue384 validates that the fix for issue #384 works correctly.
147+
// This test scans the actual workflow YAML from the issue report to ensure
148+
// the SARIF output would be accepted by GitHub's CodeQL upload action.
149+
func TestSarifFormatIssue384(t *testing.T) {
150+
// Scan the testdata directory containing the workflow from issue #384
151+
scanner := NewInventoryScanner("testdata")
152+
pkg := &models.PackageInsights{
153+
Purl: "pkg:github/coveo/test-repo",
154+
SourceGitRepo: "coveo/test-repo",
155+
SourceGitRef: "refs/pull/482/merge",
156+
SourceScmType: "github",
157+
}
158+
err := scanner.Run(pkg)
159+
require.NoError(t, err)
160+
161+
// Verify the workflow was found
162+
require.NotEmpty(t, pkg.GithubActionsWorkflows, "should have found the issue-384 workflow")
163+
164+
// Find the issue-384 workflow
165+
var foundWorkflow bool
166+
for _, wf := range pkg.GithubActionsWorkflows {
167+
if wf.Path == ".github/workflows/issue-384.yml" {
168+
foundWorkflow = true
169+
break
170+
}
171+
}
172+
require.True(t, foundWorkflow, "should have found issue-384.yml workflow")
173+
174+
// Analyze with OPA to generate findings (using a basic config)
175+
opaInstance, err := opa.NewOpa(context.Background(), &models.Config{
176+
Include: []models.ConfigInclude{},
177+
})
178+
require.NoError(t, err)
179+
180+
inventory := NewInventory(opaInstance, nil, "", "")
181+
scannedPkg, err := inventory.ScanPackage(context.Background(), *pkg, "testdata")
182+
require.NoError(t, err)
183+
184+
// Generate SARIF output
185+
var buf bytes.Buffer
186+
formatter := NewFormat(&buf, "1.0.0")
187+
err = formatter.Format(context.Background(), []*models.PackageInsights{scannedPkg})
188+
require.NoError(t, err)
189+
190+
// Parse the generated SARIF
191+
var sarifOutput map[string]interface{}
192+
err = json.Unmarshal(buf.Bytes(), &sarifOutput)
193+
require.NoError(t, err)
194+
195+
// Validate critical fields that caused issue #384
196+
runs, ok := sarifOutput["runs"].([]interface{})
197+
require.True(t, ok, "runs should be an array")
198+
require.Len(t, runs, 1)
199+
200+
run, ok := runs[0].(map[string]interface{})
201+
require.True(t, ok)
202+
203+
// Verify supportedTaxonomies structure
204+
tool, ok := run["tool"].(map[string]interface{})
205+
require.True(t, ok)
206+
driver, ok := tool["driver"].(map[string]interface{})
207+
require.True(t, ok)
208+
209+
supportedTaxonomies, ok := driver["supportedTaxonomies"].([]interface{})
210+
require.True(t, ok, "supportedTaxonomies should be an array")
211+
require.Len(t, supportedTaxonomies, 1)
212+
213+
taxonomy, ok := supportedTaxonomies[0].(map[string]interface{})
214+
require.True(t, ok)
215+
216+
// Issue #384: index must be an integer, not null
217+
index, exists := taxonomy["index"]
218+
require.True(t, exists, "index field should exist")
219+
require.NotNil(t, index, "index should not be null (issue #384)")
220+
indexFloat, ok := index.(float64)
221+
require.True(t, ok, "index should be a number (issue #384)")
222+
require.InDelta(t, float64(0), indexFloat, 0.0001, "index should be 0")
223+
224+
// Issue #384: guid must be a string, not null
225+
guid, exists := taxonomy["guid"]
226+
require.True(t, exists, "guid field should exist")
227+
require.NotNil(t, guid, "guid should not be null (issue #384)")
228+
guidStr, ok := guid.(string)
229+
require.True(t, ok, "guid should be a string (issue #384)")
230+
require.NotEmpty(t, guidStr, "guid should not be empty")
231+
232+
// Verify taxonomies structure
233+
taxonomies, ok := run["taxonomies"].([]interface{})
234+
require.True(t, ok, "taxonomies should be an array")
235+
require.Len(t, taxonomies, 1)
236+
237+
taxonomyItem, ok := taxonomies[0].(map[string]interface{})
238+
require.True(t, ok)
239+
240+
// Issue #384: rules must be an array, not null
241+
rulesField, exists := taxonomyItem["rules"]
242+
require.True(t, exists, "rules field should exist in taxonomy")
243+
require.NotNil(t, rulesField, "rules should not be null (issue #384)")
244+
rulesArray, ok := rulesField.([]interface{})
245+
require.True(t, ok, "rules should be an array (issue #384)")
246+
require.NotNil(t, rulesArray, "rules array should not be nil")
247+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Issue 384 - SARIF Upload Test
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
7+
jobs:
8+
poutine:
9+
name: Boost Security.io Poutine
10+
runs-on:
11+
# these are auto-generated
12+
- runs-on=${{ github.run_id }}
13+
- runner=default_ubuntu_24_arm64
14+
- env=${{ vars.RUNS_ON_ENV_DEV }}/region=us-east-1
15+
16+
permissions:
17+
actions: read
18+
contents: read
19+
security-events: write
20+
21+
steps:
22+
- uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
23+
with:
24+
egress-policy: audit
25+
- name: Setup self-hosted runner
26+
uses: coveo-platform/setup-runner@v1.0.0
27+
- uses: actions/checkout@v5.0.0
28+
- name: poutine - GitHub Actions SAST
29+
uses: boostsecurityio/poutine-action@61bf0017ee5853beb601609f85c94249b53c26ef
30+
- name: Upload poutine SARIF file
31+
uses: github/codeql-action/upload-sarif@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
32+
with:
33+
sarif_file: results.sarif

0 commit comments

Comments
 (0)