mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 18:45:28 +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()
|
defer cache.Close()
|
||||||
|
|
||||||
mockProgress := &testutils.MockProgressClient{}
|
mockProgress := &testutils.MockProgressClient{}
|
||||||
reportingClient, err := reporting.New(&reporting.Options{}, "")
|
reportingClient, err := reporting.New(&reporting.Options{}, "", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// configureOutput configures the output logging levels to be displayed on the screen
|
||||||
func configureOutput(options *types.Options) {
|
func configureOutput(options *types.Options) {
|
||||||
// If the user desires verbose output, show verbose output
|
// 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 {
|
if options.Debug || options.DebugRequests || options.DebugResponse {
|
||||||
gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug)
|
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 {
|
if options.NoColor {
|
||||||
gologger.DefaultLogger.SetFormatter(formatter.NewCLI(true))
|
gologger.DefaultLogger.SetFormatter(formatter.NewCLI(true))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -190,7 +190,7 @@ func New(options *types.Options) (*Runner, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reportingOptions != nil {
|
if reportingOptions != nil {
|
||||||
client, err := reporting.New(reportingOptions, options.ReportingDB)
|
client, err := reporting.New(reportingOptions, options.ReportingDB, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "could not create issue reporting client")
|
return nil, errors.Wrap(err, "could not create issue reporting client")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,7 +128,7 @@ func (e *NucleiEngine) init() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// we don't support reporting config in sdk mode
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
e.interactshOpts.IssuesClient = e.rc
|
e.interactshOpts.IssuesClient = e.rc
|
||||||
|
|||||||
@ -165,10 +165,20 @@ type ResultEvent struct {
|
|||||||
// Lines is the line count for the specified match
|
// Lines is the line count for the specified match
|
||||||
Lines []int `json:"matched-line,omitempty"`
|
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:"-"`
|
FileToIndexPosition map[string]int `json:"-"`
|
||||||
Error string `json:"error,omitempty"`
|
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
|
// NewStandardWriter creates a new output writer based on user configurations
|
||||||
func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
|
func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
|
||||||
resumeBool := false
|
resumeBool := false
|
||||||
|
|||||||
@ -17,6 +17,11 @@ func WriteResult(data *output.InternalWrappedEvent, output output.Writer, progre
|
|||||||
}
|
}
|
||||||
var matched bool
|
var matched bool
|
||||||
for _, result := range data.Results {
|
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 {
|
if err := output.Write(result); err != nil {
|
||||||
gologger.Warning().Msgf("Could not write output event: %s\n", err)
|
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
|
matched = true
|
||||||
}
|
}
|
||||||
progress.IncrementMatched()
|
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
|
return matched
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,5 +11,6 @@ type Client interface {
|
|||||||
Close()
|
Close()
|
||||||
Clear()
|
Clear()
|
||||||
CreateIssue(event *output.ResultEvent) error
|
CreateIssue(event *output.ResultEvent) error
|
||||||
|
CloseIssue(event *output.ResultEvent) error
|
||||||
GetReportingOptions() *Options
|
GetReportingOptions() *Options
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,13 @@ func GetMatchedTemplateName(event *output.ResultEvent) string {
|
|||||||
return matchedTemplateName
|
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 {
|
func CreateReportDescription(event *output.ResultEvent, formatter ResultFormatter, omitRaw bool) string {
|
||||||
template := GetMatchedTemplateName(event)
|
template := GetMatchedTemplateName(event)
|
||||||
builder := &bytes.Buffer{}
|
builder := &bytes.Buffer{}
|
||||||
@ -137,6 +144,12 @@ func CreateReportDescription(event *output.ResultEvent, formatter ResultFormatte
|
|||||||
|
|
||||||
builder.WriteString("\n" + formatter.CreateHorizontalLine() + "\n")
|
builder.WriteString("\n" + formatter.CreateHorizontalLine() + "\n")
|
||||||
builder.WriteString(fmt.Sprintf("Generated by %s", formatter.CreateLink("Nuclei "+config.Version, "https://github.com/projectdiscovery/nuclei")))
|
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()
|
data := builder.String()
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
package reporting
|
package reporting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/projectdiscovery/gologger"
|
||||||
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
||||||
json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
|
json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
|
||||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
|
||||||
@ -35,8 +39,12 @@ var (
|
|||||||
|
|
||||||
// Tracker is an interface implemented by an issue tracker
|
// Tracker is an interface implemented by an issue tracker
|
||||||
type Tracker interface {
|
type Tracker interface {
|
||||||
|
// Name returns the name of the tracker
|
||||||
|
Name() string
|
||||||
// CreateIssue creates an issue in the tracker
|
// 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 determines if the event should be filtered out
|
||||||
ShouldFilter(event *output.ResultEvent) bool
|
ShouldFilter(event *output.ResultEvent) bool
|
||||||
}
|
}
|
||||||
@ -55,10 +63,17 @@ type ReportingClient struct {
|
|||||||
exporters []Exporter
|
exporters []Exporter
|
||||||
options *Options
|
options *Options
|
||||||
dedupe *dedupe.Storage
|
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
|
// 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}
|
client := &ReportingClient{options: options}
|
||||||
|
|
||||||
if options.GitHub != nil {
|
if options.GitHub != nil {
|
||||||
@ -142,6 +157,20 @@ func New(options *Options, db string) (Client, error) {
|
|||||||
client.exporters = append(client.exporters, exporter)
|
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)
|
storage, err := dedupe.New(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -195,7 +224,30 @@ func (c *ReportingClient) RegisterExporter(exporter Exporter) {
|
|||||||
|
|
||||||
// Close closes the issue tracker reporting client
|
// Close closes the issue tracker reporting client
|
||||||
func (c *ReportingClient) Close() {
|
func (c *ReportingClient) Close() {
|
||||||
c.dedupe.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 {
|
for _, exporter := range c.exporters {
|
||||||
exporter.Close()
|
exporter.Close()
|
||||||
}
|
}
|
||||||
@ -211,15 +263,37 @@ func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
|
|||||||
return nil
|
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 {
|
if unique {
|
||||||
|
event.IssueTrackers = make(map[string]output.IssueTrackerMetadata)
|
||||||
|
|
||||||
for _, tracker := range c.trackers {
|
for _, tracker := range c.trackers {
|
||||||
// process tracker specific allow/deny list
|
// process tracker specific allow/deny list
|
||||||
if tracker.ShouldFilter(event) {
|
if tracker.ShouldFilter(event) {
|
||||||
continue
|
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)
|
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 {
|
for _, exporter := range c.exporters {
|
||||||
@ -231,10 +305,25 @@ func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
|
|||||||
return err
|
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 {
|
func (c *ReportingClient) GetReportingOptions() *Options {
|
||||||
return c.options
|
return c.options
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ReportingClient) Clear() {
|
func (c *ReportingClient) Clear() {
|
||||||
c.dedupe.Clear()
|
if c.dedupe != nil {
|
||||||
|
c.dedupe.Clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,13 @@ import (
|
|||||||
sliceutil "github.com/projectdiscovery/utils/slice"
|
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
|
// Filter filters the received event and decides whether to perform
|
||||||
// reporting for it or not.
|
// reporting for it or not.
|
||||||
type Filter struct {
|
type Filter struct {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package gitea
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
@ -79,7 +80,7 @@ func New(options *Options) (*Integration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateIssue creates an issue in the tracker
|
// 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)
|
summary := format.Summary(event)
|
||||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
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)
|
customLabels, err := i.getLabelIDsByNames(labels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var issue *gitea.Issue
|
var issue *gitea.Issue
|
||||||
if i.options.DuplicateIssueCheck {
|
if i.options.DuplicateIssueCheck {
|
||||||
issue, err = i.findIssueByTitle(summary)
|
issue, err = i.findIssueByTitle(summary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue == nil {
|
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,
|
Title: summary,
|
||||||
Body: description,
|
Body: description,
|
||||||
Labels: customLabels,
|
Labels: customLabels,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
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{
|
_, _, err = i.client.CreateIssueComment(i.options.ProjectOwner, i.options.ProjectName, issue.Index, gitea.CreateIssueCommentOption{
|
||||||
Body: description,
|
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
|
// 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
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Integration) Name() string {
|
||||||
|
return "gitea"
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
@ -85,7 +86,7 @@ func New(options *Options) (*Integration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateIssue creates an issue in the tracker
|
// 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)
|
summary := format.Summary(event)
|
||||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
||||||
labels := []string{}
|
labels := []string{}
|
||||||
@ -99,11 +100,12 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) (err error) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var err error
|
||||||
var existingIssue *github.Issue
|
var existingIssue *github.Issue
|
||||||
if i.options.DuplicateIssueCheck {
|
if i.options.DuplicateIssueCheck {
|
||||||
existingIssue, err = i.findIssueByTitle(ctx, summary)
|
existingIssue, err = i.findIssueByTitle(ctx, summary)
|
||||||
if err != nil && !errors.Is(err, io.EOF) {
|
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,
|
Labels: &labels,
|
||||||
Assignees: &[]string{i.options.Username},
|
Assignees: &[]string{i.options.Username},
|
||||||
}
|
}
|
||||||
_, _, err = i.client.Issues.Create(ctx, i.options.Owner, i.options.ProjectName, req)
|
createdIssue, _, err := i.client.Issues.Create(ctx, i.options.Owner, i.options.ProjectName, req)
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &filters.CreateIssueResponse{
|
||||||
|
IssueID: strconv.FormatInt(createdIssue.GetID(), 10),
|
||||||
|
IssueURL: createdIssue.GetHTMLURL(),
|
||||||
|
}, nil
|
||||||
} else {
|
} else {
|
||||||
if existingIssue.GetState() == "closed" {
|
if existingIssue.GetState() == "closed" {
|
||||||
stateOpen := "open"
|
stateOpen := "open"
|
||||||
if _, _, err := i.client.Issues.Edit(ctx, i.options.Owner, i.options.ProjectName, *existingIssue.Number, &github.IssueRequest{
|
if _, _, err := i.client.Issues.Edit(ctx, i.options.Owner, i.options.ProjectName, *existingIssue.Number, &github.IssueRequest{
|
||||||
State: &stateOpen,
|
State: &stateOpen,
|
||||||
}); err != nil {
|
}); 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,
|
Body: &description,
|
||||||
}
|
}
|
||||||
_, _, err = i.client.Issues.CreateComment(ctx, i.options.Owner, i.options.ProjectName, *existingIssue.Number, req)
|
_, _, 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
|
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
|
// ShouldFilter determines if an issue should be logged to this tracker
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package gitlab
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ func New(options *Options) (*Integration, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateIssue creates an issue in the tracker
|
// 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)
|
summary := format.Summary(event)
|
||||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
|
||||||
labels := []string{}
|
labels := []string{}
|
||||||
@ -88,7 +89,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
|||||||
Search: &summary,
|
Search: &summary,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(issues) > 0 {
|
if len(issues) > 0 {
|
||||||
issue := issues[0]
|
issue := issues[0]
|
||||||
@ -96,7 +97,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
|||||||
Body: &description,
|
Body: &description,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if issue.State == "closed" {
|
if issue.State == "closed" {
|
||||||
reopen := "reopen"
|
reopen := "reopen"
|
||||||
@ -105,17 +106,60 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
|||||||
})
|
})
|
||||||
fmt.Sprintln(resp, err)
|
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,
|
Title: &summary,
|
||||||
Description: &description,
|
Description: &description,
|
||||||
Labels: &customLabels,
|
Labels: &customLabels,
|
||||||
AssigneeIDs: &assigneeIDs,
|
AssigneeIDs: &assigneeIDs,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &filters.CreateIssueResponse{
|
||||||
|
IssueID: strconv.FormatInt(int64(createdIssue.ID), 10),
|
||||||
|
IssueURL: createdIssue.WebURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
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
|
// ShouldFilter determines if an issue should be logged to this tracker
|
||||||
|
|||||||
@ -3,7 +3,9 @@ package jira
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/andygrunwald/go-jira"
|
"github.com/andygrunwald/go-jira"
|
||||||
"github.com/trivago/tgo/tcontainer"
|
"github.com/trivago/tgo/tcontainer"
|
||||||
@ -47,6 +49,9 @@ type Integration struct {
|
|||||||
Formatter
|
Formatter
|
||||||
jira *jira.Client
|
jira *jira.Client
|
||||||
options *Options
|
options *Options
|
||||||
|
|
||||||
|
once *sync.Once
|
||||||
|
transitionID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options contains the configuration options for jira client
|
// Options contains the configuration options for jira client
|
||||||
@ -102,11 +107,20 @@ func New(options *Options) (*Integration, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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)
|
summary := format.Summary(event)
|
||||||
labels := []string{}
|
labels := []string{}
|
||||||
severityLabel := fmt.Sprintf("Severity:%s", event.Info.SeverityHolder.Severity.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 {
|
for nestedName, nestedValue := range valueMap {
|
||||||
fmtNestedValue, ok := nestedValue.(string)
|
fmtNestedValue, ok := nestedValue.(string)
|
||||||
if !ok {
|
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, "$") {
|
if strings.HasPrefix(fmtNestedValue, "$") {
|
||||||
nestedValue = strings.TrimPrefix(fmtNestedValue, "$")
|
nestedValue = strings.TrimPrefix(fmtNestedValue, "$")
|
||||||
@ -160,8 +174,10 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fields := &jira.IssueFields{
|
fields := &jira.IssueFields{
|
||||||
|
Assignee: &jira.User{Name: i.options.AccountID},
|
||||||
Description: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
Description: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
||||||
Unknowns: customFields,
|
Unknowns: customFields,
|
||||||
|
Labels: labels,
|
||||||
Type: jira.IssueType{Name: i.options.IssueType},
|
Type: jira.IssueType{Name: i.options.IssueType},
|
||||||
Project: jira.Project{Key: i.options.ProjectName},
|
Project: jira.Project{Key: i.options.ProjectName},
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
@ -182,36 +198,92 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
|||||||
issueData := &jira.Issue{
|
issueData := &jira.Issue{
|
||||||
Fields: fields,
|
Fields: fields,
|
||||||
}
|
}
|
||||||
_, resp, err := i.jira.Issue.Create(issueData)
|
createdIssue, resp, err := i.jira.Issue.Create(issueData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var data string
|
var data string
|
||||||
if resp != nil && resp.Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
d, _ := io.ReadAll(resp.Body)
|
d, _ := io.ReadAll(resp.Body)
|
||||||
data = string(d)
|
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
|
// 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 {
|
if i.options.UpdateExisting {
|
||||||
issueID, err := i.FindExistingIssue(event)
|
issue, err := i.FindExistingIssue(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
} else if issueID != "" {
|
} else if issue.ID != "" {
|
||||||
_, _, err = i.jira.Issue.AddComment(issueID, &jira.Comment{
|
_, _, err = i.jira.Issue.AddComment(issue.ID, &jira.Comment{
|
||||||
Body: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
Body: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
||||||
})
|
})
|
||||||
return err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return getIssueResponseFromJira(&issue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return i.CreateNewIssue(event)
|
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
|
// 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)
|
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)
|
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)
|
d, _ := io.ReadAll(resp.Body)
|
||||||
data = string(d)
|
data = string(d)
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("%w => %s", err, data)
|
return jira.Issue{}, fmt.Errorf("%w => %s", err, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch resp.Total {
|
switch resp.Total {
|
||||||
case 0:
|
case 0:
|
||||||
return "", nil
|
return jira.Issue{}, nil
|
||||||
case 1:
|
case 1:
|
||||||
return chunk[0].ID, nil
|
return chunk[0], nil
|
||||||
default:
|
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)
|
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
|
// try catching unknown panics
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
ctx.LogError(fmt.Errorf("panic: %v", r))
|
ctx.LogError(fmt.Errorf("panic: %v", r))
|
||||||
|
gologger.Verbose().Msgf("panic: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user