Skip to content

Commit 834ea15

Browse files
Merge branch 'main' into feat/oss-123-azure-cosmosdb-detector
2 parents 3da7426 + 3259c4b commit 834ea15

File tree

2 files changed

+69
-2
lines changed

2 files changed

+69
-2
lines changed

pkg/sources/postman/postman_client.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
11+
"golang.org/x/time/rate"
1112

1213
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
1314
)
@@ -185,6 +186,12 @@ type Client struct {
185186

186187
// Headers to attach to every requests made with the client.
187188
Headers map[string]string
189+
190+
// Rate limiter needed for Postman API workspace enumeration. Postman API general rate limit 300 requests per minute
191+
// but is 10 calls in 10 seconds for GET /collections, GET /workspaces, and GET /workspaces/{id} endpoints.
192+
WorkspaceEnumerationRateLimiter *rate.Limiter
193+
194+
// TODO: Add rate limiter(s) for Postman API calls inside of other loops
188195
}
189196

190197
// NewClient returns a new Postman API client.
@@ -196,8 +203,9 @@ func NewClient(postmanToken string) *Client {
196203
}
197204

198205
c := &Client{
199-
HTTPClient: http.DefaultClient,
200-
Headers: bh,
206+
HTTPClient: http.DefaultClient,
207+
Headers: bh,
208+
WorkspaceEnumerationRateLimiter: rate.NewLimiter(rate.Every(time.Second), 1),
201209
}
202210

203211
return c
@@ -273,6 +281,9 @@ func (c *Client) EnumerateWorkspaces(ctx context.Context) ([]Workspace, error) {
273281
}
274282

275283
for i, workspace := range workspacesObj.Workspaces {
284+
if err := c.WorkspaceEnumerationRateLimiter.Wait(ctx); err != nil {
285+
return nil, fmt.Errorf("could not wait for rate limiter during workspace enumeration: %w", err)
286+
}
276287
tempWorkspace, err := c.GetWorkspace(ctx, workspace.ID)
277288
if err != nil {
278289
return nil, fmt.Errorf("could not get workspace %q (%s) during enumeration: %w", workspace.Name, workspace.ID, err)

pkg/sources/postman/postman_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package postman
22

33
import (
4+
"fmt"
45
"reflect"
56
"sort"
67
"strings"
78
"testing"
9+
"time"
810

911
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
12+
"gopkg.in/h2non/gock.v1"
1013

1114
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
1215
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
@@ -238,3 +241,56 @@ func TestSource_ScanVariableData(t *testing.T) {
238241
})
239242
}
240243
}
244+
245+
func TestSource_ScanEnumerateRateLimit(t *testing.T) {
246+
defer gock.Off()
247+
// Mock the API response for workspaces
248+
numWorkspaces := 3
249+
workspaceBodyString := `{"workspaces":[`
250+
for i := 0; i < numWorkspaces; i++ {
251+
workspaceBodyString += fmt.Sprintf(`{"id": "%d", "name": "workspace-%d", "type": "personal", "visibility": "personal", "createdBy": "1234"}`, i, i)
252+
if i == numWorkspaces-1 {
253+
workspaceBodyString += `]}`
254+
} else {
255+
workspaceBodyString += `,`
256+
}
257+
}
258+
gock.New("https://api.getpostman.com").
259+
Get("/workspaces").
260+
Reply(200).
261+
BodyString(workspaceBodyString)
262+
// Mock the API response for each individual workspace
263+
for i := 0; i < numWorkspaces; i++ {
264+
gock.New("https://api.getpostman.com").
265+
Get(fmt.Sprintf("/workspaces/%d", i)).
266+
Reply(200).
267+
BodyString(fmt.Sprintf(`{"workspace":{"id":"%d","name":"workspace-%d","type":"personal","description":"Test workspace number %d",
268+
"visibility":"personal","createdBy":"1234","updatedBy":"1234","createdAt":"2024-12-12T23:32:27.000Z","updatedAt":"2024-12-12T23:33:01.000Z",
269+
"collections":[{"id":"abc%d","name":"test-collection-1","uid":"1234-abc%d"},{"id":"def%d","name":"test-collection-2","uid":"1234-def%d"}],
270+
"environments":[{"id":"ghi%d","name":"test-environment-1","uid":"1234-ghi%d"},{"id":"jkl%d","name":"test-environment-2","uid":"1234-jkl%d"}]}}`, i, i, i, i, i, i, i, i, i, i, i))
271+
}
272+
273+
ctx := context.Background()
274+
s, conn := createTestSource(&sourcespb.Postman{
275+
Credential: &sourcespb.Postman_Token{
276+
Token: "super-secret-token",
277+
},
278+
})
279+
err := s.Init(ctx, "test - postman", 0, 1, false, conn, 1)
280+
if err != nil {
281+
t.Fatalf("init error: %v", err)
282+
}
283+
gock.InterceptClient(s.client.HTTPClient)
284+
285+
start := time.Now()
286+
_, err = s.client.EnumerateWorkspaces(ctx)
287+
if err != nil {
288+
t.Fatalf("enumeration error: %v", err)
289+
}
290+
elapsed := time.Since(start)
291+
// With <numWorkspaces> requests at 1 per second rate limit,
292+
// elapsed time should be at least <numWorkspaces - 1> seconds
293+
if elapsed < time.Duration(numWorkspaces-1)*time.Second {
294+
t.Errorf("Rate limiting not working as expected. Elapsed time: %v, expected at least %d seconds", elapsed, numWorkspaces-1)
295+
}
296+
}

0 commit comments

Comments
 (0)