From 2f6803e3000c43ce0ffa68a692c5677ada574fa2 Mon Sep 17 00:00:00 2001 From: chhawchharia Date: Tue, 3 Mar 2026 22:12:44 -0800 Subject: [PATCH] feat: [CI-21342]: Aws migrated and vulnerabilities fixed (#505) Made-with: Cursor --- cmd/drone-ecr/main.go | 176 +++++++++++++++++++++--------------------- go.mod | 17 +++- go.sum | 34 +++++++- 3 files changed, 133 insertions(+), 94 deletions(-) diff --git a/cmd/drone-ecr/main.go b/cmd/drone-ecr/main.go index 431b090..7b6939e 100644 --- a/cmd/drone-ecr/main.go +++ b/cmd/drone-ecr/main.go @@ -1,35 +1,31 @@ package main import ( + "context" "encoding/base64" + "errors" "fmt" - "io/ioutil" "log" "os" "os/exec" "strconv" "strings" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/ecr" + ecrtypes "github.com/aws/aws-sdk-go-v2/service/ecr/types" + "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/joho/godotenv" "github.com/sirupsen/logrus" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ecr" - docker "github.com/drone-plugins/drone-docker" ) -type ecrAPI interface { - DescribeImages(*ecr.DescribeImagesInput) (*ecr.DescribeImagesOutput, error) -} - const defaultRegion = "us-east-1" func main() { - // Load env-file if it exists first if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" { godotenv.Load(env) } @@ -50,7 +46,6 @@ func main() { skipPushIfTagExists = parseBoolOrDefault(false, getenv("PLUGIN_SKIP_PUSH_IF_TAG_EXISTS")) ) - // set the region if region == "" { region = defaultRegion } @@ -62,13 +57,15 @@ func main() { os.Setenv("AWS_SECRET_ACCESS_KEY", secret) } - sess, err := session.NewSession(&aws.Config{Region: ®ion}) + ctx := context.Background() + + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) if err != nil { - log.Fatal(fmt.Sprintf("error creating aws session: %v", err)) + log.Fatal(fmt.Sprintf("error creating aws config: %v", err)) } - svc := getECRClient(sess, assumeRole, externalId, idToken) - username, password, defaultRegistry, err := getAuthInfo(svc) + svc := getECRClient(cfg, assumeRole, externalId, idToken) + username, password, defaultRegistry, err := getAuthInfo(ctx, svc) if registry == "" { registry = defaultRegistry @@ -83,32 +80,32 @@ func main() { } if create { - err = ensureRepoExists(svc, trimHostname(repo, registry), scanOnPush) + err = ensureRepoExists(ctx, svc, trimHostname(repo, registry), scanOnPush) if err != nil { log.Fatal(fmt.Sprintf("error creating ECR repo: %v", err)) } - err = updateImageScannningConfig(svc, trimHostname(repo, registry), scanOnPush) + err = updateImageScanningConfig(ctx, svc, trimHostname(repo, registry), scanOnPush) if err != nil { log.Fatal(fmt.Sprintf("error updating scan on push for ECR repo: %v", err)) } } if lifecyclePolicy != "" { - p, err := ioutil.ReadFile(lifecyclePolicy) + p, err := os.ReadFile(lifecyclePolicy) if err != nil { log.Fatal(err) } - if err := uploadLifeCyclePolicy(svc, string(p), trimHostname(repo, registry)); err != nil { + if err := uploadLifeCyclePolicy(ctx, svc, string(p), trimHostname(repo, registry)); err != nil { log.Fatal(fmt.Sprintf("error uploading ECR lifecycle policy: %v", err)) } } if repositoryPolicy != "" { - p, err := ioutil.ReadFile(repositoryPolicy) + p, err := os.ReadFile(repositoryPolicy) if err != nil { log.Fatal(err) } - if err := uploadRepositoryPolicy(svc, string(p), trimHostname(repo, registry)); err != nil { + if err := uploadRepositoryPolicy(ctx, svc, string(p), trimHostname(repo, registry)); err != nil { log.Fatal(fmt.Sprintf("error uploading ECR repository policy. %v", err)) } } @@ -119,7 +116,6 @@ func main() { os.Setenv("DOCKER_PASSWORD", password) os.Setenv("PLUGIN_REGISTRY_TYPE", "ECR") - // Skip if tag already exits for both mutable and immutable repos if skipPushIfTagExists { tagInput := getenv("PLUGIN_TAG", "PLUGIN_TAGS") var tags []string @@ -136,7 +132,7 @@ func main() { repositoryName := trimHostname(repo, registry) for _, t := range tags { - exists, err := tagExists(svc, repositoryName, t) + exists, err := tagExists(ctx, svc, repositoryName, t) if err != nil { logrus.Fatalf("Error checking if image exists for tag %s: %v", t, err) } @@ -147,7 +143,6 @@ func main() { } } - // invoke the base docker plugin binary cmd := exec.Command(docker.GetDroneDockerExecCmd()) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -162,57 +157,63 @@ func trimHostname(repo, registry string) string { return repo } -func ensureRepoExists(svc *ecr.ECR, name string, scanOnPush bool) (err error) { - input := &ecr.CreateRepositoryInput{} - input.SetRepositoryName(name) - input.SetImageScanningConfiguration(&ecr.ImageScanningConfiguration{ScanOnPush: &scanOnPush}) - _, err = svc.CreateRepository(input) +func ensureRepoExists(ctx context.Context, svc *ecr.Client, name string, scanOnPush bool) error { + _, err := svc.CreateRepository(ctx, &ecr.CreateRepositoryInput{ + RepositoryName: aws.String(name), + ImageScanningConfiguration: &ecrtypes.ImageScanningConfiguration{ + ScanOnPush: scanOnPush, + }, + }) if err != nil { - if aerr, ok := err.(awserr.Error); ok && aerr.Code() == ecr.ErrCodeRepositoryAlreadyExistsException { - // eat it, we skip checking for existing to save two requests - err = nil + var rae *ecrtypes.RepositoryAlreadyExistsException + if errors.As(err, &rae) { + return nil } + return err } - - return + return nil } -func updateImageScannningConfig(svc *ecr.ECR, name string, scanOnPush bool) (err error) { - input := &ecr.PutImageScanningConfigurationInput{} - input.SetRepositoryName(name) - input.SetImageScanningConfiguration(&ecr.ImageScanningConfiguration{ScanOnPush: &scanOnPush}) - _, err = svc.PutImageScanningConfiguration(input) - +func updateImageScanningConfig(ctx context.Context, svc *ecr.Client, name string, scanOnPush bool) error { + _, err := svc.PutImageScanningConfiguration(ctx, &ecr.PutImageScanningConfigurationInput{ + RepositoryName: aws.String(name), + ImageScanningConfiguration: &ecrtypes.ImageScanningConfiguration{ + ScanOnPush: scanOnPush, + }, + }) return err } -func uploadLifeCyclePolicy(svc *ecr.ECR, lifecyclePolicy string, name string) (err error) { - input := &ecr.PutLifecyclePolicyInput{} - input.SetLifecyclePolicyText(lifecyclePolicy) - input.SetRepositoryName(name) - _, err = svc.PutLifecyclePolicy(input) - +func uploadLifeCyclePolicy(ctx context.Context, svc *ecr.Client, lifecyclePolicy string, name string) error { + _, err := svc.PutLifecyclePolicy(ctx, &ecr.PutLifecyclePolicyInput{ + LifecyclePolicyText: aws.String(lifecyclePolicy), + RepositoryName: aws.String(name), + }) return err } -func uploadRepositoryPolicy(svc *ecr.ECR, repositoryPolicy string, name string) (err error) { - input := &ecr.SetRepositoryPolicyInput{} - input.SetPolicyText(repositoryPolicy) - input.SetRepositoryName(name) - _, err = svc.SetRepositoryPolicy(input) - +func uploadRepositoryPolicy(ctx context.Context, svc *ecr.Client, repositoryPolicy string, name string) error { + _, err := svc.SetRepositoryPolicy(ctx, &ecr.SetRepositoryPolicyInput{ + PolicyText: aws.String(repositoryPolicy), + RepositoryName: aws.String(name), + }) return err } -func getAuthInfo(svc *ecr.ECR) (username, password, registry string, err error) { +func getAuthInfo(ctx context.Context, svc *ecr.Client) (username, password, registry string, err error) { var result *ecr.GetAuthorizationTokenOutput var decoded []byte - result, err = svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{}) + result, err = svc.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) if err != nil { return } + if len(result.AuthorizationData) == 0 { + err = fmt.Errorf("no authorization data returned from ECR") + return + } + auth := result.AuthorizationData[0] token := *auth.AuthorizationToken decoded, err = base64.StdEncoding.DecodeString(token) @@ -221,7 +222,11 @@ func getAuthInfo(svc *ecr.ECR) (username, password, registry string, err error) } registry = strings.TrimPrefix(*auth.ProxyEndpoint, "https://") - creds := strings.Split(string(decoded), ":") + creds := strings.SplitN(string(decoded), ":", 2) + if len(creds) < 2 { + err = fmt.Errorf("invalid ECR authorization token format") + return + } username = creds[0] password = creds[1] return @@ -233,7 +238,6 @@ func parseBoolOrDefault(defaultValue bool, s string) (result bool) { if err != nil { result = defaultValue } - return } @@ -247,55 +251,51 @@ func getenv(key ...string) (s string) { return } -func getECRClient(sess *session.Session, role string, externalId string, idToken string) *ecr.ECR { +func getECRClient(cfg aws.Config, role string, externalId string, idToken string) *ecr.Client { if role == "" { - return ecr.New(sess) + return ecr.NewFromConfig(cfg) } + stsSvc := sts.NewFromConfig(cfg) + if idToken != "" { - tempFile, err := os.CreateTemp("/tmp", "idToken-*.jwt") - if err != nil { - log.Fatalf("Failed to create temporary file: %v", err) - } - defer tempFile.Close() + provider := stscreds.NewWebIdentityRoleProvider(stsSvc, role, identityToken(idToken)) + cfg.Credentials = aws.NewCredentialsCache(provider) + return ecr.NewFromConfig(cfg) + } - if err := os.Chmod(tempFile.Name(), 0600); err != nil { - log.Fatalf("Failed to set file permissions: %v", err) - } - - if _, err := tempFile.WriteString(idToken); err != nil { - log.Fatalf("Failed to write ID token to temporary file: %v", err) - } - - // Create credentials using the path to the ID token file - creds := stscreds.NewWebIdentityCredentials(sess, role, "", tempFile.Name()) - return ecr.New(sess, &aws.Config{Credentials: creds}) - } else if externalId != "" { - return ecr.New(sess, &aws.Config{ - Credentials: stscreds.NewCredentials(sess, role, func(p *stscreds.AssumeRoleProvider) { - p.ExternalID = &externalId - }), + var provider *stscreds.AssumeRoleProvider + if externalId != "" { + provider = stscreds.NewAssumeRoleProvider(stsSvc, role, func(o *stscreds.AssumeRoleOptions) { + o.ExternalID = &externalId }) } else { - return ecr.New(sess, &aws.Config{ - Credentials: stscreds.NewCredentials(sess, role), - }) + provider = stscreds.NewAssumeRoleProvider(stsSvc, role) } + cfg.Credentials = aws.NewCredentialsCache(provider) + return ecr.NewFromConfig(cfg) } -func tagExists(svc ecrAPI, repository, tag string) (bool, error) { +func tagExists(ctx context.Context, svc *ecr.Client, repository, tag string) (bool, error) { input := &ecr.DescribeImagesInput{ RepositoryName: aws.String(repository), - ImageIds: []*ecr.ImageIdentifier{ + ImageIds: []ecrtypes.ImageIdentifier{ {ImageTag: aws.String(tag)}, }, } - output, err := svc.DescribeImages(input) + output, err := svc.DescribeImages(ctx, input) if err != nil { - if aerr, ok := err.(awserr.Error); ok && aerr.Code() == "ImageNotFoundException" { + var inf *ecrtypes.ImageNotFoundException + if errors.As(err, &inf) { return false, nil } return false, err } return len(output.ImageDetails) > 0, nil } + +type identityToken string + +func (t identityToken) GetIdentityToken() ([]byte, error) { + return []byte(t), nil +} diff --git a/go.mod b/go.mod index 8fc588d..e665766 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,11 @@ module github.com/drone-plugins/drone-docker require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 - github.com/aws/aws-sdk-go v1.26.7 + github.com/aws/aws-sdk-go-v2 v1.41.2 + github.com/aws/aws-sdk-go-v2/config v1.32.10 + github.com/aws/aws-sdk-go-v2/credentials v1.19.10 + github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 github.com/coreos/go-semver v0.3.0 github.com/dchest/uniuri v1.2.0 github.com/drone-plugins/drone-plugin-lib v0.4.1 @@ -22,6 +26,16 @@ require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect + github.com/aws/smithy-go v1.24.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect @@ -31,7 +45,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 8fbd4cc..8864f92 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,36 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/aws/aws-sdk-go v1.26.7 h1:ObjEnmzvSdYy8KVd3me7v/UMyCn81inLy2SyoIPoBkg= -github.com/aws/aws-sdk-go v1.26.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= +github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= +github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 h1:RtGctYMmkTerGClvdY6bHXdtly4FeYw9wz/NPz62LF8= +github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3/go.mod h1:vBfBu24Ka3/5UZtepbTV0gnc9VPLT8ok+0oDDaYAzn4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= +github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= +github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -81,8 +109,6 @@ github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56 github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743 h1:X3Xxno5Ji8idrNiUoFc7QyXpqhSYlDRYQmc7mlpMBzU= github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743/go.mod h1:KrtyD5PFj++GKkFS/7/RRrfnRhAMGQwy75GLCHWrCNs= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=