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 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.
255 lines
7.2 KiB
Go
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")
|
|
}
|