Skip to content
Open
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
77 changes: 77 additions & 0 deletions cmd/pretriage/cve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

import (
"fmt"
"strings"

jira "github.com/andygrunwald/go-jira"
)

// CVEFieldID is the JIRA custom field ID for the CVE identifier
const CVEFieldID = "customfield_12324749"

// CVEGroup represents a group of related CVE issues
type CVEGroup struct {
CVEID string
Component string
Issues []jira.Issue
}

// isVulnerability checks if an issue is of type "Vulnerability"
func isVulnerability(issue jira.Issue) bool {
if issue.Fields == nil || issue.Fields.Type.Name == "" {
return false
}
return issue.Fields.Type.Name == "Vulnerability"
}

// extractCVEID extracts the CVE identifier from an issue's custom field
func extractCVEID(issue jira.Issue) string {
if issue.Fields == nil || issue.Fields.Unknowns == nil {
return ""
}

if cveValue, ok := issue.Fields.Unknowns[CVEFieldID]; ok {
if cveStr, ok := cveValue.(string); ok {
return strings.TrimSpace(cveStr)
}
}
return ""
}

// extractComponent extracts the first component name from an issue
func extractComponent(issue jira.Issue) string {
if issue.Fields == nil || len(issue.Fields.Components) == 0 {
return "unknown"
}
return issue.Fields.Components[0].Name
}

// groupKey creates a unique key for grouping: "CVE-ID|Component"
func groupKey(issue jira.Issue) string {
cveID := extractCVEID(issue)
component := extractComponent(issue)
return fmt.Sprintf("%s|%s", cveID, component)
}

// GroupCVEIssues groups issues by CVE ID + Component
// Returns a map where key is "CVE-ID|Component" and value is the CVEGroup
func GroupCVEIssues(issues []jira.Issue) map[string]*CVEGroup {
groups := make(map[string]*CVEGroup)

for _, issue := range issues {
key := groupKey(issue)

if groups[key] == nil {
groups[key] = &CVEGroup{
CVEID: extractCVEID(issue),
Component: extractComponent(issue),
Issues: []jira.Issue{issue},
}
} else {
groups[key].Issues = append(groups[key].Issues, issue)
}
}

return groups
}
47 changes: 47 additions & 0 deletions cmd/pretriage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,54 @@ func main() {
slackClient := slack.New()

log.Print("Running the actual triage assignment...")

// Collect all issues first, separating CVEs from regular bugs
var cveIssues []jira.Issue
var regularIssues []jira.Issue

for issue := range query.SearchIssues(ctx, jiraClient, queryUntriaged) {
if isVulnerability(issue) {
cveIssues = append(cveIssues, issue)
} else {
regularIssues = append(regularIssues, issue)
}
}

// Process CVE issues: group by CVE ID + Component, assign group together
if len(cveIssues) > 0 {
log.Printf("Found %d CVE issues, grouping...", len(cveIssues))
cveGroups := GroupCVEIssues(cveIssues)
log.Printf("Grouped into %d CVE groups", len(cveGroups))

for key, group := range cveGroups {
assignee := &triagers[rand.Intn(len(triagers))]

log.Printf("Assigning CVE group %q (%d issues) to %q",
key, len(group.Issues), censorEmail(assignee.Jira))

// Assign all issues in the group to the same person
for _, issue := range group.Issues {
wg.Add(1)
go func(issue jira.Issue) {
defer wg.Done()
if err := assign(jiraClient, issue, assignee.Jira); err != nil {
gotErrors = true
log.Print(err)
}
}(issue)
}
wg.Wait()

// Send single grouped notification
if err := slackClient.Send(SLACK_HOOK, cveGroupNotification(group, assignee.Slack)); err != nil {
gotErrors = true
log.Print(err)
}
}
}

// Process regular bugs: existing individual assignment flow
for _, issue := range regularIssues {
wg.Add(1)
go func(issue jira.Issue) {
defer wg.Done()
Expand Down
28 changes: 28 additions & 0 deletions cmd/pretriage/notify.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"strings"

jira "github.com/andygrunwald/go-jira"
Expand All @@ -16,3 +17,30 @@ func notification(issue jira.Issue, slackId string) string {
notification.WriteString(slack.Link(query.JiraBaseURL+"browse/"+issue.Key, issue.Key))
return notification.String()
}

// cveGroupNotification creates a Slack message for a group of related CVE issues
func cveGroupNotification(group *CVEGroup, slackId string) string {
var notification strings.Builder
notification.WriteByte('<')
notification.WriteString(slackId)
notification.WriteString("> ")

// Format: "CVE-2024-XXXX (Component): ISSUE-1 ISSUE-2 ISSUE-3"
notification.WriteString(group.CVEID)
notification.WriteString(" (")
notification.WriteString(group.Component)
notification.WriteString("): ")

for i, issue := range group.Issues {
if i > 0 {
notification.WriteByte(' ')
}
notification.WriteString(slack.Link(query.JiraBaseURL+"browse/"+issue.Key, issue.Key))
}

if len(group.Issues) > 1 {
notification.WriteString(fmt.Sprintf(" (%d related issues)", len(group.Issues)))
}

return notification.String()
}