diff --git a/github/code-scanning.go b/github/code-scanning.go index 53200fa3ee..6717348ed7 100644 --- a/github/code-scanning.go +++ b/github/code-scanning.go @@ -173,6 +173,22 @@ type SarifAnalysis struct { ToolName *string `json:"tool_name,omitempty"` } +// CodeScanningAlertState specifies the state of a code scanning alert. +// +// GitHub API docs: https://docs.github.com/en/rest/code-scanning +type CodeScanningAlertState struct { + // State sets the state of the code scanning alert and is a required field. + // You must also provide DismissedReason when you set the state to "dismissed". + // State can be one of: "open", "dismissed". + State string `json:"state"` + // DismissedReason represents the reason for dismissing or closing the alert. + // It is required when the state is "dismissed". + // It can be one of: "false positive", "won't fix", "used in tests". + DismissedReason *string `json:"dismissed_reason,omitempty"` + // DismissedComment is associated with the dismissal of the alert. + DismissedComment *string `json:"dismissed_comment,omitempty"` +} + // SarifID identifies a sarif analysis upload. // // GitHub API docs: https://docs.github.com/en/rest/code-scanning @@ -261,6 +277,31 @@ func (s *CodeScanningService) GetAlert(ctx context.Context, owner, repo string, return a, resp, nil } +// UpdateAlert updates the state of a single code scanning alert for a repository. +// +// You must use an access token with the security_events scope to use this endpoint. +// GitHub Apps must have the security_events read permission to use this endpoint. +// +// The security alert_id is the number at the end of the security alert's URL. +// +// GitHub API docs: https://docs.github.com/en/rest/code-scanning?apiVersion=2022-11-28#update-a-code-scanning-alert +func (s *CodeScanningService) UpdateAlert(ctx context.Context, owner, repo string, id int64, stateInfo *CodeScanningAlertState) (*Alert, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/code-scanning/alerts/%v", owner, repo, id) + + req, err := s.client.NewRequest("PATCH", u, stateInfo) + if err != nil { + return nil, nil, err + } + + a := new(Alert) + resp, err := s.client.Do(ctx, req, a) + if err != nil { + return nil, resp, err + } + + return a, resp, nil +} + // UploadSarif uploads the result of code scanning job to GitHub. // // For the parameter sarif, you must first compress your SARIF file using gzip and then translate the contents of the file into a Base64 encoding string. diff --git a/github/code-scanning_test.go b/github/code-scanning_test.go index c8cf35cfba..1ae05096d3 100644 --- a/github/code-scanning_test.go +++ b/github/code-scanning_test.go @@ -613,6 +613,123 @@ func TestCodeScanningService_ListAlertsForRepo(t *testing.T) { }) } +func TestCodeScanningService_UpdateAlert(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + mux.HandleFunc("/repos/o/r/code-scanning/alerts/88", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + fmt.Fprint(w, `{"rule_id":"js/useless-expression", + "rule_severity":"warning", + "rule_description":"Expression has no effect", + "tool": { + "name": "CodeQL", + "guid": null, + "version": "1.4.0" + }, + "rule": { + "id": "useless expression", + "severity": "warning", + "description": "Expression has no effect", + "name": "useless expression", + "full_description": "Expression has no effect", + "help": "Expression has no effect" + }, + "most_recent_instance": { + "ref": "refs/heads/main", + "state": "dismissed", + "commit_sha": "abcdefg12345", + "message": { + "text": "This path depends on a user-provided value." + }, + "location": { + "path": "spec-main/api-session-spec.ts", + "start_line": 917, + "end_line": 917, + "start_column": 7, + "end_column": 18 + }, + "classifications": [ + "test" + ] + }, + "created_at":"2019-01-02T15:04:05Z", + "state":"dismissed", + "dismissed_reason": "false positive", + "dismissed_comment": "This alert is not actually correct as sanitizer is used", + "closed_by":null, + "closed_at":null, + "url":"https://api.github.com/repos/o/r/code-scanning/alerts/88", + "html_url":"https://github.com/o/r/security/code-scanning/88"}`) + }) + + ctx := context.Background() + dismissedComment := String("This alert is not actually correct as sanitizer is used") + dismissedReason := String("false positive") + state := String("dismissed") + stateInfo := &CodeScanningAlertState{State: *state, DismissedReason: dismissedReason, DismissedComment: dismissedComment} + alert, _, err := client.CodeScanning.UpdateAlert(ctx, "o", "r", 88, stateInfo) + if err != nil { + t.Errorf("CodeScanning.UpdateAlert returned error: %v", err) + } + + date := Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)} + want := &Alert{ + RuleID: String("js/useless-expression"), + RuleSeverity: String("warning"), + RuleDescription: String("Expression has no effect"), + Tool: &Tool{Name: String("CodeQL"), GUID: nil, Version: String("1.4.0")}, + Rule: &Rule{ + ID: String("useless expression"), + Severity: String("warning"), + Description: String("Expression has no effect"), + Name: String("useless expression"), + FullDescription: String("Expression has no effect"), + Help: String("Expression has no effect"), + }, + CreatedAt: &date, + State: state, + DismissedReason: dismissedReason, + DismissedComment: dismissedComment, + ClosedBy: nil, + ClosedAt: nil, + URL: String("https://api.github.com/repos/o/r/code-scanning/alerts/88"), + HTMLURL: String("https://github.com/o/r/security/code-scanning/88"), + MostRecentInstance: &MostRecentInstance{ + Ref: String("refs/heads/main"), + State: String("dismissed"), + CommitSHA: String("abcdefg12345"), + Message: &Message{ + Text: String("This path depends on a user-provided value."), + }, + Location: &Location{ + Path: String("spec-main/api-session-spec.ts"), + StartLine: Int(917), + EndLine: Int(917), + StartColumn: Int(7), + EndColumn: Int(18), + }, + Classifications: []string{"test"}, + }, + } + if !cmp.Equal(alert, want) { + t.Errorf("CodeScanning.UpdateAlert returned %+v, want %+v", alert, want) + } + + const methodName = "UpdateAlert" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.CodeScanning.UpdateAlert(ctx, "\n", "\n", -88, stateInfo) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.CodeScanning.UpdateAlert(ctx, "o", "r", 88, stateInfo) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestCodeScanningService_GetAlert(t *testing.T) { client, mux, _, teardown := setup() defer teardown() diff --git a/github/github-accessors.go b/github/github-accessors.go index 400bde8c3c..1290696d52 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -2582,6 +2582,22 @@ func (c *CodeScanningAlertEvent) GetSender() *User { return c.Sender } +// GetDismissedComment returns the DismissedComment field if it's non-nil, zero value otherwise. +func (c *CodeScanningAlertState) GetDismissedComment() string { + if c == nil || c.DismissedComment == nil { + return "" + } + return *c.DismissedComment +} + +// GetDismissedReason returns the DismissedReason field if it's non-nil, zero value otherwise. +func (c *CodeScanningAlertState) GetDismissedReason() string { + if c == nil || c.DismissedReason == nil { + return "" + } + return *c.DismissedReason +} + // GetIncompleteResults returns the IncompleteResults field if it's non-nil, zero value otherwise. func (c *CodeSearchResult) GetIncompleteResults() bool { if c == nil || c.IncompleteResults == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 3e4b469463..6babd83ef1 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -3048,6 +3048,26 @@ func TestCodeScanningAlertEvent_GetSender(tt *testing.T) { c.GetSender() } +func TestCodeScanningAlertState_GetDismissedComment(tt *testing.T) { + var zeroValue string + c := &CodeScanningAlertState{DismissedComment: &zeroValue} + c.GetDismissedComment() + c = &CodeScanningAlertState{} + c.GetDismissedComment() + c = nil + c.GetDismissedComment() +} + +func TestCodeScanningAlertState_GetDismissedReason(tt *testing.T) { + var zeroValue string + c := &CodeScanningAlertState{DismissedReason: &zeroValue} + c.GetDismissedReason() + c = &CodeScanningAlertState{} + c.GetDismissedReason() + c = nil + c.GetDismissedReason() +} + func TestCodeSearchResult_GetIncompleteResults(tt *testing.T) { var zeroValue bool c := &CodeSearchResult{IncompleteResults: &zeroValue}