Path: blob/dev/pkg/reporting/trackers/gitlab/gitlab.go
2843 views
package gitlab12import (3"fmt"4"strconv"56gitlab "gitlab.com/gitlab-org/api/client-go"78"github.com/projectdiscovery/nuclei/v3/pkg/output"9"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"10"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"11"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"12"github.com/projectdiscovery/retryablehttp-go"13)1415// Integration is a client for an issue tracker integration16type Integration struct {17client *gitlab.Client18userID int19options *Options20}2122// Options contains the configuration options for gitlab issue tracker client23type Options struct {24// BaseURL (optional) is the self-hosted gitlab application url25BaseURL string `yaml:"base-url" validate:"omitempty,url"`26// Username is the username of the gitlab user27Username string `yaml:"username" validate:"required"`28// Token is the token for gitlab account.29Token string `yaml:"token" validate:"required"`30// ProjectName is the name of the repository.31ProjectName string `yaml:"project-name" validate:"required"`32// IssueLabel is the label of the created issue type33IssueLabel string `yaml:"issue-label"`34// SeverityAsLabel (optional) sends the severity as the label of the created35// issue.36SeverityAsLabel bool `yaml:"severity-as-label"`37// AllowList contains a list of allowed events for this tracker38AllowList *filters.Filter `yaml:"allow-list"`39// DenyList contains a list of denied events for this tracker40DenyList *filters.Filter `yaml:"deny-list"`41// DuplicateIssueCheck is a bool to enable duplicate tracking issue check and update the newest42DuplicateIssueCheck bool `yaml:"duplicate-issue-check" default:"false"`43// DuplicateIssuePageSize controls how many issues are fetched per page when searching for duplicates.44// If unset or <=0, a default of 100 is used.45DuplicateIssuePageSize int `yaml:"duplicate-issue-page-size" default:"100"`46// DuplicateIssueMaxPages limits how many pages are fetched when searching for duplicates.47// If unset or <=0, all pages are fetched until exhaustion.48DuplicateIssueMaxPages int `yaml:"duplicate-issue-max-pages" default:"0"`4950HttpClient *retryablehttp.Client `yaml:"-"`51OmitRaw bool `yaml:"-"`52}5354// New creates a new issue tracker integration client based on options.55func New(options *Options) (*Integration, error) {56gitlabOpts := []gitlab.ClientOptionFunc{}57if options.BaseURL != "" {58gitlabOpts = append(gitlabOpts, gitlab.WithBaseURL(options.BaseURL))59}60if options.HttpClient != nil {61gitlabOpts = append(gitlabOpts, gitlab.WithHTTPClient(options.HttpClient.HTTPClient))62}63git, err := gitlab.NewClient(options.Token, gitlabOpts...)64if err != nil {65return nil, err66}67user, _, err := git.Users.CurrentUser()68if err != nil {69return nil, err70}71return &Integration{client: git, userID: user.ID, options: options}, nil72}7374// CreateIssue creates an issue in the tracker75func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {76summary := format.Summary(event)77description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)78labels := []string{}79severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())80if i.options.SeverityAsLabel && severityLabel != "" {81labels = append(labels, severityLabel)82}83if label := i.options.IssueLabel; label != "" {84labels = append(labels, label)85}86customLabels := gitlab.LabelOptions(labels)87assigneeIDs := []int{i.userID}8889var issue *gitlab.Issue90if i.options.DuplicateIssueCheck {91var err error92issue, err = i.findIssueByTitle(summary)93if err != nil {94return nil, err95}96}9798if issue != nil {99_, _, err := i.client.Notes.CreateIssueNote(i.options.ProjectName, issue.IID, &gitlab.CreateIssueNoteOptions{100Body: &description,101})102if err != nil {103return nil, err104}105if issue.State == "closed" {106reopen := "reopen"107_, _, err := i.client.Issues.UpdateIssue(i.options.ProjectName, issue.IID, &gitlab.UpdateIssueOptions{108StateEvent: &reopen,109})110if err != nil {111return nil, err112}113}114return &filters.CreateIssueResponse{115IssueID: strconv.FormatInt(int64(issue.ID), 10),116IssueURL: issue.WebURL,117}, nil118}119createdIssue, _, err := i.client.Issues.CreateIssue(i.options.ProjectName, &gitlab.CreateIssueOptions{120Title: &summary,121Description: &description,122Labels: &customLabels,123AssigneeIDs: &assigneeIDs,124})125if err != nil {126return nil, err127}128return &filters.CreateIssueResponse{129IssueID: strconv.FormatInt(int64(createdIssue.ID), 10),130IssueURL: createdIssue.WebURL,131}, nil132}133134func (i *Integration) Name() string {135return "gitlab"136}137138func (i *Integration) CloseIssue(event *output.ResultEvent) error {139summary := format.Summary(event)140issue, err := i.findIssueByTitle(summary)141if err != nil {142return err143}144if issue == nil {145return nil146}147148state := "close"149_, _, err = i.client.Issues.UpdateIssue(i.options.ProjectName, issue.IID, &gitlab.UpdateIssueOptions{150StateEvent: &state,151})152if err != nil {153return err154}155return nil156}157158func (i *Integration) findIssueByTitle(title string) (*gitlab.Issue, error) {159pageSize := i.options.DuplicateIssuePageSize160if pageSize <= 0 {161pageSize = 100162}163maxPages := i.options.DuplicateIssueMaxPages164165searchIn := "title"166searchState := "all"167page := 1168169for {170if maxPages > 0 && page > maxPages {171return nil, nil172}173174issues, _, err := i.client.Issues.ListProjectIssues(i.options.ProjectName, &gitlab.ListProjectIssuesOptions{175In: &searchIn,176State: &searchState,177Search: &title,178ListOptions: gitlab.ListOptions{179Page: page,180PerPage: pageSize,181},182})183if err != nil {184return nil, err185}186187for _, issue := range issues {188if issue.Title == title {189return issue, nil190}191}192193if len(issues) < pageSize {194return nil, nil195}196197page++198}199}200201// ShouldFilter determines if an issue should be logged to this tracker202func (i *Integration) ShouldFilter(event *output.ResultEvent) bool {203if i.options.AllowList != nil && !i.options.AllowList.GetMatch(event) {204return false205}206207if i.options.DenyList != nil && i.options.DenyList.GetMatch(event) {208return false209}210211return true212}213214215