From 8090f69e3dc31b6a781b058196cfdb64274f97c5 Mon Sep 17 00:00:00 2001 From: James Salt Date: Tue, 2 Jun 2026 15:05:12 +0100 Subject: [PATCH] BCH-1290: Extend CertificateContext with renewal_eligibility, issued_at, type Adds three fields from DescribeCertificate that are required by the ACM TLS Endpoints control coverage: renewal_eligibility (ELIGIBLE/INELIGIBLE), issued_at (*time.Time, omitempty), and type (AMAZON_ISSUED/IMPORTED/PRIVATE). All three are already returned by the existing DescribeCertificate call with no additional IAM permissions required. Co-Authored-By: Claude Sonnet 4.6 --- internal/data.go | 6 ++ internal/data_test.go | 127 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) diff --git a/internal/data.go b/internal/data.go index c6e1fbb..a16c7f3 100644 --- a/internal/data.go +++ b/internal/data.go @@ -35,7 +35,10 @@ type CertificateContext struct { DomainName string `json:"domain_name"` Status string `json:"status"` NotAfter *time.Time `json:"not_after,omitempty"` + IssuedAt *time.Time `json:"issued_at,omitempty"` KeyAlgorithm string `json:"key_algorithm"` + Type string `json:"type,omitempty"` + RenewalEligibility string `json:"renewal_eligibility,omitempty"` TransparencyLoggingPreference string `json:"transparency_logging_preference"` DomainValidationOptions []DomainValidationOption `json:"domain_validation_options"` InUseBy []string `json:"in_use_by"` @@ -160,7 +163,10 @@ func (df *DataFetcher) fetchCertificate(ctx context.Context, client ACMClient, r DomainName: aws.ToString(detail.DomainName), Status: string(detail.Status), NotAfter: detail.NotAfter, + IssuedAt: detail.IssuedAt, KeyAlgorithm: strings.ReplaceAll(string(detail.KeyAlgorithm), "-", "_"), + Type: string(detail.Type), + RenewalEligibility: string(detail.RenewalEligibility), TransparencyLoggingPreference: transparencyPref, DomainValidationOptions: dvos, InUseBy: inUseBy, diff --git a/internal/data_test.go b/internal/data_test.go index 76dcaed..f03bcaf 100644 --- a/internal/data_test.go +++ b/internal/data_test.go @@ -252,6 +252,133 @@ func TestFetchData_KeyTypeFilter(t *testing.T) { } } +func TestFetchData_NewFields_AmazonIssuedEligible(t *testing.T) { + arn := "arn:aws:acm:us-east-1:123456789012:certificate/eligible" + issuedAt := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) + notAfter := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + + mock := &mockACMClient{ + listCertificates: func(_ context.Context, _ *acm.ListCertificatesInput, _ ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) { + return &acm.ListCertificatesOutput{ + CertificateSummaryList: []types.CertificateSummary{{CertificateArn: aws.String(arn)}}, + }, nil + }, + describeCertificate: func(_ context.Context, _ *acm.DescribeCertificateInput, _ ...func(*acm.Options)) (*acm.DescribeCertificateOutput, error) { + return &acm.DescribeCertificateOutput{ + Certificate: &types.CertificateDetail{ + CertificateArn: aws.String(arn), + DomainName: aws.String("example.com"), + Status: types.CertificateStatusIssued, + NotAfter: ¬After, + IssuedAt: &issuedAt, + Type: types.CertificateTypeAmazonIssued, + RenewalEligibility: types.RenewalEligibilityEligible, + }, + }, nil + }, + listTagsForCertificate: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) { + return &acm.ListTagsForCertificateOutput{}, nil + }, + } + + f := newTestFetcher([]string{"us-east-1"}, mock) + certs, err := f.FetchData(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + c := certs[0] + if c.Type != "AMAZON_ISSUED" { + t.Errorf("type: want %q, got %q", "AMAZON_ISSUED", c.Type) + } + if c.RenewalEligibility != "ELIGIBLE" { + t.Errorf("renewal_eligibility: want %q, got %q", "ELIGIBLE", c.RenewalEligibility) + } + if c.IssuedAt == nil || !c.IssuedAt.Equal(issuedAt) { + t.Errorf("issued_at: want %v, got %v", issuedAt, c.IssuedAt) + } +} + +func TestFetchData_NewFields_AmazonIssuedIneligible(t *testing.T) { + arn := "arn:aws:acm:us-east-1:123456789012:certificate/ineligible" + + mock := &mockACMClient{ + listCertificates: func(_ context.Context, _ *acm.ListCertificatesInput, _ ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) { + return &acm.ListCertificatesOutput{ + CertificateSummaryList: []types.CertificateSummary{{CertificateArn: aws.String(arn)}}, + }, nil + }, + describeCertificate: func(_ context.Context, _ *acm.DescribeCertificateInput, _ ...func(*acm.Options)) (*acm.DescribeCertificateOutput, error) { + return &acm.DescribeCertificateOutput{ + Certificate: &types.CertificateDetail{ + CertificateArn: aws.String(arn), + DomainName: aws.String("example.com"), + Status: types.CertificateStatusIssued, + Type: types.CertificateTypeAmazonIssued, + RenewalEligibility: types.RenewalEligibilityIneligible, + }, + }, nil + }, + listTagsForCertificate: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) { + return &acm.ListTagsForCertificateOutput{}, nil + }, + } + + f := newTestFetcher([]string{"us-east-1"}, mock) + certs, err := f.FetchData(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + c := certs[0] + if c.RenewalEligibility != "INELIGIBLE" { + t.Errorf("renewal_eligibility: want %q, got %q", "INELIGIBLE", c.RenewalEligibility) + } + if c.IssuedAt != nil { + t.Errorf("issued_at: want nil for cert without IssuedAt, got %v", c.IssuedAt) + } +} + +func TestFetchData_NewFields_ImportedCert(t *testing.T) { + arn := "arn:aws:acm:us-east-1:123456789012:certificate/imported" + + mock := &mockACMClient{ + listCertificates: func(_ context.Context, _ *acm.ListCertificatesInput, _ ...func(*acm.Options)) (*acm.ListCertificatesOutput, error) { + return &acm.ListCertificatesOutput{ + CertificateSummaryList: []types.CertificateSummary{{CertificateArn: aws.String(arn)}}, + }, nil + }, + describeCertificate: func(_ context.Context, _ *acm.DescribeCertificateInput, _ ...func(*acm.Options)) (*acm.DescribeCertificateOutput, error) { + return &acm.DescribeCertificateOutput{ + Certificate: &types.CertificateDetail{ + CertificateArn: aws.String(arn), + DomainName: aws.String("example.com"), + Status: types.CertificateStatusIssued, + Type: types.CertificateTypeImported, + // RenewalEligibility and IssuedAt are zero/nil for imported certs + }, + }, nil + }, + listTagsForCertificate: func(_ context.Context, _ *acm.ListTagsForCertificateInput, _ ...func(*acm.Options)) (*acm.ListTagsForCertificateOutput, error) { + return &acm.ListTagsForCertificateOutput{}, nil + }, + } + + f := newTestFetcher([]string{"us-east-1"}, mock) + certs, err := f.FetchData(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + c := certs[0] + if c.Type != "IMPORTED" { + t.Errorf("type: want %q, got %q", "IMPORTED", c.Type) + } + if c.RenewalEligibility != "" { + t.Errorf("renewal_eligibility: want %q for imported cert, got %q", "", c.RenewalEligibility) + } + if c.IssuedAt != nil { + t.Errorf("issued_at: want nil for imported cert, got %v", c.IssuedAt) + } +} + func TestFetchData_AccountIDFromARN(t *testing.T) { cases := []struct { arn string