diff --git a/engine/engine.go b/engine/engine.go index 62ba302c..efe12b43 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -478,7 +478,7 @@ func buildSecret( pluginName string, ) (*secrets.Secret, error) { gitInfo := item.GetGitInfo() - itemId, err := getFindingId(item, &value) + findingID, err := getFindingId(item, &value) if err != nil { return nil, fmt.Errorf("failed to get finding ID: %w", err) } @@ -509,7 +509,7 @@ func buildSecret( } secret := &secrets.Secret{ - ID: itemId, + ID: findingID, Source: item.GetSource(), RuleID: value.RuleID, StartLine: startLine, @@ -520,6 +520,15 @@ func buildSecret( LineContent: lineContent, RuleDescription: value.Description, } + + if pluginName == "confluence" { + if pageID, ok := plugins.ParseConfluenceItemID(item.GetID()); ok { + if secret.ExtraDetails == nil { + secret.ExtraDetails = make(map[string]interface{}) + } + secret.ExtraDetails["confluence.pageId"] = pageID + } + } return secret, nil } diff --git a/engine/engine_test.go b/engine/engine_test.go index a7213ddd..918cf7df 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -1223,3 +1223,36 @@ func TestProcessScoreWithoutValidation(t *testing.T) { }) } } + +func TestBuildSecret(t *testing.T) { + t.Run("confluence plugin sets page id in extra details", func(t *testing.T) { + rawPlugin := plugins.NewConfluencePlugin() + confluencePlugin, ok := rawPlugin.(*plugins.ConfluencePlugin) + pageID := "6995346180" + version := 9 + itemID := confluencePlugin.NewConfluenceItemID(pageID, version) + pluginName := confluencePlugin.GetName() + sourceURL := "https://example.atlassian.net/wiki/spaces/SCS/pages/" + pageID + content := "dummy" + it := &item{ + id: itemID, + source: sourceURL, + content: &content, + } + finding := report.Finding{ + RuleID: "github-pat", + StartLine: 1, + EndLine: 1, + Line: "token=SECRET", + Secret: "SECRET", + Description: "test finding", + } + secret, err := buildSecret(context.Background(), it, finding, pluginName) + require.NoError(t, err) + require.NotNil(t, secret) + require.NotNil(t, secret.ExtraDetails, "ExtraDetails should not be nil for confluence plugin") + value, ok := secret.ExtraDetails["confluence.pageId"] + assert.True(t, ok, "ExtraDetails should contain key %q", "confluence.pageId") + assert.Equal(t, pageID, value) + }) +} diff --git a/lib/reporting/report_test.go b/lib/reporting/report_test.go index 12c038ce..b1457f94 100644 --- a/lib/reporting/report_test.go +++ b/lib/reporting/report_test.go @@ -20,6 +20,7 @@ import ( var ( ruleID1 = "ruleID1" ruleID2 = "ruleID2" + ruleID4 = "ruleID4" result1 = &secrets.Secret{ ID: "ID1", Source: "file1", @@ -64,6 +65,24 @@ var ( CvssScore: 0.0, RuleDescription: "Rule Description", } + // result for confluence.pageId validation + result4 = &secrets.Secret{ + ID: "ID4", + Source: "file4", + RuleID: "ruleID4", + StartLine: 0, + EndLine: 0, + LineContent: "line content4", + StartColumn: 11, + EndColumn: 130, + Value: "value 4", + ValidationStatus: secrets.UnknownResult, + CvssScore: 0.0, + RuleDescription: "Rule Description", + ExtraDetails: map[string]interface{}{ + "confluence.pageId": "1234567890", + }, + } ) // test expected outputs @@ -81,6 +100,12 @@ var ( Text: result2.RuleDescription, }, } + rule4Sarif = &SarifRule{ + ID: ruleID4, + FullDescription: &Message{ + Text: result4.RuleDescription, + }, + } // sarif results result1Sarif = Results{ Message: Message{ @@ -175,6 +200,38 @@ var ( "cvssScore": result3.CvssScore, }, } + result4Sarif = Results{ + Message: Message{ + Text: createMessageText(result4.RuleID, result4.Source), + }, + RuleId: ruleID4, + Locations: []Locations{ + { + PhysicalLocation: PhysicalLocation{ + ArtifactLocation: ArtifactLocation{ + URI: result4.Source, + }, + Region: Region{ + StartLine: result4.StartLine, + StartColumn: result4.StartColumn, + EndLine: result4.EndLine, + EndColumn: result4.EndColumn, + Snippet: Snippet{ + Text: result4.Value, + Properties: Properties{ + "lineContent": strings.TrimSpace(result4.LineContent), + }, + }, + }, + }, + }, + }, + Properties: Properties{ + "validationStatus": string(result4.ValidationStatus), + "cvssScore": result4.CvssScore, + "confluence.pageId": result4.ExtraDetails["confluence.pageId"], + }, + } ) func TestAddSecretToFile(t *testing.T) { @@ -293,6 +350,33 @@ func TestGetOutputSarif(t *testing.T) { }, }, }, + { + name: "includes confluence.pageId in sarif result properties", + arg: &Report{ + TotalItemsScanned: 1, + TotalSecretsFound: 1, + Results: map[string][]*secrets.Secret{ + "secret1": {result4}, + }, + }, + wantErr: false, + want: []Runs{ + { + Tool: Tool{ + Driver: Driver{ + Name: "report", + SemanticVersion: "1", + Rules: []*SarifRule{ + rule4Sarif, + }, + }, + }, + Results: []Results{ + result4Sarif, + }, + }, + }, + }, } for _, tt := range tests { diff --git a/lib/reporting/sarif.go b/lib/reporting/sarif.go index 797688ba..65d5e98b 100644 --- a/lib/reporting/sarif.go +++ b/lib/reporting/sarif.go @@ -89,18 +89,26 @@ func getResults(report *Report) []Results { return results } - for _, secrets := range report.Results { - for _, secret := range secrets { + for _, secretsSlice := range report.Results { + for _, secret := range secretsSlice { + props := Properties{ + "validationStatus": secret.ValidationStatus, + "cvssScore": secret.CvssScore, + } + + if secret.ExtraDetails != nil { + if pageID, ok := secret.ExtraDetails["confluence.pageId"]; ok { + props["confluence.pageId"] = pageID + } + } + r := Results{ Message: Message{ Text: createMessageText(secret.RuleID, secret.Source), }, - RuleId: secret.RuleID, - Locations: getLocation(secret), - Properties: Properties{ - "validationStatus": secret.ValidationStatus, - "cvssScore": secret.CvssScore, - }, + RuleId: secret.RuleID, + Locations: getLocation(secret), + Properties: props, } results = append(results, r) } diff --git a/plugins/confluence.go b/plugins/confluence.go index f105882a..a075b6fc 100644 --- a/plugins/confluence.go +++ b/plugins/confluence.go @@ -469,7 +469,7 @@ func chunkStrings(input []string, chunkSize int) [][]string { // convertPageToItem converts a Confluence Page into an ISourceItem. func (p *ConfluencePlugin) convertPageToItem(page *Page) ISourceItem { - itemID := fmt.Sprintf("%s-%s-%s", p.GetName(), page.ID, strconv.Itoa(page.Version.Number)) + itemID := p.NewConfluenceItemID(page.ID, page.Version.Number) sourceURL := "" if resolvedURL, ok := p.resolveConfluenceSourceURL(page, page.Version.Number); ok { @@ -668,3 +668,31 @@ func isValidNumericID(s string) bool { } return true } + +// NewConfluenceItemID builds the item ID for a Confluence page. +func (p *ConfluencePlugin) NewConfluenceItemID(pageID string, version int) string { + return fmt.Sprintf("%s-%s-%d", p.GetName(), pageID, version) +} + +// ParseConfluenceItemID extracts the Confluence page ID from an item ID +// produced by NewConfluenceItemID. It returns ("", false) if the ID does not +// conform to the expected pattern. +func ParseConfluenceItemID(id string) (string, bool) { + parts := strings.Split(id, "-") + if len(parts) != 3 { + return "", false + } + + // Last segment must be an integer version. + if _, err := strconv.Atoi(parts[len(parts)-1]); err != nil { + return "", false + } + + // Second-to-last segment must be a valid numeric pageId. + pageID := parts[len(parts)-2] + if !isValidNumericID(pageID) { + return "", false + } + + return pageID, true +} diff --git a/plugins/confluence_test.go b/plugins/confluence_test.go index 5886bb81..e4e287b2 100644 --- a/plugins/confluence_test.go +++ b/plugins/confluence_test.go @@ -1699,6 +1699,23 @@ func TestTrimNonEmpty(t *testing.T) { } } +func TestNewConfluenceItemID(t *testing.T) { + p := &ConfluencePlugin{} + pageID := "6995346180" + version := 9 + expectedID := "confluence-6995346180-9" + actual := p.NewConfluenceItemID(pageID, version) + assert.Equal(t, expectedID, actual) +} + +func TestParseConfluenceItemID(t *testing.T) { + p := &ConfluencePlugin{} + id := p.NewConfluenceItemID("123456", 3) + actualPageID, ok := ParseConfluenceItemID(id) + assert.True(t, ok) + assert.Equal(t, "123456", actualPageID) +} + func newPluginWithMock(t *testing.T) (*ConfluencePlugin, *gomock.Controller, *MockConfluenceClient, *chunk.MockIChunk) { t.Helper() ctrl := gomock.NewController(t)