mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 20:25:27 +00:00
feat: issue tracker URLs in JSON + misc fixes (#4855)
* feat: issue tracker URLs in JSON + misc fixes * misc changes * feat: status update support for issues * feat: report metadata generation hook support * feat: added CLI summary of tickets created * misc changes
This commit is contained in:
parent
b1b4f0fe76
commit
fd024a3e8d
@ -69,7 +69,7 @@ func executeNucleiAsLibrary(templatePath, templateURL string) ([]string, error)
|
||||
defer cache.Close()
|
||||
|
||||
mockProgress := &testutils.MockProgressClient{}
|
||||
reportingClient, err := reporting.New(&reporting.Options{}, "")
|
||||
reportingClient, err := reporting.New(&reporting.Options{}, "", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -290,12 +290,14 @@ func createReportingOptions(options *types.Options) (*reporting.Options, error)
|
||||
// configureOutput configures the output logging levels to be displayed on the screen
|
||||
func configureOutput(options *types.Options) {
|
||||
// If the user desires verbose output, show verbose output
|
||||
if options.Verbose || options.Validate {
|
||||
gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose)
|
||||
}
|
||||
if options.Debug || options.DebugRequests || options.DebugResponse {
|
||||
gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug)
|
||||
}
|
||||
// Debug takes precedence before verbose
|
||||
// because debug is a lower logging level.
|
||||
if options.Verbose || options.Validate {
|
||||
gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose)
|
||||
}
|
||||
if options.NoColor {
|
||||
gologger.DefaultLogger.SetFormatter(formatter.NewCLI(true))
|
||||
}
|
||||
|
||||
@ -190,7 +190,7 @@ func New(options *types.Options) (*Runner, error) {
|
||||
}
|
||||
|
||||
if reportingOptions != nil {
|
||||
client, err := reporting.New(reportingOptions, options.ReportingDB)
|
||||
client, err := reporting.New(reportingOptions, options.ReportingDB, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not create issue reporting client")
|
||||
}
|
||||
|
||||
@ -128,7 +128,7 @@ func (e *NucleiEngine) init() error {
|
||||
return err
|
||||
}
|
||||
// we don't support reporting config in sdk mode
|
||||
if e.rc, err = reporting.New(&reporting.Options{}, ""); err != nil {
|
||||
if e.rc, err = reporting.New(&reporting.Options{}, "", false); err != nil {
|
||||
return err
|
||||
}
|
||||
e.interactshOpts.IssuesClient = e.rc
|
||||
|
||||
@ -165,10 +165,20 @@ type ResultEvent struct {
|
||||
// Lines is the line count for the specified match
|
||||
Lines []int `json:"matched-line,omitempty"`
|
||||
|
||||
// IssueTrackers is the metadata for issue trackers
|
||||
IssueTrackers map[string]IssueTrackerMetadata `json:"issue_trackers,omitempty"`
|
||||
|
||||
FileToIndexPosition map[string]int `json:"-"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type IssueTrackerMetadata struct {
|
||||
// IssueID is the ID of the issue created
|
||||
IssueID string `json:"id,omitempty"`
|
||||
// IssueURL is the URL of the issue created
|
||||
IssueURL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// NewStandardWriter creates a new output writer based on user configurations
|
||||
func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
|
||||
resumeBool := false
|
||||
|
||||
@ -17,6 +17,11 @@ func WriteResult(data *output.InternalWrappedEvent, output output.Writer, progre
|
||||
}
|
||||
var matched bool
|
||||
for _, result := range data.Results {
|
||||
if issuesClient != nil {
|
||||
if err := issuesClient.CreateIssue(result); err != nil {
|
||||
gologger.Warning().Msgf("Could not create issue on tracker: %s", err)
|
||||
}
|
||||
}
|
||||
if err := output.Write(result); err != nil {
|
||||
gologger.Warning().Msgf("Could not write output event: %s\n", err)
|
||||
}
|
||||
@ -24,12 +29,6 @@ func WriteResult(data *output.InternalWrappedEvent, output output.Writer, progre
|
||||
matched = true
|
||||
}
|
||||
progress.IncrementMatched()
|
||||
|
||||
if issuesClient != nil {
|
||||
if err := issuesClient.CreateIssue(result); err != nil {
|
||||
gologger.Warning().Msgf("Could not create issue on tracker: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
@ -11,5 +11,6 @@ type Client interface {
|
||||
Close()
|
||||
Clear()
|
||||
CreateIssue(event *output.ResultEvent) error
|
||||
CloseIssue(event *output.ResultEvent) error
|
||||
GetReportingOptions() *Options
|
||||
}
|
||||
|
||||
@ -34,6 +34,13 @@ func GetMatchedTemplateName(event *output.ResultEvent) string {
|
||||
return matchedTemplateName
|
||||
}
|
||||
|
||||
type reportMetadataEditorHook func(event *output.ResultEvent, formatter ResultFormatter) string
|
||||
|
||||
var (
|
||||
// ReportGenerationMetadataHooks are the hooks for adding metadata to the report
|
||||
ReportGenerationMetadataHooks []reportMetadataEditorHook
|
||||
)
|
||||
|
||||
func CreateReportDescription(event *output.ResultEvent, formatter ResultFormatter, omitRaw bool) string {
|
||||
template := GetMatchedTemplateName(event)
|
||||
builder := &bytes.Buffer{}
|
||||
@ -137,6 +144,12 @@ func CreateReportDescription(event *output.ResultEvent, formatter ResultFormatte
|
||||
|
||||
builder.WriteString("\n" + formatter.CreateHorizontalLine() + "\n")
|
||||
builder.WriteString(fmt.Sprintf("Generated by %s", formatter.CreateLink("Nuclei "+config.Version, "https://github.com/projectdiscovery/nuclei")))
|
||||
|
||||
if len(ReportGenerationMetadataHooks) > 0 {
|
||||
for _, hook := range ReportGenerationMetadataHooks {
|
||||
builder.WriteString(hook(event, formatter))
|
||||
}
|
||||
}
|
||||
data := builder.String()
|
||||
return data
|
||||
}
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||
json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
|
||||
@ -35,8 +39,12 @@ var (
|
||||
|
||||
// Tracker is an interface implemented by an issue tracker
|
||||
type Tracker interface {
|
||||
// Name returns the name of the tracker
|
||||
Name() string
|
||||
// CreateIssue creates an issue in the tracker
|
||||
CreateIssue(event *output.ResultEvent) error
|
||||
CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error)
|
||||
// CloseIssue closes an issue in the tracker
|
||||
CloseIssue(event *output.ResultEvent) error
|
||||
// ShouldFilter determines if the event should be filtered out
|
||||
ShouldFilter(event *output.ResultEvent) bool
|
||||
}
|
||||
@ -55,10 +63,17 @@ type ReportingClient struct {
|
||||
exporters []Exporter
|
||||
options *Options
|
||||
dedupe *dedupe.Storage
|
||||
|
||||
stats map[string]*IssueTrackerStats
|
||||
}
|
||||
|
||||
type IssueTrackerStats struct {
|
||||
Created atomic.Int32
|
||||
Failed atomic.Int32
|
||||
}
|
||||
|
||||
// New creates a new nuclei issue tracker reporting client
|
||||
func New(options *Options, db string) (Client, error) {
|
||||
func New(options *Options, db string, doNotDedupe bool) (Client, error) {
|
||||
client := &ReportingClient{options: options}
|
||||
|
||||
if options.GitHub != nil {
|
||||
@ -142,6 +157,20 @@ func New(options *Options, db string) (Client, error) {
|
||||
client.exporters = append(client.exporters, exporter)
|
||||
}
|
||||
|
||||
if doNotDedupe {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
client.stats = make(map[string]*IssueTrackerStats)
|
||||
for _, tracker := range client.trackers {
|
||||
trackerName := tracker.Name()
|
||||
|
||||
client.stats[trackerName] = &IssueTrackerStats{
|
||||
Created: atomic.Int32{},
|
||||
Failed: atomic.Int32{},
|
||||
}
|
||||
}
|
||||
|
||||
storage, err := dedupe.New(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -195,7 +224,30 @@ func (c *ReportingClient) RegisterExporter(exporter Exporter) {
|
||||
|
||||
// Close closes the issue tracker reporting client
|
||||
func (c *ReportingClient) Close() {
|
||||
// If we have stats for the trackers, print them
|
||||
if len(c.stats) > 0 {
|
||||
for _, tracker := range c.trackers {
|
||||
trackerName := tracker.Name()
|
||||
|
||||
if stats, ok := c.stats[trackerName]; ok {
|
||||
created := stats.Created.Load()
|
||||
if created == 0 {
|
||||
continue
|
||||
}
|
||||
var msgBuilder strings.Builder
|
||||
msgBuilder.WriteString(fmt.Sprintf("%d %s tickets created successfully", created, trackerName))
|
||||
failed := stats.Failed.Load()
|
||||
if failed > 0 {
|
||||
msgBuilder.WriteString(fmt.Sprintf(", %d failed", failed))
|
||||
}
|
||||
gologger.Info().Msgf(msgBuilder.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.dedupe != nil {
|
||||
c.dedupe.Close()
|
||||
}
|
||||
for _, exporter := range c.exporters {
|
||||
exporter.Close()
|
||||
}
|
||||
@ -211,15 +263,37 @@ func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
unique, err := c.dedupe.Index(event)
|
||||
var err error
|
||||
unique := true
|
||||
if c.dedupe != nil {
|
||||
unique, err = c.dedupe.Index(event)
|
||||
}
|
||||
if unique {
|
||||
event.IssueTrackers = make(map[string]output.IssueTrackerMetadata)
|
||||
|
||||
for _, tracker := range c.trackers {
|
||||
// process tracker specific allow/deny list
|
||||
if tracker.ShouldFilter(event) {
|
||||
continue
|
||||
}
|
||||
if trackerErr := tracker.CreateIssue(event); trackerErr != nil {
|
||||
trackerName := tracker.Name()
|
||||
stats, statsOk := c.stats[trackerName]
|
||||
|
||||
reportData, trackerErr := tracker.CreateIssue(event)
|
||||
if trackerErr != nil {
|
||||
if statsOk {
|
||||
_ = stats.Failed.Add(1)
|
||||
}
|
||||
err = multierr.Append(err, trackerErr)
|
||||
continue
|
||||
}
|
||||
if statsOk {
|
||||
_ = stats.Created.Add(1)
|
||||
}
|
||||
|
||||
event.IssueTrackers[tracker.Name()] = output.IssueTrackerMetadata{
|
||||
IssueID: reportData.IssueID,
|
||||
IssueURL: reportData.IssueURL,
|
||||
}
|
||||
}
|
||||
for _, exporter := range c.exporters {
|
||||
@ -231,10 +305,25 @@ func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// CloseIssue closes an issue in the tracker
|
||||
func (c *ReportingClient) CloseIssue(event *output.ResultEvent) error {
|
||||
for _, tracker := range c.trackers {
|
||||
if tracker.ShouldFilter(event) {
|
||||
continue
|
||||
}
|
||||
if err := tracker.CloseIssue(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ReportingClient) GetReportingOptions() *Options {
|
||||
return c.options
|
||||
}
|
||||
|
||||
func (c *ReportingClient) Clear() {
|
||||
if c.dedupe != nil {
|
||||
c.dedupe.Clear()
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,13 @@ import (
|
||||
sliceutil "github.com/projectdiscovery/utils/slice"
|
||||
)
|
||||
|
||||
// CreateIssueResponse is a response to creating an issue
|
||||
// in a tracker
|
||||
type CreateIssueResponse struct {
|
||||
IssueID string `json:"issue_id"`
|
||||
IssueURL string `json:"issue_url"`
|
||||
}
|
||||
|
||||
// Filter filters the received event and decides whether to perform
|
||||
// reporting for it or not.
|
||||
type Filter struct {
|
||||
|
||||
@ -3,6 +3,7 @@ package gitea
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
@ -79,7 +80,7 @@ func New(options *Options) (*Integration, error) {
|
||||
}
|
||||
|
||||
// CreateIssue creates an issue in the tracker
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
|
||||
summary := format.Summary(event)
|
||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
||||
|
||||
@ -93,32 +94,47 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
}
|
||||
customLabels, err := i.getLabelIDsByNames(labels)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var issue *gitea.Issue
|
||||
if i.options.DuplicateIssueCheck {
|
||||
issue, err = i.findIssueByTitle(summary)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if issue == nil {
|
||||
_, _, err = i.client.CreateIssue(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateIssueOption{
|
||||
createdIssue, _, err := i.client.CreateIssue(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateIssueOption{
|
||||
Title: summary,
|
||||
Body: description,
|
||||
Labels: customLabels,
|
||||
})
|
||||
|
||||
return err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filters.CreateIssueResponse{
|
||||
IssueID: strconv.FormatInt(createdIssue.Index, 10),
|
||||
IssueURL: createdIssue.URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
_, _, err = i.client.CreateIssueComment(i.options.ProjectOwner, i.options.ProjectName, issue.Index, gitea.CreateIssueCommentOption{
|
||||
Body: description,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filters.CreateIssueResponse{
|
||||
IssueID: strconv.FormatInt(issue.Index, 10),
|
||||
IssueURL: issue.URL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return err
|
||||
func (i *Integration) CloseIssue(event *output.ResultEvent) error {
|
||||
// TODO: Implement
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldFilter determines if an issue should be logged to this tracker
|
||||
@ -192,3 +208,7 @@ func (i *Integration) getLabelIDsByNames(labels []string) ([]int64, error) {
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (i *Integration) Name() string {
|
||||
return "gitea"
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
@ -85,7 +86,7 @@ func New(options *Options) (*Integration, error) {
|
||||
}
|
||||
|
||||
// CreateIssue creates an issue in the tracker
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) (err error) {
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
|
||||
summary := format.Summary(event)
|
||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
||||
labels := []string{}
|
||||
@ -99,11 +100,12 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) (err error) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
var err error
|
||||
var existingIssue *github.Issue
|
||||
if i.options.DuplicateIssueCheck {
|
||||
existingIssue, err = i.findIssueByTitle(ctx, summary)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,15 +116,21 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) (err error) {
|
||||
Labels: &labels,
|
||||
Assignees: &[]string{i.options.Username},
|
||||
}
|
||||
_, _, err = i.client.Issues.Create(ctx, i.options.Owner, i.options.ProjectName, req)
|
||||
return err
|
||||
createdIssue, _, err := i.client.Issues.Create(ctx, i.options.Owner, i.options.ProjectName, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filters.CreateIssueResponse{
|
||||
IssueID: strconv.FormatInt(createdIssue.GetID(), 10),
|
||||
IssueURL: createdIssue.GetHTMLURL(),
|
||||
}, nil
|
||||
} else {
|
||||
if existingIssue.GetState() == "closed" {
|
||||
stateOpen := "open"
|
||||
if _, _, err := i.client.Issues.Edit(ctx, i.options.Owner, i.options.ProjectName, *existingIssue.Number, &github.IssueRequest{
|
||||
State: &stateOpen,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error reopening issue %d: %s", *existingIssue.Number, err)
|
||||
return nil, fmt.Errorf("error reopening issue %d: %s", *existingIssue.Number, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,8 +138,39 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) (err error) {
|
||||
Body: &description,
|
||||
}
|
||||
_, _, err = i.client.Issues.CreateComment(ctx, i.options.Owner, i.options.ProjectName, *existingIssue.Number, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filters.CreateIssueResponse{
|
||||
IssueID: strconv.FormatInt(existingIssue.GetID(), 10),
|
||||
IssueURL: existingIssue.GetHTMLURL(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Integration) CloseIssue(event *output.ResultEvent) error {
|
||||
ctx := context.Background()
|
||||
summary := format.Summary(event)
|
||||
|
||||
existingIssue, err := i.findIssueByTitle(ctx, summary)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
if existingIssue == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stateClosed := "closed"
|
||||
if _, _, err := i.client.Issues.Edit(ctx, i.options.Owner, i.options.ProjectName, *existingIssue.Number, &github.IssueRequest{
|
||||
State: &stateClosed,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error closing issue %d: %s", *existingIssue.Number, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Integration) Name() string {
|
||||
return "github"
|
||||
}
|
||||
|
||||
// ShouldFilter determines if an issue should be logged to this tracker
|
||||
|
||||
@ -2,6 +2,7 @@ package gitlab
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
@ -66,7 +67,7 @@ func New(options *Options) (*Integration, error) {
|
||||
}
|
||||
|
||||
// CreateIssue creates an issue in the tracker
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
|
||||
summary := format.Summary(event)
|
||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
||||
labels := []string{}
|
||||
@ -88,7 +89,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
Search: &summary,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
issue := issues[0]
|
||||
@ -96,7 +97,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
Body: &description,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if issue.State == "closed" {
|
||||
reopen := "reopen"
|
||||
@ -105,18 +106,61 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
})
|
||||
fmt.Sprintln(resp, err)
|
||||
}
|
||||
return err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filters.CreateIssueResponse{
|
||||
IssueID: strconv.FormatInt(int64(issue.ID), 10),
|
||||
IssueURL: issue.WebURL,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
_, _, err := i.client.Issues.CreateIssue(i.options.ProjectName, &gitlab.CreateIssueOptions{
|
||||
createdIssue, _, err := i.client.Issues.CreateIssue(i.options.ProjectName, &gitlab.CreateIssueOptions{
|
||||
Title: &summary,
|
||||
Description: &description,
|
||||
Labels: &customLabels,
|
||||
AssigneeIDs: &assigneeIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filters.CreateIssueResponse{
|
||||
IssueID: strconv.FormatInt(int64(createdIssue.ID), 10),
|
||||
IssueURL: createdIssue.WebURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *Integration) Name() string {
|
||||
return "gitlab"
|
||||
}
|
||||
|
||||
func (i *Integration) CloseIssue(event *output.ResultEvent) error {
|
||||
searchIn := "title"
|
||||
searchState := "all"
|
||||
|
||||
summary := format.Summary(event)
|
||||
issues, _, err := i.client.Issues.ListProjectIssues(i.options.ProjectName, &gitlab.ListProjectIssuesOptions{
|
||||
In: &searchIn,
|
||||
State: &searchState,
|
||||
Search: &summary,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(issues) <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
issue := issues[0]
|
||||
state := "close"
|
||||
_, _, err = i.client.Issues.UpdateIssue(i.options.ProjectName, issue.IID, &gitlab.UpdateIssueOptions{
|
||||
StateEvent: &state,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldFilter determines if an issue should be logged to this tracker
|
||||
func (i *Integration) ShouldFilter(event *output.ResultEvent) bool {
|
||||
|
||||
@ -3,7 +3,9 @@ package jira
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/andygrunwald/go-jira"
|
||||
"github.com/trivago/tgo/tcontainer"
|
||||
@ -47,6 +49,9 @@ type Integration struct {
|
||||
Formatter
|
||||
jira *jira.Client
|
||||
options *Options
|
||||
|
||||
once *sync.Once
|
||||
transitionID string
|
||||
}
|
||||
|
||||
// Options contains the configuration options for jira client
|
||||
@ -102,11 +107,20 @@ func New(options *Options) (*Integration, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Integration{jira: jiraClient, options: options}, nil
|
||||
integration := &Integration{
|
||||
jira: jiraClient,
|
||||
options: options,
|
||||
once: &sync.Once{},
|
||||
}
|
||||
return integration, nil
|
||||
}
|
||||
|
||||
func (i *Integration) Name() string {
|
||||
return "jira"
|
||||
}
|
||||
|
||||
// CreateNewIssue creates a new issue in the tracker
|
||||
func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
||||
func (i *Integration) CreateNewIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
|
||||
summary := format.Summary(event)
|
||||
labels := []string{}
|
||||
severityLabel := fmt.Sprintf("Severity:%s", event.Info.SeverityHolder.Severity.String())
|
||||
@ -127,7 +141,7 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
||||
for nestedName, nestedValue := range valueMap {
|
||||
fmtNestedValue, ok := nestedValue.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf(`couldn't iterate on nested item "%s": %s`, nestedName, nestedValue)
|
||||
return nil, fmt.Errorf(`couldn't iterate on nested item "%s": %s`, nestedName, nestedValue)
|
||||
}
|
||||
if strings.HasPrefix(fmtNestedValue, "$") {
|
||||
nestedValue = strings.TrimPrefix(fmtNestedValue, "$")
|
||||
@ -160,8 +174,10 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
||||
}
|
||||
}
|
||||
fields := &jira.IssueFields{
|
||||
Assignee: &jira.User{Name: i.options.AccountID},
|
||||
Description: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
||||
Unknowns: customFields,
|
||||
Labels: labels,
|
||||
Type: jira.IssueType{Name: i.options.IssueType},
|
||||
Project: jira.Project{Key: i.options.ProjectName},
|
||||
Summary: summary,
|
||||
@ -182,36 +198,92 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
||||
issueData := &jira.Issue{
|
||||
Fields: fields,
|
||||
}
|
||||
_, resp, err := i.jira.Issue.Create(issueData)
|
||||
createdIssue, resp, err := i.jira.Issue.Create(issueData)
|
||||
if err != nil {
|
||||
var data string
|
||||
if resp != nil && resp.Body != nil {
|
||||
d, _ := io.ReadAll(resp.Body)
|
||||
data = string(d)
|
||||
}
|
||||
return fmt.Errorf("%w => %s", err, data)
|
||||
return nil, fmt.Errorf("%w => %s", err, data)
|
||||
}
|
||||
return nil
|
||||
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
|
||||
}
|
||||
|
||||
// CreateIssue creates an issue in the tracker or updates the existing one
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
|
||||
if i.options.UpdateExisting {
|
||||
issueID, err := i.FindExistingIssue(event)
|
||||
issue, err := i.FindExistingIssue(event)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if issueID != "" {
|
||||
_, _, err = i.jira.Issue.AddComment(issueID, &jira.Comment{
|
||||
return nil, err
|
||||
} else if issue.ID != "" {
|
||||
_, _, err = i.jira.Issue.AddComment(issue.ID, &jira.Comment{
|
||||
Body: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
||||
})
|
||||
return err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return getIssueResponseFromJira(&issue)
|
||||
}
|
||||
}
|
||||
return i.CreateNewIssue(event)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// FindExistingIssue checks if the issue already exists and returns its ID
|
||||
func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, error) {
|
||||
func (i *Integration) FindExistingIssue(event *output.ResultEvent) (jira.Issue, error) {
|
||||
template := format.GetMatchedTemplateName(event)
|
||||
jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND status != \"%s\" AND project = \"%s\"", template, event.Host, i.options.StatusNot, i.options.ProjectName)
|
||||
|
||||
@ -226,17 +298,17 @@ func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, erro
|
||||
d, _ := io.ReadAll(resp.Body)
|
||||
data = string(d)
|
||||
}
|
||||
return "", fmt.Errorf("%w => %s", err, data)
|
||||
return jira.Issue{}, fmt.Errorf("%w => %s", err, data)
|
||||
}
|
||||
|
||||
switch resp.Total {
|
||||
case 0:
|
||||
return "", nil
|
||||
return jira.Issue{}, nil
|
||||
case 1:
|
||||
return chunk[0].ID, nil
|
||||
return chunk[0], nil
|
||||
default:
|
||||
gologger.Warning().Msgf("Discovered multiple opened issues %s for the host %s: The issue [%s] will be updated.", template, event.Host, chunk[0].ID)
|
||||
return chunk[0].ID, nil
|
||||
return chunk[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -114,6 +114,7 @@ func (e *TemplateExecuter) Execute(ctx *scan.ScanContext) (bool, error) {
|
||||
// try catching unknown panics
|
||||
if r := recover(); r != nil {
|
||||
ctx.LogError(fmt.Errorf("panic: %v", r))
|
||||
gologger.Verbose().Msgf("panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user