diff --git a/github/github-accessors.go b/github/github-accessors.go index 0a8f83ee2e1..ce58d7ed06f 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -6734,6 +6734,22 @@ func (c *CustomOrgRoles) GetUpdatedAt() Timestamp { return *c.UpdatedAt } +// GetPatternScope returns the PatternScope field if it's non-nil, zero value otherwise. +func (c *CustomPatternBackfillScan) GetPatternScope() string { + if c == nil || c.PatternScope == nil { + return "" + } + return *c.PatternScope +} + +// GetPatternSlug returns the PatternSlug field if it's non-nil, zero value otherwise. +func (c *CustomPatternBackfillScan) GetPatternSlug() string { + if c == nil || c.PatternSlug == nil { + return "" + } + return *c.PatternSlug +} + // GetDefaultValue returns the DefaultValue field if it's non-nil, zero value otherwise. func (c *CustomProperty) GetDefaultValue() string { if c == nil || c.DefaultValue == nil { @@ -21254,6 +21270,14 @@ func (p *PushEventRepository) GetWatchersCount() int { return *p.WatchersCount } +// GetExpireAt returns the ExpireAt field if it's non-nil, zero value otherwise. +func (p *PushProtectionBypass) GetExpireAt() Timestamp { + if p == nil || p.ExpireAt == nil { + return Timestamp{} + } + return *p.ExpireAt +} + // GetActionsRunnerRegistration returns the ActionsRunnerRegistration field. func (r *RateLimits) GetActionsRunnerRegistration() *Rate { if r == nil { @@ -26174,6 +26198,22 @@ func (s *SecretScanningValidityChecks) GetStatus() string { return *s.Status } +// GetCompletedAt returns the CompletedAt field if it's non-nil, zero value otherwise. +func (s *SecretsScan) GetCompletedAt() Timestamp { + if s == nil || s.CompletedAt == nil { + return Timestamp{} + } + return *s.CompletedAt +} + +// GetStartedAt returns the StartedAt field if it's non-nil, zero value otherwise. +func (s *SecretsScan) GetStartedAt() Timestamp { + if s == nil || s.StartedAt == nil { + return Timestamp{} + } + return *s.StartedAt +} + // GetAuthor returns the Author field. func (s *SecurityAdvisory) GetAuthor() *User { if s == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 3c78a1436b8..983e90b3a74 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -8802,6 +8802,28 @@ func TestCustomOrgRoles_GetUpdatedAt(tt *testing.T) { c.GetUpdatedAt() } +func TestCustomPatternBackfillScan_GetPatternScope(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CustomPatternBackfillScan{PatternScope: &zeroValue} + c.GetPatternScope() + c = &CustomPatternBackfillScan{} + c.GetPatternScope() + c = nil + c.GetPatternScope() +} + +func TestCustomPatternBackfillScan_GetPatternSlug(tt *testing.T) { + tt.Parallel() + var zeroValue string + c := &CustomPatternBackfillScan{PatternSlug: &zeroValue} + c.GetPatternSlug() + c = &CustomPatternBackfillScan{} + c.GetPatternSlug() + c = nil + c.GetPatternSlug() +} + func TestCustomProperty_GetDefaultValue(tt *testing.T) { tt.Parallel() var zeroValue string @@ -27438,6 +27460,17 @@ func TestPushEventRepository_GetWatchersCount(tt *testing.T) { p.GetWatchersCount() } +func TestPushProtectionBypass_GetExpireAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + p := &PushProtectionBypass{ExpireAt: &zeroValue} + p.GetExpireAt() + p = &PushProtectionBypass{} + p.GetExpireAt() + p = nil + p.GetExpireAt() +} + func TestRateLimits_GetActionsRunnerRegistration(tt *testing.T) { tt.Parallel() r := &RateLimits{} @@ -33747,6 +33780,28 @@ func TestSecretScanningValidityChecks_GetStatus(tt *testing.T) { s.GetStatus() } +func TestSecretsScan_GetCompletedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + s := &SecretsScan{CompletedAt: &zeroValue} + s.GetCompletedAt() + s = &SecretsScan{} + s.GetCompletedAt() + s = nil + s.GetCompletedAt() +} + +func TestSecretsScan_GetStartedAt(tt *testing.T) { + tt.Parallel() + var zeroValue Timestamp + s := &SecretsScan{StartedAt: &zeroValue} + s.GetStartedAt() + s = &SecretsScan{} + s.GetStartedAt() + s = nil + s.GetStartedAt() +} + func TestSecurityAdvisory_GetAuthor(tt *testing.T) { tt.Parallel() s := &SecurityAdvisory{} diff --git a/github/secret_scanning.go b/github/secret_scanning.go index f75800f6e0b..2cedb5a4c80 100644 --- a/github/secret_scanning.go +++ b/github/secret_scanning.go @@ -120,6 +120,53 @@ type SecretScanningAlertUpdateOptions struct { ResolutionComment *string `json:"resolution_comment,omitempty"` } +// PushProtectionBypassRequest represents the parameters for CreatePushProtectionBypass. +type PushProtectionBypassRequest struct { + // The reason for bypassing push protection. + // Can be one of: false_positive, used_in_tests, will_fix_later + Reason string `json:"reason"` + // PlaceholderID is an identifier used for the bypass request. + // GitHub Secret Scanning provides you with a unique PlaceholderID associated with that specific blocked push. + PlaceholderID string `json:"placeholder_id"` +} + +// PushProtectionBypass represents the response from CreatePushProtectionBypass. +type PushProtectionBypass struct { + // The reason for bypassing push protection. + Reason string `json:"reason"` + // The time that the bypass will expire in ISO 8601 format. + ExpireAt *Timestamp `json:"expire_at"` + // The token type this bypass is for. + TokenType string `json:"token_type"` +} + +// SecretsScan represents the common fields for a secret scanning scan. +type SecretsScan struct { + Type string `json:"type"` + Status string `json:"status"` + CompletedAt *Timestamp `json:"completed_at,omitempty"` + StartedAt *Timestamp `json:"started_at,omitempty"` +} + +// CustomPatternBackfillScan represents a scan with an associated custom pattern. +type CustomPatternBackfillScan struct { + SecretsScan + PatternSlug *string `json:"pattern_slug,omitempty"` + PatternScope *string `json:"pattern_scope,omitempty"` +} + +// SecretScanningScanHistory is the top-level struct for the secret scanning API response. +type SecretScanningScanHistory struct { + // Information on incremental scan performed by secret scanning on the repository. + IncrementalScans []*SecretsScan `json:"incremental_scans,omitempty"` + // Information on backfill scan performed by secret scanning on the repository. + BackfillScans []*SecretsScan `json:"backfill_scans,omitempty"` + // Information on pattern update scan performed by secret scanning on the repository. + PatternUpdateScans []*SecretsScan `json:"pattern_update_scans,omitempty"` + // Information on custom pattern backfill scan performed by secret scanning on the repository. + CustomPatternBackfillScans []*CustomPatternBackfillScan `json:"custom_pattern_backfill_scans,omitempty"` +} + // ListAlertsForEnterprise lists secret scanning alerts for eligible repositories in an enterprise, from newest to oldest. // // To use this endpoint, you must be a member of the enterprise, and you must use an access token with the repo scope or @@ -285,3 +332,52 @@ func (s *SecretScanningService) ListLocationsForAlert(ctx context.Context, owner return locations, resp, nil } + +// CreatePushProtectionBypass creates a push protection bypass for a given repository. +// +// To use this endpoint, you must be an administrator for the repository or organization, and you must use an access token with +// the repo scope or security_events scope. +// +// GitHub API docs: https://docs.github.com/rest/secret-scanning/secret-scanning#create-a-push-protection-bypass +// +//meta:operation POST /repos/{owner}/{repo}/secret-scanning/push-protection-bypasses +func (s *SecretScanningService) CreatePushProtectionBypass(ctx context.Context, owner, repo string, request PushProtectionBypassRequest) (*PushProtectionBypass, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/secret-scanning/push-protection-bypasses", owner, repo) + + req, err := s.client.NewRequest("POST", u, request) + if err != nil { + return nil, nil, err + } + + var pushProtectionBypass *PushProtectionBypass + resp, err := s.client.Do(ctx, req, &pushProtectionBypass) + if err != nil { + return nil, resp, err + } + return pushProtectionBypass, resp, nil +} + +// GetScanHistory fetches the secret scanning history for a given repository. +// +// To use this endpoint, you must be an administrator for the repository or organization, and you must use an access token with +// the repo scope or security_events scope and gitHub advanced security or secret scanning must be enabled. +// +// GitHub API docs: https://docs.github.com/rest/secret-scanning/secret-scanning#get-secret-scanning-scan-history-for-a-repository +// +//meta:operation GET /repos/{owner}/{repo}/secret-scanning/scan-history +func (s *SecretScanningService) GetScanHistory(ctx context.Context, owner, repo string) (*SecretScanningScanHistory, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/secret-scanning/scan-history", owner, repo) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var secretScanningHistory *SecretScanningScanHistory + resp, err := s.client.Do(ctx, req, &secretScanningHistory) + if err != nil { + return nil, resp, err + } + + return secretScanningHistory, resp, nil +} diff --git a/github/secret_scanning_test.go b/github/secret_scanning_test.go index 6f29a588dc6..08507442376 100644 --- a/github/secret_scanning_test.go +++ b/github/secret_scanning_test.go @@ -617,3 +617,128 @@ func TestSecretScanningAlertUpdateOptions_Marshal(t *testing.T) { testJSONMarshal(t, u, want) } + +func TestSecretScanningService_CreatePushProtectionBypass(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + owner := "o" + repo := "r" + + mux.HandleFunc(fmt.Sprintf("/repos/%v/%v/secret-scanning/push-protection-bypasses", owner, repo), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + var v *PushProtectionBypassRequest + assertNilError(t, json.NewDecoder(r.Body).Decode(&v)) + want := &PushProtectionBypassRequest{Reason: "valid reason", PlaceholderID: "bypass-123"} + if !cmp.Equal(v, want) { + t.Errorf("Request body = %+v, want %+v", v, want) + } + + fmt.Fprint(w, `{ + "reason": "valid reason", + "expire_at": "2018-01-01T00:00:00Z", + "token_type": "github_token" + }`) + }) + + ctx := context.Background() + opts := PushProtectionBypassRequest{Reason: "valid reason", PlaceholderID: "bypass-123"} + + bypass, _, err := client.SecretScanning.CreatePushProtectionBypass(ctx, owner, repo, opts) + if err != nil { + t.Errorf("SecretScanning.CreatePushProtectionBypass returned error: %v", err) + } + + expireTime := Timestamp{time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)} + want := &PushProtectionBypass{ + Reason: "valid reason", + ExpireAt: &expireTime, + TokenType: "github_token", + } + + if !cmp.Equal(bypass, want) { + t.Errorf("SecretScanning.CreatePushProtectionBypass returned %+v, want %+v", bypass, want) + } + const methodName = "CreatePushProtectionBypass" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.SecretScanning.CreatePushProtectionBypass(ctx, "\n", "\n", opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.SecretScanning.CreatePushProtectionBypass(ctx, "o", "r", opts) + return resp, err + }) +} + +func TestSecretScanningService_GetScanHistory(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + owner := "o" + repo := "r" + + mux.HandleFunc(fmt.Sprintf("/repos/%v/%v/secret-scanning/scan-history", owner, repo), func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "incremental_scans": [ + { + "type": "incremental", + "status": "success", + "completed_at": "2025-07-29T10:00:00Z", + "started_at": "2025-07-29T09:55:00Z" + } + ], + "backfill_scans": [], + "pattern_update_scans": [], + "custom_pattern_backfill_scans": [ + { + "type": "custom_backfill", + "status": "in_progress", + "completed_at": null, + "started_at": "2025-07-29T09:00:00Z", + "pattern_slug": "my-custom-pattern", + "pattern_scope": "organization" + } + ] + }`) + }) + + ctx := context.Background() + + history, _, err := client.SecretScanning.GetScanHistory(ctx, owner, repo) + if err != nil { + t.Errorf("SecretScanning.GetScanHistory returned error: %v", err) + } + + incrementalScanStartAt := Timestamp{time.Date(2025, time.July, 29, 9, 55, 0, 0, time.UTC)} + incrementalScancompleteAt := Timestamp{time.Date(2025, time.July, 29, 10, 0, 0, 0, time.UTC)} + customPatternBackfillScanStartedAt := Timestamp{time.Date(2025, time.July, 29, 9, 0, 0, 0, time.UTC)} + + want := &SecretScanningScanHistory{ + IncrementalScans: []*SecretsScan{ + {Type: "incremental", Status: "success", CompletedAt: &incrementalScancompleteAt, StartedAt: &incrementalScanStartAt}, + }, + BackfillScans: []*SecretsScan{}, + PatternUpdateScans: []*SecretsScan{}, + CustomPatternBackfillScans: []*CustomPatternBackfillScan{ + { + SecretsScan: SecretsScan{Type: "custom_backfill", Status: "in_progress", CompletedAt: nil, StartedAt: &customPatternBackfillScanStartedAt}, + PatternSlug: Ptr("my-custom-pattern"), + PatternScope: Ptr("organization"), + }, + }, + } + + if !cmp.Equal(history, want) { + t.Errorf("SecretScanning.GetScanHistory returned %+v, want %+v", history, want) + } + const methodName = "GetScanHistory" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.SecretScanning.GetScanHistory(ctx, "\n", "\n") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + _, resp, err := client.SecretScanning.GetScanHistory(ctx, "o", "r") + return resp, err + }) +}