Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/reporting/trackers/gitea/gitea.go
2869 views
1
package gitea
2
3
import (
4
"fmt"
5
"net/url"
6
"strconv"
7
"strings"
8
9
"code.gitea.io/sdk/gitea"
10
"github.com/pkg/errors"
11
"github.com/projectdiscovery/nuclei/v3/pkg/output"
12
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
13
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"
14
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
15
"github.com/projectdiscovery/retryablehttp-go"
16
)
17
18
// Integration is a client for an issue tracker integration
19
type Integration struct {
20
client *gitea.Client
21
options *Options
22
}
23
24
// Options contains the configuration options for gitea issue tracker client
25
type Options struct {
26
// BaseURL (optional) is the self-hosted Gitea application url
27
BaseURL string `yaml:"base-url" validate:"omitempty,url"`
28
// Token is the token for gitea account.
29
Token string `yaml:"token" validate:"required"`
30
// ProjectOwner is the owner (user or org) of the repository.
31
ProjectOwner string `yaml:"project-owner" validate:"required"`
32
// ProjectName is the name of the repository.
33
ProjectName string `yaml:"project-name" validate:"required"`
34
// IssueLabel is the label of the created issue type
35
IssueLabel string `yaml:"issue-label"`
36
// SeverityAsLabel (optional) adds the severity as the label of the created
37
// issue.
38
SeverityAsLabel bool `yaml:"severity-as-label"`
39
// AllowList contains a list of allowed events for this tracker
40
AllowList *filters.Filter `yaml:"allow-list"`
41
// DenyList contains a list of denied events for this tracker
42
DenyList *filters.Filter `yaml:"deny-list"`
43
// DuplicateIssueCheck is a bool to enable duplicate tracking issue check and update the newest
44
DuplicateIssueCheck bool `yaml:"duplicate-issue-check" default:"false"`
45
// DuplicateIssuePageSize controls how many issues are fetched per page when searching for duplicates.
46
// If unset or <=0, a default of 100 is used.
47
DuplicateIssuePageSize int `yaml:"duplicate-issue-page-size" default:"100"`
48
// DuplicateIssueMaxPages limits how many pages are fetched when searching for duplicates.
49
// If unset or <=0, all pages are fetched until exhaustion.
50
DuplicateIssueMaxPages int `yaml:"duplicate-issue-max-pages" default:"0"`
51
52
HttpClient *retryablehttp.Client `yaml:"-"`
53
OmitRaw bool `yaml:"-"`
54
}
55
56
// New creates a new issue tracker integration client based on options.
57
func New(options *Options) (*Integration, error) {
58
59
var opts []gitea.ClientOption
60
opts = append(opts, gitea.SetToken(options.Token))
61
62
if options.HttpClient != nil {
63
opts = append(opts, gitea.SetHTTPClient(options.HttpClient.HTTPClient))
64
}
65
66
var remote string
67
if options.BaseURL != "" {
68
parsed, err := url.Parse(options.BaseURL)
69
if err != nil {
70
return nil, errors.Wrap(err, "could not parse custom baseurl")
71
}
72
if !strings.HasSuffix(parsed.Path, "/") {
73
parsed.Path += "/"
74
}
75
remote = parsed.String()
76
} else {
77
remote = `https://gitea.com/`
78
}
79
80
git, err := gitea.NewClient(remote, opts...)
81
if err != nil {
82
return nil, err
83
}
84
85
return &Integration{client: git, options: options}, nil
86
}
87
88
// CreateIssue creates an issue in the tracker
89
func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
90
summary := format.Summary(event)
91
description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)
92
93
labels := []string{}
94
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
95
if i.options.SeverityAsLabel && severityLabel != "" {
96
labels = append(labels, severityLabel)
97
}
98
if label := i.options.IssueLabel; label != "" {
99
labels = append(labels, label)
100
}
101
customLabels, err := i.getLabelIDsByNames(labels)
102
if err != nil {
103
return nil, err
104
}
105
106
var issue *gitea.Issue
107
if i.options.DuplicateIssueCheck {
108
issue, err = i.findIssueByTitle(summary)
109
if err != nil {
110
return nil, err
111
}
112
}
113
114
if issue == nil {
115
createdIssue, _, err := i.client.CreateIssue(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateIssueOption{
116
Title: summary,
117
Body: description,
118
Labels: customLabels,
119
})
120
if err != nil {
121
return nil, err
122
}
123
return &filters.CreateIssueResponse{
124
IssueID: strconv.FormatInt(createdIssue.Index, 10),
125
IssueURL: createdIssue.URL,
126
}, nil
127
}
128
129
_, _, err = i.client.CreateIssueComment(i.options.ProjectOwner, i.options.ProjectName, issue.Index, gitea.CreateIssueCommentOption{
130
Body: description,
131
})
132
if err != nil {
133
return nil, err
134
}
135
return &filters.CreateIssueResponse{
136
IssueID: strconv.FormatInt(issue.Index, 10),
137
IssueURL: issue.URL,
138
}, nil
139
}
140
141
func (i *Integration) CloseIssue(event *output.ResultEvent) error {
142
// TODO: Implement
143
return nil
144
}
145
146
// ShouldFilter determines if an issue should be logged to this tracker
147
func (i *Integration) ShouldFilter(event *output.ResultEvent) bool {
148
if i.options.AllowList != nil && !i.options.AllowList.GetMatch(event) {
149
return false
150
}
151
152
if i.options.DenyList != nil && i.options.DenyList.GetMatch(event) {
153
return false
154
}
155
156
return true
157
}
158
159
func (i *Integration) findIssueByTitle(title string) (*gitea.Issue, error) {
160
// Fetch issues in pages to ensure older issues are also checked for duplicates.
161
pageSize := i.options.DuplicateIssuePageSize
162
if pageSize <= 0 {
163
pageSize = 100
164
}
165
maxPages := i.options.DuplicateIssueMaxPages
166
167
opts := gitea.ListIssueOption{
168
State: "all",
169
ListOptions: gitea.ListOptions{
170
Page: 1,
171
PageSize: pageSize,
172
},
173
}
174
175
for {
176
if maxPages > 0 && opts.Page > maxPages {
177
return nil, nil
178
}
179
180
issueList, _, err := i.client.ListRepoIssues(i.options.ProjectOwner, i.options.ProjectName, opts)
181
if err != nil {
182
return nil, err
183
}
184
185
for _, issue := range issueList {
186
if issue.Title == title {
187
return issue, nil
188
}
189
}
190
191
if len(issueList) < opts.PageSize {
192
// Last page reached.
193
return nil, nil
194
}
195
196
opts.Page++
197
}
198
}
199
200
func (i *Integration) getLabelIDsByNames(labels []string) ([]int64, error) {
201
202
var ids []int64
203
204
existingLabels, _, err := i.client.ListRepoLabels(i.options.ProjectOwner, i.options.ProjectName, gitea.ListLabelsOptions{
205
ListOptions: gitea.ListOptions{Page: -1},
206
})
207
if err != nil {
208
return nil, err
209
}
210
211
getLabel := func(name string) int64 {
212
for _, existingLabel := range existingLabels {
213
if existingLabel.Name == name {
214
return existingLabel.ID
215
}
216
}
217
return -1
218
}
219
220
for _, label := range labels {
221
labelID := getLabel(label)
222
if labelID == -1 {
223
newLabel, _, err := i.client.CreateLabel(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateLabelOption{
224
Name: label,
225
Color: `#00aabb`,
226
Description: label,
227
})
228
if err != nil {
229
return nil, err
230
}
231
232
ids = append(ids, newLabel.ID)
233
} else {
234
ids = append(ids, labelID)
235
}
236
}
237
238
return ids, nil
239
}
240
241
func (i *Integration) Name() string {
242
return "gitea"
243
}
244
245