Path: blob/dev/pkg/reporting/trackers/gitea/gitea.go
2070 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"`4445HttpClient *retryablehttp.Client `yaml:"-"`46OmitRaw bool `yaml:"-"`47}4849// New creates a new issue tracker integration client based on options.50func New(options *Options) (*Integration, error) {5152var opts []gitea.ClientOption53opts = append(opts, gitea.SetToken(options.Token))5455if options.HttpClient != nil {56opts = append(opts, gitea.SetHTTPClient(options.HttpClient.HTTPClient))57}5859var remote string60if options.BaseURL != "" {61parsed, err := url.Parse(options.BaseURL)62if err != nil {63return nil, errors.Wrap(err, "could not parse custom baseurl")64}65if !strings.HasSuffix(parsed.Path, "/") {66parsed.Path += "/"67}68remote = parsed.String()69} else {70remote = `https://gitea.com/`71}7273git, err := gitea.NewClient(remote, opts...)74if err != nil {75return nil, err76}7778return &Integration{client: git, options: options}, nil79}8081// CreateIssue creates an issue in the tracker82func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {83summary := format.Summary(event)84description := format.CreateReportDescription(event, util.MarkdownFormatter{}, i.options.OmitRaw)8586labels := []string{}87severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())88if i.options.SeverityAsLabel && severityLabel != "" {89labels = append(labels, severityLabel)90}91if label := i.options.IssueLabel; label != "" {92labels = append(labels, label)93}94customLabels, err := i.getLabelIDsByNames(labels)95if err != nil {96return nil, err97}9899var issue *gitea.Issue100if i.options.DuplicateIssueCheck {101issue, err = i.findIssueByTitle(summary)102if err != nil {103return nil, err104}105}106107if issue == nil {108createdIssue, _, err := i.client.CreateIssue(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateIssueOption{109Title: summary,110Body: description,111Labels: customLabels,112})113if err != nil {114return nil, err115}116return &filters.CreateIssueResponse{117IssueID: strconv.FormatInt(createdIssue.Index, 10),118IssueURL: createdIssue.URL,119}, nil120}121122_, _, err = i.client.CreateIssueComment(i.options.ProjectOwner, i.options.ProjectName, issue.Index, gitea.CreateIssueCommentOption{123Body: description,124})125if err != nil {126return nil, err127}128return &filters.CreateIssueResponse{129IssueID: strconv.FormatInt(issue.Index, 10),130IssueURL: issue.URL,131}, nil132}133134func (i *Integration) CloseIssue(event *output.ResultEvent) error {135// TODO: Implement136return nil137}138139// ShouldFilter determines if an issue should be logged to this tracker140func (i *Integration) ShouldFilter(event *output.ResultEvent) bool {141if i.options.AllowList != nil && !i.options.AllowList.GetMatch(event) {142return false143}144145if i.options.DenyList != nil && i.options.DenyList.GetMatch(event) {146return false147}148149return true150}151152func (i *Integration) findIssueByTitle(title string) (*gitea.Issue, error) {153154issueList, _, err := i.client.ListRepoIssues(i.options.ProjectOwner, i.options.ProjectName, gitea.ListIssueOption{155State: "all",156})157if err != nil {158return nil, err159}160161for _, issue := range issueList {162if issue.Title == title {163return issue, nil164}165}166167return nil, nil168}169170func (i *Integration) getLabelIDsByNames(labels []string) ([]int64, error) {171172var ids []int64173174existingLabels, _, err := i.client.ListRepoLabels(i.options.ProjectOwner, i.options.ProjectName, gitea.ListLabelsOptions{175ListOptions: gitea.ListOptions{Page: -1},176})177if err != nil {178return nil, err179}180181getLabel := func(name string) int64 {182for _, existingLabel := range existingLabels {183if existingLabel.Name == name {184return existingLabel.ID185}186}187return -1188}189190for _, label := range labels {191labelID := getLabel(label)192if labelID == -1 {193newLabel, _, err := i.client.CreateLabel(i.options.ProjectOwner, i.options.ProjectName, gitea.CreateLabelOption{194Name: label,195Color: `#00aabb`,196Description: label,197})198if err != nil {199return nil, err200}201202ids = append(ids, newLabel.ID)203} else {204ids = append(ids, labelID)205}206}207208return ids, nil209}210211func (i *Integration) Name() string {212return "gitea"213}214215216