diff --git a/cve-vulner-manager/common/analysis.go b/cve-vulner-manager/common/analysis.go index d2bb73e35ecf49f401b254f753ca9087b8e29b50..dde77746ea7f3fd66f983e4b4da03369ba3edf28 100644 --- a/cve-vulner-manager/common/analysis.go +++ b/cve-vulner-manager/common/analysis.go @@ -18,6 +18,11 @@ const ( TypeAffected = "Affected" TypeUnaffected = "Unaffected" TypeUnderInvestigation = "Under Investigation" + + StateOpen = "open" + StateProcessing = "progressing" + StateSuspend = "suspended" + StateClosed = "closed" ) var AnalysisUnaffected = map[string]struct{}{ @@ -56,4 +61,58 @@ var ( AnalysisNotExecute, AnalysisCodeNotPresent, ) + + analysisStateMap = map[string]string{ + AnalysisStill: StateSuspend, + AnalysisNoPatch: StateSuspend, + AnalysisUpgrade: StateSuspend, + AnalysisOutScope: StateClosed, + AnalysisNotFix: StateClosed, + AnalysisComponentNotPresent: StateClosed, + AnalysisMitigationsExist: StateClosed, + AnalysisCannotControlled: StateClosed, + AnalysisNotExecute: StateClosed, + AnalysisCodeNotPresent: StateClosed, + } + + statePriority = []string{ + StateClosed, + StateSuspend, + StateProcessing, + StateOpen, + } + + stateNameMap = map[string]string{ + StateClosed: "已完成", + StateSuspend: "已挂起", + StateProcessing: "进行中", + StateOpen: "待办的", + } + + stateIdMap = map[string]int64{ + StateClosed: 437577, + StateSuspend: 751673, + StateProcessing: 437576, + StateOpen: 437575, + } ) + +// GetAnalysisStateMap returns a map containing the analysis states and their corresponding values. +func GetAnalysisStateMap() map[string]string { + return analysisStateMap +} + +// GetStatePriority returns the priority of states as a slice of strings. +func GetStatePriority() []string { + return statePriority +} + +// GetStateNameMap returns a map of state names to their corresponding values. +func GetStateNameMap() map[string]string { + return stateNameMap +} + +// GetStateIdMap returns a map of state names to their corresponding IDs. +func GetStateIdMap() map[string]int64 { + return stateIdMap +} diff --git a/cve-vulner-manager/controllers/hook.go b/cve-vulner-manager/controllers/hook.go index 3113497082cc19e526c9ea6cd943405fda353300..183908952b88edc900c09c7d53dea9344f0d5110 100644 --- a/cve-vulner-manager/controllers/hook.go +++ b/cve-vulner-manager/controllers/hook.go @@ -14,6 +14,10 @@ import ( "sync" "time" + "github.com/opensourceways/go-gitee/gitee" + "github.com/opensourceways/server-common-lib/utils" + "k8s.io/apimachinery/pkg/util/sets" + "cvevulner/common" "cvevulner/cve-timed-task/tabletask" "cvevulner/models" @@ -480,26 +484,13 @@ func closeIssueProc(issueHook *models.IssuePayload, issueTmp *models.IssueTempla } return } - issueTmp.IssueLabel = unFix - issueTmp.StatusName = "open" - issueTmp.Status = 1 - issuePrFlag := VerifyIssueAsPr(issueTmp, *cveCenter, false, - assignee, issueHook.Sender.UserName) - if issuePrFlag { - issueTmp.StatusName = issueHook.Issue.StateName - issueTmp.Status = 3 - if isNormalCloseIssue(issueTmp.CveId, issueTmp.IssueStatus) { - issueTmp.IssueStatus = 2 - cveCenter.IsExport = 3 - issueTmp.IssueLabel = fixed - } else { - issueTmp.IssueStatus = 6 - cveCenter.IsExport = 2 - issueTmp.IssueLabel = unFix - } - } else { - issueTmp.IssueStatus = 1 - cveCenter.IsExport = 0 + + if err := SetIssueStateByReason(issueTmp, issueHook.Issue.StateName); err != nil { + logs.Error("SetIssueStateByReason of failed:", issueTmp.IssueNum, err) + } + + if issueTmp.IsIssueComplete() { + issueTmp.ResetLabel() } } } @@ -1329,7 +1320,7 @@ func otherMaintainerApprove( } func maintainerApprove(issueTmp *models.IssueTemplate, cuAccount, owner, token, fixed, - unfixed string, organizationID int8) { + unfixed string, organizationID int8, payload *models.CommentPayload) { if msg, _, ok := taskhandler.CheckIssueAnalysisComplete(issueTmp, organizationID); !ok { //send comment to issue na := "\n**请确认分析内容的准确性,待分析内容请填写完整,否则将无法关闭当前issue.**" @@ -1348,43 +1339,23 @@ func maintainerApprove(issueTmp *models.IssueTemplate, cuAccount, owner, token, if err != nil { return } - issueTmp.IssueLabel = unfixed - issueTmp.StatusName = "open" - assignee := getMaintainer(issueTmp.Repo, cuAccount, issueTmp.Assignee) - issuePrFlag := VerifyIssueAsPr(issueTmp, cveCenter, false, assignee, cuAccount) - if issuePrFlag { - issueTmp.IssueLabel = fixed - issueTmp.StatusName = "closed" - taskhandler.AddCommentToIssue(fmt.Sprintf(`@%v 你已审核模板内容, cve-manager 将关闭issue!`, - cuAccount), issueTmp.IssueNum, owner, issueTmp.Repo, token) - _, issueErr := taskhandler.UpdateIssueToGit(token, owner, issueTmp.Repo, - cveCenter, *issueTmp) - if issueErr == nil { - logs.Info("Initiate an issue to close, issuetmp: ", issueTmp) - } else { - logs.Error("Issue closing operation failed, issuetmp: ", issueTmp, ",issueErr: ", issueErr) - return - } - //issueTmp.SaAuditFlag = 1 - issueTmp.Status = 3 - if isNormalCloseIssue(issueTmp.CveId, issueTmp.IssueStatus) { - issueTmp.IssueStatus = 2 - cveCenter.IsExport = 3 - } else { - issueTmp.IssueStatus = 6 - cveCenter.IsExport = 2 - } - updateBool := updateTempAndCenter(*issueTmp, cveCenter, token, owner) - if !updateBool { - return - } + + if err = SetIssueStateByReason(issueTmp, payload.Issue.StateName); err != nil { + logs.Error(webhookCommentLogTag, "SetIssueStateByReason, err: ", err, ",issueTmp: ", issueTmp.IssueNum) } + + if issueTmp.IsIssueComplete() { + issueTmp.ResetLabel() + } + + updateTempAndCenter(*issueTmp, cveCenter, token, owner) + return } } func securityApprove(issueTmp *models.IssueTemplate, cuAccount, owner, token, - fixed, unfixed string, organizationID int8) { + fixed, unfixed string, organizationID int8, payload *models.CommentPayload) { if msg, _, ok := taskhandler.CheckIssueAnalysisComplete(issueTmp, organizationID); !ok { //send comment to issue na := "\n**请确认分析内容的准确性,待分析内容请填写完整,否则将无法关闭当前issue.**" @@ -1402,37 +1373,17 @@ func securityApprove(issueTmp *models.IssueTemplate, cuAccount, owner, token, if err != nil { return } - issueTmp.IssueLabel = unfixed - issueTmp.StatusName = "open" - assignee := getMaintainer(issueTmp.Repo, cuAccount, issueTmp.Assignee) - issuePrFlag := VerifyIssueAsPr(issueTmp, cveCenter, false, assignee, cuAccount) - if issuePrFlag { - issueTmp.IssueLabel = fixed - issueTmp.StatusName = "closed" - taskhandler.AddCommentToIssue(fmt.Sprintf(`@%v 你已审核模板内容,cve-manager 将关闭issue!`, - cuAccount), issueTmp.IssueNum, owner, issueTmp.Repo, token) - _, issueErr := taskhandler.UpdateIssueToGit(token, owner, issueTmp.Repo, - cveCenter, *issueTmp) - if issueErr == nil { - logs.Info("Initiate an issue to close, issuetmp: ", issueTmp) - } else { - logs.Error("issue close operation failed, issuetmp: ", issueTmp, ",issueErr: ", issueErr) - return - } - issueTmp.SaAuditFlag = 1 - issueTmp.Status = 3 - if isNormalCloseIssue(issueTmp.CveId, issueTmp.IssueStatus) { - issueTmp.IssueStatus = 2 - cveCenter.IsExport = 3 - } else { - issueTmp.IssueStatus = 6 - cveCenter.IsExport = 2 - } - updateBool := updateTempAndCenter(*issueTmp, cveCenter, token, owner) - if !updateBool { - return - } + + if err = SetIssueStateByReason(issueTmp, payload.Issue.StateName); err != nil { + logs.Error(webhookCommentLogTag, "SetIssueStateByReason, err: ", err, ",issueTmp: ", issueTmp.IssueNum) } + + if issueTmp.IsIssueComplete() { + issueTmp.ResetLabel() + } + + updateTempAndCenter(*issueTmp, cveCenter, token, owner) + return } } @@ -1582,11 +1533,11 @@ func handleIssueComment(payload models.CommentPayload) { } if mtAuditFlag { comLock.Lock() - maintainerApprove(&issueTmp, cuAccount, owner, accessToken, fixed, unfixed, vc.OrganizationID) + maintainerApprove(&issueTmp, cuAccount, owner, accessToken, fixed, unfixed, vc.OrganizationID, &payload) comLock.Unlock() } else { comLock.Lock() - securityApprove(&issueTmp, cuAccount, owner, accessToken, fixed, unfixed, vc.OrganizationID) + securityApprove(&issueTmp, cuAccount, owner, accessToken, fixed, unfixed, vc.OrganizationID, &payload) comLock.Unlock() } } @@ -1916,6 +1867,16 @@ func analysisComment(owner, accessToken, path string, cuAccount string, cBody st if err != nil { logs.Error(webhookCommentLogTag, "changeOpenEulerScoreStatus, err: ", err, ",issueTmp: ", issueTmp.IssueNum) } + + if err = SetIssueStateByReason(&issueTmp, payload.Issue.StateName); err != nil { + logs.Error(webhookCommentLogTag, "SetIssueStateByReason, err: ", err, ",issueTmp: ", issueTmp.IssueNum) + } + + if issueTmp.IsIssueComplete() { + issueTmp.ResetLabel() + } + + updateTempAndCenter(issueTmp, v, accessToken, owner) } } else { na := "\n**请确认分析内容的准确性, 确认无误后, 您可以进行后续步骤, 否则您可以继续分析.**" @@ -2462,3 +2423,149 @@ func gitDelIssueProc(issueHook *models.IssuePayload, organizationID int8) error DelOrgIssue(issueHook, organizationID) return nil } + +// SetIssueStateByReason sets the issue state based on the given reason. +func SetIssueStateByReason(issue *models.IssueTemplate, remoteStateName string) error { + state, err := GetIssueStateByAllReason(issue) + if err != nil { + return err + } + + // issue本身的状态和计算后的状态一致,则不做任何操作 + stateNameMap := common.GetStateNameMap() + stateName, ok := stateNameMap[state] + if !ok || stateName == remoteStateName { + return nil + } + + issue.StatusName = state + switch state { + case common.StateOpen: + issue.Status = 1 + case common.StateProcessing: + issue.Status = 2 + case common.StateSuspend: + issue.Status = 5 + case common.StateClosed: + issue.Status = 3 + } + + models.UpdateIssueTemplate(issue, "Status", "StatusName") + + stateIdMap := common.GetStateIdMap() + stateId, ok := stateIdMap[state] + if !ok { + return errors.New("can not find state id") + } + + return taskhandler.UpdateIssueStateToGitee(issue.IssueId, stateId) +} + +// GetIssueStateByAllReason retrieves the issue state based on all reasons associated with the issue. +func GetIssueStateByAllReason(issue *models.IssueTemplate) (string, error) { + allState, err := StateByAllReason(issue) + if err != nil { + return "", err + } + + return MaxPriorityState(allState), nil +} + +// MaxPriorityState returns the state with the highest priority from the given list of states. +func MaxPriorityState(allState []string) string { + var maxPriority string + for _, v := range common.GetStatePriority() { + for _, state := range allState { + if v == state { + maxPriority = v + } + } + } + + return maxPriority +} + +// StateByAllReason return all reason from the given issue. +func StateByAllReason(issue *models.IssueTemplate) ([]string, error) { + var mergedPR []string + var err error + if issue.ContainsWillFix() { + if mergedPR, err = GetMergedPR(issue); err != nil { + return nil, err + } + } + + split := strings.Split(issue.AnalysisVersion, ",") + var allState []string + for _, v := range split { + allState = append(allState, StateFromReason(v, mergedPR)) + } + + return allState, nil +} + +// GetRelatedPR retrieves related pull requests for a given issue template. +func GetRelatedPR(issue *models.IssueTemplate) ([]gitee.PullRequest, error) { + token := beego.AppConfig.String("gitee::git_token") + + endpoint := fmt.Sprintf("https://gitee.com/api/v5/repos/%v/issues/%v/pull_requests?access_token=%s&repo=%s", + issue.Owner, issue.IssueNum, token, issue.Repo, + ) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + + const retryTimes = 3 + cli := utils.NewHttpClient(retryTimes) + b, _, err := cli.Download(req) + if err != nil { + return nil, err + } + + var prs []gitee.PullRequest + err = json.Unmarshal(b, &prs) + + return prs, err +} + +// GetMergedPR retrieves a list of merged pull requests related to the given issue. +func GetMergedPR(issue *models.IssueTemplate) ([]string, error) { + prs, err := GetRelatedPR(issue) + if err != nil { + return nil, err + } + + var mergedPR []string + for _, pr := range prs { + if pr.State == "merged" { + mergedPR = append(mergedPR, pr.Base.Ref) + } + } + + return mergedPR, nil +} + +// StateFromReason determines the state of a pull request based on the branch and reason provided. +// It returns the state as a string. If the branch and reason are not valid or the reason is empty, +// it returns the common.StateOpen. +func StateFromReason(branchAndReason string, mergedPR []string) string { + const splitLen = 2 + item := strings.Split(strings.ReplaceAll(branchAndReason, ":", ":"), ":") + if len(item) != splitLen || item[1] == "" { + return common.StateOpen + } + + analysisStateMap := common.GetAnalysisStateMap() + if v, ok := analysisStateMap[item[1]]; ok { + return v + } + + // 原因为正常修复的分支,需要看对应分支的PR是否合入来决定状态 + mergedPRSets := sets.NewString(mergedPR...) + if mergedPRSets.Has(item[0]) { + return common.StateClosed + } else { + return common.StateProcessing + } +} diff --git a/cve-vulner-manager/cve-ddd/infrastructure/repositoryimpl/impl.go b/cve-vulner-manager/cve-ddd/infrastructure/repositoryimpl/impl.go index 0bbd723bce3a093675fa0e2872c2a9819aeb3f2e..b87f7682c2d7f9fefb8ed31d36541941c62ec5c2 100644 --- a/cve-vulner-manager/cve-ddd/infrastructure/repositoryimpl/impl.go +++ b/cve-vulner-manager/cve-ddd/infrastructure/repositoryimpl/impl.go @@ -31,7 +31,7 @@ join cve_security_notice c on a.cve_id=c.cve_id where a.cve_num in (%s) and a.cve_status = 2 and a.organizate_id = 1 -and b.status < 4 +and b.status in (1,2,3,5) ` if opt.Component != "" { sql += fmt.Sprintf(`and a.pack_name = "%s"`, opt.Component) @@ -119,7 +119,7 @@ func (impl repositoryImpl) GetAllIssue() (data domain.CollectedDataSlice, err er sql := `select a.*, b.affect_product, b.will_fix_product from cve_issue_template a join cve_security_notice b on a.cve_id=b.cve_id where a.cve_id in (select cve_id from cve_vuln_center where cve_status = 2 and is_export in (0,3) and organizate_id = 1) - and a.status < 4 + and a.status in (1,2,3,5) ` var issueTemp []list diff --git a/cve-vulner-manager/models/cve.go b/cve-vulner-manager/models/cve.go index 5d7a9fb08557aced022b9b218456fb2d369dc03d..2692a66dd15234d65c424bbf19d0d3da70d7274f 100644 --- a/cve-vulner-manager/models/cve.go +++ b/cve-vulner-manager/models/cve.go @@ -1121,7 +1121,7 @@ is_export in (0,3) and pack_name in ('%s') and organizate_id = 1) and status < 4 func GetUnffectIssueNumber(startTime string, cves []string) (issueTemp []IssueTemplate, err error) { var sql string if len(cves) == 0 { - sql = `SELECT * FROM cve_issue_template WHERE STATUS <= 3 AND issue_status in (1,2,3,6) AND cve_id IN ( + sql = `SELECT * FROM cve_issue_template WHERE STATUS in (1,2,3,5) AND issue_status in (1,2,3,6) AND cve_id IN ( SELECT DISTINCT cve_id FROM cve_vuln_center WHERE cve_status = 2 AND is_export IN (0,3) and organizate_id = 1) AND create_time >= '%s'` } else { diff --git a/cve-vulner-manager/models/issue.go b/cve-vulner-manager/models/issue.go index 77306983a873d33c9420842b010d5c748c62923b..c3987ac52a65cb57ce212d00cfc382512d07a7b0 100644 --- a/cve-vulner-manager/models/issue.go +++ b/cve-vulner-manager/models/issue.go @@ -6,6 +6,8 @@ import ( "strings" "sync" + "github.com/astaxie/beego" + "cvevulner/common" "cvevulner/util" @@ -809,6 +811,38 @@ func (t *IssueTemplate) HasAffected() bool { return false } +// ContainsWillFix checks if the AnalysisVersion contains the common.AnalysisWillFix string. +func (t *IssueTemplate) ContainsWillFix() bool { + return strings.Contains(t.AnalysisVersion, common.AnalysisWillFix) +} + +// IsAllUnaffected splits the AnalysisVersion by commas and checks if all parts are unaffected. +func (t *IssueTemplate) IsAllUnaffected() bool { + const splitLen = 2 + split := strings.Split(t.AnalysisVersion, ",") + for _, v := range split { + item := strings.Split(strings.ReplaceAll(v, ":", ":"), ":") + if len(item) != splitLen { + return false + } + + if _, ok := common.AnalysisUnaffected[item[1]]; !ok { + return false + } + } + + return true +} + +// ResetLabel resets the issue label based on whether the issue is all unaffected or fixed. +func (t *IssueTemplate) ResetLabel() { + if t.IsAllUnaffected() { + t.IssueLabel = beego.AppConfig.String("labeUnaffected") + } else { + t.IssueLabel = beego.AppConfig.String("labelFixed") + } +} + // HasBranch issue的分析说明分支是否包含指定分支 func (t *IssueTemplate) HasBranch(branch string) bool { split := strings.Split(t.AffectedVersion, ",") @@ -821,7 +855,6 @@ func (t *IssueTemplate) HasBranch(branch string) bool { if item[0] == branch { return true } - } return false diff --git a/cve-vulner-manager/taskhandler/issue.go b/cve-vulner-manager/taskhandler/issue.go index 75da6b9103a1bc9596f7708e0e93686687a5562c..f3b407ea14fcdc4b5b9f5ded5f094f3efdc1370d 100644 --- a/cve-vulner-manager/taskhandler/issue.go +++ b/cve-vulner-manager/taskhandler/issue.go @@ -1,6 +1,7 @@ package taskhandler import ( + "encoding/json" "fmt" "strings" "time" @@ -288,3 +289,30 @@ func UpdateEntIssueDetail(enterpriseId, issueId int64, token, planAt, deadLine s return } } + +type updateIssueState struct { + AccessToken string `json:"access_token"` + IssueStateId int64 `json:"issue_state_id"` +} + +// UpdateIssueStateToGitee update issue state with enterprise api. +func UpdateIssueStateToGitee(issueId, stateId int64) error { + at := models.AuthTokenInfo{OrganizationID: models.OrganizationIdOpeneuler} + if err := models.QueryAuthTokenById(&at, "organizate_id"); err != nil { + return err + } + + url := fmt.Sprintf("https://api.gitee.com/enterprises/%v/issues/%v", at.EnId, issueId) + request := updateIssueState{ + AccessToken: at.AccessToken, + IssueStateId: stateId, + } + + payload, err := json.Marshal(request) + if err != nil { + return err + } + + _, err = util.HTTPPutMap(url, string(payload)) + return err +}