Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions github/github-accessors.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions github/github-accessors_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions github/secret_scanning.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,52 @@ type SecretScanningAlertUpdateOptions struct {
ResolutionComment *string `json:"resolution_comment,omitempty"`
}

// PushProtectionBypassRequest represents the parameters for CreatePushProtectionBypass.
type PushProtectionBypassRequest struct {
// Reason provides a justification for the push protection bypass.
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"`
}

// CustomPatternScan represents a scan with an associated custom pattern.
type CustomPatternScan struct {
SecretsScan
PatternSlug *string `json:"pattern_slug,omitempty"`
PatternScope *string `json:"pattern_scope,omitempty"`
}

// SecretScanningHistory is the top-level struct for the secret scanning API response.
type SecretScanningHistory 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 []*CustomPatternScan `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
Expand Down Expand Up @@ -285,3 +331,51 @@ 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 responsePushProtectionBypass *PushProtectionBypass
resp, err := s.client.Do(ctx, req, &responsePushProtectionBypass)
if err != nil {
return nil, resp, err
}
return responsePushProtectionBypass, 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) (*SecretScanningHistory, *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 *SecretScanningHistory
resp, err := s.client.Do(ctx, req, &secretScanningHistory)
if err != nil {
return nil, resp, err
}

return secretScanningHistory, resp, nil
}
125 changes: 125 additions & 0 deletions github/secret_scanning_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

startAt1 := Timestamp{time.Date(2025, time.July, 29, 9, 55, 0, 0, time.UTC)}
completeAt1 := Timestamp{time.Date(2025, time.July, 29, 10, 0, 0, 0, time.UTC)}
startAt2 := Timestamp{time.Date(2025, time.July, 29, 9, 0, 0, 0, time.UTC)}

want := &SecretScanningHistory{
IncrementalScans: []*SecretsScan{
{Type: "incremental", Status: "success", CompletedAt: &completeAt1, StartedAt: &startAt1},
},
BackfillScans: []*SecretsScan{},
PatternUpdateScans: []*SecretsScan{},
CustomPatternBackfillScans: []*CustomPatternScan{
{
SecretsScan: SecretsScan{Type: "custom_backfill", Status: "in_progress", CompletedAt: nil, StartedAt: &startAt2},
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
})
}