Files
trufflehog/pkg/sources/s3/s3_test.go
Shahzad Haider 30d5bb91af
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Lint / man-page-staleness (push) Waiting to run
Lint / semgrep (push) Waiting to run
Lint / checksecretparts (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Release / mark-latest (push) Canceled after 0s
Release / Release (push) Canceled after 0s
S3: surface bucket listing failures and fix multi-role object count (#5035)
* s3: surface bucket listing failures for explicitly configured buckets

Listing failures were logged at V(3) whenever a role was assumed, hiding
access and role-assumption errors for buckets the user explicitly asked
to scan. Suppression now applies only in list-all-buckets mode, and a
bucket_list_errors_total metric records every listing failure.

* s3: accumulate object count across role passes

scanBuckets runs once per configured role and reset its object counter
each pass, so the final progress message only reflected the last role's
count. Multi-role scans could report 0 objects scanned even when earlier
roles scanned objects.
2026-06-18 12:17:10 +05:00

255 lines
7.2 KiB
Go

package s3
import (
"encoding/base64"
"fmt"
"os"
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/anypb"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/credentialspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
func TestSource_Init_IncludeAndIgnoreBucketsError(t *testing.T) {
conn, err := anypb.New(&sourcespb.S3{
Credential: &sourcespb.S3_AccessKey{
AccessKey: &credentialspb.KeySecret{
Key: "ignored for test",
Secret: "ignore for test",
},
},
Buckets: []string{"a"},
IgnoreBuckets: []string{"b"},
})
assert.NoError(t, err)
s := Source{}
err = s.Init(context.Background(), "s3 test source", 0, 0, false, conn, 1)
assert.Error(t, err)
}
func TestSource_ListErrorsAreExpected(t *testing.T) {
tests := []struct {
name string
role string
buckets []string
want bool
}{
{
name: "no role, no explicit buckets",
want: false,
},
{
name: "no role, explicit buckets",
buckets: []string{"bucket-a"},
want: false,
},
{
name: "role without explicit buckets, denials are expected",
role: "arn:aws:iam::123456789012:role/some-role",
want: true,
},
{
name: "role with explicit buckets, denials are errors",
role: "arn:aws:iam::123456789012:role/some-role",
buckets: []string{"bucket-a"},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conn, err := anypb.New(&sourcespb.S3{
Credential: &sourcespb.S3_AccessKey{
AccessKey: &credentialspb.KeySecret{
Key: "ignored for test",
Secret: "ignored for test",
},
},
Buckets: tt.buckets,
Roles: []string{tt.role},
})
require.NoError(t, err)
s := Source{}
require.NoError(t, s.Init(context.Background(), "s3 test source", 0, 0, false, conn, 1))
assert.Equal(t, tt.want, s.listErrorsAreExpected(tt.role))
})
}
}
func TestSource_ScanBucketsReportsCumulativeObjectCount(t *testing.T) {
conn, err := anypb.New(&sourcespb.S3{
Credential: &sourcespb.S3_Unauthenticated{},
})
require.NoError(t, err)
s := Source{}
require.NoError(t, s.Init(context.Background(), "s3 test source", 0, 0, false, conn, 1))
// Simulate a later role pass after an earlier pass already scanned three
// objects. The pass below scans no buckets, so the completion message must
// still report the cumulative total rather than resetting to zero.
totalObjectCount := uint64(3)
s.scanBuckets(context.Background(), nil, "", nil, make(chan *sources.Chunk, 1), &totalObjectCount)
assert.Equal(t, uint64(3), totalObjectCount)
assert.Contains(t, s.Message, "3 objects scanned")
}
func TestSource_Chunks(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
secret, err := common.GetTestSecret(ctx)
if err != nil {
t.Fatal(fmt.Errorf("failed to access secret: %v", err))
}
s3key := secret.MustGetField("AWS_S3_KEY")
s3secret := secret.MustGetField("AWS_S3_SECRET")
type init struct {
name string
verify bool
connection *sourcespb.S3
setEnv map[string]string
}
tests := []struct {
name string
init init
wantErr bool
wantChunkData string
}{
{
name: "gets chunks",
init: init{
connection: &sourcespb.S3{
Credential: &sourcespb.S3_AccessKey{
AccessKey: &credentialspb.KeySecret{
Key: s3key,
Secret: s3secret,
},
},
Buckets: []string{"truffletestbucket-s3-tests"},
},
},
wantErr: false,
wantChunkData: `W2RlZmF1bHRdCmF3c19hY2Nlc3Nfa2V5X2lkID0gQUtJQTM1T0hYMkRTT1pHNjQ3TkgKYXdzX3NlY3JldF9hY2Nlc3Nfa2V5ID0gUXk5OVMrWkIvQ1dsRk50eFBBaWQ3Z0d6dnNyWGhCQjd1ckFDQUxwWgpvdXRwdXQgPSBqc29uCnJlZ2lvbiA9IHVzLWVhc3QtMg==`,
},
{
name: "gets chunks after assuming role",
// This test will attempt to scan every bucket in the account, but the role policy blocks access to every
// bucket except the one we want. This (expected behavior) causes errors in the test log output, but these
// errors shouldn't actually cause test failures.
init: init{
connection: &sourcespb.S3{
Roles: []string{"arn:aws:iam::619888638459:role/s3-test-assume-role"},
},
setEnv: map[string]string{
"AWS_ACCESS_KEY_ID": s3key,
"AWS_SECRET_ACCESS_KEY": s3secret,
},
},
wantErr: false,
wantChunkData: `W2RlZmF1bHRdCmF3c19zZWNyZXRfYWNjZXNzX2tleSA9IFF5OTlTK1pCL0NXbEZOdHhQQWlkN2dHenZzclhoQkI3dXJBQ0FMcFoKYXdzX2FjY2Vzc19rZXlfaWQgPSBBS0lBMzVPSFgyRFNPWkc2NDdOSApvdXRwdXQgPSBqc29uCnJlZ2lvbiA9IHVzLWVhc3QtMg==`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.name == "gets chunks after assuming role" {
t.Skip("skipping until our test environment stabilizes enough that we know how we're going to handle this")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
for k, v := range tt.init.setEnv {
t.Setenv(k, v)
}
s := Source{}
conn, err := anypb.New(tt.init.connection)
if err != nil {
t.Fatal(err)
}
err = s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 8)
if (err != nil) != tt.wantErr {
t.Errorf("Source.Init() error = %v, wantErr %v", err, tt.wantErr)
return
}
chunksCh := make(chan *sources.Chunk, 1)
go func() {
defer close(chunksCh)
err = s.Chunks(ctx, chunksCh)
if (err != nil) != tt.wantErr {
t.Errorf("Source.Chunks() error = %v, wantErr %v", err, tt.wantErr)
os.Exit(1)
}
}()
waitFn := func() {
receivedFirstChunk := false
for {
select {
case <-ctx.Done():
t.Errorf("TestSource_Chunks timed out: %v", ctx.Err())
return
case gotChunk, ok := <-chunksCh:
if !ok {
t.Logf("Source.Chunks() finished, channel closed")
assert.Equal(t, "", s.GetProgress().EncodedResumeInfo)
assert.Equal(t, int64(100), s.GetProgress().PercentComplete)
return
}
if receivedFirstChunk {
// wantChunkData is the first chunk data. After the first chunk has
// been received and matched below, we want to drain chunksCh
// so Source.Chunks() can finish completely.
continue
}
receivedFirstChunk = true
wantData, _ := base64.StdEncoding.DecodeString(tt.wantChunkData)
if diff := pretty.Compare(gotChunk.Data, wantData); diff != "" {
t.Logf("%s: Source.Chunks() diff: (-got +want)\n%s", tt.name, diff)
}
}
}
}
waitFn()
})
}
}
func TestSource_UnmarshalSourceUnit(t *testing.T) {
s := Source{}
unitJSON := `{
"Bucket": "my-test-bucket",
"Role": "my-test-role"
}`
unit, err := s.UnmarshalSourceUnit([]byte(unitJSON))
require.NoError(t, err, "UnmarshalSourceUnit should not return an error")
s3Unit, ok := unit.(S3SourceUnit)
require.True(t, ok, "Unmarshaled unit should be of type S3SourceUnit")
assert.Equal(t, "my-test-bucket", s3Unit.Bucket, "Bucket field should match")
assert.Equal(t, "my-test-role", s3Unit.Role, "Role field should match")
}