Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions cmd/kosli/attestJira.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,22 @@ func (o *attestJiraOptions) run(args []string) error {
if err != nil {
return err
}
jiraIssueKeyPattern := jira.MakeJiraIssueKeyPattern(o.projectKeys)

issueIDs, commitInfo, err := gv.MatchPatternInCommitMessageORBranchName(jiraIssueKeyPattern, o.payload.Commit.Sha1,
o.secondarySource, o.ignoreBranchMatch)
commitInfo, err := gv.GetCommitInfoFromCommitSHA(o.payload.Commit.Sha1, true, []string{})
if err != nil {
return err
}

// Search commit message, branch name, and secondary source for Jira issue keys,
// filtering out false positives from multi-segment identifiers like CVE-2026-41284.
searchTexts := []string{commitInfo.Message}
if !o.ignoreBranchMatch {
searchTexts = append(searchTexts, commitInfo.Branch)
}
if o.secondarySource != "" {
searchTexts = append(searchTexts, o.secondarySource)
}
combinedText := strings.Join(searchTexts, "\n")
issueIDs := jira.FindJiraIssueKeys(combinedText, o.projectKeys)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the old MatchPatternInCommitMessageORBranchName returned []string{} for no matches, while FindJiraIssueKeys returns nil. This is fine here since range nil is a no-op in Go — just calling it out for awareness.

logger.Debug("Checked for Jira issue references in Git commit %s on branch %s commit message:\n%s", commitInfo.Sha1, commitInfo.Branch, commitInfo.Message)
logger.Debug("the following Jira references are found in commit message or branch name: %v", issueIDs)

Expand Down
58 changes: 58 additions & 0 deletions internal/jira/jira.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"sort"
"strings"

jira "github.com/andygrunwald/go-jira"
Expand Down Expand Up @@ -115,3 +117,59 @@ func MakeJiraIssueKeyPattern(projectKeys []string) string {
return `(` + strings.Join(projectKeys, "|") + `)-[0-9]+`
}
}

// FindJiraIssueKeys finds all Jira issue keys in text, filtering out
// partial matches from multi-segment identifiers like CVE-2026-41284.
// A match is discarded if every occurrence in text is immediately
// followed by "-<digit>".
func FindJiraIssueKeys(text string, projectKeys []string) []string {
pattern := MakeJiraIssueKeyPattern(projectKeys)
re := regexp.MustCompile(pattern)
candidates := re.FindAllString(text, -1)

// Deduplicate
seen := make(map[string]struct{})
var unique []string
for _, c := range candidates {
if _, ok := seen[c]; !ok {
seen[c] = struct{}{}
unique = append(unique, c)
}
}

// Filter out matches that are always followed by -<digit> in the text
dashDigit := regexp.MustCompile(`^-\d`)
var result []string
for _, m := range unique {
if isPartialMultiSegment(text, m, dashDigit) {
continue
}
result = append(result, m)
}

sort.Strings(result)
if len(result) == 0 {
return nil
}
return result
}

// isPartialMultiSegment returns true if every occurrence of match in text
// is immediately followed by a "-<digit>" suffix, indicating it is part
// of a longer multi-segment identifier (e.g. CVE-2026-41284).
// Precondition: match must exist in text (guaranteed when called from FindJiraIssueKeys).
func isPartialMultiSegment(text, match string, dashDigit *regexp.Regexp) bool {
start := 0
for {
idx := strings.Index(text[start:], match)
if idx < 0 {
break
}
afterIdx := start + idx + len(match)
if afterIdx >= len(text) || !dashDigit.MatchString(text[afterIdx:]) {
return false
}
start = start + idx + 1
}
return true
Comment thread
dangrondahl marked this conversation as resolved.
}
91 changes: 91 additions & 0 deletions internal/jira/jira_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package jira
import (
"regexp"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMakeJiraIssueKey(t *testing.T) {
Expand Down Expand Up @@ -81,3 +83,92 @@ func TestMakeJiraIssueKey(t *testing.T) {
})
}
}

func TestFindJiraIssueKeys(t *testing.T) {
tests := []struct {
name string
text string
projectKeys []string
want []string
}{
{
name: "Jira key alongside CVE identifiers",
text: "PROJ-42: Upgrade dependency for CVE-2026-41284 / CVE-2026-42498",
projectKeys: []string{},
want: []string{"PROJ-42"},
},
{
name: "CVE identifier is not matched",
text: "fix CVE-2026-41284",
projectKeys: []string{},
want: nil,
},
{
name: "multiple CVE identifiers produce no matches",
text: "CVE-2026-41284 and CVE-2025-12345",
projectKeys: []string{},
want: nil,
},
{
name: "CWE-like multi-segment identifier is not matched",
text: "addresses CWE-79-1234",
projectKeys: []string{},
want: nil,
},
{
name: "multiple valid Jira keys",
text: "PROJ-1 and PROJ-2 are related",
projectKeys: []string{},
want: []string{"PROJ-1", "PROJ-2"},
},
{
name: "Jira key at end of string",
text: "fix for PROJ-999",
projectKeys: []string{},
want: []string{"PROJ-999"},
},
{
name: "standalone Jira key",
text: "ABC-123",
projectKeys: []string{},
want: []string{"ABC-123"},
},
{
name: "with project keys filters to specified projects",
text: "PROJ-10: fix for OTHER-789",
projectKeys: []string{"PROJ"},
want: []string{"PROJ-10"},
},
{
name: "with project keys still rejects CVE-like matches",
text: "PROJ-10: Upgrade for CVE-2026-41284",
projectKeys: []string{"PROJ", "CVE"},
want: []string{"PROJ-10"},
},
{
name: "key appears both standalone and in multi-segment identifier",
text: "CVE-2026-41284 and also CVE-2026 standalone",
projectKeys: []string{},
want: []string{"CVE-2026"},
},
{
name: "duplicate keys are deduplicated",
text: "PROJ-42 and PROJ-42 again",
projectKeys: []string{},
want: []string{"PROJ-42"},
},
{
name: "empty text returns nil",
text: "",
projectKeys: []string{},
want: nil,
},
}

Comment thread
dangrondahl marked this conversation as resolved.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FindJiraIssueKeys(tt.text, tt.projectKeys)
assert.Equal(t, tt.want, got)
})
}
}
Loading