Path: blob/dev/pkg/reporting/trackers/gitea/gitea.go
2869 views
package gitea12import (3"fmt"4"net/url"5"strconv"6"strings"78"code.gitea.io/sdk/gitea"9"github.com/pkg/errors"10"github.com/projectdiscovery/nuclei/v3/pkg/output"11"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"12"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"13"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"14"github.com/projectdiscovery/retryablehttp-go"15)1617// Integration is a client for an issue tracker integration18type Integration struct {19client *gitea.Client20options *Options21}2223// Options contains the configuration options for gitea issue tracker client24type Options struct {25// BaseURL (optional) is the self-hosted Gitea application url26BaseURL string `yaml:"base-url" validate:"omitempty,url"`27// Token is the token for gitea account.28Token string `yaml:"token" validate:"required"`29// ProjectOwner is the owner (user or org) of the repository.30ProjectOwner string `yaml:"project-owner" validate:"required"`31// ProjectName is the name of the repository.32ProjectName string `yaml:"project-name" validate:"required"`33// IssueLabel is the label of the created issue type34IssueLabel string `yaml:"issue-label"`35// SeverityAsLabel (optional) adds the severity as the label of the created36// issue.37SeverityAsLabel bool `yaml:"severity-as-label"`38// AllowList contains a list of allowed events for this tracker39AllowList *filters.Filter `yaml:"allow-list"`40// DenyList contains a list of denied events for this tracker41DenyList *filters.Filter `yaml:"deny-list"`42// DuplicateIssueCheck is a bool to enable duplicate tracking issue check and update the newest43DuplicateIssueCheck 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.46DuplicateIssuePageSize 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.49DuplicateIssueMaxPages int `yaml:"duplicate-issue-max-pages" default:"0"`5051HttpClient *retryablehttp.Client `yaml:"-"`52OmitRaw bool `yaml:"-"`53}5455// New creates a new issue tracker integration client based on options.56func New(options *Options) (*Integration, error) {5758var opts []gitea.ClientOption59opts = append(opts, gitea.SetToken(options.Token))6061if options.HttpClient != nil {62opts = append(opts, gitea.SetHTTPClient(options.HttpClient.HTTPClient))63}6465var remote string66if options.BaseURL != "" {67parsed, err := url.Parse(options.BaseURL)68if err != nil {69return nil, errors.Wrap(err, "could not parse custom baseurl")70}71if !strings.HasSuffix(parsed.Path, "/") {72parsed.Path += "/"73}74remote = parsed.String()75} else {76remote = `https://gitea.com/`77}7879git, err := gitea.NewClient(remote, opts...)80if err != nil {81return nil, err82}8384return &Integration{client: git, options: options}, nil85}8687// CreateIssue creates an issue in the tracker88func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {89summary := format.Summary(event)90description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)9192labels := []string{}93severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())94if i.options.SeverityAsLabel && severityLabel != "" {95labels = append(labels, severityLabel)96}97if label := i.options.IssueLabel; label != "" {98labels = append(labels, label)99}100customLabels, err := i.getLabelIDsByNames(labels)101if err != nil {102return nil, err103}104105var issue *gitea.Issue106if i.options.DuplicateIssueCheck {107issue, err = i.findIssueByTitle(summary)108if err != nil {109return nil, err110}111}112113if issue == nil {114createdIssue, _, err := i.client.CreateIssue(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateIssueOption{115Title: summary,116Body: description,117Labels: customLabels,118})119if err != nil {120return nil, err121}122return &filters.CreateIssueResponse{123IssueID: strconv.FormatInt(createdIssue.Index, 10),124IssueURL: createdIssue.URL,125}, nil126}127128_, _, err = i.client.CreateIssueComment(i.options.ProjectOwner, i.options.ProjectName, issue.Index, gitea.CreateIssueCommentOption{129Body: description,130})131if err != nil {132return nil, err133}134return &filters.CreateIssueResponse{135IssueID: strconv.FormatInt(issue.Index, 10),136IssueURL: issue.URL,137}, nil138}139140func (i *Integration) CloseIssue(event *output.ResultEvent) error {141// TODO: Implement142return nil143}144145// ShouldFilter determines if an issue should be logged to this tracker146func (i *Integration) ShouldFilter(event *output.ResultEvent) bool {147if i.options.AllowList != nil && !i.options.AllowList.GetMatch(event) {148return false149}150151if i.options.DenyList != nil && i.options.DenyList.GetMatch(event) {152return false153}154155return true156}157158func (i *Integration) findIssueByTitle(title string) (*gitea.Issue, error) {159// Fetch issues in pages to ensure older issues are also checked for duplicates.160pageSize := i.options.DuplicateIssuePageSize161if pageSize <= 0 {162pageSize = 100163}164maxPages := i.options.DuplicateIssueMaxPages165166opts := gitea.ListIssueOption{167State: "all",168ListOptions: gitea.ListOptions{169Page: 1,170PageSize: pageSize,171},172}173174for {175if maxPages > 0 && opts.Page > maxPages {176return nil, nil177}178179issueList, _, err := i.client.ListRepoIssues(i.options.ProjectOwner, i.options.ProjectName, opts)180if err != nil {181return nil, err182}183184for _, issue := range issueList {185if issue.Title == title {186return issue, nil187}188}189190if len(issueList) < opts.PageSize {191// Last page reached.192return nil, nil193}194195opts.Page++196}197}198199func (i *Integration) getLabelIDsByNames(labels []string) ([]int64, error) {200201var ids []int64202203existingLabels, _, err := i.client.ListRepoLabels(i.options.ProjectOwner, i.options.ProjectName, gitea.ListLabelsOptions{204ListOptions: gitea.ListOptions{Page: -1},205})206if err != nil {207return nil, err208}209210getLabel := func(name string) int64 {211for _, existingLabel := range existingLabels {212if existingLabel.Name == name {213return existingLabel.ID214}215}216return -1217}218219for _, label := range labels {220labelID := getLabel(label)221if labelID == -1 {222newLabel, _, err := i.client.CreateLabel(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateLabelOption{223Name: label,224Color: `#00aabb`,225Description: label,226})227if err != nil {228return nil, err229}230231ids = append(ids, newLabel.ID)232} else {233ids = append(ids, labelID)234}235}236237return ids, nil238}239240func (i *Integration) Name() string {241return "gitea"242}243244245