diff --git a/internal/cloud/aws/tags.go b/internal/cloud/aws/tags.go index 6b462e4..01a20c8 100644 --- a/internal/cloud/aws/tags.go +++ b/internal/cloud/aws/tags.go @@ -81,6 +81,9 @@ func (p *Provider) AuditTags(ctx context.Context, required []string) ([]cloud.Ta } func (p *Provider) auditEC2Tags(ctx context.Context, required []string) ([]cloud.TagFinding, error) { + if p.ec2 == nil { + return nil, nil + } pager := ec2.NewDescribeInstancesPaginator(p.ec2, &ec2.DescribeInstancesInput{}) var findings []cloud.TagFinding @@ -116,6 +119,9 @@ func (p *Provider) auditEC2Tags(ctx context.Context, required []string) ([]cloud } func (p *Provider) auditS3Tags(ctx context.Context, required []string) ([]cloud.TagFinding, error) { + if p.s3 == nil { + return nil, nil + } listOut, err := p.s3.ListBuckets(ctx, &s3.ListBucketsInput{}) if err != nil { return nil, fmt.Errorf("list buckets: %w", err) @@ -157,6 +163,9 @@ func (p *Provider) auditS3Tags(ctx context.Context, required []string) ([]cloud. } func (p *Provider) auditRDSTags(ctx context.Context, required []string) ([]cloud.TagFinding, error) { + if p.rds == nil { + return nil, nil + } pager := rds.NewDescribeDBInstancesPaginator(p.rds, &rds.DescribeDBInstancesInput{}) var findings []cloud.TagFinding @@ -190,6 +199,9 @@ func (p *Provider) auditRDSTags(ctx context.Context, required []string) ([]cloud } func (p *Provider) auditLambdaTags(ctx context.Context, required []string) ([]cloud.TagFinding, error) { + if p.lambda == nil { + return nil, nil + } var findings []cloud.TagFinding var marker *string for { diff --git a/internal/cloud/aws/tags_test.go b/internal/cloud/aws/tags_test.go index 800f2d3..5e4af4a 100644 --- a/internal/cloud/aws/tags_test.go +++ b/internal/cloud/aws/tags_test.go @@ -120,6 +120,20 @@ func TestAuditTags_NoRequiredReturnsEmpty(t *testing.T) { } } +// A Provider with no clients wired (partial construction) must dispatch through +// every auditor without panicking — each nil-guards its client. With required +// tags set this exercises the dispatch path past the no-required early return. +func TestAuditTags_NilClientsNoPanic(t *testing.T) { + p := &Provider{} + got, err := p.AuditTags(context.Background(), []string{"Owner"}) + if err != nil { + t.Fatalf("expected no error with nil clients, got %v", err) + } + if got != nil { + t.Fatalf("expected nil findings with nil clients, got %v", got) + } +} + func TestAuditTags_EC2(t *testing.T) { mock := &tagsMockEC2{ instances: []ec2types.Instance{ diff --git a/internal/cloud/k8s/rbac.go b/internal/cloud/k8s/rbac.go index 5c631b1..20cc428 100644 --- a/internal/cloud/k8s/rbac.go +++ b/internal/cloud/k8s/rbac.go @@ -11,6 +11,12 @@ import ( "github.com/nanohype/cloudgov/internal/cloud" ) +// Provider satisfies the full cloud.K8sRBACProvider interface (Name, Detect, +// ContextName, ScanRBAC). Callers use the concrete type, so this assertion keeps +// the interface contract compiler-enforced — and documents that Name/Detect are +// interface requirements, not dead code. +var _ cloud.K8sRBACProvider = (*Provider)(nil) + // rbacAPI is the narrow K8s RBAC surface used by this package. type rbacAPI interface { ListClusterRoles(ctx context.Context) ([]rbacv1.ClusterRole, error)