Skip to content

Commit 84acce0

Browse files
feat: Add two new Secret Scanning API endpoints (#3687)
1 parent 29b4fac commit 84acce0

File tree

4 files changed

+316
-0
lines changed

4 files changed

+316
-0
lines changed

github/github-accessors.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

github/github-accessors_test.go

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

github/secret_scanning.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,53 @@ type SecretScanningAlertUpdateOptions struct {
120120
ResolutionComment *string `json:"resolution_comment,omitempty"`
121121
}
122122

123+
// PushProtectionBypassRequest represents the parameters for CreatePushProtectionBypass.
124+
type PushProtectionBypassRequest struct {
125+
// The reason for bypassing push protection.
126+
// Can be one of: false_positive, used_in_tests, will_fix_later
127+
Reason string `json:"reason"`
128+
// PlaceholderID is an identifier used for the bypass request.
129+
// GitHub Secret Scanning provides you with a unique PlaceholderID associated with that specific blocked push.
130+
PlaceholderID string `json:"placeholder_id"`
131+
}
132+
133+
// PushProtectionBypass represents the response from CreatePushProtectionBypass.
134+
type PushProtectionBypass struct {
135+
// The reason for bypassing push protection.
136+
Reason string `json:"reason"`
137+
// The time that the bypass will expire in ISO 8601 format.
138+
ExpireAt *Timestamp `json:"expire_at"`
139+
// The token type this bypass is for.
140+
TokenType string `json:"token_type"`
141+
}
142+
143+
// SecretsScan represents the common fields for a secret scanning scan.
144+
type SecretsScan struct {
145+
Type string `json:"type"`
146+
Status string `json:"status"`
147+
CompletedAt *Timestamp `json:"completed_at,omitempty"`
148+
StartedAt *Timestamp `json:"started_at,omitempty"`
149+
}
150+
151+
// CustomPatternBackfillScan represents a scan with an associated custom pattern.
152+
type CustomPatternBackfillScan struct {
153+
SecretsScan
154+
PatternSlug *string `json:"pattern_slug,omitempty"`
155+
PatternScope *string `json:"pattern_scope,omitempty"`
156+
}
157+
158+
// SecretScanningScanHistory is the top-level struct for the secret scanning API response.
159+
type SecretScanningScanHistory struct {
160+
// Information on incremental scan performed by secret scanning on the repository.
161+
IncrementalScans []*SecretsScan `json:"incremental_scans,omitempty"`
162+
// Information on backfill scan performed by secret scanning on the repository.
163+
BackfillScans []*SecretsScan `json:"backfill_scans,omitempty"`
164+
// Information on pattern update scan performed by secret scanning on the repository.
165+
PatternUpdateScans []*SecretsScan `json:"pattern_update_scans,omitempty"`
166+
// Information on custom pattern backfill scan performed by secret scanning on the repository.
167+
CustomPatternBackfillScans []*CustomPatternBackfillScan `json:"custom_pattern_backfill_scans,omitempty"`
168+
}
169+
123170
// ListAlertsForEnterprise lists secret scanning alerts for eligible repositories in an enterprise, from newest to oldest.
124171
//
125172
// 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
285332

286333
return locations, resp, nil
287334
}
335+
336+
// CreatePushProtectionBypass creates a push protection bypass for a given repository.
337+
//
338+
// To use this endpoint, you must be an administrator for the repository or organization, and you must use an access token with
339+
// the repo scope or security_events scope.
340+
//
341+
// GitHub API docs: https://docs.github.com/rest/secret-scanning/secret-scanning#create-a-push-protection-bypass
342+
//
343+
//meta:operation POST /repos/{owner}/{repo}/secret-scanning/push-protection-bypasses
344+
func (s *SecretScanningService) CreatePushProtectionBypass(ctx context.Context, owner, repo string, request PushProtectionBypassRequest) (*PushProtectionBypass, *Response, error) {
345+
u := fmt.Sprintf("repos/%v/%v/secret-scanning/push-protection-bypasses", owner, repo)
346+
347+
req, err := s.client.NewRequest("POST", u, request)
348+
if err != nil {
349+
return nil, nil, err
350+
}
351+
352+
var pushProtectionBypass *PushProtectionBypass
353+
resp, err := s.client.Do(ctx, req, &pushProtectionBypass)
354+
if err != nil {
355+
return nil, resp, err
356+
}
357+
return pushProtectionBypass, resp, nil
358+
}
359+
360+
// GetScanHistory fetches the secret scanning history for a given repository.
361+
//
362+
// To use this endpoint, you must be an administrator for the repository or organization, and you must use an access token with
363+
// the repo scope or security_events scope and gitHub advanced security or secret scanning must be enabled.
364+
//
365+
// GitHub API docs: https://docs.github.com/rest/secret-scanning/secret-scanning#get-secret-scanning-scan-history-for-a-repository
366+
//
367+
//meta:operation GET /repos/{owner}/{repo}/secret-scanning/scan-history
368+
func (s *SecretScanningService) GetScanHistory(ctx context.Context, owner, repo string) (*SecretScanningScanHistory, *Response, error) {
369+
u := fmt.Sprintf("repos/%v/%v/secret-scanning/scan-history", owner, repo)
370+
371+
req, err := s.client.NewRequest("GET", u, nil)
372+
if err != nil {
373+
return nil, nil, err
374+
}
375+
376+
var secretScanningHistory *SecretScanningScanHistory
377+
resp, err := s.client.Do(ctx, req, &secretScanningHistory)
378+
if err != nil {
379+
return nil, resp, err
380+
}
381+
382+
return secretScanningHistory, resp, nil
383+
}

github/secret_scanning_test.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,128 @@ func TestSecretScanningAlertUpdateOptions_Marshal(t *testing.T) {
616616

617617
testJSONMarshal(t, u, want)
618618
}
619+
620+
func TestSecretScanningService_CreatePushProtectionBypass(t *testing.T) {
621+
t.Parallel()
622+
client, mux, _ := setup(t)
623+
624+
owner := "o"
625+
repo := "r"
626+
627+
mux.HandleFunc(fmt.Sprintf("/repos/%v/%v/secret-scanning/push-protection-bypasses", owner, repo), func(w http.ResponseWriter, r *http.Request) {
628+
testMethod(t, r, "POST")
629+
var v *PushProtectionBypassRequest
630+
assertNilError(t, json.NewDecoder(r.Body).Decode(&v))
631+
want := &PushProtectionBypassRequest{Reason: "valid reason", PlaceholderID: "bypass-123"}
632+
if !cmp.Equal(v, want) {
633+
t.Errorf("Request body = %+v, want %+v", v, want)
634+
}
635+
636+
fmt.Fprint(w, `{
637+
"reason": "valid reason",
638+
"expire_at": "2018-01-01T00:00:00Z",
639+
"token_type": "github_token"
640+
}`)
641+
})
642+
643+
ctx := t.Context()
644+
opts := PushProtectionBypassRequest{Reason: "valid reason", PlaceholderID: "bypass-123"}
645+
646+
bypass, _, err := client.SecretScanning.CreatePushProtectionBypass(ctx, owner, repo, opts)
647+
if err != nil {
648+
t.Errorf("SecretScanning.CreatePushProtectionBypass returned error: %v", err)
649+
}
650+
651+
expireTime := Timestamp{time.Date(2018, time.January, 1, 0, 0, 0, 0, time.UTC)}
652+
want := &PushProtectionBypass{
653+
Reason: "valid reason",
654+
ExpireAt: &expireTime,
655+
TokenType: "github_token",
656+
}
657+
658+
if !cmp.Equal(bypass, want) {
659+
t.Errorf("SecretScanning.CreatePushProtectionBypass returned %+v, want %+v", bypass, want)
660+
}
661+
const methodName = "CreatePushProtectionBypass"
662+
testBadOptions(t, methodName, func() (err error) {
663+
_, _, err = client.SecretScanning.CreatePushProtectionBypass(ctx, "\n", "\n", opts)
664+
return err
665+
})
666+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
667+
_, resp, err := client.SecretScanning.CreatePushProtectionBypass(ctx, "o", "r", opts)
668+
return resp, err
669+
})
670+
}
671+
672+
func TestSecretScanningService_GetScanHistory(t *testing.T) {
673+
t.Parallel()
674+
client, mux, _ := setup(t)
675+
676+
owner := "o"
677+
repo := "r"
678+
679+
mux.HandleFunc(fmt.Sprintf("/repos/%v/%v/secret-scanning/scan-history", owner, repo), func(w http.ResponseWriter, r *http.Request) {
680+
testMethod(t, r, "GET")
681+
fmt.Fprint(w, `{
682+
"incremental_scans": [
683+
{
684+
"type": "incremental",
685+
"status": "success",
686+
"completed_at": "2025-07-29T10:00:00Z",
687+
"started_at": "2025-07-29T09:55:00Z"
688+
}
689+
],
690+
"backfill_scans": [],
691+
"pattern_update_scans": [],
692+
"custom_pattern_backfill_scans": [
693+
{
694+
"type": "custom_backfill",
695+
"status": "in_progress",
696+
"completed_at": null,
697+
"started_at": "2025-07-29T09:00:00Z",
698+
"pattern_slug": "my-custom-pattern",
699+
"pattern_scope": "organization"
700+
}
701+
]
702+
}`)
703+
})
704+
705+
ctx := t.Context()
706+
707+
history, _, err := client.SecretScanning.GetScanHistory(ctx, owner, repo)
708+
if err != nil {
709+
t.Errorf("SecretScanning.GetScanHistory returned error: %v", err)
710+
}
711+
712+
incrementalScanStartAt := Timestamp{time.Date(2025, time.July, 29, 9, 55, 0, 0, time.UTC)}
713+
incrementalScancompleteAt := Timestamp{time.Date(2025, time.July, 29, 10, 0, 0, 0, time.UTC)}
714+
customPatternBackfillScanStartedAt := Timestamp{time.Date(2025, time.July, 29, 9, 0, 0, 0, time.UTC)}
715+
716+
want := &SecretScanningScanHistory{
717+
IncrementalScans: []*SecretsScan{
718+
{Type: "incremental", Status: "success", CompletedAt: &incrementalScancompleteAt, StartedAt: &incrementalScanStartAt},
719+
},
720+
BackfillScans: []*SecretsScan{},
721+
PatternUpdateScans: []*SecretsScan{},
722+
CustomPatternBackfillScans: []*CustomPatternBackfillScan{
723+
{
724+
SecretsScan: SecretsScan{Type: "custom_backfill", Status: "in_progress", CompletedAt: nil, StartedAt: &customPatternBackfillScanStartedAt},
725+
PatternSlug: Ptr("my-custom-pattern"),
726+
PatternScope: Ptr("organization"),
727+
},
728+
},
729+
}
730+
731+
if !cmp.Equal(history, want) {
732+
t.Errorf("SecretScanning.GetScanHistory returned %+v, want %+v", history, want)
733+
}
734+
const methodName = "GetScanHistory"
735+
testBadOptions(t, methodName, func() (err error) {
736+
_, _, err = client.SecretScanning.GetScanHistory(ctx, "\n", "\n")
737+
return err
738+
})
739+
testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
740+
_, resp, err := client.SecretScanning.GetScanHistory(ctx, "o", "r")
741+
return resp, err
742+
})
743+
}

0 commit comments

Comments
 (0)