Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/reporting/trackers/gitlab/gitlab.go
2843 views
1
package gitlab
2
3
import (
4
"fmt"
5
"strconv"
6
7
gitlab "gitlab.com/gitlab-org/api/client-go"
8
9
"github.com/projectdiscovery/nuclei/v3/pkg/output"
10
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
11
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"
12
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
13
"github.com/projectdiscovery/retryablehttp-go"
14
)
15
16
// Integration is a client for an issue tracker integration
17
type Integration struct {
18
client *gitlab.Client
19
userID int
20
options *Options
21
}
22
23
// Options contains the configuration options for gitlab issue tracker client
24
type Options struct {
25
// BaseURL (optional) is the self-hosted gitlab application url
26
BaseURL string `yaml:"base-url" validate:"omitempty,url"`
27
// Username is the username of the gitlab user
28
Username string `yaml:"username" validate:"required"`
29
// Token is the token for gitlab account.
30
Token string `yaml:"token" validate:"required"`
31
// ProjectName is the name of the repository.
32
ProjectName string `yaml:"project-name" validate:"required"`
33
// IssueLabel is the label of the created issue type
34
IssueLabel string `yaml:"issue-label"`
35
// SeverityAsLabel (optional) sends the severity as the label of the created
36
// issue.
37
SeverityAsLabel bool `yaml:"severity-as-label"`
38
// AllowList contains a list of allowed events for this tracker
39
AllowList *filters.Filter `yaml:"allow-list"`
40
// DenyList contains a list of denied events for this tracker
41
DenyList *filters.Filter `yaml:"deny-list"`
42
// DuplicateIssueCheck is a bool to enable duplicate tracking issue check and update the newest
43
DuplicateIssueCheck bool `yaml:"duplicate-issue-check" default:"false"`
44
// DuplicateIssuePageSize controls how many issues are fetched per page when searching for duplicates.
45
// If unset or <=0, a default of 100 is used.
46
DuplicateIssuePageSize int `yaml:"duplicate-issue-page-size" default:"100"`
47
// DuplicateIssueMaxPages limits how many pages are fetched when searching for duplicates.
48
// If unset or <=0, all pages are fetched until exhaustion.
49
DuplicateIssueMaxPages int `yaml:"duplicate-issue-max-pages" default:"0"`
50
51
HttpClient *retryablehttp.Client `yaml:"-"`
52
OmitRaw bool `yaml:"-"`
53
}
54
55
// New creates a new issue tracker integration client based on options.
56
func New(options *Options) (*Integration, error) {
57
gitlabOpts := []gitlab.ClientOptionFunc{}
58
if options.BaseURL != "" {
59
gitlabOpts = append(gitlabOpts, gitlab.WithBaseURL(options.BaseURL))
60
}
61
if options.HttpClient != nil {
62
gitlabOpts = append(gitlabOpts, gitlab.WithHTTPClient(options.HttpClient.HTTPClient))
63
}
64
git, err := gitlab.NewClient(options.Token, gitlabOpts...)
65
if err != nil {
66
return nil, err
67
}
68
user, _, err := git.Users.CurrentUser()
69
if err != nil {
70
return nil, err
71
}
72
return &Integration{client: git, userID: user.ID, options: options}, nil
73
}
74
75
// CreateIssue creates an issue in the tracker
76
func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
77
summary := format.Summary(event)
78
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
79
labels := []string{}
80
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
81
if i.options.SeverityAsLabel && severityLabel != "" {
82
labels = append(labels, severityLabel)
83
}
84
if label := i.options.IssueLabel; label != "" {
85
labels = append(labels, label)
86
}
87
customLabels := gitlab.LabelOptions(labels)
88
assigneeIDs := []int{i.userID}
89
90
var issue *gitlab.Issue
91
if i.options.DuplicateIssueCheck {
92
var err error
93
issue, err = i.findIssueByTitle(summary)
94
if err != nil {
95
return nil, err
96
}
97
}
98
99
if issue != nil {
100
_, _, err := i.client.Notes.CreateIssueNote(i.options.ProjectName, issue.IID, &gitlab.CreateIssueNoteOptions{
101
Body: &description,
102
})
103
if err != nil {
104
return nil, err
105
}
106
if issue.State == "closed" {
107
reopen := "reopen"
108
_, _, err := i.client.Issues.UpdateIssue(i.options.ProjectName, issue.IID, &gitlab.UpdateIssueOptions{
109
StateEvent: &reopen,
110
})
111
if err != nil {
112
return nil, err
113
}
114
}
115
return &filters.CreateIssueResponse{
116
IssueID: strconv.FormatInt(int64(issue.ID), 10),
117
IssueURL: issue.WebURL,
118
}, nil
119
}
120
createdIssue, _, err := i.client.Issues.CreateIssue(i.options.ProjectName, &gitlab.CreateIssueOptions{
121
Title: &summary,
122
Description: &description,
123
Labels: &customLabels,
124
AssigneeIDs: &assigneeIDs,
125
})
126
if err != nil {
127
return nil, err
128
}
129
return &filters.CreateIssueResponse{
130
IssueID: strconv.FormatInt(int64(createdIssue.ID), 10),
131
IssueURL: createdIssue.WebURL,
132
}, nil
133
}
134
135
func (i *Integration) Name() string {
136
return "gitlab"
137
}
138
139
func (i *Integration) CloseIssue(event *output.ResultEvent) error {
140
summary := format.Summary(event)
141
issue, err := i.findIssueByTitle(summary)
142
if err != nil {
143
return err
144
}
145
if issue == nil {
146
return nil
147
}
148
149
state := "close"
150
_, _, err = i.client.Issues.UpdateIssue(i.options.ProjectName, issue.IID, &gitlab.UpdateIssueOptions{
151
StateEvent: &state,
152
})
153
if err != nil {
154
return err
155
}
156
return nil
157
}
158
159
func (i *Integration) findIssueByTitle(title string) (*gitlab.Issue, error) {
160
pageSize := i.options.DuplicateIssuePageSize
161
if pageSize <= 0 {
162
pageSize = 100
163
}
164
maxPages := i.options.DuplicateIssueMaxPages
165
166
searchIn := "title"
167
searchState := "all"
168
page := 1
169
170
for {
171
if maxPages > 0 && page > maxPages {
172
return nil, nil
173
}
174
175
issues, _, err := i.client.Issues.ListProjectIssues(i.options.ProjectName, &gitlab.ListProjectIssuesOptions{
176
In: &searchIn,
177
State: &searchState,
178
Search: &title,
179
ListOptions: gitlab.ListOptions{
180
Page: page,
181
PerPage: pageSize,
182
},
183
})
184
if err != nil {
185
return nil, err
186
}
187
188
for _, issue := range issues {
189
if issue.Title == title {
190
return issue, nil
191
}
192
}
193
194
if len(issues) < pageSize {
195
return nil, nil
196
}
197
198
page++
199
}
200
}
201
202
// ShouldFilter determines if an issue should be logged to this tracker
203
func (i *Integration) ShouldFilter(event *output.ResultEvent) bool {
204
if i.options.AllowList != nil && !i.options.AllowList.GetMatch(event) {
205
return false
206
}
207
208
if i.options.DenyList != nil && i.options.DenyList.GetMatch(event) {
209
return false
210
}
211
212
return true
213
}
214
215