Skip to content

Commit ae20577

Browse files
🐛 add a codeowner expansion limit to prevent api exhaustion (#4817)
* add a codeowner expansion limit to prevent api exhaustion Some repositories have 5,000+ CODEOWNERS, which completely exhaust API quota. 100 was arbitrarily chosen, as a large but not extreme amount of API quota to try and stay under. https://github.com/DefinitelyTyped/DefinitelyTyped/blob/95890e39aca378c38427afd218e76cc2bbd3fc31/.github/CODEOWNERS Signed-off-by: Spencer Schrock <[email protected]> * add unit test for limiting contributors Signed-off-by: Spencer Schrock <[email protected]> --------- Signed-off-by: Spencer Schrock <[email protected]>
1 parent 11e9ecb commit ae20577

File tree

2 files changed

+106
-2
lines changed

2 files changed

+106
-2
lines changed

clients/githubrepo/contributors.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"context"
1919
"fmt"
2020
"io"
21-
"maps"
2221
"net/http"
2322
"os"
2423
"strings"
@@ -30,6 +29,8 @@ import (
3029
"github.com/ossf/scorecard/v5/clients"
3130
)
3231

32+
const codeownersLimit = 100
33+
3334
// these are the paths where CODEOWNERS files can be found see
3435
// https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location
3536
//
@@ -75,7 +76,7 @@ func (handler *contributorsHandler) setup(codeOwnerFile io.ReadCloser) error {
7576
return
7677
}
7778

78-
for contributor := range maps.Values(contributors) {
79+
for _, contributor := range contributors {
7980
orgs, _, err := handler.ghClient.Organizations.List(handler.ctx, contributor.Login, nil)
8081
// This call can fail due to token scopes. So ignore error.
8182
if err == nil {
@@ -132,12 +133,16 @@ func mapCodeOwners(handler *contributorsHandler, codeOwnerFile io.ReadCloser, co
132133

133134
// expanding owners
134135
owners := make([]*clients.User, 0)
136+
outer:
135137
for _, rule := range ruleset {
136138
for _, owner := range rule.Owners {
137139
switch owner.Type {
138140
case codeowners.UsernameOwner:
139141
// if usernameOwner just add to owners list
140142
owners = append(owners, &clients.User{Login: owner.Value, NumContributions: 0, IsCodeOwner: true})
143+
if len(owners) >= codeownersLimit {
144+
break outer
145+
}
141146
case codeowners.TeamOwner:
142147
// if teamOwner expand and add to owners list (only accessible by org members with read:org token scope)
143148
splitTeam := strings.Split(owner.Value, "/")
@@ -151,6 +156,9 @@ func mapCodeOwners(handler *contributorsHandler, codeOwnerFile io.ReadCloser, co
151156
if err == nil && response.StatusCode == http.StatusOK {
152157
for _, user := range users {
153158
owners = append(owners, &clients.User{Login: user.GetLogin(), NumContributions: 0, IsCodeOwner: true})
159+
if len(owners) >= codeownersLimit {
160+
break outer
161+
}
154162
}
155163
}
156164
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2025 OpenSSF Scorecard Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package githubrepo
15+
16+
import (
17+
"bytes"
18+
"encoding/json"
19+
"errors"
20+
"fmt"
21+
"io"
22+
"net/http"
23+
"strings"
24+
"testing"
25+
26+
"github.com/google/go-github/v53/github"
27+
28+
"github.com/ossf/scorecard/v5/clients"
29+
)
30+
31+
type codeownerRoundTripper struct{}
32+
33+
const tooManyCodeowners = 105
34+
35+
func (c codeownerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
36+
switch req.URL.Path {
37+
case "/orgs/ossf-tests/teams/foo/members":
38+
type member struct {
39+
Login string `json:"login"`
40+
}
41+
members := make([]member, 0, tooManyCodeowners)
42+
for i := range tooManyCodeowners {
43+
members = append(members, member{Login: fmt.Sprintf("user%d", i)})
44+
}
45+
jsonResp, err := json.Marshal(members)
46+
if err != nil {
47+
return nil, err
48+
}
49+
return &http.Response{
50+
Status: "200 OK",
51+
StatusCode: http.StatusOK,
52+
Body: io.NopCloser(bytes.NewReader(jsonResp)),
53+
}, nil
54+
default:
55+
return nil, errors.New("unsupported URL")
56+
}
57+
}
58+
59+
func Test_mapCodeOwners(t *testing.T) {
60+
t.Parallel()
61+
httpClient := &http.Client{
62+
Transport: codeownerRoundTripper{},
63+
}
64+
client := github.NewClient(httpClient)
65+
handler := &contributorsHandler{
66+
ghClient: client,
67+
ctx: t.Context(),
68+
}
69+
t.Run("one team with more too many codeowners", func(t *testing.T) {
70+
t.Parallel()
71+
codeowners := io.NopCloser(strings.NewReader("* @ossf-tests/foo\n"))
72+
contributors := map[string]clients.User{}
73+
mapCodeOwners(handler, codeowners, contributors)
74+
got := len(contributors)
75+
if got != codeownersLimit {
76+
t.Errorf("wanted less than %d CODEOWNERs, got %d", codeownersLimit, got)
77+
}
78+
})
79+
80+
t.Run("too many individual codeowners", func(t *testing.T) {
81+
t.Parallel()
82+
var sb strings.Builder
83+
sb.WriteRune('*')
84+
for i := range tooManyCodeowners {
85+
sb.WriteString(fmt.Sprintf(" @user%d", i))
86+
}
87+
sb.WriteString("\n")
88+
codeowners := io.NopCloser(strings.NewReader(sb.String()))
89+
contributors := map[string]clients.User{}
90+
mapCodeOwners(handler, codeowners, contributors)
91+
got := len(contributors)
92+
if got != codeownersLimit {
93+
t.Errorf("wanted less than %d CODEOWNERs, got %d", codeownersLimit, got)
94+
}
95+
})
96+
}

0 commit comments

Comments
 (0)