2021-02-02 12:10:47 +05:30
package jira
import (
"fmt"
2022-02-23 13:54:46 +01:00
"io"
2024-03-10 22:02:42 +05:30
"net/url"
2021-02-02 12:10:47 +05:30
"strings"
2024-03-10 22:02:42 +05:30
"sync"
2021-02-02 12:10:47 +05:30
2021-07-19 21:04:08 +03:00
"github.com/andygrunwald/go-jira"
2023-03-10 15:55:54 -07:00
"github.com/trivago/tgo/tcontainer"
2021-11-25 15:18:46 +02:00
2021-09-01 11:43:02 +02:00
"github.com/projectdiscovery/gologger"
2023-10-17 17:44:13 +05:30
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"
2024-03-02 14:55:13 +02:00
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
2022-03-09 12:31:12 +01:00
"github.com/projectdiscovery/retryablehttp-go"
2021-02-02 12:10:47 +05:30
)
2023-06-22 14:27:32 +03:00
type Formatter struct {
util . MarkdownFormatter
}
func ( jiraFormatter * Formatter ) MakeBold ( text string ) string {
return "*" + text + "*"
}
func ( jiraFormatter * Formatter ) CreateCodeBlock ( title string , content string , _ string ) string {
return fmt . Sprintf ( "\n%s\n{code}\n%s\n{code}\n" , jiraFormatter . MakeBold ( title ) , content )
}
func ( jiraFormatter * Formatter ) CreateTable ( headers [ ] string , rows [ ] [ ] string ) ( string , error ) {
table , err := jiraFormatter . MarkdownFormatter . CreateTable ( headers , rows )
if err != nil {
return "" , err
}
tableRows := strings . Split ( table , "\n" )
tableRowsWithoutHeaderSeparator := append ( tableRows [ : 1 ] , tableRows [ 2 : ] ... )
return strings . Join ( tableRowsWithoutHeaderSeparator , "\n" ) , nil
}
func ( jiraFormatter * Formatter ) CreateLink ( title string , url string ) string {
return fmt . Sprintf ( "[%s|%s]" , title , url )
}
2021-08-05 18:19:59 +03:00
// Integration is a client for an issue tracker integration
2021-02-02 12:10:47 +05:30
type Integration struct {
2023-06-22 14:27:32 +03:00
Formatter
2021-02-02 12:10:47 +05:30
jira * jira . Client
options * Options
2024-03-10 22:02:42 +05:30
once * sync . Once
transitionID string
2021-02-02 12:10:47 +05:30
}
// Options contains the configuration options for jira client
type Options struct {
2021-10-18 20:52:35 +02:00
// Cloud value (optional) is set to true when Jira cloud is used
2023-01-10 22:49:01 +05:30
Cloud bool ` yaml:"cloud" json:"cloud" `
2021-10-18 20:52:35 +02:00
// UpdateExisting value (optional) if true, the existing opened issue is updated
2023-01-10 22:49:01 +05:30
UpdateExisting bool ` yaml:"update-existing" json:"update_existing" `
2021-10-19 17:15:58 +02:00
// URL is the URL of the jira server
2023-01-10 22:49:01 +05:30
URL string ` yaml:"url" json:"url" validate:"required" `
2021-10-19 17:15:58 +02:00
// AccountID is the accountID of the jira user.
2023-01-10 22:49:01 +05:30
AccountID string ` yaml:"account-id" json:"account_id" validate:"required" `
2021-10-19 17:15:58 +02:00
// Email is the email of the user for jira instance
2023-01-10 22:49:01 +05:30
Email string ` yaml:"email" json:"email" validate:"required,email" `
2021-10-19 17:15:58 +02:00
// Token is the token for jira instance.
2023-01-10 22:49:01 +05:30
Token string ` yaml:"token" json:"token" validate:"required" `
2021-10-19 17:15:58 +02:00
// ProjectName is the name of the project.
2023-01-10 22:49:01 +05:30
ProjectName string ` yaml:"project-name" json:"project_name" validate:"required" `
2021-10-18 20:52:35 +02:00
// IssueType (optional) is the name of the created issue type
2023-01-10 22:49:01 +05:30
IssueType string ` yaml:"issue-type" json:"issue_type" `
2021-10-19 17:15:58 +02:00
// SeverityAsLabel (optional) sends the severity as the label of the created
// issue.
2023-01-10 22:49:01 +05:30
SeverityAsLabel bool ` yaml:"severity-as-label" json:"severity_as_label" `
2024-03-02 14:55:13 +02:00
// AllowList contains a list of allowed events for this tracker
AllowList * filters . Filter ` yaml:"allow-list" `
// DenyList contains a list of denied events for this tracker
DenyList * filters . Filter ` yaml:"deny-list" `
2023-01-10 22:49:01 +05:30
// Severity (optional) is the severity of the issue.
2023-03-10 15:55:54 -07:00
Severity [ ] string ` yaml:"severity" json:"severity" `
2023-01-10 22:49:01 +05:30
HttpClient * retryablehttp . Client ` yaml:"-" json:"-" `
2023-03-10 15:55:54 -07:00
// for each customfield specified in the configuration options
// we will create a map of customfield name to the value
// that will be used to create the issue
2023-03-20 00:48:39 +01:00
CustomFields map [ string ] interface { } ` yaml:"custom-fields" json:"custom_fields" `
2023-03-10 15:55:54 -07:00
StatusNot string ` yaml:"status-not" json:"status_not" `
2024-01-27 01:36:25 +03:00
OmitRaw bool ` yaml:"-" `
2022-09-27 02:40:34 +05:30
}
2022-05-12 05:10:14 -05:00
2021-02-02 12:10:47 +05:30
// New creates a new issue tracker integration client based on options.
func New ( options * Options ) ( * Integration , error ) {
2021-06-04 13:11:09 +02:00
username := options . Email
if ! options . Cloud {
username = options . AccountID
}
2021-02-02 12:10:47 +05:30
tp := jira . BasicAuthTransport {
2021-06-04 13:11:09 +02:00
Username : username ,
2021-02-02 12:10:47 +05:30
Password : options . Token ,
}
2022-03-09 12:31:12 +01:00
if options . HttpClient != nil {
tp . Transport = options . HttpClient . HTTPClient . Transport
}
2021-02-02 12:10:47 +05:30
jiraClient , err := jira . NewClient ( tp . Client ( ) , options . URL )
if err != nil {
return nil , err
}
2024-03-10 22:02:42 +05:30
integration := & Integration {
jira : jiraClient ,
options : options ,
once : & sync . Once { } ,
}
return integration , nil
}
func ( i * Integration ) Name ( ) string {
return "jira"
2021-02-02 12:10:47 +05:30
}
2021-06-04 18:19:23 +02:00
// CreateNewIssue creates a new issue in the tracker
2024-03-10 22:02:42 +05:30
func ( i * Integration ) CreateNewIssue ( event * output . ResultEvent ) ( * filters . CreateIssueResponse , error ) {
2021-02-02 12:10:47 +05:30
summary := format . Summary ( event )
2021-10-18 20:45:46 +02:00
labels := [ ] string { }
2022-03-07 17:18:27 +01:00
severityLabel := fmt . Sprintf ( "Severity:%s" , event . Info . SeverityHolder . Severity . String ( ) )
2021-10-19 17:15:58 +02:00
if i . options . SeverityAsLabel && severityLabel != "" {
2021-10-18 20:45:46 +02:00
labels = append ( labels , severityLabel )
}
if label := i . options . IssueType ; label != "" {
labels = append ( labels , label )
}
2023-03-10 15:55:54 -07:00
// for each custom value, take the name of the custom field and
// set the value of the custom field to the value specified in the
// configuration options
customFields := tcontainer . NewMarshalMap ( )
for name , value := range i . options . CustomFields {
//customFields[name] = map[string]interface{}{"value": value}
if valueMap , ok := value . ( map [ interface { } ] interface { } ) ; ok {
// Iterate over nested map
for nestedName , nestedValue := range valueMap {
2023-03-20 00:48:39 +01:00
fmtNestedValue , ok := nestedValue . ( string )
if ! ok {
2024-03-10 22:02:42 +05:30
return nil , fmt . Errorf ( ` couldn't iterate on nested item "%s": %s ` , nestedName , nestedValue )
2023-03-20 00:48:39 +01:00
}
if strings . HasPrefix ( fmtNestedValue , "$" ) {
nestedValue = strings . TrimPrefix ( fmtNestedValue , "$" )
2023-03-10 15:55:54 -07:00
switch nestedValue {
case "CVSSMetrics" :
nestedValue = event . Info . Classification . CVSSMetrics
case "CVEID" :
nestedValue = event . Info . Classification . CVEID
case "CWEID" :
nestedValue = event . Info . Classification . CWEID
case "CVSSScore" :
nestedValue = event . Info . Classification . CVSSScore
case "Host" :
nestedValue = event . Host
case "Severity" :
nestedValue = event . Info . SeverityHolder
case "Name" :
nestedValue = event . Info . Name
}
}
switch nestedName {
case "id" :
customFields [ name ] = map [ string ] interface { } { "id" : nestedValue }
case "name" :
customFields [ name ] = map [ string ] interface { } { "value" : nestedValue }
case "freeform" :
customFields [ name ] = nestedValue
}
}
}
}
2021-06-04 18:19:23 +02:00
fields := & jira . IssueFields {
2024-03-10 22:02:42 +05:30
Assignee : & jira . User { Name : i . options . AccountID } ,
2024-01-27 01:36:25 +03:00
Description : format . CreateReportDescription ( event , i , i . options . OmitRaw ) ,
2023-03-10 15:55:54 -07:00
Unknowns : customFields ,
2024-03-10 22:02:42 +05:30
Labels : labels ,
2021-06-04 18:19:23 +02:00
Type : jira . IssueType { Name : i . options . IssueType } ,
Project : jira . Project { Key : i . options . ProjectName } ,
Summary : summary ,
}
// On-prem version of Jira server does not use AccountID
if ! i . options . Cloud {
fields = & jira . IssueFields {
Assignee : & jira . User { Name : i . options . AccountID } ,
2024-01-27 01:36:25 +03:00
Description : format . CreateReportDescription ( event , i , i . options . OmitRaw ) ,
2021-02-02 12:10:47 +05:30
Type : jira . IssueType { Name : i . options . IssueType } ,
Project : jira . Project { Key : i . options . ProjectName } ,
Summary : summary ,
2022-03-07 17:11:20 +01:00
Labels : labels ,
2023-03-10 15:55:54 -07:00
Unknowns : customFields ,
2021-06-04 13:11:09 +02:00
}
2021-06-04 18:19:23 +02:00
}
2021-06-04 17:14:26 +02:00
2021-06-04 18:19:23 +02:00
issueData := & jira . Issue {
Fields : fields ,
}
2024-03-10 22:02:42 +05:30
createdIssue , resp , err := i . jira . Issue . Create ( issueData )
2021-06-04 18:19:23 +02:00
if err != nil {
var data string
if resp != nil && resp . Body != nil {
2022-02-23 13:54:46 +01:00
d , _ := io . ReadAll ( resp . Body )
2021-06-04 18:19:23 +02:00
data = string ( d )
2021-06-04 17:14:26 +02:00
}
2024-03-10 22:02:42 +05:30
return nil , fmt . Errorf ( "%w => %s" , err , data )
2021-06-04 18:19:23 +02:00
}
2024-03-10 22:02:42 +05:30
return getIssueResponseFromJira ( createdIssue )
}
func getIssueResponseFromJira ( issue * jira . Issue ) ( * filters . CreateIssueResponse , error ) {
parsed , err := url . Parse ( issue . Self )
if err != nil {
return nil , err
}
parsed . Path = fmt . Sprintf ( "/browse/%s" , issue . Key )
issueURL := parsed . String ( )
return & filters . CreateIssueResponse {
IssueID : issue . ID ,
IssueURL : issueURL ,
} , nil
2021-06-04 18:19:23 +02:00
}
// CreateIssue creates an issue in the tracker or updates the existing one
2024-03-10 22:02:42 +05:30
func ( i * Integration ) CreateIssue ( event * output . ResultEvent ) ( * filters . CreateIssueResponse , error ) {
2021-06-04 18:19:23 +02:00
if i . options . UpdateExisting {
2024-03-10 22:02:42 +05:30
issue , err := i . FindExistingIssue ( event )
2021-06-04 17:14:26 +02:00
if err != nil {
2024-03-10 22:02:42 +05:30
return nil , err
} else if issue . ID != "" {
_ , _ , err = i . jira . Issue . AddComment ( issue . ID , & jira . Comment {
2024-01-27 01:36:25 +03:00
Body : format . CreateReportDescription ( event , i , i . options . OmitRaw ) ,
2021-06-04 18:19:23 +02:00
} )
2024-03-10 22:02:42 +05:30
if err != nil {
return nil , err
}
return getIssueResponseFromJira ( & issue )
2021-06-04 17:14:26 +02:00
}
2021-06-04 13:11:09 +02:00
}
2021-06-04 18:19:23 +02:00
return i . CreateNewIssue ( event )
2021-06-04 17:14:26 +02:00
}
2024-03-10 22:02:42 +05:30
func ( i * Integration ) CloseIssue ( event * output . ResultEvent ) error {
if i . options . StatusNot == "" {
return nil
}
issue , err := i . FindExistingIssue ( event )
if err != nil {
return err
} else if issue . ID != "" {
// Lazy load the transitions ID in case it's not set
i . once . Do ( func ( ) {
transitions , _ , err := i . jira . Issue . GetTransitions ( issue . ID )
if err != nil {
return
}
for _ , transition := range transitions {
if transition . Name == i . options . StatusNot {
i . transitionID = transition . ID
break
}
}
} )
if i . transitionID == "" {
return nil
}
transition := jira . CreateTransitionPayload {
Transition : jira . TransitionPayload {
ID : i . transitionID ,
} ,
}
_ , err = i . jira . Issue . DoTransitionWithPayload ( issue . ID , transition )
if err != nil {
return err
}
}
return nil
}
2021-06-04 17:14:26 +02:00
// FindExistingIssue checks if the issue already exists and returns its ID
2024-03-10 22:02:42 +05:30
func ( i * Integration ) FindExistingIssue ( event * output . ResultEvent ) ( jira . Issue , error ) {
2023-06-22 14:27:32 +03:00
template := format . GetMatchedTemplateName ( event )
2023-11-26 20:43:57 +11:00
jql := fmt . Sprintf ( "summary ~ \"%s\" AND summary ~ \"%s\" AND status != \"%s\" AND project = \"%s\"" , template , event . Host , i . options . StatusNot , i . options . ProjectName )
2021-06-04 13:11:09 +02:00
2021-09-01 11:30:22 +02:00
searchOptions := & jira . SearchOptions {
2021-06-04 17:14:26 +02:00
MaxResults : 1 , // if any issue exists, then we won't create a new one
2021-02-02 12:10:47 +05:30
}
2021-06-04 17:14:26 +02:00
2021-09-01 11:30:22 +02:00
chunk , resp , err := i . jira . Issue . Search ( jql , searchOptions )
2021-02-02 12:10:47 +05:30
if err != nil {
var data string
if resp != nil && resp . Body != nil {
2022-02-23 13:54:46 +01:00
d , _ := io . ReadAll ( resp . Body )
2021-02-02 12:10:47 +05:30
data = string ( d )
}
2024-03-10 22:02:42 +05:30
return jira . Issue { } , fmt . Errorf ( "%w => %s" , err , data )
2021-06-04 17:14:26 +02:00
}
switch resp . Total {
case 0 :
2024-03-10 22:02:42 +05:30
return jira . Issue { } , nil
2021-06-04 17:14:26 +02:00
case 1 :
2024-03-10 22:02:42 +05:30
return chunk [ 0 ] , nil
2021-06-04 17:14:26 +02:00
default :
2021-06-04 18:19:23 +02:00
gologger . Warning ( ) . Msgf ( "Discovered multiple opened issues %s for the host %s: The issue [%s] will be updated." , template , event . Host , chunk [ 0 ] . ID )
2024-03-10 22:02:42 +05:30
return chunk [ 0 ] , nil
2021-02-02 12:10:47 +05:30
}
}
2024-03-02 14:55:13 +02:00
// ShouldFilter determines if an issue should be logged to this tracker
func ( i * Integration ) ShouldFilter ( event * output . ResultEvent ) bool {
if i . options . AllowList != nil && i . options . AllowList . GetMatch ( event ) {
2024-05-10 21:59:03 +05:30
return false
2024-03-02 14:55:13 +02:00
}
if i . options . DenyList != nil && i . options . DenyList . GetMatch ( event ) {
return true
}
return false
}