From 623bb4fefd186462d01be0ee660c8d34a5ffe681 Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Tue, 8 Sep 2026 19:00:31 +0100 Subject: [PATCH 1/7] fix(snapshot azure): only send Azure credentials to Azure Container Registry snapshot azure took the registry host from each Web App's own linuxFxVersion and built an authenticated azcontainerregistry client for it, with no check that the host was an Azure Container Registry. A registry that answers with an ordinary 401 Bearer challenge makes the SDK fetch an AAD token for the containerregistry.azure.net audience and POST it to /oauth2/exchange, so any host named in an app's container configuration could collect that token. Anyone with Microsoft.Web/sites/config/write on a scanned resource group could set that host. The built-in Website Contributor role grants it and carries no Microsoft.ContainerRegistry actions at all, so this turned a role with no registry access into one that could read every registry the snapshot service principal could reach. Resolution is now layered, and only the ACR path attaches the credential: - a digest-pinned reference already carries the fingerprint, so it is read straight from the reference with no registry call - an Azure Container Registry login server is read as before, with the Azure credential - any other host is read through digest.OciSha256 with no credentials The third case also fixes an existing failure. A non-ACR host previously errored out (the exchange returns 405 on ghcr.io and 404 on Docker Hub), and because one app's error cancels the whole snapshot, a single app on a third-party registry broke the entire environment report. Those apps now resolve correctly when the image is public, and produce an error naming the app and host, and pointing at --digests-source logs, when it is not. Reading the digest from the App Service API instead would avoid the registry entirely, but no such field exists: digest and sha256 appear nowhere in the Microsoft.Web/sites specs from 2023-01-01 through 2026-07-15, and siteContainers is configuration only. App Service re-pulls on restart and scale-out, so instances of one app can run different digests and no single field could describe them. Also drops a redundant GetManifestProperties call that re-read the digest already returned by GetManifest, validates every fingerprint through digest.ValidateDigest, and adds the parseImageName tests that were absent. Co-Authored-By: Claude Opus 5 --- cmd/kosli/root.go | 2 +- cmd/kosli/snapshotAzureApps.go | 7 + internal/azure/azure_apps.go | 116 ++++++++++++- internal/azure/image_fingerprint_test.go | 209 +++++++++++++++++++++++ 4 files changed, 325 insertions(+), 9 deletions(-) create mode 100644 internal/azure/image_fingerprint_test.go diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index f2d180ce3..50295c192 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -203,7 +203,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file, azureTenantIdFlag = "Azure tenant ID." azureSubscriptionIdFlag = "Azure subscription ID." azureResourceGroupNameFlag = "Azure resource group name." - azureDigestsSourceFlag = "[defaulted] Where to get the digests from. Valid values are 'acr' and 'logs'." + azureDigestsSourceFlag = "[defaulted] Where to get the digests from. Valid values are 'acr' and 'logs'. With 'acr', Azure credentials are only sent to Azure Container Registry login servers; an app whose image comes from any other registry is read without credentials, so a private third-party registry needs 'logs'." githubTokenFlag = "Github token." githubOrgFlag = "Github organization. (defaulted if you are running in GitHub Actions: https://docs.kosli.com/integrations/ci_cd )." githubBaseURLFlag = "[optional] GitHub base URL (only needed for GitHub Enterprise installations)." diff --git a/cmd/kosli/snapshotAzureApps.go b/cmd/kosli/snapshotAzureApps.go index bd0b12582..8ee90ac3e 100644 --- a/cmd/kosli/snapshotAzureApps.go +++ b/cmd/kosli/snapshotAzureApps.go @@ -24,6 +24,13 @@ will not match. See https://learn.microsoft.com/en-us/azure/azure-functions/functions-app-settings#website_run_from_package For zip-deployed apps, the fingerprint respects a ^.kosli_ignore^ file at the root of the deployed package. + +With ^--digests-source acr^, the registry is taken from each app's own container configuration. Azure +credentials are only ever sent to an Azure Container Registry login server. An app whose image comes +from any other registry is read without credentials, which works for a public image but not a private +one; report those apps with ^--digests-source logs^ instead. + +^--dry-run^ suppresses only the request to Kosli. Azure discovery and registry lookups still run. ` + kosliIgnoreDesc + azureAuthDesc const snapshotAzureAppsExample = ` diff --git a/internal/azure/azure_apps.go b/internal/azure/azure_apps.go index 76a7b0215..929e630a4 100644 --- a/internal/azure/azure_apps.go +++ b/internal/azure/azure_apps.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/url" "os" @@ -21,6 +22,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" armappservice "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2" smithyTime "github.com/aws/smithy-go/time" + "github.com/kosli-dev/cli/internal/digest" "github.com/kosli-dev/cli/internal/logger" "github.com/kosli-dev/cli/internal/server" ) @@ -377,7 +379,7 @@ func (azureClient *AzureClient) fingerprintDockerService(app *armappservice.Site if azureClient.Credentials.DigestsSource == "acr" { fingerprintSource = "acr" - fingerprint, err = azureClient.GetImageFingerprintFromRegistry(imageName, logger) + fingerprint, err = azureClient.GetImageFingerprint(imageName, logger) // Handle exception when image is not found in the registry but is found in the environment if err != nil { return AppData{}, err @@ -399,38 +401,136 @@ func (azureClient *AzureClient) fingerprintDockerService(app *armappservice.Site return AppData{*app.Name, *app.Kind, fingerprintSource, map[string]string{imageName: fingerprint}, startedAt}, nil } -func (azureClient *AzureClient) GetImageFingerprintFromRegistry(imageName string, logger *logger.Logger) (fingerprint string, err error) { +// acrLoginServerSuffixes are the Azure Container Registry login-server suffixes +// for the public, China and US Government clouds. The Azure SDK publishes only +// the token audience per cloud, not the login-server suffix. +var acrLoginServerSuffixes = []string{".azurecr.io", ".azurecr.cn", ".azurecr.us"} + +// imageFingerprintSource is how an image reference is resolved to a fingerprint. +type imageFingerprintSource int + +const ( + // fingerprintFromPinnedDigest takes the fingerprint from the reference itself. + fingerprintFromPinnedDigest imageFingerprintSource = iota + // fingerprintFromACR reads it from Azure Container Registry, authenticated + // with the Azure credential. + fingerprintFromACR + // fingerprintFromAnonymousRegistry reads it from any other registry with no + // credential attached. + fingerprintFromAnonymousRegistry +) + +// isACRLoginServer reports whether host is an Azure Container Registry login +// server, matching on a whole label so that "azurecr.io.example.com" is not one. +func isACRLoginServer(host string) bool { + h := strings.ToLower(host) + if hostWithoutPort, _, err := net.SplitHostPort(h); err == nil { + h = hostWithoutPort + } + for _, suffix := range acrLoginServerSuffixes { + if len(h) > len(suffix) && strings.HasSuffix(h, suffix) { + return true + } + } + return false +} + +// pinnedDigest returns the fingerprint of a digest-pinned reference, whose tag +// parseImageName returns as "sha256:". +func pinnedDigest(tag string) (string, bool) { + fingerprint, pinned := strings.CutPrefix(tag, "sha256:") + if !pinned { + return "", false + } + if err := digest.ValidateDigest(fingerprint); err != nil { + return "", false + } + return fingerprint, true +} + +// classifyImageReference decides how a reference is resolved. Only +// fingerprintFromACR attaches the Azure credential, so this is the boundary that +// keeps that credential away from a registry host taken from an app's own +// configuration. +func classifyImageReference(registryHost, tag string) imageFingerprintSource { + if _, pinned := pinnedDigest(tag); pinned { + return fingerprintFromPinnedDigest + } + if isACRLoginServer(registryHost) { + return fingerprintFromACR + } + return fingerprintFromAnonymousRegistry +} + +// GetImageFingerprint resolves the fingerprint of a container image referenced +// by a Web App. The registry host comes from the app's own configuration, which +// anyone with write access to that app controls, so the Azure credential is +// attached only for an Azure Container Registry login server. +func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *logger.Logger) (string, error) { registryUrl, repoName, tag := parseImageName(imageName) + if registryUrl == "" { + return "", fmt.Errorf("image name [%s] does not name a registry host", imageName) + } + registryHost := strings.TrimPrefix(registryUrl, "https://") + + switch classifyImageReference(registryHost, tag) { + case fingerprintFromPinnedDigest: + fingerprint, _ := pinnedDigest(tag) + logger.Debug("For image '%s' took fingerprint '%s' from the pinned digest", imageName, fingerprint) + return fingerprint, nil + case fingerprintFromACR: + return azureClient.acrImageFingerprint(imageName, registryUrl, repoName, tag, logger) + default: + return anonymousImageFingerprint(imageName, registryHost, logger) + } +} +// acrImageFingerprint reads a fingerprint from Azure Container Registry using +// the Azure credential supplied to Kosli. +func (azureClient *AzureClient) acrImageFingerprint(imageName, registryUrl, repoName, tag string, logger *logger.Logger) (string, error) { credentials, err := azidentity.NewClientSecretCredential(azureClient.Credentials.TenantId, azureClient.Credentials.ClientId, azureClient.Credentials.ClientSecret, nil) if err != nil { return "", err } - AcrClient, err := azcontainerregistry.NewClient(registryUrl, credentials, nil) + acrClient, err := azcontainerregistry.NewClient(registryUrl, credentials, nil) if err != nil { return "", err } - manifestRes, err := AcrClient.GetManifest(context.TODO(), repoName, tag, + manifestRes, err := acrClient.GetManifest(context.TODO(), repoName, tag, &azcontainerregistry.ClientGetManifestOptions{Accept: to.Ptr("application/vnd.docker.distribution.manifest.v2+json")}) if err != nil { return "", err } + if manifestRes.DockerContentDigest == nil { + return "", fmt.Errorf("no digest returned for image [%s]", imageName) + } - manifestPropsRes, err := AcrClient.GetManifestProperties(context.TODO(), repoName, *manifestRes.DockerContentDigest, nil) - if err != nil { + fingerprint := strings.TrimPrefix(*manifestRes.DockerContentDigest, "sha256:") + if err := digest.ValidateDigest(fingerprint); err != nil { return "", err } - fingerprint = strings.TrimPrefix(*manifestPropsRes.Manifest.Digest, "sha256:") - logger.Debug("For image '%s' got fingerprint '%s' from ACR", imageName, fingerprint) return fingerprint, nil } +// anonymousImageFingerprint reads a fingerprint from a registry outside Azure +// Container Registry without attaching any credential. +func anonymousImageFingerprint(imageName, registryHost string, logger *logger.Logger) (string, error) { + fingerprint, err := digest.OciSha256(imageName, "", "") + if err != nil { + return "", fmt.Errorf("failed to get the fingerprint of image [%s] from [%s]: %s. Azure credentials are only sent to Azure Container Registry, so a registry needing other credentials cannot be read here. Use --digests-source logs to report this app", imageName, registryHost, err) + } + + logger.Debug("For image '%s' got fingerprint '%s' from '%s' without credentials", imageName, fingerprint, registryHost) + + return fingerprint, nil +} + func parseImageName(imageName string) (registryUrl, repoName, tag string) { // Parse the image name to extract the repository name and tag // Example: tookyregistry.azurecr.io/tooky/sha256:latest diff --git a/internal/azure/image_fingerprint_test.go b/internal/azure/image_fingerprint_test.go new file mode 100644 index 000000000..beb9cfee7 --- /dev/null +++ b/internal/azure/image_fingerprint_test.go @@ -0,0 +1,209 @@ +package azure + +import ( + "strings" + "testing" + + "github.com/kosli-dev/cli/internal/logger" + "github.com/stretchr/testify/require" +) + +func TestParseImageName(t *testing.T) { + for _, tc := range []struct { + name string + imageName string + registryUrl string + repoName string + tag string + }{ + { + name: "acr image with a tag", + imageName: "myregistry.azurecr.io/myrepo/myapp:1.0", + registryUrl: "https://myregistry.azurecr.io", + repoName: "myrepo/myapp", + tag: "1.0", + }, + { + name: "acr image pinned to a digest", + imageName: "myregistry.azurecr.io/myapp@sha256:" + strings.Repeat("a", 64), + registryUrl: "https://myregistry.azurecr.io", + repoName: "myapp", + tag: "sha256:" + strings.Repeat("a", 64), + }, + { + name: "image without a tag defaults to latest", + imageName: "myregistry.azurecr.io/myapp", + registryUrl: "https://myregistry.azurecr.io", + repoName: "myapp", + tag: "latest", + }, + { + name: "third party registry", + imageName: "ghcr.io/owner/app:v2", + registryUrl: "https://ghcr.io", + repoName: "owner/app", + tag: "v2", + }, + { + name: "host with a port", + imageName: "registry.example.com:8443/app:v1", + registryUrl: "https://registry.example.com:8443", + repoName: "app", + tag: "v1", + }, + { + name: "no registry host means nothing is parsed", + imageName: "nginx:latest", + registryUrl: "", + repoName: "", + tag: "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + registryUrl, repoName, tag := parseImageName(tc.imageName) + require.Equal(t, tc.registryUrl, registryUrl) + require.Equal(t, tc.repoName, repoName) + require.Equal(t, tc.tag, tag) + }) + } +} + +func TestIsACRLoginServer(t *testing.T) { + for _, tc := range []struct { + host string + want bool + }{ + {host: "myregistry.azurecr.io", want: true}, + {host: "MyRegistry.AzureCR.IO", want: true}, + {host: "myregistry.azurecr.cn", want: true}, + {host: "myregistry.azurecr.us", want: true}, + {host: "myregistry.azurecr.io:443", want: true}, + // A suffix on its own names no registry. + {host: "azurecr.io", want: false}, + {host: ".azurecr.io", want: false}, + // The suffix must be the end of the host, not a label inside it. + {host: "azurecr.io.attacker.example", want: false}, + {host: "myregistry.azurecr.io.attacker.example", want: false}, + // Nor may it merely appear in a longer label. + {host: "notazurecr.io", want: false}, + {host: "ghcr.io", want: false}, + {host: "registry-1.docker.io", want: false}, + {host: "mcr.microsoft.com", want: false}, + {host: "169.254.169.254", want: false}, + {host: "attacker.example:8443", want: false}, + {host: "", want: false}, + } { + t.Run(tc.host, func(t *testing.T) { + require.Equal(t, tc.want, isACRLoginServer(tc.host)) + }) + } +} + +func TestPinnedDigest(t *testing.T) { + validSha256 := strings.Repeat("a", 64) + + fingerprint, pinned := pinnedDigest("sha256:" + validSha256) + require.True(t, pinned) + require.Equal(t, validSha256, fingerprint) + + for _, tag := range []string{ + "latest", + "1.0", + "sha256:tooshort", + "sha256:" + strings.Repeat("z", 64), // 64 chars but not hex + "sha256:", + "", + } { + t.Run(tag, func(t *testing.T) { + _, pinned := pinnedDigest(tag) + require.False(t, pinned) + }) + } +} + +// TestClassifyImageReferenceKeepsAzureCredentialForACROnly is the regression +// test for the credential-forwarding vulnerability: the Azure credential is +// attached only on the fingerprintFromACR path, so any host that is not an ACR +// login server must classify as something else. +func TestClassifyImageReferenceKeepsAzureCredentialForACROnly(t *testing.T) { + validSha256 := strings.Repeat("a", 64) + + for _, tc := range []struct { + name string + registryHost string + tag string + want imageFingerprintSource + }{ + { + name: "acr host with a tag authenticates to acr", + registryHost: "myregistry.azurecr.io", + tag: "1.0", + want: fingerprintFromACR, + }, + { + name: "attacker controlled host is resolved anonymously", + registryHost: "attacker.example", + tag: "1.0", + want: fingerprintFromAnonymousRegistry, + }, + { + name: "acr lookalike host is resolved anonymously", + registryHost: "azurecr.io.attacker.example", + tag: "1.0", + want: fingerprintFromAnonymousRegistry, + }, + { + name: "third party registry is resolved anonymously", + registryHost: "ghcr.io", + tag: "v2", + want: fingerprintFromAnonymousRegistry, + }, + { + name: "instance metadata address is resolved anonymously", + registryHost: "169.254.169.254", + tag: "latest", + want: fingerprintFromAnonymousRegistry, + }, + { + name: "a pinned digest needs no registry at all", + registryHost: "myregistry.azurecr.io", + tag: "sha256:" + validSha256, + want: fingerprintFromPinnedDigest, + }, + { + name: "a pinned digest on an attacker host needs no registry either", + registryHost: "attacker.example", + tag: "sha256:" + validSha256, + want: fingerprintFromPinnedDigest, + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.want, classifyImageReference(tc.registryHost, tc.tag)) + }) + } +} + +// TestGetImageFingerprintDoesNotAuthenticateToNonACRHost asserts the whole +// resolver, not just the classifier: a non-ACR host must not reach Azure +// authentication. The Azure credentials here are deliberately invalid, so an +// attempt to use them would surface as an Azure credential error. +func TestGetImageFingerprintDoesNotAuthenticateToNonACRHost(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{ + TenantId: "00000000-0000-0000-0000-000000000000", + ClientId: "00000000-0000-0000-0000-000000000000", + ClientSecret: "not-a-real-secret", + DigestsSource: "acr", + }} + + // A closed local port, so the lookup fails immediately and reaches no registry. + _, err := client.GetImageFingerprint("127.0.0.1:1/owner/app:v1", logger.NewStandardLogger()) + require.Error(t, err) + require.Contains(t, err.Error(), "Azure credentials are only sent to Azure Container Registry") +} + +func TestGetImageFingerprintRejectsImageWithoutRegistryHost(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{}} + _, err := client.GetImageFingerprint("nginx:latest", logger.NewStandardLogger()) + require.Error(t, err) + require.Contains(t, err.Error(), "does not name a registry host") +} From 69589ad0b7e6c3b2564077073e766d7655158ace Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Tue, 8 Sep 2026 19:40:07 +0100 Subject: [PATCH 2/7] fix(snapshot azure): classify the registry with a real reference parser Review found that the suffix check could be talked into disagreeing with the client about which host it was naming, which reopened the leak this branch exists to close. net.SplitHostPort does not validate the port, so for "myregistry.azurecr.io:443@attacker.example" it returns a host of "myregistry.azurecr.io" and the suffix check passes, while url.Parse reads the same string as userinfo plus a host of attacker.example. The Azure credential therefore went to the attacker. With the parser removed as a mutation the request is visible in the test output: Get "https://myregistry.azurecr.io:***@attacker.example/v2/repo:tag/manifests/tag" parseImageName is replaced by reference.ParseNormalizedNamed, which rejects all three smuggling forms. Classification runs on reference.Domain, and the reference handed to a resolver is the canonical form that was classified, so the two cannot disagree about which registry is contacted. Normalising also removes the need for a special case for references with no registry host: "nginx:latest" becomes docker.io/library/nginx:latest and takes the anonymous arm as an ordinary case. Those references previously errored out and, because one app's error cancels the run, took the whole snapshot with them. digest.OciSha256Anonymous replaces OciSha256 with empty credentials on the anonymous path. Empty credentials leave DockerAuthConfig nil, which makes containers/image fall back to credential discovery, so a runner holding a docker login for a host could present it to a registry named in an app's configuration. Proven with a planted DOCKER_CONFIG: nil sent Basic bGVha2VkdXNlcjpsZWFrZWRwYXNz, a non-nil empty config sent Basic Og==. The digest-pinned short-circuit added earlier on this branch is removed. It skipped the existence check the old code got from GetManifest. In its place GetImageFingerprint now holds the registry to a pinned reference, erroring when the returned digest differs from the pinned one, because docker.GetDigest reports the registry's header without cross-checking it. Also: %w instead of %s so callers can inspect the wrapped error, a shorter error message, trailing-dot hosts trimmed before the suffix check, and a func-var seam so tests assert which resolver was chosen and with what reference. All new tests verified red against the specific bug each one covers. Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- internal/azure/azure_apps.go | 146 ++++++----- internal/azure/image_fingerprint_test.go | 304 ++++++++++++++++------- internal/digest/digest.go | 16 +- 4 files changed, 298 insertions(+), 170 deletions(-) diff --git a/go.mod b/go.mod index a5fd06f2b..cba5aeeb1 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/aws/smithy-go v1.28.1 github.com/containerd/errdefs v1.0.0 github.com/containers/image/v5 v5.36.2 + github.com/distribution/reference v0.6.0 github.com/go-git/go-billy/v5 v5.9.1 github.com/go-git/go-git/v5 v5.19.2 github.com/go-playground/validator/v10 v10.30.3 @@ -106,7 +107,6 @@ require ( github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect - github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.3.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect diff --git a/internal/azure/azure_apps.go b/internal/azure/azure_apps.go index 929e630a4..62b56756b 100644 --- a/internal/azure/azure_apps.go +++ b/internal/azure/azure_apps.go @@ -22,6 +22,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" armappservice "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2" smithyTime "github.com/aws/smithy-go/time" + "github.com/distribution/reference" "github.com/kosli-dev/cli/internal/digest" "github.com/kosli-dev/cli/internal/logger" "github.com/kosli-dev/cli/internal/server" @@ -410,22 +411,20 @@ var acrLoginServerSuffixes = []string{".azurecr.io", ".azurecr.cn", ".azurecr.us type imageFingerprintSource int const ( - // fingerprintFromPinnedDigest takes the fingerprint from the reference itself. - fingerprintFromPinnedDigest imageFingerprintSource = iota - // fingerprintFromACR reads it from Azure Container Registry, authenticated - // with the Azure credential. - fingerprintFromACR + // fingerprintFromACR reads the fingerprint from Azure Container Registry, + // authenticated with the Azure credential. + fingerprintFromACR imageFingerprintSource = iota // fingerprintFromAnonymousRegistry reads it from any other registry with no // credential attached. fingerprintFromAnonymousRegistry ) -// isACRLoginServer reports whether host is an Azure Container Registry login +// isACRLoginServer reports whether domain is an Azure Container Registry login // server, matching on a whole label so that "azurecr.io.example.com" is not one. -func isACRLoginServer(host string) bool { - h := strings.ToLower(host) +func isACRLoginServer(domain string) bool { + h := strings.TrimSuffix(strings.ToLower(domain), ".") if hostWithoutPort, _, err := net.SplitHostPort(h); err == nil { - h = hostWithoutPort + h = strings.TrimSuffix(hostWithoutPort, ".") } for _, suffix := range acrLoginServerSuffixes { if len(h) > len(suffix) && strings.HasSuffix(h, suffix) { @@ -435,54 +434,72 @@ func isACRLoginServer(host string) bool { return false } -// pinnedDigest returns the fingerprint of a digest-pinned reference, whose tag -// parseImageName returns as "sha256:". -func pinnedDigest(tag string) (string, bool) { - fingerprint, pinned := strings.CutPrefix(tag, "sha256:") - if !pinned { - return "", false - } - if err := digest.ValidateDigest(fingerprint); err != nil { - return "", false - } - return fingerprint, true -} - // classifyImageReference decides how a reference is resolved. Only // fingerprintFromACR attaches the Azure credential, so this is the boundary that -// keeps that credential away from a registry host taken from an app's own +// keeps that credential away from a registry taken from an app's own // configuration. -func classifyImageReference(registryHost, tag string) imageFingerprintSource { - if _, pinned := pinnedDigest(tag); pinned { - return fingerprintFromPinnedDigest - } - if isACRLoginServer(registryHost) { +func classifyImageReference(domain string) imageFingerprintSource { + if isACRLoginServer(domain) { return fingerprintFromACR } return fingerprintFromAnonymousRegistry } +// parseImageReference splits an App Service image reference into its registry +// domain, repository path, canonical form, and pinned digest if it has one. +// +// It uses the same normalising parser as the registry clients rather than +// splitting the string by hand, because a hand-rolled split can be talked into +// disagreeing with the client about which host it named: +// "reg.azurecr.io:443@attacker.example/repo:tag" passes a suffix check on the +// registry component but resolves to attacker.example as a URL. The parser +// rejects it. +func parseImageReference(imageName string) (domain, path, canonical, pinnedDigest string, err error) { + named, err := reference.ParseNormalizedNamed(imageName) + if err != nil { + return "", "", "", "", fmt.Errorf("failed to parse the image name [%s]: %w", imageName, err) + } + + if digested, ok := named.(reference.Digested); ok { + pinnedDigest = strings.TrimPrefix(digested.Digest().String(), "sha256:") + } + + named = reference.TagNameOnly(named) + + return reference.Domain(named), reference.Path(named), named.String(), pinnedDigest, nil +} + // GetImageFingerprint resolves the fingerprint of a container image referenced -// by a Web App. The registry host comes from the app's own configuration, which +// by a Web App. The registry comes from the app's own configuration, which // anyone with write access to that app controls, so the Azure credential is // attached only for an Azure Container Registry login server. func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *logger.Logger) (string, error) { - registryUrl, repoName, tag := parseImageName(imageName) - if registryUrl == "" { - return "", fmt.Errorf("image name [%s] does not name a registry host", imageName) - } - registryHost := strings.TrimPrefix(registryUrl, "https://") - - switch classifyImageReference(registryHost, tag) { - case fingerprintFromPinnedDigest: - fingerprint, _ := pinnedDigest(tag) - logger.Debug("For image '%s' took fingerprint '%s' from the pinned digest", imageName, fingerprint) - return fingerprint, nil - case fingerprintFromACR: - return azureClient.acrImageFingerprint(imageName, registryUrl, repoName, tag, logger) - default: - return anonymousImageFingerprint(imageName, registryHost, logger) + domain, path, canonical, pinnedDigest, err := parseImageReference(imageName) + if err != nil { + return "", err + } + + // The reference handed to a resolver is the one that was classified, so the + // two cannot disagree about which registry is contacted. + tagOrDigest := canonical[strings.LastIndexAny(canonical, ":@")+1:] + + var fingerprint string + if classifyImageReference(domain) == fingerprintFromACR { + fingerprint, err = azureClient.acrImageFingerprint(canonical, "https://"+domain, path, tagOrDigest, logger) + } else { + fingerprint, err = anonymousImageFingerprint(canonical, domain, logger) + } + if err != nil { + return "", err + } + + // A pinned reference is a claim about which image is deployed, so hold the + // registry to it rather than reporting whichever digest it returned. + if pinnedDigest != "" && fingerprint != pinnedDigest { + return "", fmt.Errorf("image [%s] is pinned to digest sha256:%s but [%s] reported sha256:%s", imageName, pinnedDigest, domain, fingerprint) } + + return fingerprint, nil } // acrImageFingerprint reads a fingerprint from Azure Container Registry using @@ -518,46 +535,23 @@ func (azureClient *AzureClient) acrImageFingerprint(imageName, registryUrl, repo return fingerprint, nil } +// anonymousFingerprint resolves a fingerprint with no credential presented. It +// is a variable so tests can assert the reference the resolver is handed. +var anonymousFingerprint = digest.OciSha256Anonymous + // anonymousImageFingerprint reads a fingerprint from a registry outside Azure -// Container Registry without attaching any credential. -func anonymousImageFingerprint(imageName, registryHost string, logger *logger.Logger) (string, error) { - fingerprint, err := digest.OciSha256(imageName, "", "") +// Container Registry, presenting no credential. +func anonymousImageFingerprint(imageName, domain string, logger *logger.Logger) (string, error) { + fingerprint, err := anonymousFingerprint(imageName) if err != nil { - return "", fmt.Errorf("failed to get the fingerprint of image [%s] from [%s]: %s. Azure credentials are only sent to Azure Container Registry, so a registry needing other credentials cannot be read here. Use --digests-source logs to report this app", imageName, registryHost, err) + return "", fmt.Errorf("failed to get the fingerprint of image [%s] from [%s]: %w. Azure credentials are only sent to Azure Container Registry; use --digests-source logs for this app", imageName, domain, err) } - logger.Debug("For image '%s' got fingerprint '%s' from '%s' without credentials", imageName, fingerprint, registryHost) + logger.Debug("For image '%s' got fingerprint '%s' from '%s' with no credentials", imageName, fingerprint, domain) return fingerprint, nil } -func parseImageName(imageName string) (registryUrl, repoName, tag string) { - // Parse the image name to extract the repository name and tag - // Example: tookyregistry.azurecr.io/tooky/sha256:latest - splitFullImageName := strings.SplitN(imageName, "/", 2) - if len(splitFullImageName) != 2 { - return "", "", "" - } - - registryUrl = fmt.Sprintf("https://%s", splitFullImageName[0]) - - if strings.Contains(splitFullImageName[1], "@sha256:") { - // Example: tookyregistry.azurecr.io/tooky@sha256:cb29a6..7 - imageNameAndTag := strings.SplitN(splitFullImageName[1], "@", 2) - repoName = imageNameAndTag[0] - tag = imageNameAndTag[1] - } else if strings.Contains(splitFullImageName[1], ":") { - imageNameAndTag := strings.SplitN(splitFullImageName[1], ":", 2) - repoName = imageNameAndTag[0] - tag = imageNameAndTag[1] - } else { - repoName = splitFullImageName[1] - tag = "latest" - } - - return registryUrl, repoName, tag -} - func (app *AppData) IsEmpty() bool { return app.AppName == "" && len(app.Digests) == 0 && app.StartedAt == 0 } diff --git a/internal/azure/image_fingerprint_test.go b/internal/azure/image_fingerprint_test.go index beb9cfee7..e408077a0 100644 --- a/internal/azure/image_fingerprint_test.go +++ b/internal/azure/image_fingerprint_test.go @@ -1,6 +1,7 @@ package azure import ( + "errors" "strings" "testing" @@ -8,66 +9,147 @@ import ( "github.com/stretchr/testify/require" ) -func TestParseImageName(t *testing.T) { +func TestParseImageReference(t *testing.T) { + validSha256 := strings.Repeat("a", 64) + for _, tc := range []struct { name string imageName string - registryUrl string - repoName string - tag string + domain string + path string + canonical string + pinnedSha string + wantErrText string }{ { - name: "acr image with a tag", - imageName: "myregistry.azurecr.io/myrepo/myapp:1.0", - registryUrl: "https://myregistry.azurecr.io", - repoName: "myrepo/myapp", - tag: "1.0", + name: "acr image with a tag", + imageName: "myregistry.azurecr.io/myrepo/myapp:1.0", + domain: "myregistry.azurecr.io", + path: "myrepo/myapp", + canonical: "myregistry.azurecr.io/myrepo/myapp:1.0", + }, + { + name: "acr image pinned to a digest", + imageName: "myregistry.azurecr.io/myapp@sha256:" + validSha256, + domain: "myregistry.azurecr.io", + path: "myapp", + canonical: "myregistry.azurecr.io/myapp@sha256:" + validSha256, + pinnedSha: validSha256, + }, + { + name: "image without a tag defaults to latest", + imageName: "myregistry.azurecr.io/myapp", + domain: "myregistry.azurecr.io", + path: "myapp", + canonical: "myregistry.azurecr.io/myapp:latest", + }, + { + name: "third party registry", + imageName: "ghcr.io/owner/app:v2", + domain: "ghcr.io", + path: "owner/app", + canonical: "ghcr.io/owner/app:v2", + }, + { + name: "host with a port", + imageName: "registry.example.com:8443/app:v1", + domain: "registry.example.com:8443", + path: "app", + canonical: "registry.example.com:8443/app:v1", }, { - name: "acr image pinned to a digest", - imageName: "myregistry.azurecr.io/myapp@sha256:" + strings.Repeat("a", 64), - registryUrl: "https://myregistry.azurecr.io", - repoName: "myapp", - tag: "sha256:" + strings.Repeat("a", 64), + name: "docker hub short form is normalised", + imageName: "nginx:latest", + domain: "docker.io", + path: "library/nginx", + canonical: "docker.io/library/nginx:latest", }, { - name: "image without a tag defaults to latest", - imageName: "myregistry.azurecr.io/myapp", - registryUrl: "https://myregistry.azurecr.io", - repoName: "myapp", - tag: "latest", + name: "docker hub user image is normalised", + imageName: "myuser/myimage:tag", + domain: "docker.io", + path: "myuser/myimage", + canonical: "docker.io/myuser/myimage:tag", }, + // Regression: a registry component that a suffix check reads as ACR but a + // URL parser resolves to a different host must be rejected outright, or the + // Azure credential is handed to that other host. { - name: "third party registry", - imageName: "ghcr.io/owner/app:v2", - registryUrl: "https://ghcr.io", - repoName: "owner/app", - tag: "v2", + name: "acr host smuggled into userinfo with a port is rejected", + imageName: "myregistry.azurecr.io:443@attacker.example/repo:tag", + wantErrText: "failed to parse the image name", }, { - name: "host with a port", - imageName: "registry.example.com:8443/app:v1", - registryUrl: "https://registry.example.com:8443", - repoName: "app", - tag: "v1", + name: "acr host smuggled into userinfo without a numeric port is rejected", + imageName: "myregistry.azurecr.io:x@attacker.example/repo:tag", + wantErrText: "failed to parse the image name", }, { - name: "no registry host means nothing is parsed", - imageName: "nginx:latest", - registryUrl: "", - repoName: "", - tag: "", + name: "acr host smuggled into userinfo with no port is rejected", + imageName: "myregistry.azurecr.io@attacker.example/repo:tag", + wantErrText: "failed to parse the image name", }, } { t.Run(tc.name, func(t *testing.T) { - registryUrl, repoName, tag := parseImageName(tc.imageName) - require.Equal(t, tc.registryUrl, registryUrl) - require.Equal(t, tc.repoName, repoName) - require.Equal(t, tc.tag, tag) + domain, path, canonical, pinned, err := parseImageReference(tc.imageName) + if tc.wantErrText != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErrText) + return + } + require.NoError(t, err) + require.Equal(t, tc.domain, domain) + require.Equal(t, tc.path, path) + require.Equal(t, tc.canonical, canonical) + require.Equal(t, tc.pinnedSha, pinned) + }) + } +} + +// TestGetImageFingerprintRejectsSmuggledACRHost is the regression test for the +// bypass: the credential must not reach a host that only looks like ACR. +func TestGetImageFingerprintRejectsSmuggledACRHost(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{ + TenantId: "00000000-0000-0000-0000-000000000000", + ClientId: "00000000-0000-0000-0000-000000000000", + ClientSecret: "not-a-real-secret", + DigestsSource: "acr", + }} + + for _, imageName := range []string{ + "myregistry.azurecr.io:443@attacker.example/repo:tag", + "myregistry.azurecr.io:x@attacker.example/repo:tag", + "myregistry.azurecr.io@attacker.example/repo:tag", + } { + t.Run(imageName, func(t *testing.T) { + got := stubAnonymousFingerprint(t, strings.Repeat("a", 64), nil) + + _, err := client.GetImageFingerprint(imageName, logger.NewStandardLogger()) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse the image name") + require.Empty(t, *got, "must not resolve at all") }) } } +// TestGetImageFingerprintHoldsTheRegistryToAPinnedDigest stops a registry +// reporting a digest other than the one the reference pinned. +func TestGetImageFingerprintHoldsTheRegistryToAPinnedDigest(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{DigestsSource: "acr"}} + pinned := strings.Repeat("a", 64) + other := strings.Repeat("b", 64) + + stubAnonymousFingerprint(t, other, nil) + _, err := client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) + require.Error(t, err) + require.Contains(t, err.Error(), "is pinned to digest sha256:"+pinned) + + stubAnonymousFingerprint(t, pinned, nil) + fingerprint, err := client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) + require.NoError(t, err) + require.Equal(t, pinned, fingerprint) +} + func TestIsACRLoginServer(t *testing.T) { for _, tc := range []struct { host string @@ -99,111 +181,151 @@ func TestIsACRLoginServer(t *testing.T) { } } -func TestPinnedDigest(t *testing.T) { - validSha256 := strings.Repeat("a", 64) - - fingerprint, pinned := pinnedDigest("sha256:" + validSha256) - require.True(t, pinned) - require.Equal(t, validSha256, fingerprint) - - for _, tag := range []string{ - "latest", - "1.0", - "sha256:tooshort", - "sha256:" + strings.Repeat("z", 64), // 64 chars but not hex - "sha256:", - "", - } { - t.Run(tag, func(t *testing.T) { - _, pinned := pinnedDigest(tag) - require.False(t, pinned) - }) - } -} - // TestClassifyImageReferenceKeepsAzureCredentialForACROnly is the regression // test for the credential-forwarding vulnerability: the Azure credential is // attached only on the fingerprintFromACR path, so any host that is not an ACR // login server must classify as something else. func TestClassifyImageReferenceKeepsAzureCredentialForACROnly(t *testing.T) { - validSha256 := strings.Repeat("a", 64) - for _, tc := range []struct { name string registryHost string - tag string want imageFingerprintSource }{ { - name: "acr host with a tag authenticates to acr", + name: "acr host authenticates to acr", registryHost: "myregistry.azurecr.io", - tag: "1.0", + want: fingerprintFromACR, + }, + { + name: "acr host written as an fqdn still authenticates to acr", + registryHost: "myregistry.azurecr.io.", want: fingerprintFromACR, }, { name: "attacker controlled host is resolved anonymously", registryHost: "attacker.example", - tag: "1.0", want: fingerprintFromAnonymousRegistry, }, { name: "acr lookalike host is resolved anonymously", registryHost: "azurecr.io.attacker.example", - tag: "1.0", want: fingerprintFromAnonymousRegistry, }, { name: "third party registry is resolved anonymously", registryHost: "ghcr.io", - tag: "v2", want: fingerprintFromAnonymousRegistry, }, { name: "instance metadata address is resolved anonymously", registryHost: "169.254.169.254", - tag: "latest", want: fingerprintFromAnonymousRegistry, }, + } { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.want, classifyImageReference(tc.registryHost)) + }) + } +} + +// stubAnonymousFingerprint replaces the anonymous resolver for the duration of a +// test and records the reference it was handed. +func stubAnonymousFingerprint(t *testing.T, fingerprint string, err error) *string { + t.Helper() + original := anonymousFingerprint + var got string + anonymousFingerprint = func(imageName string) (string, error) { + got = imageName + return fingerprint, err + } + t.Cleanup(func() { anonymousFingerprint = original }) + return &got +} + +// TestGetImageFingerprintRoutesNonACRHostsAnonymously covers the dispatch in +// GetImageFingerprint, not just the classifier, so that swapping the arms would +// fail a test. It also asserts the exact reference handed to the resolver, which +// a wiring bug (passing repoName instead of the full image name) would break. +func TestGetImageFingerprintRoutesNonACRHostsAnonymously(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{ + TenantId: "00000000-0000-0000-0000-000000000000", + ClientId: "00000000-0000-0000-0000-000000000000", + ClientSecret: "not-a-real-secret", + DigestsSource: "acr", + }} + validSha256 := strings.Repeat("a", 64) + + for _, tc := range []struct { + name string + imageName string + wantRef string + }{ { - name: "a pinned digest needs no registry at all", - registryHost: "myregistry.azurecr.io", - tag: "sha256:" + validSha256, - want: fingerprintFromPinnedDigest, + name: "third party registry", + imageName: "ghcr.io/owner/app:v2", + wantRef: "ghcr.io/owner/app:v2", }, { - name: "a pinned digest on an attacker host needs no registry either", - registryHost: "attacker.example", - tag: "sha256:" + validSha256, - want: fingerprintFromPinnedDigest, + name: "attacker controlled host", + imageName: "attacker.example/repo:latest", + wantRef: "attacker.example/repo:latest", + }, + { + name: "docker hub short form is normalised before resolving", + imageName: "nginx:latest", + wantRef: "docker.io/library/nginx:latest", + }, + { + name: "docker hub user image is normalised before resolving", + imageName: "myuser/myimage:tag", + wantRef: "docker.io/myuser/myimage:tag", + }, + { + name: "digest pinned reference on a non acr host", + imageName: "ghcr.io/owner/app@sha256:" + validSha256, + wantRef: "ghcr.io/owner/app@sha256:" + validSha256, }, } { t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.want, classifyImageReference(tc.registryHost, tc.tag)) + got := stubAnonymousFingerprint(t, validSha256, nil) + + fingerprint, err := client.GetImageFingerprint(tc.imageName, logger.NewStandardLogger()) + require.NoError(t, err) + require.Equal(t, validSha256, fingerprint) + require.Equal(t, tc.wantRef, *got, "the full image reference must reach the anonymous resolver") }) } } -// TestGetImageFingerprintDoesNotAuthenticateToNonACRHost asserts the whole -// resolver, not just the classifier: a non-ACR host must not reach Azure -// authentication. The Azure credentials here are deliberately invalid, so an -// attempt to use them would surface as an Azure credential error. -func TestGetImageFingerprintDoesNotAuthenticateToNonACRHost(t *testing.T) { +// TestGetImageFingerprintUsesACRForACRHost asserts the other arm of the +// dispatch: an ACR host must not be resolved anonymously. An empty tenant id +// makes the Azure credential fail to construct, so the ACR arm returns before +// any network request and the test needs no registry. +func TestGetImageFingerprintUsesACRForACRHost(t *testing.T) { client := &AzureClient{Credentials: AzureStaticCredentials{ - TenantId: "00000000-0000-0000-0000-000000000000", + TenantId: "", ClientId: "00000000-0000-0000-0000-000000000000", ClientSecret: "not-a-real-secret", DigestsSource: "acr", }} - // A closed local port, so the lookup fails immediately and reaches no registry. - _, err := client.GetImageFingerprint("127.0.0.1:1/owner/app:v1", logger.NewStandardLogger()) + got := stubAnonymousFingerprint(t, strings.Repeat("a", 64), nil) + + _, err := client.GetImageFingerprint("myregistry.azurecr.io/app:v1", logger.NewStandardLogger()) require.Error(t, err) - require.Contains(t, err.Error(), "Azure credentials are only sent to Azure Container Registry") + require.Empty(t, *got, "an ACR host must not be resolved anonymously") + require.NotContains(t, err.Error(), "--digests-source logs", + "the error must come from the ACR arm, not the anonymous one") } -func TestGetImageFingerprintRejectsImageWithoutRegistryHost(t *testing.T) { - client := &AzureClient{Credentials: AzureStaticCredentials{}} - _, err := client.GetImageFingerprint("nginx:latest", logger.NewStandardLogger()) +// TestAnonymousImageFingerprintWrapsTheUnderlyingError keeps the error +// wrapped so callers can inspect it, and keeps the actionable hint. +func TestAnonymousImageFingerprintWrapsTheUnderlyingError(t *testing.T) { + sentinel := errors.New("registry unreachable") + stubAnonymousFingerprint(t, "", sentinel) + + _, err := anonymousImageFingerprint("ghcr.io/owner/app:v2", "ghcr.io", logger.NewStandardLogger()) require.Error(t, err) - require.Contains(t, err.Error(), "does not name a registry host") + require.ErrorIs(t, err, sentinel) + require.Contains(t, err.Error(), "--digests-source logs") } diff --git a/internal/digest/digest.go b/internal/digest/digest.go index 2eea9efe7..fa63949f6 100644 --- a/internal/digest/digest.go +++ b/internal/digest/digest.go @@ -84,8 +84,6 @@ func DirSha256(dirPath string, excludePaths []string, logger *logger.Logger) (st // OciSha256 gets the digest of a docker/OCI image from its registry func OciSha256(artifactName string, registryUsername string, registryPassword string) (string, error) { - imageName := fmt.Sprintf("//%s", artifactName) - ctx := context.Background() sysCtx := &types.SystemContext{} // Only set explicit credentials when provided. When DockerAuthConfig is nil, // the containers/image library falls back to credential discovery from auth @@ -98,6 +96,20 @@ func OciSha256(artifactName string, registryUsername string, registryPassword st Password: registryPassword, } } + return ociSha256(artifactName, sysCtx) +} + +// OciSha256Anonymous gets the digest of a docker/OCI image from its registry +// without presenting any credential. A non-nil but empty DockerAuthConfig is +// what stops containers/image falling back to credential discovery, so no +// credential the host happens to hold is presented to the registry. +func OciSha256Anonymous(artifactName string) (string, error) { + return ociSha256(artifactName, &types.SystemContext{DockerAuthConfig: &types.DockerAuthConfig{}}) +} + +func ociSha256(artifactName string, sysCtx *types.SystemContext) (string, error) { + imageName := fmt.Sprintf("//%s", artifactName) + ctx := context.Background() // Parse image reference ref, err := docker.ParseReference(imageName) From c9ab50011b2a6efdc3753f5f75ce76f211e7e716 Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Tue, 8 Sep 2026 20:18:16 +0100 Subject: [PATCH 3/7] fix(snapshot azure): reject non-sha256 registry digests and normalise the reference once Review round 2 found that deriving tagOrDigest by slicing the canonical reference stripped the algorithm prefix from a digest-pinned reference, so ACR was handed a bare hex string, read it as a tag, and returned MANIFEST_UNKNOWN. One app's error cancels the run, so a single digest-pinned ACR app took down the whole environment report. main did not have that bug for digest-only references. Following that up found an unhandled panic on the path this branch opens. ociSha256 ended with: return strings.Split(digest.String(), "sha256:")[1], nil docker.GetDigest returns whatever Docker-Content-Digest says, and go-digest accepts sha384 and sha512, for which Split yields one element and [1] panics. OciSha256Anonymous exists to be pointed at a registry taken from app configuration, so one hostile registry and one response header killed the whole kosli snapshot azure process. Fixed with Algorithm()/Encoded(), which also covers kosli attest artifact. The ACR arm now parses its header the same way instead of trimming a prefix. Reference handling is consolidated into a pure planImageFingerprint returning a fingerprintPlan, so every value crossing the boundary is table-testable without a seam. Normalising once also fixes tag+digest references, which containers/image refuses when a reference carries both: those previously cancelled the snapshot on the anonymous arm. A non-sha256 pin is now rejected at parse time, since ValidateDigest accepts only 64 hex and such a pin can never produce a Kosli fingerprint. The unreachable trailing-dot trim is removed. net.SplitHostPort is kept: reference.Domain can produce IPv6 literals, for which strings.Cut yields "[" and "[2001". Every new guard verified red by mutation, including the panic. Co-Authored-By: Claude Opus 5 --- go.mod | 2 +- internal/azure/azure_apps.go | 131 ++++++--- internal/azure/image_fingerprint_test.go | 348 ++++++++++------------- internal/digest/digest.go | 20 +- internal/digest/oci_anonymous_test.go | 79 +++++ 5 files changed, 341 insertions(+), 239 deletions(-) create mode 100644 internal/digest/oci_anonymous_test.go diff --git a/go.mod b/go.mod index cba5aeeb1..4113c9966 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.5.1 github.com/open-policy-agent/opa v1.20.1 + github.com/opencontainers/go-digest v1.0.0 github.com/otiai10/copy v1.14.1 github.com/owenrumney/go-sarif/v2 v2.3.3 github.com/pkg/errors v0.9.1 @@ -193,7 +194,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo/v2 v2.32.0 // indirect github.com/onsi/gomega v1.40.0 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect github.com/otiai10/mint v1.6.3 // indirect diff --git a/internal/azure/azure_apps.go b/internal/azure/azure_apps.go index 62b56756b..26d6cf131 100644 --- a/internal/azure/azure_apps.go +++ b/internal/azure/azure_apps.go @@ -26,6 +26,7 @@ import ( "github.com/kosli-dev/cli/internal/digest" "github.com/kosli-dev/cli/internal/logger" "github.com/kosli-dev/cli/internal/server" + godigest "github.com/opencontainers/go-digest" ) type AzureStaticCredentials struct { @@ -422,9 +423,9 @@ const ( // isACRLoginServer reports whether domain is an Azure Container Registry login // server, matching on a whole label so that "azurecr.io.example.com" is not one. func isACRLoginServer(domain string) bool { - h := strings.TrimSuffix(strings.ToLower(domain), ".") + h := strings.ToLower(domain) if hostWithoutPort, _, err := net.SplitHostPort(h); err == nil { - h = strings.TrimSuffix(hostWithoutPort, ".") + h = hostWithoutPort } for _, suffix := range acrLoginServerSuffixes { if len(h) > len(suffix) && strings.HasSuffix(h, suffix) { @@ -434,39 +435,77 @@ func isACRLoginServer(domain string) bool { return false } -// classifyImageReference decides how a reference is resolved. Only -// fingerprintFromACR attaches the Azure credential, so this is the boundary that -// keeps that credential away from a registry taken from an app's own -// configuration. -func classifyImageReference(domain string) imageFingerprintSource { - if isACRLoginServer(domain) { - return fingerprintFromACR - } - return fingerprintFromAnonymousRegistry +// fingerprintPlan is how one image reference will be resolved. It is decided +// before anything is contacted, so a test can assert every value that crosses +// the boundary rather than only which resolver ran. +type fingerprintPlan struct { + source imageFingerprintSource + // domain is the registry the reference names, as the parser reports it. + domain string + // reference is the canonical form handed to a resolver. Classification and + // resolution use this same value, so they cannot disagree about the host. + reference string + // repoPath and tagOrDigest address the manifest on the ACR arm. + repoPath string + tagOrDigest string + // pinnedFingerprint is the sha256 hex a digest-pinned reference claims, or + // empty when the reference is not pinned. + pinnedFingerprint string } -// parseImageReference splits an App Service image reference into its registry -// domain, repository path, canonical form, and pinned digest if it has one. +// planImageFingerprint decides how an App Service image reference is resolved. // -// It uses the same normalising parser as the registry clients rather than -// splitting the string by hand, because a hand-rolled split can be talked into -// disagreeing with the client about which host it named: +// The reference is parsed with the same normalising parser the registry clients +// use rather than being split by hand, because a hand-rolled split can be talked +// into disagreeing with the client about which host it named: // "reg.azurecr.io:443@attacker.example/repo:tag" passes a suffix check on the // registry component but resolves to attacker.example as a URL. The parser // rejects it. -func parseImageReference(imageName string) (domain, path, canonical, pinnedDigest string, err error) { +// +// Only fingerprintFromACR attaches the Azure credential, so the classification +// here is what keeps that credential away from a registry named in an app's own +// configuration. +func planImageFingerprint(imageName string) (fingerprintPlan, error) { named, err := reference.ParseNormalizedNamed(imageName) if err != nil { - return "", "", "", "", fmt.Errorf("failed to parse the image name [%s]: %w", imageName, err) + return fingerprintPlan{}, fmt.Errorf("failed to parse the image name [%s]: %w", imageName, err) } + var plan fingerprintPlan + if digested, ok := named.(reference.Digested); ok { - pinnedDigest = strings.TrimPrefix(digested.Digest().String(), "sha256:") + // Kosli fingerprints are sha256, so a reference pinned to any other + // algorithm can never produce one. + if digested.Digest().Algorithm() != godigest.SHA256 { + return fingerprintPlan{}, fmt.Errorf("image [%s] is pinned to a %s digest; Kosli fingerprints are sha256", imageName, digested.Digest().Algorithm()) + } + plan.pinnedFingerprint = digested.Digest().Encoded() + // A digest is authoritative when a reference carries both, and + // containers/image refuses a reference holding a tag and a digest + // together, so drop the tag. + named, err = reference.WithDigest(reference.TrimNamed(named), digested.Digest()) + if err != nil { + return fingerprintPlan{}, fmt.Errorf("failed to normalise the image name [%s]: %w", imageName, err) + } + plan.tagOrDigest = digested.Digest().String() + } else { + named = reference.TagNameOnly(named) + tagged, ok := named.(reference.Tagged) + if !ok { + return fingerprintPlan{}, fmt.Errorf("image [%s] names neither a tag nor a digest", imageName) + } + plan.tagOrDigest = tagged.Tag() } - named = reference.TagNameOnly(named) + plan.domain = reference.Domain(named) + plan.repoPath = reference.Path(named) + plan.reference = named.String() + plan.source = fingerprintFromAnonymousRegistry + if isACRLoginServer(plan.domain) { + plan.source = fingerprintFromACR + } - return reference.Domain(named), reference.Path(named), named.String(), pinnedDigest, nil + return plan, nil } // GetImageFingerprint resolves the fingerprint of a container image referenced @@ -474,29 +513,26 @@ func parseImageReference(imageName string) (domain, path, canonical, pinnedDiges // anyone with write access to that app controls, so the Azure credential is // attached only for an Azure Container Registry login server. func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *logger.Logger) (string, error) { - domain, path, canonical, pinnedDigest, err := parseImageReference(imageName) + plan, err := planImageFingerprint(imageName) if err != nil { return "", err } - // The reference handed to a resolver is the one that was classified, so the - // two cannot disagree about which registry is contacted. - tagOrDigest := canonical[strings.LastIndexAny(canonical, ":@")+1:] - var fingerprint string - if classifyImageReference(domain) == fingerprintFromACR { - fingerprint, err = azureClient.acrImageFingerprint(canonical, "https://"+domain, path, tagOrDigest, logger) + if plan.source == fingerprintFromACR { + fingerprint, err = azureClient.acrImageFingerprint(plan, logger) } else { - fingerprint, err = anonymousImageFingerprint(canonical, domain, logger) + fingerprint, err = anonymousImageFingerprint(plan, logger) } if err != nil { return "", err } - // A pinned reference is a claim about which image is deployed, so hold the - // registry to it rather than reporting whichever digest it returned. - if pinnedDigest != "" && fingerprint != pinnedDigest { - return "", fmt.Errorf("image [%s] is pinned to digest sha256:%s but [%s] reported sha256:%s", imageName, pinnedDigest, domain, fingerprint) + // A pinned reference is a claim about which image is deployed, and neither + // resolver checks the digest it is given against the one it gets back, so + // hold the registry to it here. + if plan.pinnedFingerprint != "" && fingerprint != plan.pinnedFingerprint { + return "", fmt.Errorf("image [%s] is pinned to digest sha256:%s but [%s] reported sha256:%s", imageName, plan.pinnedFingerprint, plan.domain, fingerprint) } return fingerprint, nil @@ -504,33 +540,42 @@ func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *lo // acrImageFingerprint reads a fingerprint from Azure Container Registry using // the Azure credential supplied to Kosli. -func (azureClient *AzureClient) acrImageFingerprint(imageName, registryUrl, repoName, tag string, logger *logger.Logger) (string, error) { +func (azureClient *AzureClient) acrImageFingerprint(plan fingerprintPlan, logger *logger.Logger) (string, error) { credentials, err := azidentity.NewClientSecretCredential(azureClient.Credentials.TenantId, azureClient.Credentials.ClientId, azureClient.Credentials.ClientSecret, nil) if err != nil { return "", err } - acrClient, err := azcontainerregistry.NewClient(registryUrl, credentials, nil) + acrClient, err := azcontainerregistry.NewClient("https://"+plan.domain, credentials, nil) if err != nil { return "", err } - manifestRes, err := acrClient.GetManifest(context.TODO(), repoName, tag, + manifestRes, err := acrClient.GetManifest(context.TODO(), plan.repoPath, plan.tagOrDigest, &azcontainerregistry.ClientGetManifestOptions{Accept: to.Ptr("application/vnd.docker.distribution.manifest.v2+json")}) if err != nil { return "", err } if manifestRes.DockerContentDigest == nil { - return "", fmt.Errorf("no digest returned for image [%s]", imageName) + return "", fmt.Errorf("no digest returned for image [%s]", plan.reference) } - fingerprint := strings.TrimPrefix(*manifestRes.DockerContentDigest, "sha256:") + // The header is a raw string from the SDK, so parse it rather than trimming a + // prefix off it: a registry chooses the algorithm it answers with. + returnedDigest, err := godigest.Parse(*manifestRes.DockerContentDigest) + if err != nil { + return "", fmt.Errorf("registry reported an unparseable digest for image [%s]: %w", plan.reference, err) + } + if returnedDigest.Algorithm() != godigest.SHA256 { + return "", fmt.Errorf("registry reported a %s digest for image [%s]; Kosli fingerprints are sha256", returnedDigest.Algorithm(), plan.reference) + } + fingerprint := returnedDigest.Encoded() if err := digest.ValidateDigest(fingerprint); err != nil { return "", err } - logger.Debug("For image '%s' got fingerprint '%s' from ACR", imageName, fingerprint) + logger.Debug("For image '%s' got fingerprint '%s' from ACR", plan.reference, fingerprint) return fingerprint, nil } @@ -541,13 +586,13 @@ var anonymousFingerprint = digest.OciSha256Anonymous // anonymousImageFingerprint reads a fingerprint from a registry outside Azure // Container Registry, presenting no credential. -func anonymousImageFingerprint(imageName, domain string, logger *logger.Logger) (string, error) { - fingerprint, err := anonymousFingerprint(imageName) +func anonymousImageFingerprint(plan fingerprintPlan, logger *logger.Logger) (string, error) { + fingerprint, err := anonymousFingerprint(plan.reference) if err != nil { - return "", fmt.Errorf("failed to get the fingerprint of image [%s] from [%s]: %w. Azure credentials are only sent to Azure Container Registry; use --digests-source logs for this app", imageName, domain, err) + return "", fmt.Errorf("failed to get the fingerprint of image [%s] from [%s]: %w. Azure credentials are only sent to Azure Container Registry; use --digests-source logs for this app", plan.reference, plan.domain, err) } - logger.Debug("For image '%s' got fingerprint '%s' from '%s' with no credentials", imageName, fingerprint, domain) + logger.Debug("For image '%s' got fingerprint '%s' from '%s' with no credentials", plan.reference, fingerprint, plan.domain) return fingerprint, nil } diff --git a/internal/azure/image_fingerprint_test.go b/internal/azure/image_fingerprint_test.go index e408077a0..30bbdf49f 100644 --- a/internal/azure/image_fingerprint_test.go +++ b/internal/azure/image_fingerprint_test.go @@ -9,71 +9,109 @@ import ( "github.com/stretchr/testify/require" ) -func TestParseImageReference(t *testing.T) { - validSha256 := strings.Repeat("a", 64) +func TestPlanImageFingerprint(t *testing.T) { + sha := strings.Repeat("a", 64) for _, tc := range []struct { name string imageName string - domain string - path string - canonical string - pinnedSha string + want fingerprintPlan wantErrText string }{ { - name: "acr image with a tag", + name: "acr image with a tag authenticates to acr", imageName: "myregistry.azurecr.io/myrepo/myapp:1.0", - domain: "myregistry.azurecr.io", - path: "myrepo/myapp", - canonical: "myregistry.azurecr.io/myrepo/myapp:1.0", + want: fingerprintPlan{ + source: fingerprintFromACR, domain: "myregistry.azurecr.io", + reference: "myregistry.azurecr.io/myrepo/myapp:1.0", + repoPath: "myrepo/myapp", tagOrDigest: "1.0", + }, }, { - name: "acr image pinned to a digest", - imageName: "myregistry.azurecr.io/myapp@sha256:" + validSha256, - domain: "myregistry.azurecr.io", - path: "myapp", - canonical: "myregistry.azurecr.io/myapp@sha256:" + validSha256, - pinnedSha: validSha256, + // Regression: the digest must keep its algorithm prefix, or ACR reads + // the bare hex as a tag and returns MANIFEST_UNKNOWN. + name: "acr image pinned to a digest keeps the sha256 prefix", + imageName: "myregistry.azurecr.io/myapp@sha256:" + sha, + want: fingerprintPlan{ + source: fingerprintFromACR, domain: "myregistry.azurecr.io", + reference: "myregistry.azurecr.io/myapp@sha256:" + sha, + repoPath: "myapp", tagOrDigest: "sha256:" + sha, + pinnedFingerprint: sha, + }, + }, + { + // containers/image refuses a reference holding both, so the tag is + // dropped and the digest wins. + name: "tag and digest together drops the tag", + imageName: "ghcr.io/owner/app:v1@sha256:" + sha, + want: fingerprintPlan{ + source: fingerprintFromAnonymousRegistry, domain: "ghcr.io", + reference: "ghcr.io/owner/app@sha256:" + sha, + repoPath: "owner/app", tagOrDigest: "sha256:" + sha, + pinnedFingerprint: sha, + }, }, { name: "image without a tag defaults to latest", imageName: "myregistry.azurecr.io/myapp", - domain: "myregistry.azurecr.io", - path: "myapp", - canonical: "myregistry.azurecr.io/myapp:latest", + want: fingerprintPlan{ + source: fingerprintFromACR, domain: "myregistry.azurecr.io", + reference: "myregistry.azurecr.io/myapp:latest", + repoPath: "myapp", tagOrDigest: "latest", + }, + }, + { + name: "acr host with a port authenticates to acr", + imageName: "myregistry.azurecr.io:443/myapp:v1", + want: fingerprintPlan{ + source: fingerprintFromACR, domain: "myregistry.azurecr.io:443", + reference: "myregistry.azurecr.io:443/myapp:v1", + repoPath: "myapp", tagOrDigest: "v1", + }, }, { - name: "third party registry", + name: "third party registry resolves anonymously", imageName: "ghcr.io/owner/app:v2", - domain: "ghcr.io", - path: "owner/app", - canonical: "ghcr.io/owner/app:v2", + want: fingerprintPlan{ + source: fingerprintFromAnonymousRegistry, domain: "ghcr.io", + reference: "ghcr.io/owner/app:v2", repoPath: "owner/app", tagOrDigest: "v2", + }, }, { - name: "host with a port", - imageName: "registry.example.com:8443/app:v1", - domain: "registry.example.com:8443", - path: "app", - canonical: "registry.example.com:8443/app:v1", + name: "attacker controlled host resolves anonymously", + imageName: "attacker.example/repo:latest", + want: fingerprintPlan{ + source: fingerprintFromAnonymousRegistry, domain: "attacker.example", + reference: "attacker.example/repo:latest", repoPath: "repo", tagOrDigest: "latest", + }, + }, + { + name: "acr lookalike host resolves anonymously", + imageName: "azurecr.io.attacker.example/repo:latest", + want: fingerprintPlan{ + source: fingerprintFromAnonymousRegistry, domain: "azurecr.io.attacker.example", + reference: "azurecr.io.attacker.example/repo:latest", repoPath: "repo", tagOrDigest: "latest", + }, }, { name: "docker hub short form is normalised", imageName: "nginx:latest", - domain: "docker.io", - path: "library/nginx", - canonical: "docker.io/library/nginx:latest", + want: fingerprintPlan{ + source: fingerprintFromAnonymousRegistry, domain: "docker.io", + reference: "docker.io/library/nginx:latest", repoPath: "library/nginx", tagOrDigest: "latest", + }, }, { name: "docker hub user image is normalised", imageName: "myuser/myimage:tag", - domain: "docker.io", - path: "myuser/myimage", - canonical: "docker.io/myuser/myimage:tag", + want: fingerprintPlan{ + source: fingerprintFromAnonymousRegistry, domain: "docker.io", + reference: "docker.io/myuser/myimage:tag", repoPath: "myuser/myimage", tagOrDigest: "tag", + }, }, - // Regression: a registry component that a suffix check reads as ACR but a - // URL parser resolves to a different host must be rejected outright, or the - // Azure credential is handed to that other host. + // Regression: a registry component a suffix check reads as ACR but a URL + // parser resolves elsewhere must be rejected, or the Azure credential is + // handed to that other host. { name: "acr host smuggled into userinfo with a port is rejected", imageName: "myregistry.azurecr.io:443@attacker.example/repo:tag", @@ -89,67 +127,31 @@ func TestParseImageReference(t *testing.T) { imageName: "myregistry.azurecr.io@attacker.example/repo:tag", wantErrText: "failed to parse the image name", }, + { + name: "trailing dot fqdn is rejected by the parser", + imageName: "myregistry.azurecr.io./myapp:v1", + wantErrText: "failed to parse the image name", + }, + { + name: "a non sha256 pin cannot produce a kosli fingerprint", + imageName: "ghcr.io/owner/app@sha512:" + strings.Repeat("c", 128), + wantErrText: "pinned to a sha512 digest", + }, } { t.Run(tc.name, func(t *testing.T) { - domain, path, canonical, pinned, err := parseImageReference(tc.imageName) + got, err := planImageFingerprint(tc.imageName) if tc.wantErrText != "" { require.Error(t, err) require.Contains(t, err.Error(), tc.wantErrText) + require.Equal(t, fingerprintPlan{}, got) return } require.NoError(t, err) - require.Equal(t, tc.domain, domain) - require.Equal(t, tc.path, path) - require.Equal(t, tc.canonical, canonical) - require.Equal(t, tc.pinnedSha, pinned) - }) - } -} - -// TestGetImageFingerprintRejectsSmuggledACRHost is the regression test for the -// bypass: the credential must not reach a host that only looks like ACR. -func TestGetImageFingerprintRejectsSmuggledACRHost(t *testing.T) { - client := &AzureClient{Credentials: AzureStaticCredentials{ - TenantId: "00000000-0000-0000-0000-000000000000", - ClientId: "00000000-0000-0000-0000-000000000000", - ClientSecret: "not-a-real-secret", - DigestsSource: "acr", - }} - - for _, imageName := range []string{ - "myregistry.azurecr.io:443@attacker.example/repo:tag", - "myregistry.azurecr.io:x@attacker.example/repo:tag", - "myregistry.azurecr.io@attacker.example/repo:tag", - } { - t.Run(imageName, func(t *testing.T) { - got := stubAnonymousFingerprint(t, strings.Repeat("a", 64), nil) - - _, err := client.GetImageFingerprint(imageName, logger.NewStandardLogger()) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to parse the image name") - require.Empty(t, *got, "must not resolve at all") + require.Equal(t, tc.want, got) }) } } -// TestGetImageFingerprintHoldsTheRegistryToAPinnedDigest stops a registry -// reporting a digest other than the one the reference pinned. -func TestGetImageFingerprintHoldsTheRegistryToAPinnedDigest(t *testing.T) { - client := &AzureClient{Credentials: AzureStaticCredentials{DigestsSource: "acr"}} - pinned := strings.Repeat("a", 64) - other := strings.Repeat("b", 64) - - stubAnonymousFingerprint(t, other, nil) - _, err := client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) - require.Error(t, err) - require.Contains(t, err.Error(), "is pinned to digest sha256:"+pinned) - - stubAnonymousFingerprint(t, pinned, nil) - fingerprint, err := client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) - require.NoError(t, err) - require.Equal(t, pinned, fingerprint) -} - func TestIsACRLoginServer(t *testing.T) { for _, tc := range []struct { host string @@ -163,16 +165,19 @@ func TestIsACRLoginServer(t *testing.T) { // A suffix on its own names no registry. {host: "azurecr.io", want: false}, {host: ".azurecr.io", want: false}, - // The suffix must be the end of the host, not a label inside it. + // The suffix must end the host, not sit inside it. {host: "azurecr.io.attacker.example", want: false}, {host: "myregistry.azurecr.io.attacker.example", want: false}, - // Nor may it merely appear in a longer label. {host: "notazurecr.io", want: false}, {host: "ghcr.io", want: false}, {host: "registry-1.docker.io", want: false}, {host: "mcr.microsoft.com", want: false}, {host: "169.254.169.254", want: false}, {host: "attacker.example:8443", want: false}, + // Domains reference.Domain can produce for a local registry. + {host: "localhost:5000", want: false}, + {host: "[::1]:5000", want: false}, + {host: "[2001:db8::1]", want: false}, {host: "", want: false}, } { t.Run(tc.host, func(t *testing.T) { @@ -181,55 +186,8 @@ func TestIsACRLoginServer(t *testing.T) { } } -// TestClassifyImageReferenceKeepsAzureCredentialForACROnly is the regression -// test for the credential-forwarding vulnerability: the Azure credential is -// attached only on the fingerprintFromACR path, so any host that is not an ACR -// login server must classify as something else. -func TestClassifyImageReferenceKeepsAzureCredentialForACROnly(t *testing.T) { - for _, tc := range []struct { - name string - registryHost string - want imageFingerprintSource - }{ - { - name: "acr host authenticates to acr", - registryHost: "myregistry.azurecr.io", - want: fingerprintFromACR, - }, - { - name: "acr host written as an fqdn still authenticates to acr", - registryHost: "myregistry.azurecr.io.", - want: fingerprintFromACR, - }, - { - name: "attacker controlled host is resolved anonymously", - registryHost: "attacker.example", - want: fingerprintFromAnonymousRegistry, - }, - { - name: "acr lookalike host is resolved anonymously", - registryHost: "azurecr.io.attacker.example", - want: fingerprintFromAnonymousRegistry, - }, - { - name: "third party registry is resolved anonymously", - registryHost: "ghcr.io", - want: fingerprintFromAnonymousRegistry, - }, - { - name: "instance metadata address is resolved anonymously", - registryHost: "169.254.169.254", - want: fingerprintFromAnonymousRegistry, - }, - } { - t.Run(tc.name, func(t *testing.T) { - require.Equal(t, tc.want, classifyImageReference(tc.registryHost)) - }) - } -} - -// stubAnonymousFingerprint replaces the anonymous resolver for the duration of a -// test and records the reference it was handed. +// stubAnonymousFingerprint replaces the anonymous resolver for one test and +// records the reference it was handed. func stubAnonymousFingerprint(t *testing.T, fingerprint string, err error) *string { t.Helper() original := anonymousFingerprint @@ -242,72 +200,39 @@ func stubAnonymousFingerprint(t *testing.T, fingerprint string, err error) *stri return &got } -// TestGetImageFingerprintRoutesNonACRHostsAnonymously covers the dispatch in -// GetImageFingerprint, not just the classifier, so that swapping the arms would -// fail a test. It also asserts the exact reference handed to the resolver, which -// a wiring bug (passing repoName instead of the full image name) would break. +// TestGetImageFingerprintRoutesNonACRHostsAnonymously covers the dispatch and +// asserts the exact reference handed to the resolver, which a wiring bug would +// break. func TestGetImageFingerprintRoutesNonACRHostsAnonymously(t *testing.T) { client := &AzureClient{Credentials: AzureStaticCredentials{ - TenantId: "00000000-0000-0000-0000-000000000000", - ClientId: "00000000-0000-0000-0000-000000000000", - ClientSecret: "not-a-real-secret", - DigestsSource: "acr", + TenantId: "00000000-0000-0000-0000-000000000000", DigestsSource: "acr", }} - validSha256 := strings.Repeat("a", 64) + sha := strings.Repeat("a", 64) - for _, tc := range []struct { - name string - imageName string - wantRef string - }{ - { - name: "third party registry", - imageName: "ghcr.io/owner/app:v2", - wantRef: "ghcr.io/owner/app:v2", - }, - { - name: "attacker controlled host", - imageName: "attacker.example/repo:latest", - wantRef: "attacker.example/repo:latest", - }, - { - name: "docker hub short form is normalised before resolving", - imageName: "nginx:latest", - wantRef: "docker.io/library/nginx:latest", - }, - { - name: "docker hub user image is normalised before resolving", - imageName: "myuser/myimage:tag", - wantRef: "docker.io/myuser/myimage:tag", - }, - { - name: "digest pinned reference on a non acr host", - imageName: "ghcr.io/owner/app@sha256:" + validSha256, - wantRef: "ghcr.io/owner/app@sha256:" + validSha256, - }, + for _, tc := range []struct{ name, imageName, wantRef string }{ + {"third party registry", "ghcr.io/owner/app:v2", "ghcr.io/owner/app:v2"}, + {"attacker controlled host", "attacker.example/repo:latest", "attacker.example/repo:latest"}, + {"docker hub short form", "nginx:latest", "docker.io/library/nginx:latest"}, + {"docker hub user image", "myuser/myimage:tag", "docker.io/myuser/myimage:tag"}, + {"pinned on a non acr host", "ghcr.io/owner/app@sha256:" + sha, "ghcr.io/owner/app@sha256:" + sha}, + {"tag and digest drops the tag", "ghcr.io/owner/app:v1@sha256:" + sha, "ghcr.io/owner/app@sha256:" + sha}, } { t.Run(tc.name, func(t *testing.T) { - got := stubAnonymousFingerprint(t, validSha256, nil) + got := stubAnonymousFingerprint(t, sha, nil) fingerprint, err := client.GetImageFingerprint(tc.imageName, logger.NewStandardLogger()) require.NoError(t, err) - require.Equal(t, validSha256, fingerprint) - require.Equal(t, tc.wantRef, *got, "the full image reference must reach the anonymous resolver") + require.Equal(t, sha, fingerprint) + require.Equal(t, tc.wantRef, *got, "the canonical reference must reach the resolver") }) } } -// TestGetImageFingerprintUsesACRForACRHost asserts the other arm of the -// dispatch: an ACR host must not be resolved anonymously. An empty tenant id -// makes the Azure credential fail to construct, so the ACR arm returns before -// any network request and the test needs no registry. +// TestGetImageFingerprintUsesACRForACRHost asserts the other arm. An empty +// tenant id makes the Azure credential fail to construct, so the ACR arm returns +// before any network request. func TestGetImageFingerprintUsesACRForACRHost(t *testing.T) { - client := &AzureClient{Credentials: AzureStaticCredentials{ - TenantId: "", - ClientId: "00000000-0000-0000-0000-000000000000", - ClientSecret: "not-a-real-secret", - DigestsSource: "acr", - }} + client := &AzureClient{Credentials: AzureStaticCredentials{TenantId: "", DigestsSource: "acr"}} got := stubAnonymousFingerprint(t, strings.Repeat("a", 64), nil) @@ -318,13 +243,52 @@ func TestGetImageFingerprintUsesACRForACRHost(t *testing.T) { "the error must come from the ACR arm, not the anonymous one") } -// TestAnonymousImageFingerprintWrapsTheUnderlyingError keeps the error -// wrapped so callers can inspect it, and keeps the actionable hint. +func TestGetImageFingerprintRejectsSmuggledACRHost(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{ + TenantId: "00000000-0000-0000-0000-000000000000", DigestsSource: "acr", + }} + + for _, imageName := range []string{ + "myregistry.azurecr.io:443@attacker.example/repo:tag", + "myregistry.azurecr.io:x@attacker.example/repo:tag", + "myregistry.azurecr.io@attacker.example/repo:tag", + } { + t.Run(imageName, func(t *testing.T) { + got := stubAnonymousFingerprint(t, strings.Repeat("a", 64), nil) + + _, err := client.GetImageFingerprint(imageName, logger.NewStandardLogger()) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse the image name") + require.Empty(t, *got, "must not resolve at all") + }) + } +} + +// TestGetImageFingerprintHoldsTheRegistryToAPinnedDigest stops a registry +// reporting a digest other than the one the reference pinned. Neither resolver +// checks this, so it is checked here. +func TestGetImageFingerprintHoldsTheRegistryToAPinnedDigest(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{DigestsSource: "acr"}} + pinned := strings.Repeat("a", 64) + other := strings.Repeat("b", 64) + + stubAnonymousFingerprint(t, other, nil) + _, err := client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) + require.Error(t, err) + require.Contains(t, err.Error(), "is pinned to digest sha256:"+pinned) + + stubAnonymousFingerprint(t, pinned, nil) + fingerprint, err := client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) + require.NoError(t, err) + require.Equal(t, pinned, fingerprint) +} + func TestAnonymousImageFingerprintWrapsTheUnderlyingError(t *testing.T) { sentinel := errors.New("registry unreachable") stubAnonymousFingerprint(t, "", sentinel) - _, err := anonymousImageFingerprint("ghcr.io/owner/app:v2", "ghcr.io", logger.NewStandardLogger()) + plan := fingerprintPlan{reference: "ghcr.io/owner/app:v2", domain: "ghcr.io"} + _, err := anonymousImageFingerprint(plan, logger.NewStandardLogger()) require.Error(t, err) require.ErrorIs(t, err, sentinel) require.Contains(t, err.Error(), "--digests-source logs") diff --git a/internal/digest/digest.go b/internal/digest/digest.go index fa63949f6..d029be761 100644 --- a/internal/digest/digest.go +++ b/internal/digest/digest.go @@ -22,6 +22,7 @@ import ( "github.com/kosli-dev/cli/internal/requests" "github.com/kosli-dev/cli/internal/utils" "github.com/moby/moby/client" + godigest "github.com/opencontainers/go-digest" "github.com/yargevad/filepathx" ) @@ -104,7 +105,14 @@ func OciSha256(artifactName string, registryUsername string, registryPassword st // what stops containers/image falling back to credential discovery, so no // credential the host happens to hold is presented to the registry. func OciSha256Anonymous(artifactName string) (string, error) { - return ociSha256(artifactName, &types.SystemContext{DockerAuthConfig: &types.DockerAuthConfig{}}) + return ociSha256(artifactName, anonymousSystemContext()) +} + +// anonymousSystemContext presents no credential. The empty DockerAuthConfig is +// deliberately non-nil: a nil one makes containers/image fall back to credential +// discovery from auth files and credential helpers. +func anonymousSystemContext() *types.SystemContext { + return &types.SystemContext{DockerAuthConfig: &types.DockerAuthConfig{}} } func ociSha256(artifactName string, sysCtx *types.SystemContext) (string, error) { @@ -121,11 +129,17 @@ func ociSha256(artifactName string, sysCtx *types.SystemContext) (string, error) } // Compute digest - digest, err := docker.GetDigest(ctx, sysCtx, ref) + remoteDigest, err := docker.GetDigest(ctx, sysCtx, ref) if err != nil { return "", fmt.Errorf("failed to get digest for %s: %w", imageName, err) } - return strings.Split(digest.String(), "sha256:")[1], nil + // A registry chooses the algorithm it answers with, and go-digest accepts + // sha384 and sha512 as well as sha256. Kosli fingerprints are sha256, so + // reject anything else rather than mangling it. + if remoteDigest.Algorithm() != godigest.SHA256 { + return "", fmt.Errorf("registry reported a %s digest for %s; Kosli fingerprints are sha256", remoteDigest.Algorithm(), imageName) + } + return remoteDigest.Encoded(), nil } // calculateDirContentSha256 calculates a sha256 digest for a directory content diff --git a/internal/digest/oci_anonymous_test.go b/internal/digest/oci_anonymous_test.go new file mode 100644 index 000000000..fa95b39a3 --- /dev/null +++ b/internal/digest/oci_anonymous_test.go @@ -0,0 +1,79 @@ +package digest + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/containers/image/v5/types" + "github.com/stretchr/testify/require" +) + +// fakeRegistry answers the manifest HEAD with a chosen Docker-Content-Digest. +func fakeRegistry(t *testing.T, contentDigest string) string { + t.Helper() + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v2/" { + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Docker-Content-Digest", contentDigest) + w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") + w.WriteHeader(http.StatusOK) + })) + t.Cleanup(srv.Close) + return strings.TrimPrefix(srv.URL, "https://") +} + +func insecureAnonymousContext() *types.SystemContext { + return &types.SystemContext{ + DockerAuthConfig: &types.DockerAuthConfig{}, + DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, + } +} + +// TestOciSha256RejectsNonSha256RegistryDigest covers a registry answering with +// an algorithm other than sha256. go-digest accepts sha384 and sha512, so +// without an explicit check the digest string cannot be split as assumed. +func TestOciSha256RejectsNonSha256RegistryDigest(t *testing.T) { + for _, tc := range []struct { + algorithm string + contentDigest string + }{ + {algorithm: "sha384", contentDigest: "sha384:" + strings.Repeat("b", 96)}, + {algorithm: "sha512", contentDigest: "sha512:" + strings.Repeat("c", 128)}, + } { + t.Run(tc.algorithm, func(t *testing.T) { + host := fakeRegistry(t, tc.contentDigest) + + fingerprint, err := ociSha256(host+"/repo:tag", insecureAnonymousContext()) + + require.Error(t, err) + require.Empty(t, fingerprint) + require.Contains(t, err.Error(), "Kosli fingerprints are sha256") + require.Contains(t, err.Error(), tc.algorithm) + }) + } +} + +func TestOciSha256ReturnsTheSha256Fingerprint(t *testing.T) { + want := strings.Repeat("a", 64) + host := fakeRegistry(t, "sha256:"+want) + + fingerprint, err := ociSha256(host+"/repo:tag", insecureAnonymousContext()) + + require.NoError(t, err) + require.Equal(t, want, fingerprint) +} + +// TestOciSha256AnonymousPresentsNoStoredCredential guards the credential +// boundary: a nil DockerAuthConfig makes containers/image fall back to +// credential discovery, so the anonymous helper must set a non-nil empty one. +func TestOciSha256AnonymousPresentsNoStoredCredential(t *testing.T) { + sysCtx := anonymousSystemContext() + require.NotNil(t, sysCtx.DockerAuthConfig, "a nil DockerAuthConfig falls back to credential discovery") + require.Empty(t, sysCtx.DockerAuthConfig.Username) + require.Empty(t, sysCtx.DockerAuthConfig.Password) + require.Empty(t, sysCtx.DockerAuthConfig.IdentityToken) +} From e0bbc8ef9049ff8d938db7385df779bc5ace4dbd Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Tue, 8 Sep 2026 20:42:53 +0100 Subject: [PATCH 4/7] refactor(digest): keep the sha256 digest rule in one place and cover the ACR arm Round 3 review pointed out that hardening the registry-digest parsing had left two independent copies of it, in internal/digest and internal/azure, and that the copy without coverage was the one that could drift. There were in fact three: planImageFingerprint had its own algorithm check for the pin supplied by app configuration. internal/digest now exports Sha256FingerprintFromDigest for a raw wire string and Sha256Fingerprint for an already-parsed digest, so a typed value never has to be turned back into a string to be checked. All three call sites use them, godigest.SHA256 appears once in the codebase, and internal/azure no longer imports go-digest. Dropped the ValidateDigest call in the ACR arm. godigest.Parse already rejects non-hex, uppercase, wrong-length and empty input, so after the algorithm check the value is exactly 64 lowercase hex and that guard could not fail. The coverage half of the finding was measurable: mutating the ACR arm to drop the helper entirely left the suite passing, so its error branches had no coverage. acrImageFingerprint now takes client options, nil in production, and tests pass a transport pointed at a fake TLS registry. That covers sha512, sha384, unparseable and missing-header responses plus the success path, with no package-level state. Both mutations that previously proved nothing now fail. Co-Authored-By: Claude Opus 5 --- internal/azure/azure_apps.go | 32 ++++------ internal/azure/image_fingerprint_test.go | 79 +++++++++++++++++++++++- internal/digest/digest.go | 33 ++++++++-- internal/digest/oci_anonymous_test.go | 32 ++++++++++ 4 files changed, 149 insertions(+), 27 deletions(-) diff --git a/internal/azure/azure_apps.go b/internal/azure/azure_apps.go index 26d6cf131..47443117a 100644 --- a/internal/azure/azure_apps.go +++ b/internal/azure/azure_apps.go @@ -26,7 +26,6 @@ import ( "github.com/kosli-dev/cli/internal/digest" "github.com/kosli-dev/cli/internal/logger" "github.com/kosli-dev/cli/internal/server" - godigest "github.com/opencontainers/go-digest" ) type AzureStaticCredentials struct { @@ -474,12 +473,12 @@ func planImageFingerprint(imageName string) (fingerprintPlan, error) { var plan fingerprintPlan if digested, ok := named.(reference.Digested); ok { - // Kosli fingerprints are sha256, so a reference pinned to any other - // algorithm can never produce one. - if digested.Digest().Algorithm() != godigest.SHA256 { - return fingerprintPlan{}, fmt.Errorf("image [%s] is pinned to a %s digest; Kosli fingerprints are sha256", imageName, digested.Digest().Algorithm()) + // A reference pinned to an algorithm Kosli cannot fingerprint can never + // match, so reject it here rather than after a pointless round trip. + plan.pinnedFingerprint, err = digest.Sha256Fingerprint(digested.Digest()) + if err != nil { + return fingerprintPlan{}, fmt.Errorf("image [%s] is pinned to a digest Kosli cannot use: %w", imageName, err) } - plan.pinnedFingerprint = digested.Digest().Encoded() // A digest is authoritative when a reference carries both, and // containers/image refuses a reference holding a tag and a digest // together, so drop the tag. @@ -520,7 +519,7 @@ func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *lo var fingerprint string if plan.source == fingerprintFromACR { - fingerprint, err = azureClient.acrImageFingerprint(plan, logger) + fingerprint, err = azureClient.acrImageFingerprint(plan, nil, logger) } else { fingerprint, err = anonymousImageFingerprint(plan, logger) } @@ -540,14 +539,16 @@ func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *lo // acrImageFingerprint reads a fingerprint from Azure Container Registry using // the Azure credential supplied to Kosli. -func (azureClient *AzureClient) acrImageFingerprint(plan fingerprintPlan, logger *logger.Logger) (string, error) { +// clientOptions is nil in production; tests pass options carrying a transport +// pointed at a fake registry, so the arm is exercised without package-level state. +func (azureClient *AzureClient) acrImageFingerprint(plan fingerprintPlan, clientOptions *azcontainerregistry.ClientOptions, logger *logger.Logger) (string, error) { credentials, err := azidentity.NewClientSecretCredential(azureClient.Credentials.TenantId, azureClient.Credentials.ClientId, azureClient.Credentials.ClientSecret, nil) if err != nil { return "", err } - acrClient, err := azcontainerregistry.NewClient("https://"+plan.domain, credentials, nil) + acrClient, err := azcontainerregistry.NewClient("https://"+plan.domain, credentials, clientOptions) if err != nil { return "", err } @@ -561,18 +562,9 @@ func (azureClient *AzureClient) acrImageFingerprint(plan fingerprintPlan, logger return "", fmt.Errorf("no digest returned for image [%s]", plan.reference) } - // The header is a raw string from the SDK, so parse it rather than trimming a - // prefix off it: a registry chooses the algorithm it answers with. - returnedDigest, err := godigest.Parse(*manifestRes.DockerContentDigest) + fingerprint, err := digest.Sha256FingerprintFromDigest(*manifestRes.DockerContentDigest) if err != nil { - return "", fmt.Errorf("registry reported an unparseable digest for image [%s]: %w", plan.reference, err) - } - if returnedDigest.Algorithm() != godigest.SHA256 { - return "", fmt.Errorf("registry reported a %s digest for image [%s]; Kosli fingerprints are sha256", returnedDigest.Algorithm(), plan.reference) - } - fingerprint := returnedDigest.Encoded() - if err := digest.ValidateDigest(fingerprint); err != nil { - return "", err + return "", fmt.Errorf("registry reported a digest Kosli cannot use for image [%s]: %w", plan.reference, err) } logger.Debug("For image '%s' got fingerprint '%s' from ACR", plan.reference, fingerprint) diff --git a/internal/azure/image_fingerprint_test.go b/internal/azure/image_fingerprint_test.go index 30bbdf49f..5092dda90 100644 --- a/internal/azure/image_fingerprint_test.go +++ b/internal/azure/image_fingerprint_test.go @@ -2,9 +2,14 @@ package azure import ( "errors" + "net/http" + "net/http/httptest" "strings" "testing" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "github.com/kosli-dev/cli/internal/logger" "github.com/stretchr/testify/require" ) @@ -135,7 +140,7 @@ func TestPlanImageFingerprint(t *testing.T) { { name: "a non sha256 pin cannot produce a kosli fingerprint", imageName: "ghcr.io/owner/app@sha512:" + strings.Repeat("c", 128), - wantErrText: "pinned to a sha512 digest", + wantErrText: "pinned to a digest Kosli cannot use", }, } { t.Run(tc.name, func(t *testing.T) { @@ -293,3 +298,75 @@ func TestAnonymousImageFingerprintWrapsTheUnderlyingError(t *testing.T) { require.ErrorIs(t, err, sentinel) require.Contains(t, err.Error(), "--digests-source logs") } + +// fakeACR answers the manifest request with a chosen Docker-Content-Digest, or +// omits the header entirely when contentDigest is empty. +func fakeACR(t *testing.T, contentDigest string) (fingerprintPlan, *azcontainerregistry.ClientOptions) { + t.Helper() + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if contentDigest != "" { + w.Header().Set("Docker-Content-Digest", contentDigest) + } + w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"schemaVersion":2}`)) + })) + t.Cleanup(srv.Close) + + plan := fingerprintPlan{ + source: fingerprintFromACR, domain: strings.TrimPrefix(srv.URL, "https://"), + reference: "fake/app:v1", repoPath: "app", tagOrDigest: "v1", + } + options := &azcontainerregistry.ClientOptions{ + ClientOptions: azcore.ClientOptions{Transport: srv.Client()}, + } + return plan, options +} + +// TestACRImageFingerprintRejectsUnusableDigests covers the ACR arm's own error +// branches, which have no coverage otherwise because the client talks to a +// registry. The digest rule itself lives in internal/digest; this asserts the +// arm is actually wired to it. +func TestACRImageFingerprintRejectsUnusableDigests(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{ + TenantId: "00000000-0000-0000-0000-000000000000", + ClientId: "00000000-0000-0000-0000-000000000000", + ClientSecret: "not-a-real-secret", + }} + + for _, tc := range []struct { + name string + contentDigest string + wantErrText string + }{ + {name: "sha512 digest", contentDigest: "sha512:" + strings.Repeat("c", 128), wantErrText: "algorithm is sha512"}, + {name: "sha384 digest", contentDigest: "sha384:" + strings.Repeat("b", 96), wantErrText: "algorithm is sha384"}, + {name: "unparseable digest", contentDigest: "not-a-digest", wantErrText: "unparseable digest"}, + {name: "missing digest header", contentDigest: "", wantErrText: "no digest returned"}, + } { + t.Run(tc.name, func(t *testing.T) { + plan, options := fakeACR(t, tc.contentDigest) + + fingerprint, err := client.acrImageFingerprint(plan, options, logger.NewStandardLogger()) + + require.Error(t, err) + require.Empty(t, fingerprint) + require.Contains(t, err.Error(), tc.wantErrText) + }) + } +} + +func TestACRImageFingerprintReturnsTheSha256Hex(t *testing.T) { + client := &AzureClient{Credentials: AzureStaticCredentials{ + TenantId: "00000000-0000-0000-0000-000000000000", + ClientId: "00000000-0000-0000-0000-000000000000", + ClientSecret: "not-a-real-secret", + }} + want := strings.Repeat("a", 64) + plan, options := fakeACR(t, "sha256:"+want) + + fingerprint, err := client.acrImageFingerprint(plan, options, logger.NewStandardLogger()) + + require.NoError(t, err) + require.Equal(t, want, fingerprint) +} diff --git a/internal/digest/digest.go b/internal/digest/digest.go index d029be761..7bbcc3111 100644 --- a/internal/digest/digest.go +++ b/internal/digest/digest.go @@ -108,6 +108,29 @@ func OciSha256Anonymous(artifactName string) (string, error) { return ociSha256(artifactName, anonymousSystemContext()) } +// Sha256FingerprintFromDigest turns a registry-supplied digest string into the +// hex fingerprint Kosli uses, rejecting any algorithm other than sha256. A +// registry chooses the algorithm it answers with, so this is the single place +// that rule is applied. +func Sha256FingerprintFromDigest(digestString string) (string, error) { + parsed, err := godigest.Parse(digestString) + if err != nil { + return "", fmt.Errorf("unparseable digest %q: %w", digestString, err) + } + return Sha256Fingerprint(parsed) +} + +// Sha256Fingerprint is the same rule for a digest that is already parsed, so a +// typed value does not have to be turned back into a string to be checked. +func Sha256Fingerprint(parsed godigest.Digest) (string, error) { + if parsed.Algorithm() != godigest.SHA256 { + return "", fmt.Errorf("digest algorithm is %s, but Kosli fingerprints are sha256", parsed.Algorithm()) + } + // godigest.Parse has already validated the charset and length, so the + // encoded portion is exactly 64 lowercase hex characters here. + return parsed.Encoded(), nil +} + // anonymousSystemContext presents no credential. The empty DockerAuthConfig is // deliberately non-nil: a nil one makes containers/image fall back to credential // discovery from auth files and credential helpers. @@ -133,13 +156,11 @@ func ociSha256(artifactName string, sysCtx *types.SystemContext) (string, error) if err != nil { return "", fmt.Errorf("failed to get digest for %s: %w", imageName, err) } - // A registry chooses the algorithm it answers with, and go-digest accepts - // sha384 and sha512 as well as sha256. Kosli fingerprints are sha256, so - // reject anything else rather than mangling it. - if remoteDigest.Algorithm() != godigest.SHA256 { - return "", fmt.Errorf("registry reported a %s digest for %s; Kosli fingerprints are sha256", remoteDigest.Algorithm(), imageName) + fingerprint, err := Sha256Fingerprint(remoteDigest) + if err != nil { + return "", fmt.Errorf("registry reported a digest Kosli cannot use for %s: %w", imageName, err) } - return remoteDigest.Encoded(), nil + return fingerprint, nil } // calculateDirContentSha256 calculates a sha256 digest for a directory content diff --git a/internal/digest/oci_anonymous_test.go b/internal/digest/oci_anonymous_test.go index fa95b39a3..9bb180525 100644 --- a/internal/digest/oci_anonymous_test.go +++ b/internal/digest/oci_anonymous_test.go @@ -77,3 +77,35 @@ func TestOciSha256AnonymousPresentsNoStoredCredential(t *testing.T) { require.Empty(t, sysCtx.DockerAuthConfig.Password) require.Empty(t, sysCtx.DockerAuthConfig.IdentityToken) } + +func TestSha256FingerprintFromDigest(t *testing.T) { + validHex := strings.Repeat("a", 64) + + for _, tc := range []struct { + name string + digest string + want string + wantErrText string + }{ + {name: "sha256 digest yields its hex", digest: "sha256:" + validHex, want: validHex}, + {name: "sha384 is rejected", digest: "sha384:" + strings.Repeat("b", 96), wantErrText: "algorithm is sha384"}, + {name: "sha512 is rejected", digest: "sha512:" + strings.Repeat("c", 128), wantErrText: "algorithm is sha512"}, + {name: "empty is rejected", digest: "", wantErrText: "unparseable digest"}, + {name: "bare hex without an algorithm is rejected", digest: validHex, wantErrText: "unparseable digest"}, + {name: "non hex is rejected", digest: "sha256:" + strings.Repeat("z", 64), wantErrText: "unparseable digest"}, + {name: "uppercase hex is rejected", digest: "sha256:" + strings.Repeat("A", 64), wantErrText: "unparseable digest"}, + {name: "wrong length is rejected", digest: "sha256:" + strings.Repeat("a", 63), wantErrText: "unparseable digest"}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := Sha256FingerprintFromDigest(tc.digest) + if tc.wantErrText != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.wantErrText) + require.Empty(t, got) + return + } + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } +} From 98e2f1e1510fafdce46abc75c75ba018cec419cc Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Tue, 8 Sep 2026 21:00:29 +0100 Subject: [PATCH 5/7] fix(snapshot azure): fail closed on an unset resolver and name the failing app Round 4 review. Three points, all one-liners, all on the security boundary or its diagnosability. fingerprintFromACR was iota, so fingerprintPlan{} claimed the credential-bearing arm. Nothing reached the dispatch with a zero plan because err is checked first, but the field that decides whether an AAD token is sent should fail closed. The order is swapped so an unset or partially built plan resolves anonymously, and TestPlanImageFingerprint's require.Equal on the error cases now asserts something meaningful instead of asserting the rejected plan claims ACR. One app's error cancels the whole run, and the error did not say which app. It is now wrapped once in the goroutine that collects it rather than at each of the seven return sites, so every path is covered by one line, including the zip and logs paths. The PR description previously claimed the error named the app; it did not. Sha256Fingerprint took a godigest.Digest, which is a string type, and called Algorithm()/Encoded() on it without validating. Handing it "sha256:hello" returned "hello" with a nil error, and "sha256:" returned an empty fingerprint. Both callers are validated so this was not live, but it is exported from the package responsible for fingerprint integrity. It now calls Validate() first. The pinned-digest mismatch message reports plan.reference, matching the arms rather than mixing the configured and canonical forms. Co-Authored-By: Claude Opus 5 --- go.sum | 274 +++++++++++++++++++++++ internal/azure/azure_apps.go | 18 +- internal/azure/image_fingerprint_test.go | 8 + internal/digest/digest.go | 5 + internal/digest/oci_anonymous_test.go | 25 +++ 5 files changed, 323 insertions(+), 7 deletions(-) diff --git a/go.sum b/go.sum index 38522714c..8b5d116b8 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,141 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +bitbucket.org/bertimus9/systemstat v0.5.0/go.mod h1:EkUWPp8lKFPMXP8vnbpT5JDI0W/sTiLZAvN8ONWErHY= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= +buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/accessapproval v1.13.0/go.mod h1:7bmInw17bQX+ZPi7YmReC3xKymDrMmxXaUnaI6zQOqI= +cloud.google.com/go/accesscontextmanager v1.15.0/go.mod h1:YjW9urferk8i9ALwBF3bmdcogZeQYRn2yWwR8nkhsBc= +cloud.google.com/go/aiplatform v1.126.0/go.mod h1:iR3za3evdprLe1XL2pLu0cYVCuTbc87QG0pgvcgiJlE= +cloud.google.com/go/analytics v0.35.0/go.mod h1:V9Qef2N0y8GDqQ9FTlmM2XpDEMYonZJRPSUNGZlPCcc= +cloud.google.com/go/apigateway v1.13.0/go.mod h1:pvEpOuuOIw2ev9VCcOyVkDXHHL4lvgMuqIe7XjJ8JoU= +cloud.google.com/go/apigeeconnect v1.12.0/go.mod h1:mYJekCKZHc2ia5yZX5lwtexTn9CzsOfb6+sh/2hi42Q= +cloud.google.com/go/apigeeregistry v1.1.0/go.mod h1:4ZFhQlxMuyfDMz9ORDSV8FPZtf2yPQkKjigsFtrrE4Y= +cloud.google.com/go/appengine v1.15.0/go.mod h1:/8gGZsOX5GDjOo4mAWk8IV59p2991dxTbEtKIlhDjzU= +cloud.google.com/go/area120 v0.15.0/go.mod h1:jD1fw9W4xxIZMY68g7PpbCPleoeGddFs5jPcdhfg3+Y= +cloud.google.com/go/artifactregistry v1.26.0/go.mod h1:c5FPi5GtDBP+OAr5kKhCBNQDT9ZgAyobXQjekx93VWs= +cloud.google.com/go/asset v1.28.0/go.mod h1:Pnvjhay8/FgodOH9uJC8OkfJfRtSnNIIU4WSxg5JfJw= +cloud.google.com/go/assuredworkloads v1.19.0/go.mod h1:/UGGtFCMokM3sGJ4FxjfmLvvFpPa5I/Oz68mwk4Su+0= cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo= cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/automl v1.21.0/go.mod h1:MNbhUevuECzM3jqSOM7hmOedOdRJkm8xbbXW44SU15U= +cloud.google.com/go/baremetalsolution v1.10.0/go.mod h1:xhhT9VQiKPFd2fUs4oeDSRrxV0sb0PGeVmuZoUE2cBA= +cloud.google.com/go/batch v1.20.0/go.mod h1:ABT/5QqsIDsONa+n/8C7XYPjwh/kjOEPXukcRTaMsCg= +cloud.google.com/go/beyondcorp v1.8.0/go.mod h1:aVxzwamO8H4GXWQHowBAmL0KYNfYpW4E6Do2wfP0RYs= +cloud.google.com/go/bigquery v1.79.0/go.mod h1:QTt5tgZxqqvZs3dOZKpvriGqy+CdvY9LyetirFZRPOE= +cloud.google.com/go/bigtable v1.47.0/go.mod h1:GUM6PdkG3rrDse9kugqvX5+ktwo3ldfLtLi1VFn5Wj4= +cloud.google.com/go/billing v1.26.0/go.mod h1:axqDO1uHegh7u5qngkTfqN1djAeLGsWAFAblERgmgEk= +cloud.google.com/go/binaryauthorization v1.16.0/go.mod h1:E+iC5Avu4pdItdzGiSGHnh6TfQrl+KmPxDDg/T/VuHs= +cloud.google.com/go/certificatemanager v1.15.0/go.mod h1:8dfGG2/TbUpCNqsCF/TIMOGV0OVvU6nhkZWTU4MmCXU= +cloud.google.com/go/channel v1.27.0/go.mod h1:9ekufBLXuQ6j1oyqtDSIp29qWU5EwCi8WUi9qkLn3MA= +cloud.google.com/go/cloudbuild v1.32.0/go.mod h1:mYgcM8CMaPmAnO7GxSQ9ADAxVRwS+1b7s6WVkt29OXY= +cloud.google.com/go/clouddms v1.14.0/go.mod h1:qSwET2Q27cJ4wCDsPsbkagXqQqkWfOy+gU3RjMsT/c8= +cloud.google.com/go/cloudtasks v1.18.0/go.mod h1:3KeCxwtGEyaySL7CR3lMmEa2I4mq1ynXdgmfNiO4RYE= +cloud.google.com/go/compute v1.62.0/go.mod h1:Xm6PbsLgBpAg4va77ljbBdpMjzuU+uPp5Ze2dnZq7lw= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/contactcenterinsights v1.23.0/go.mod h1:uB/kygbfYH/gWEq3NEgq3QRI7/MvpjFyX81ajcW5YAI= +cloud.google.com/go/container v1.51.0/go.mod h1:EvqoT2eXfxLweXXUlhAMGR0sOAB00XPzEjoL01esSDs= +cloud.google.com/go/containeranalysis v0.19.0/go.mod h1:Zq0XHzUIa0oTa7H6aSR8HWqeJnoRI9syUcYJzfozjZQ= +cloud.google.com/go/datacatalog v1.33.0/go.mod h1:/EMN04S73fZcPdtNg86VYLDrhi2HheMehQtMCS86Klk= +cloud.google.com/go/dataflow v0.16.0/go.mod h1:BWhSrIGmsMfuYj3J+nJ2Tw7tplRR6r28kvRiqCD3WlQ= +cloud.google.com/go/dataform v1.2.0/go.mod h1:Lhkjd6L04/nBqsEo7S9Tx7D+Vm0pDDDZuKczewAJuX0= +cloud.google.com/go/datafusion v1.14.0/go.mod h1:2z+uDUKkLPacNNos5lW1Jf1IRDoFyeE+glJ4hmxF2Uc= +cloud.google.com/go/datalabeling v0.15.0/go.mod h1:H8WSRKD9XYCDXDlZE3bPgvV7UYI0F05e+ufKev2AFc8= +cloud.google.com/go/dataplex v1.36.0/go.mod h1:ftgNMXBt+wJ4wPVNvYJ3UY3VTZtKS/i/uFEQppaEbKk= +cloud.google.com/go/dataproc/v2 v2.25.0/go.mod h1:hkiM6kzc8CwLGoquMN1oghyhuI1fE0girmChH4h9W7w= +cloud.google.com/go/dataqna v0.13.0/go.mod h1:XiVVFTOEJLBSvm3ILbyjXngGQYpjb/66MSksqz/56fs= +cloud.google.com/go/datastore v1.25.0/go.mod h1:jvJVNe+S2nHVIndV1H/B4s9K3MLsTMqOKlxSrzHTxB4= +cloud.google.com/go/datastream v1.21.0/go.mod h1:z9AlkQGdXqkeyO5HE+D6sYbOkLJYB4BCZpXFPX/1Vpo= +cloud.google.com/go/deploy v1.33.0/go.mod h1:QdF3plD8D5gV2RmkTXBB6cHrq490WlpFr1SChdOJO2Y= +cloud.google.com/go/dialogflow v1.84.0/go.mod h1:OU8Lj1aw5Vr2hl9ifW+vsKnc2b4iJH+41U7nZ4whg3U= +cloud.google.com/go/dlp v1.34.0/go.mod h1:+haQd/n0QTv5BK7wZnCk2qctd5sfKL50jjh9E6N0d/Q= +cloud.google.com/go/documentai v1.49.0/go.mod h1:VyQA+SxPnCPlVLSJ5UcFx+LQm8JCzK7uUXdkOaAHvG8= +cloud.google.com/go/domains v0.16.0/go.mod h1:O5AhaEyUAgZC2X4M10nSu3dQt2cJLtbjhtrNrdeSPF8= +cloud.google.com/go/edgecontainer v1.10.0/go.mod h1:g4xb11IzVWa9peXNTlnNguKP8uJVvMK4zZeDlGS2Wus= +cloud.google.com/go/errorreporting v0.9.0/go.mod h1:V7ojx7z76JITDZNGyDNkIIa9nNEkQzF6Yj+VHl2YF84= +cloud.google.com/go/essentialcontacts v1.12.0/go.mod h1:W8fTL17jP6vmsPHQaCT5rOjWGohEssuqDUroxnjST0A= +cloud.google.com/go/eventarc v1.25.0/go.mod h1:ncY2NKHKiX+sUjIfxVozrivvmJQ4HWo2znxms7AxlP8= +cloud.google.com/go/filestore v1.16.0/go.mod h1:szr35omqptDEuXgBbJ8PdVdYM3lf/Md96kNufWr1tVs= +cloud.google.com/go/firestore v1.24.0/go.mod h1:5aojyjN4olKUnBZDCRWwM+NsdrrCX3t1qfyERZGOonM= +cloud.google.com/go/functions v1.25.0/go.mod h1:b/tqakoKeAkj9RspEjqswWf5299Lkz9C/742QUD3OEk= +cloud.google.com/go/gkebackup v1.14.0/go.mod h1:kaD4l/s0ONcb3L9iHC8PzG1XkC5ggPwA/KAl6yAyQGs= +cloud.google.com/go/gkeconnect v1.0.0/go.mod h1:5iWSBQzMIRLwUHUWVhxxcNK45ZPE8ntyBgE0MkavlqQ= +cloud.google.com/go/gkehub v0.22.0/go.mod h1:WiXX1w9ZHwKZVUDwL//YQfjfWS7yE0I/ym3smZn9iwE= +cloud.google.com/go/gkemulticloud v1.12.0/go.mod h1:vLNCxGah7pPIoNSX4Yx+hb8klqA0lzzXTWBSut9KzRo= +cloud.google.com/go/gsuiteaddons v1.12.0/go.mod h1:rm/XT7wmwOFGn7jmWtVV65QmZCakzTbHLSojIC4Hskg= cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk= cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY= +cloud.google.com/go/iap v1.17.0/go.mod h1:b+r+yjrss2WmAEzNrQQjlEdD5E9B8c47mOF7XnqT+z0= +cloud.google.com/go/ids v1.11.0/go.mod h1:+drdvU0pQ4x5uYiWCv364VOeIpTN/PETBrdR51D4Tjk= +cloud.google.com/go/iot v1.13.0/go.mod h1:62W4n2fe/Ct66NWJEfCB5suZ3XsL5Atx+MxFjScr+9s= +cloud.google.com/go/kms v1.32.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA= +cloud.google.com/go/language v1.18.0/go.mod h1:xSeiVB4UiA9wYmFy2GWjf1Mb1K3uR1Yi/80qoqTxH04= +cloud.google.com/go/lifesciences v0.16.0/go.mod h1:axEwGa3A63+vCXIis+0Zkseu8KecqtNoSn7x0zyjJfM= +cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA= cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= +cloud.google.com/go/managedidentities v1.13.0/go.mod h1:lUYH5r6QEJTHqjgga0WFeiieqJ0iRwEuQSk20O41Vj0= +cloud.google.com/go/maps v1.37.0/go.mod h1:oalKFBmf2eHmdr3OvfEiiBlOakNlVitYYEPcM3TTUB4= +cloud.google.com/go/mediatranslation v0.13.0/go.mod h1:kjZrowuigFr+Bf1HM1TCtp1a3E3kfG1ovPK5VEuaNAQ= +cloud.google.com/go/memcache v1.17.0/go.mod h1:QQpFWgJvrFaQ6DgmitHejdbkLg8SJfHg5BzltKEWSt0= +cloud.google.com/go/metastore v1.20.0/go.mod h1:/bhZoizjM5iOrqWJeAFDw7c16C783wEftqofnJgKKYI= +cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM= +cloud.google.com/go/networkconnectivity v1.27.0/go.mod h1:pCnczH2W/cnLSlnsnN+VzBoXlM81ZoUGuuacFBGThyw= +cloud.google.com/go/networkmanagement v1.30.0/go.mod h1:3SBf5T7jyGzw5jqJWE7TUDRhIl2E029jggbeoFEgt5E= +cloud.google.com/go/networksecurity v0.19.0/go.mod h1:VWDFX+stDgzZYDsCX1Wy/JO9Tlw7g/V1UHbiORVgqq0= +cloud.google.com/go/notebooks v1.18.0/go.mod h1:fXU6A3TJ2YobFy6fxOr4tKZZ8QgTjdJAqDIykOB85Gk= +cloud.google.com/go/optimization v1.12.0/go.mod h1:28gzCUmeCLcT4vctGEo71QF4b60TYkKQo5y8Gs2KPq8= +cloud.google.com/go/orchestration v1.17.0/go.mod h1:Lf/Czqh4Jfy3IFpvDkKWjfjkYFI+tj6nAjq5ihivrq4= +cloud.google.com/go/orgpolicy v1.20.0/go.mod h1:9LHqEGx5P5dhansdKTNIEXpM+QbebAIOs66+HUID4aQ= +cloud.google.com/go/osconfig v1.22.0/go.mod h1:bUL0FaSR2ahPcFRRYnd6a0LyUzsQYIdUpBq8Tmxg8fE= +cloud.google.com/go/oslogin v1.18.0/go.mod h1:3Oa36T3781Mv+yCSVYlfasi7auHjfPFqvNOd1q92umc= +cloud.google.com/go/phishingprotection v0.13.0/go.mod h1:2gyYqwNjePPEocXDkDve3EuJPaRqN/E7fp28K3arR0k= +cloud.google.com/go/policytroubleshooter v1.16.0/go.mod h1:FZg3IW3exF6wc9eO/iBYijsGqiiCzc9mjZhsxgATXYA= +cloud.google.com/go/privatecatalog v0.16.0/go.mod h1:Dq1bSHRRaDqFr7Rb7UntXVjh1reeY6YdzYicL0EPTrM= +cloud.google.com/go/pubsub v1.51.0/go.mod h1:NERXf11sd82UV3VnflcUj8POIyQUXT/QwrKlxD8di/I= +cloud.google.com/go/pubsub/v2 v2.6.0/go.mod h1:4anqvV/w8Pcgu2tO0qr2XgsF3GXHowzryfQ5gOnVmWY= +cloud.google.com/go/pubsublite v1.10.0/go.mod h1:o9NVNBY4m8LubZqRCJtBdxpjP8DAsYizsxC6Z1vI7Dk= +cloud.google.com/go/recaptchaenterprise/v2 v2.26.0/go.mod h1:+ntF70/j7qBa6G/pwmYA0mkBcDeTCXV6WDqUL7GObfs= +cloud.google.com/go/recommendationengine v0.15.0/go.mod h1:Yx45rCF3A5fLSeXxSkXOCTXSBDBogrQnR7kUTJHwYxw= +cloud.google.com/go/recommender v1.19.0/go.mod h1:LRh+1HJjLx2kDE3S65AIlG/lvwA0llEFWYPD/QtgoaU= +cloud.google.com/go/redis v1.24.0/go.mod h1:ebtw9WLFKswecHO2ifNykuteNJNwoPqMCHz4UI11kF4= +cloud.google.com/go/resourcemanager v1.16.0/go.mod h1:Hn4HPkLRnTuiUhFEFJg736Brt7BwlS84xYU06sc3STc= +cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= +cloud.google.com/go/retail v1.32.0/go.mod h1:t9w9mBarD59BnFHTST2LoiCP5608ZlEfniHLgA6OoH0= cloud.google.com/go/run v1.22.0 h1:U56fxJWdrT+yjo4S/Vrtw5m69NdNL11Cyv9jX2JOi1s= cloud.google.com/go/run v1.22.0/go.mod h1:Wo0aTNrqfftGmbxPPraeOxSUDUZ2c7IVNg2dk8Qm1Bs= +cloud.google.com/go/scheduler v1.16.0/go.mod h1:0hsZg0MZJADyke1lutI0FHAYJR8Dtm8oIivXkmpACkA= +cloud.google.com/go/secretmanager v1.20.0/go.mod h1:9OmSuOeiiUicANglrbdKWSnT3gYkRcXuUQDk7dDW0zU= +cloud.google.com/go/security v1.26.0/go.mod h1:nd0i5OHXtJduMt0n6UnEojy7fiTfnfj/PSDeD7LAD+c= +cloud.google.com/go/securitycenter v1.45.0/go.mod h1:7mAlzsCsKlEVmciAFORl431laDGpoKGFkSQndAzFs30= +cloud.google.com/go/servicedirectory v1.17.0/go.mod h1:CtgjXS1idj3s9Q6tB68021Rzk8Q6decV6+ldXC1BoBk= +cloud.google.com/go/shell v1.13.0/go.mod h1:9WWf3xHQUElP5fL/lB9IJ/MMMnN2W/T86cBp+pXFFWo= +cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= +cloud.google.com/go/speech v1.36.0/go.mod h1:tiSA8MiX49o1ngq5Ww2JFTvfjKxtAuBKY/UIH6coCPg= +cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= +cloud.google.com/go/storagetransfer v1.19.0/go.mod h1:sy4ImXynHkm9CKmbILtmzLN36PHh7JOhUTpqXf5SvMs= +cloud.google.com/go/talent v1.14.0/go.mod h1:jieYQngp1YqRtqV2t92w3LTrjuLV05kMM4BZMUUneaw= +cloud.google.com/go/texttospeech v1.22.0/go.mod h1:bAksATiWPKaw8r8wVgANa4GkVdsyFE4y9ulRzKyuJec= +cloud.google.com/go/tpu v1.14.0/go.mod h1:1pggTTG5npfxea6vYjyl60Fg09VgbM7efBgVjnFZjpo= +cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= +cloud.google.com/go/translate v1.18.0/go.mod h1:aRVIE+P+7fngk8HwwFAgis5QA7wphGpKrFpdNoWtGCM= +cloud.google.com/go/video v1.33.0/go.mod h1:hEx8TNpQT6kdjMVsywePvT8BCb63Ee3F/R0GRa9wnzo= +cloud.google.com/go/videointelligence v1.17.0/go.mod h1:Phxz7AQpvXoOvz+KrrOZEJRo4CDgYXMDVDqhCtdF1jc= +cloud.google.com/go/vision/v2 v2.15.0/go.mod h1:DUdjdFkXqPvEoPC4WDYFvYCn0LlAZ4vVz29A0bXvW90= +cloud.google.com/go/vmmigration v1.16.0/go.mod h1:ILrSjXnHMpdamkkAU8fjMKKMsH27B6FLC5kv/6TkLy0= +cloud.google.com/go/vmwareengine v1.9.0/go.mod h1:zXXuUaIpvDhsV6sR+JdQfcQ4V5+pDarrp7FW7nOdS2I= +cloud.google.com/go/vpcaccess v1.14.0/go.mod h1:MxbVgr+2fpIFIEIdSmgnb8ykNWRPVtslpmWijp7an68= +cloud.google.com/go/webrisk v1.17.0/go.mod h1:ypwCZ+G/SXyUZ+x3ppxn1hu+6tDifGNd/OpwPtCdJHI= +cloud.google.com/go/websecurityscanner v1.12.0/go.mod h1:cZSc9HqoFdccL1mqZtPIInOd4R8PBGwI20wdnrz6AO8= +cloud.google.com/go/workflows v1.20.0/go.mod h1:TC9yx7VpjGdBBeKM8FG2EMtms5Q9nyTqI+2uV9bDNs4= +cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo= @@ -38,21 +158,34 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/ github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= +github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= +github.com/Microsoft/hnslib v0.1.3/go.mod h1:5vTyBey4N/VI2ZTNh2gdWhkPMefSbCFYjpvVwye+qtI= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andygrunwald/go-jira v1.17.0 h1:bbu5H676l6MaNcV6A7VDIAjIOQVgzNGEhNAwNI/Cjgo= github.com/andygrunwald/go-jira v1.17.0/go.mod h1:tiZsPUu9824bwcI2BUXatE4hJbs9rUOif0nv1lkq1hQ= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= @@ -106,22 +239,33 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradleyfalzon/ghinstallation/v2 v2.0.3/go.mod h1:tlgi+JWCXnKFx/Y4WtnDbZEINo31N5bcvnCoqieefmk= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= +github.com/container-storage-interface/spec v1.12.1-0.20260720052920-cd9e7ad1ae09/go.mod h1:apAI5+IWKBXTKh293aCL8t3f2Z9qzjhwaI3ghwGoR94= +github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= +github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/containerd/ttrpc v1.2.9/go.mod h1:jjtQRwXm4DL3KsHKW8vDiUOV6wO0hi6IPhmJhxU7aEs= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= @@ -130,11 +274,18 @@ github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpV github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= +github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= +github.com/coredns/corefile-migration v1.0.34/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= +github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= @@ -143,6 +294,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dgraph-io/badger/v4 v4.9.5 h1:zT46OMrF3ntqsfI3ynKp7hUkQrGlcK2CX5psQmH0iW0= @@ -165,6 +317,7 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= @@ -174,8 +327,10 @@ github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= @@ -220,6 +375,7 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -281,18 +437,22 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cadvisor/lib v0.60.5/go.mod h1:htHKT0OSYO6zaik+iLOo3Wj1mf/5l8uV5TJaqLz13oM= github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= @@ -303,11 +463,17 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= +github.com/google/go-github/v39 v39.0.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-github/v42 v42.0.0 h1:YNT0FwjPrEysRkLIiKuEfSvBPCGKphW5aS5PxwaoLec= github.com/google/go-github/v42 v42.0.0/go.mod h1:jgg/jvyI0YlDOM1/ps6XYh04HNQ3vKf0CVko62/EhRg= +github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= +github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -327,6 +493,8 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -335,15 +503,25 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= +github.com/huandu/go-sqlbuilder v1.42.1/go.mod h1:BEm32AHl29lzKDeV3HAIkzrz9cgRyumkDohHeGYYBoM= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/ishidawataru/sctp v0.0.0-20250521072954-ae8eb7fa7995/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= @@ -379,32 +557,42 @@ github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzH github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= +github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg= github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/maxcnunes/httpfake v1.2.4 h1:l7s/N7zuG6XpzG+5dUolg5SSoR3hANQxqzAkv+lREko= github.com/maxcnunes/httpfake v1.2.4/go.mod h1:rWVxb0bLKtOUM/5hN3UO1VEdEitz1hfcTXs7UyiK6r0= +github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= +github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/ipvs v1.1.0/go.mod h1:4VJMWuf098bsUMmZEiD4Tjk/O7mOn3l1PTD3s4OoYAs= github.com/moby/moby v28.5.2+incompatible h1:hIn6qcenb3JY1E3STwqEbBvJ8bha+u1LpqjX4CBvNCk= github.com/moby/moby v28.5.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= @@ -413,12 +601,14 @@ github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJ github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -429,20 +619,30 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/open-policy-agent/opa v1.20.1 h1:wSC3zjHbRyt7X3daV/DsjnhDywzB3l0m0gWhgX1W2vQ= github.com/open-policy-agent/opa v1.20.1/go.mod h1:pxxSP1noAirD8UJ7PgAjoRw39IE0Bk/JRFkUP3+51lU= +github.com/opencontainers/cgroups v0.0.7/go.mod h1:hPBRvnBhLZueEN0eJyozMeM3HeFGYlZW9KnO//px6G4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= @@ -468,6 +668,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -478,27 +680,41 @@ github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+ github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/reeflective/readline v1.3.0/go.mod h1:bOpqx2/VqGlIoobyWR1Vgt/p5FiMfIHj4OicPuw6RfU= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= +github.com/sigstore/fulcio v1.6.6/go.mod h1:BhQ22lwaebDgIxVBEYOOqLRcN5+xOV+C9bh/GUXRhOk= +github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -512,6 +728,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= @@ -525,6 +743,7 @@ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWD github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= @@ -537,6 +756,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= @@ -545,8 +766,11 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0= github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -559,29 +783,47 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeonx/timeago v1.0.0-rc5 h1:pwcQGpaH3eLfPtXeyPA4DmHWjoQt0Ea7/++FwpxqLxg= github.com/xeonx/timeago v1.0.0-rc5/go.mod h1:qDLrYEFynLO7y5Ho7w3GwgtYgpy5UfhcXIIQvMKVDkA= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24= gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= +go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= +go.etcd.io/etcd/api/v3 v3.7.0/go.mod h1:EcTihnwAQ0BQNh5dfAdaFVFdchuo7EP0HlX7TV3jz/A= +go.etcd.io/etcd/client/pkg/v3 v3.7.0/go.mod h1:cnzZGIUzSfjEwLC6UBVsSXlEK1eepS/JUD7wE6PLRT0= +go.etcd.io/etcd/client/v3 v3.7.0/go.mod h1:DJ382WuwjmbowjPDyaaQ0idWXy4dh91XRhe4FOrb9vM= +go.etcd.io/etcd/pkg/v3 v3.7.0/go.mod h1:fDQYyc8rOC1Yl4EZLh0O1OjHzwJW7jAly80+BpD1M9o= +go.etcd.io/etcd/server/v3 v3.7.0/go.mod h1:v7N1dPdSW2vzyxGsbDwK4X0sCe3SYNlQ+9eNIMGzdxQ= +go.etcd.io/raft/v3 v3.7.0/go.mod h1:6gX6T2X907DjnjsFLODnTxba77stjs84W9gTTI0GUNA= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/prometheus v0.69.0/go.mod h1:AAaS6xs5AyqMdR3Ir0nSWK+QudL2XM8Vbw5INzUxNc8= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s= +go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.69.0/go.mod h1:z5qVAACw5BI1yBkWfJRcU4qp+JDQ4fdFmdS4hTJVhH0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= @@ -594,6 +836,7 @@ go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6Tb go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= @@ -633,6 +876,7 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= @@ -647,15 +891,19 @@ golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.295.0 h1:SSqFeEVjnK5SKo6t7D0E0M7EfX8SP7K0+OJd2Ly5FzU= google.golang.org/api v0.295.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU= google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg= google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20260819154853-08b0e4226688/go.mod h1:832FQwEl9OKXy5rHqEY2U7uF7Bg+Hs7Zo72IIq+dYZ4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= @@ -669,8 +917,11 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -692,28 +943,48 @@ k8s.io/cli-runtime v0.37.0 h1:U3XakUeirBQJMz5688r04z74SIHSE7V5SIZ6Ho5JyBM= k8s.io/cli-runtime v0.37.0/go.mod h1:qiQMFkKwFFuPH6zy953On+nc3qfpEHAIDrJmAuRz5Vg= k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ= k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0= +k8s.io/cloud-provider v0.37.0/go.mod h1:q17nnGXsvnd9o24QY6Mtvs+pSitAMUKOWkx6lT7G9tY= +k8s.io/cluster-bootstrap v0.37.0/go.mod h1:nvYg1xT7nXPc5XTRmFYS/x617p5BXNviL8+d1mvMdMo= +k8s.io/code-generator v0.37.0/go.mod h1:qg7E/uDlyvevVRL1V8+h2z9UWmi/8gxaRka/lVXUBdk= k8s.io/component-base v0.37.0 h1:3SdSa4+itMdFTDFTeR8CxKGmSTSMXFlKL4ky8OqjguM= k8s.io/component-base v0.37.0/go.mod h1:LjOebp4R9y6LODWZQv102ZQxGheLcDO2ZJLAw6bbh4I= k8s.io/component-helpers v0.37.0 h1:tPz4goLftoiUBfYWZgPlP4Srkd/PciNz9q+0ReOfyxY= k8s.io/component-helpers v0.37.0/go.mod h1:wDAmi8hduKu3YGqKg5a0wxCpoql39DkTmdQ60bpLpkc= k8s.io/controller-manager v0.37.0 h1:0Wsb77PqGypDXtvSj423aKIq3JziipJFPU+qgUDaoRs= k8s.io/controller-manager v0.37.0/go.mod h1:x9OhyZqt8NfTN7CzeRHTgfpMzP43o/3PQwQgJhL5gsk= +k8s.io/cri-api v0.37.0/go.mod h1:6V8Gb6wznJYVPToj8CLxgbxsFeeGIswNZCE7cy+xT90= +k8s.io/cri-client v0.37.0/go.mod h1:x/nROOizpdhU/VGcokYhxtWioDPMhqinRRCHWOoVL2c= +k8s.io/csi-translation-lib v0.37.0/go.mod h1:1ug+iTy/0S+1VplKrTOslXamIH8+bVvmglmMT8VYq00= +k8s.io/dynamic-resource-allocation v0.37.0/go.mod h1:Nkono0X3H5tNnEf3IOK0Ml+I7HB4o0/UXaiiNCgB2Vw= +k8s.io/endpointslice v0.37.0/go.mod h1:PbotyEtd2DIpPHPp9ZgJsJHCBJuAEh2Oz21V5MyAYX4= +k8s.io/externaljwt v0.37.0/go.mod h1:MfUIWFM2xxGdIMdh3aOsMAZj8sFgHbngEd+ajFzD1qk= +k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3/go.mod h1:yvyl3l9E+UxlqOMUULdKTAYB0rEhsmjr7+2Vb/1pCSo= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kms v0.37.0/go.mod h1:0U6zfwLkfJr9O/jLgz2GARnKLh2uIhkq2EeaU66J5ow= +k8s.io/kube-aggregator v0.37.0/go.mod h1:Uy1F5FUItTfFfsx1T+cP+7uqKvV3J8/jjBxVzc/ICYU= +k8s.io/kube-controller-manager v0.37.0/go.mod h1:Nd/BuJWTyLB5BTm1oOZuH4qrD+uZ8rgmSaEgBM166k8= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= +k8s.io/kube-proxy v0.37.0/go.mod h1:XKt9orRIUAjuZVddE7OAs1tpK8pHypiUGtVBwVR6cPQ= +k8s.io/kube-scheduler v0.37.0/go.mod h1:CPdW3moznhYQ68iyzhblQuVX+VDB6PX2ch9SJ7v1gBI= k8s.io/kubectl v0.37.0 h1:cici6hiofx93ASldmprDmZF55SfhVt4o3HniltVLjTc= k8s.io/kubectl v0.37.0/go.mod h1:RSeEl8e/yqDx6srG8Azr0uAtVPNIZljA0PNh9HCBcdg= k8s.io/kubelet v0.37.0 h1:VhaZanjlE5CkoAPAjKw0DH+Q0BYXVfYekZgzZDAJMMg= k8s.io/kubelet v0.37.0/go.mod h1:PHXfQuVqsTzFVqOeP67UUNv5Ajri+dLGoeaEnW6gGjE= k8s.io/kubernetes v1.37.0 h1:TDSD+Izz9inqs7SmtJCAu7L5/DuYW9hAZ3GBdF6Ddkw= k8s.io/kubernetes v1.37.0/go.mod h1:zAK7e3i5feyBL/bhucQnA5VRKuyrS8F+ijHJkZzQaWw= +k8s.io/metrics v0.37.0/go.mod h1:E14Jt50A9sFS2m3gLRdTA0ya7IliI0Sx3NxS5VKoCvM= +k8s.io/mount-utils v0.37.0/go.mod h1:uGJRC7u42QGYbHlHxt/5J2rXrfgv3XNYU3I0ZXVnAck= k8s.io/pod-security-admission v0.37.0 h1:5lx9eMh47oWJy2EQDHJ82yoYj6/KiTomzzcpDS1be14= k8s.io/pod-security-admission v0.37.0/go.mod h1:TaR1x79zQ3WBo2Avt49YiqJ55xwfak6KCdgVW30SHUc= +k8s.io/sample-apiserver v0.37.0/go.mod h1:1E7DVxhX3fKbVaaRzUxFcw1o3l6Gjo3wxsbTiKtD5sE= k8s.io/streaming v0.37.0 h1:iPBUZLZiKt5bV+lxJurASMOV07VuBhNpiwJt2//AWrM= k8s.io/streaming v0.37.0/go.mod h1:APlJR26ZWRcVy5bIEj0QRrKUXROtBHPcxl2NT7EAzPU= +k8s.io/system-validators v1.12.1/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw= k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs= k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 h1:/YpDJ4vReG7ZmzSpBGxduXgywWkJU9zHubgJG03MT+Y= @@ -722,8 +993,10 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.33.0 h1:AjvDv3vOygb/VKLVQW87lfktIBzkxR8Ump9DjxC8+Lk= sigs.k8s.io/kind v0.33.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= +sigs.k8s.io/knftables v0.0.22/go.mod h1:tig4GDnk1aG8sNsKW/iqid/PGgVLxJLEZkuUlO5C44Y= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kustomize/v5 v5.8.1/go.mod h1:0vFa5pQ/elNEQMyiAJuGku9rhAMzz7u9+61hRqFKiwY= sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= @@ -732,3 +1005,4 @@ sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3C sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= diff --git a/internal/azure/azure_apps.go b/internal/azure/azure_apps.go index 47443117a..cce66924b 100644 --- a/internal/azure/azure_apps.go +++ b/internal/azure/azure_apps.go @@ -102,6 +102,9 @@ func (staticCreds *AzureStaticCredentials) GetAzureAppsData(logger *logger.Logge data, err := azureClient.NewAppData(app, logger) if err != nil { + // One app's error cancels the run, so say which app it was. Wrapped + // here rather than at each return so every path is covered once. + err = fmt.Errorf("app [%s]: %w", *app.Name, err) select { case errs <- err: default: @@ -411,12 +414,13 @@ var acrLoginServerSuffixes = []string{".azurecr.io", ".azurecr.cn", ".azurecr.us type imageFingerprintSource int const ( - // fingerprintFromACR reads the fingerprint from Azure Container Registry, - // authenticated with the Azure credential. - fingerprintFromACR imageFingerprintSource = iota - // fingerprintFromAnonymousRegistry reads it from any other registry with no - // credential attached. - fingerprintFromAnonymousRegistry + // fingerprintFromAnonymousRegistry reads the fingerprint from a registry with + // no credential attached. It is the zero value deliberately: an unset or + // partially built plan must never select the credential-bearing arm. + fingerprintFromAnonymousRegistry imageFingerprintSource = iota + // fingerprintFromACR reads it from Azure Container Registry, authenticated + // with the Azure credential. + fingerprintFromACR ) // isACRLoginServer reports whether domain is an Azure Container Registry login @@ -531,7 +535,7 @@ func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *lo // resolver checks the digest it is given against the one it gets back, so // hold the registry to it here. if plan.pinnedFingerprint != "" && fingerprint != plan.pinnedFingerprint { - return "", fmt.Errorf("image [%s] is pinned to digest sha256:%s but [%s] reported sha256:%s", imageName, plan.pinnedFingerprint, plan.domain, fingerprint) + return "", fmt.Errorf("image [%s] is pinned to digest sha256:%s but [%s] reported sha256:%s", plan.reference, plan.pinnedFingerprint, plan.domain, fingerprint) } return fingerprint, nil diff --git a/internal/azure/image_fingerprint_test.go b/internal/azure/image_fingerprint_test.go index 5092dda90..807437a28 100644 --- a/internal/azure/image_fingerprint_test.go +++ b/internal/azure/image_fingerprint_test.go @@ -370,3 +370,11 @@ func TestACRImageFingerprintReturnsTheSha256Hex(t *testing.T) { require.NoError(t, err) require.Equal(t, want, fingerprint) } + +// TestZeroValueSourceIsAnonymous is a fail-closed guarantee: the field that +// decides whether the Azure credential is sent must not default to sending it. +func TestZeroValueSourceIsAnonymous(t *testing.T) { + var unset fingerprintPlan + require.Equal(t, fingerprintFromAnonymousRegistry, unset.source, + "an unset plan must not select the credential-bearing arm") +} diff --git a/internal/digest/digest.go b/internal/digest/digest.go index 7bbcc3111..eda19674e 100644 --- a/internal/digest/digest.go +++ b/internal/digest/digest.go @@ -123,6 +123,11 @@ func Sha256FingerprintFromDigest(digestString string) (string, error) { // Sha256Fingerprint is the same rule for a digest that is already parsed, so a // typed value does not have to be turned back into a string to be checked. func Sha256Fingerprint(parsed godigest.Digest) (string, error) { + // godigest.Digest is a string type, so a caller can hand over an unvalidated + // one and Algorithm()/Encoded() would just split it on the colon. + if err := parsed.Validate(); err != nil { + return "", fmt.Errorf("invalid digest %q: %w", parsed.String(), err) + } if parsed.Algorithm() != godigest.SHA256 { return "", fmt.Errorf("digest algorithm is %s, but Kosli fingerprints are sha256", parsed.Algorithm()) } diff --git a/internal/digest/oci_anonymous_test.go b/internal/digest/oci_anonymous_test.go index 9bb180525..7c36e0a63 100644 --- a/internal/digest/oci_anonymous_test.go +++ b/internal/digest/oci_anonymous_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/containers/image/v5/types" + godigest "github.com/opencontainers/go-digest" "github.com/stretchr/testify/require" ) @@ -109,3 +110,27 @@ func TestSha256FingerprintFromDigest(t *testing.T) { }) } } + +// TestSha256FingerprintValidatesWhatItIsHanded covers the exported typed entry +// point. godigest.Digest is a string type, so an unvalidated value must not be +// split into a fingerprint. +func TestSha256FingerprintValidatesWhatItIsHanded(t *testing.T) { + for _, tc := range []struct{ name, digest string }{ + {name: "non hex encoded portion", digest: "sha256:hello"}, + {name: "empty encoded portion", digest: "sha256:"}, + {name: "uppercase encoded portion", digest: "sha256:" + strings.Repeat("A", 64)}, + {name: "wrong length", digest: "sha256:abc"}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := Sha256Fingerprint(godigest.Digest(tc.digest)) + require.Error(t, err) + require.Empty(t, got) + require.Contains(t, err.Error(), "invalid digest") + }) + } + + valid := strings.Repeat("a", 64) + got, err := Sha256Fingerprint(godigest.Digest("sha256:" + valid)) + require.NoError(t, err) + require.Equal(t, valid, got) +} From d7babe1bd77ecb6bec3d1e00bf86490c85a160fb Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Tue, 8 Sep 2026 21:03:06 +0100 Subject: [PATCH 6/7] chore: revert unrelated go.sum churn An earlier `go mod tidy` in this branch added 274 lines of unrelated /go.mod hashes to go.sum, for modules that appear nowhere in go.mod (cloud.google.com, buf.build, bitbucket.org and others). Running tidy again removes them, so they were an artefact of the module graph state at the time rather than anything this change needs. go.sum is now identical to master. go.mod keeps only the two intended promotions: distribution/reference and opencontainers/go-digest move from indirect to direct, because internal/azure and internal/digest now import them directly. Co-Authored-By: Claude Opus 5 --- go.sum | 274 --------------------------------------------------------- 1 file changed, 274 deletions(-) diff --git a/go.sum b/go.sum index 8b5d116b8..38522714c 100644 --- a/go.sum +++ b/go.sum @@ -1,141 +1,21 @@ al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA= al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= -bitbucket.org/bertimus9/systemstat v0.5.0/go.mod h1:EkUWPp8lKFPMXP8vnbpT5JDI0W/sTiLZAvN8ONWErHY= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= -buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= -buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/accessapproval v1.13.0/go.mod h1:7bmInw17bQX+ZPi7YmReC3xKymDrMmxXaUnaI6zQOqI= -cloud.google.com/go/accesscontextmanager v1.15.0/go.mod h1:YjW9urferk8i9ALwBF3bmdcogZeQYRn2yWwR8nkhsBc= -cloud.google.com/go/aiplatform v1.126.0/go.mod h1:iR3za3evdprLe1XL2pLu0cYVCuTbc87QG0pgvcgiJlE= -cloud.google.com/go/analytics v0.35.0/go.mod h1:V9Qef2N0y8GDqQ9FTlmM2XpDEMYonZJRPSUNGZlPCcc= -cloud.google.com/go/apigateway v1.13.0/go.mod h1:pvEpOuuOIw2ev9VCcOyVkDXHHL4lvgMuqIe7XjJ8JoU= -cloud.google.com/go/apigeeconnect v1.12.0/go.mod h1:mYJekCKZHc2ia5yZX5lwtexTn9CzsOfb6+sh/2hi42Q= -cloud.google.com/go/apigeeregistry v1.1.0/go.mod h1:4ZFhQlxMuyfDMz9ORDSV8FPZtf2yPQkKjigsFtrrE4Y= -cloud.google.com/go/appengine v1.15.0/go.mod h1:/8gGZsOX5GDjOo4mAWk8IV59p2991dxTbEtKIlhDjzU= -cloud.google.com/go/area120 v0.15.0/go.mod h1:jD1fw9W4xxIZMY68g7PpbCPleoeGddFs5jPcdhfg3+Y= -cloud.google.com/go/artifactregistry v1.26.0/go.mod h1:c5FPi5GtDBP+OAr5kKhCBNQDT9ZgAyobXQjekx93VWs= -cloud.google.com/go/asset v1.28.0/go.mod h1:Pnvjhay8/FgodOH9uJC8OkfJfRtSnNIIU4WSxg5JfJw= -cloud.google.com/go/assuredworkloads v1.19.0/go.mod h1:/UGGtFCMokM3sGJ4FxjfmLvvFpPa5I/Oz68mwk4Su+0= cloud.google.com/go/auth v0.23.2 h1:pxSCpfiji41hpzpPdMCftEUCezpgpqmmDdYiAjCKXxo= cloud.google.com/go/auth v0.23.2/go.mod h1:4DhBRcqvtljQN3dJ57qtqbib5ZGCYE5f2crfiiC2EM0= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/automl v1.21.0/go.mod h1:MNbhUevuECzM3jqSOM7hmOedOdRJkm8xbbXW44SU15U= -cloud.google.com/go/baremetalsolution v1.10.0/go.mod h1:xhhT9VQiKPFd2fUs4oeDSRrxV0sb0PGeVmuZoUE2cBA= -cloud.google.com/go/batch v1.20.0/go.mod h1:ABT/5QqsIDsONa+n/8C7XYPjwh/kjOEPXukcRTaMsCg= -cloud.google.com/go/beyondcorp v1.8.0/go.mod h1:aVxzwamO8H4GXWQHowBAmL0KYNfYpW4E6Do2wfP0RYs= -cloud.google.com/go/bigquery v1.79.0/go.mod h1:QTt5tgZxqqvZs3dOZKpvriGqy+CdvY9LyetirFZRPOE= -cloud.google.com/go/bigtable v1.47.0/go.mod h1:GUM6PdkG3rrDse9kugqvX5+ktwo3ldfLtLi1VFn5Wj4= -cloud.google.com/go/billing v1.26.0/go.mod h1:axqDO1uHegh7u5qngkTfqN1djAeLGsWAFAblERgmgEk= -cloud.google.com/go/binaryauthorization v1.16.0/go.mod h1:E+iC5Avu4pdItdzGiSGHnh6TfQrl+KmPxDDg/T/VuHs= -cloud.google.com/go/certificatemanager v1.15.0/go.mod h1:8dfGG2/TbUpCNqsCF/TIMOGV0OVvU6nhkZWTU4MmCXU= -cloud.google.com/go/channel v1.27.0/go.mod h1:9ekufBLXuQ6j1oyqtDSIp29qWU5EwCi8WUi9qkLn3MA= -cloud.google.com/go/cloudbuild v1.32.0/go.mod h1:mYgcM8CMaPmAnO7GxSQ9ADAxVRwS+1b7s6WVkt29OXY= -cloud.google.com/go/clouddms v1.14.0/go.mod h1:qSwET2Q27cJ4wCDsPsbkagXqQqkWfOy+gU3RjMsT/c8= -cloud.google.com/go/cloudtasks v1.18.0/go.mod h1:3KeCxwtGEyaySL7CR3lMmEa2I4mq1ynXdgmfNiO4RYE= -cloud.google.com/go/compute v1.62.0/go.mod h1:Xm6PbsLgBpAg4va77ljbBdpMjzuU+uPp5Ze2dnZq7lw= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/contactcenterinsights v1.23.0/go.mod h1:uB/kygbfYH/gWEq3NEgq3QRI7/MvpjFyX81ajcW5YAI= -cloud.google.com/go/container v1.51.0/go.mod h1:EvqoT2eXfxLweXXUlhAMGR0sOAB00XPzEjoL01esSDs= -cloud.google.com/go/containeranalysis v0.19.0/go.mod h1:Zq0XHzUIa0oTa7H6aSR8HWqeJnoRI9syUcYJzfozjZQ= -cloud.google.com/go/datacatalog v1.33.0/go.mod h1:/EMN04S73fZcPdtNg86VYLDrhi2HheMehQtMCS86Klk= -cloud.google.com/go/dataflow v0.16.0/go.mod h1:BWhSrIGmsMfuYj3J+nJ2Tw7tplRR6r28kvRiqCD3WlQ= -cloud.google.com/go/dataform v1.2.0/go.mod h1:Lhkjd6L04/nBqsEo7S9Tx7D+Vm0pDDDZuKczewAJuX0= -cloud.google.com/go/datafusion v1.14.0/go.mod h1:2z+uDUKkLPacNNos5lW1Jf1IRDoFyeE+glJ4hmxF2Uc= -cloud.google.com/go/datalabeling v0.15.0/go.mod h1:H8WSRKD9XYCDXDlZE3bPgvV7UYI0F05e+ufKev2AFc8= -cloud.google.com/go/dataplex v1.36.0/go.mod h1:ftgNMXBt+wJ4wPVNvYJ3UY3VTZtKS/i/uFEQppaEbKk= -cloud.google.com/go/dataproc/v2 v2.25.0/go.mod h1:hkiM6kzc8CwLGoquMN1oghyhuI1fE0girmChH4h9W7w= -cloud.google.com/go/dataqna v0.13.0/go.mod h1:XiVVFTOEJLBSvm3ILbyjXngGQYpjb/66MSksqz/56fs= -cloud.google.com/go/datastore v1.25.0/go.mod h1:jvJVNe+S2nHVIndV1H/B4s9K3MLsTMqOKlxSrzHTxB4= -cloud.google.com/go/datastream v1.21.0/go.mod h1:z9AlkQGdXqkeyO5HE+D6sYbOkLJYB4BCZpXFPX/1Vpo= -cloud.google.com/go/deploy v1.33.0/go.mod h1:QdF3plD8D5gV2RmkTXBB6cHrq490WlpFr1SChdOJO2Y= -cloud.google.com/go/dialogflow v1.84.0/go.mod h1:OU8Lj1aw5Vr2hl9ifW+vsKnc2b4iJH+41U7nZ4whg3U= -cloud.google.com/go/dlp v1.34.0/go.mod h1:+haQd/n0QTv5BK7wZnCk2qctd5sfKL50jjh9E6N0d/Q= -cloud.google.com/go/documentai v1.49.0/go.mod h1:VyQA+SxPnCPlVLSJ5UcFx+LQm8JCzK7uUXdkOaAHvG8= -cloud.google.com/go/domains v0.16.0/go.mod h1:O5AhaEyUAgZC2X4M10nSu3dQt2cJLtbjhtrNrdeSPF8= -cloud.google.com/go/edgecontainer v1.10.0/go.mod h1:g4xb11IzVWa9peXNTlnNguKP8uJVvMK4zZeDlGS2Wus= -cloud.google.com/go/errorreporting v0.9.0/go.mod h1:V7ojx7z76JITDZNGyDNkIIa9nNEkQzF6Yj+VHl2YF84= -cloud.google.com/go/essentialcontacts v1.12.0/go.mod h1:W8fTL17jP6vmsPHQaCT5rOjWGohEssuqDUroxnjST0A= -cloud.google.com/go/eventarc v1.25.0/go.mod h1:ncY2NKHKiX+sUjIfxVozrivvmJQ4HWo2znxms7AxlP8= -cloud.google.com/go/filestore v1.16.0/go.mod h1:szr35omqptDEuXgBbJ8PdVdYM3lf/Md96kNufWr1tVs= -cloud.google.com/go/firestore v1.24.0/go.mod h1:5aojyjN4olKUnBZDCRWwM+NsdrrCX3t1qfyERZGOonM= -cloud.google.com/go/functions v1.25.0/go.mod h1:b/tqakoKeAkj9RspEjqswWf5299Lkz9C/742QUD3OEk= -cloud.google.com/go/gkebackup v1.14.0/go.mod h1:kaD4l/s0ONcb3L9iHC8PzG1XkC5ggPwA/KAl6yAyQGs= -cloud.google.com/go/gkeconnect v1.0.0/go.mod h1:5iWSBQzMIRLwUHUWVhxxcNK45ZPE8ntyBgE0MkavlqQ= -cloud.google.com/go/gkehub v0.22.0/go.mod h1:WiXX1w9ZHwKZVUDwL//YQfjfWS7yE0I/ym3smZn9iwE= -cloud.google.com/go/gkemulticloud v1.12.0/go.mod h1:vLNCxGah7pPIoNSX4Yx+hb8klqA0lzzXTWBSut9KzRo= -cloud.google.com/go/gsuiteaddons v1.12.0/go.mod h1:rm/XT7wmwOFGn7jmWtVV65QmZCakzTbHLSojIC4Hskg= cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk= cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY= -cloud.google.com/go/iap v1.17.0/go.mod h1:b+r+yjrss2WmAEzNrQQjlEdD5E9B8c47mOF7XnqT+z0= -cloud.google.com/go/ids v1.11.0/go.mod h1:+drdvU0pQ4x5uYiWCv364VOeIpTN/PETBrdR51D4Tjk= -cloud.google.com/go/iot v1.13.0/go.mod h1:62W4n2fe/Ct66NWJEfCB5suZ3XsL5Atx+MxFjScr+9s= -cloud.google.com/go/kms v1.32.0/go.mod h1:CSGvW6GnMQbY+1nOHcIzhMtHSbExXlOmCKjWtYVjcpA= -cloud.google.com/go/language v1.18.0/go.mod h1:xSeiVB4UiA9wYmFy2GWjf1Mb1K3uR1Yi/80qoqTxH04= -cloud.google.com/go/lifesciences v0.16.0/go.mod h1:axEwGa3A63+vCXIis+0Zkseu8KecqtNoSn7x0zyjJfM= -cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA= cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= -cloud.google.com/go/managedidentities v1.13.0/go.mod h1:lUYH5r6QEJTHqjgga0WFeiieqJ0iRwEuQSk20O41Vj0= -cloud.google.com/go/maps v1.37.0/go.mod h1:oalKFBmf2eHmdr3OvfEiiBlOakNlVitYYEPcM3TTUB4= -cloud.google.com/go/mediatranslation v0.13.0/go.mod h1:kjZrowuigFr+Bf1HM1TCtp1a3E3kfG1ovPK5VEuaNAQ= -cloud.google.com/go/memcache v1.17.0/go.mod h1:QQpFWgJvrFaQ6DgmitHejdbkLg8SJfHg5BzltKEWSt0= -cloud.google.com/go/metastore v1.20.0/go.mod h1:/bhZoizjM5iOrqWJeAFDw7c16C783wEftqofnJgKKYI= -cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM= -cloud.google.com/go/networkconnectivity v1.27.0/go.mod h1:pCnczH2W/cnLSlnsnN+VzBoXlM81ZoUGuuacFBGThyw= -cloud.google.com/go/networkmanagement v1.30.0/go.mod h1:3SBf5T7jyGzw5jqJWE7TUDRhIl2E029jggbeoFEgt5E= -cloud.google.com/go/networksecurity v0.19.0/go.mod h1:VWDFX+stDgzZYDsCX1Wy/JO9Tlw7g/V1UHbiORVgqq0= -cloud.google.com/go/notebooks v1.18.0/go.mod h1:fXU6A3TJ2YobFy6fxOr4tKZZ8QgTjdJAqDIykOB85Gk= -cloud.google.com/go/optimization v1.12.0/go.mod h1:28gzCUmeCLcT4vctGEo71QF4b60TYkKQo5y8Gs2KPq8= -cloud.google.com/go/orchestration v1.17.0/go.mod h1:Lf/Czqh4Jfy3IFpvDkKWjfjkYFI+tj6nAjq5ihivrq4= -cloud.google.com/go/orgpolicy v1.20.0/go.mod h1:9LHqEGx5P5dhansdKTNIEXpM+QbebAIOs66+HUID4aQ= -cloud.google.com/go/osconfig v1.22.0/go.mod h1:bUL0FaSR2ahPcFRRYnd6a0LyUzsQYIdUpBq8Tmxg8fE= -cloud.google.com/go/oslogin v1.18.0/go.mod h1:3Oa36T3781Mv+yCSVYlfasi7auHjfPFqvNOd1q92umc= -cloud.google.com/go/phishingprotection v0.13.0/go.mod h1:2gyYqwNjePPEocXDkDve3EuJPaRqN/E7fp28K3arR0k= -cloud.google.com/go/policytroubleshooter v1.16.0/go.mod h1:FZg3IW3exF6wc9eO/iBYijsGqiiCzc9mjZhsxgATXYA= -cloud.google.com/go/privatecatalog v0.16.0/go.mod h1:Dq1bSHRRaDqFr7Rb7UntXVjh1reeY6YdzYicL0EPTrM= -cloud.google.com/go/pubsub v1.51.0/go.mod h1:NERXf11sd82UV3VnflcUj8POIyQUXT/QwrKlxD8di/I= -cloud.google.com/go/pubsub/v2 v2.6.0/go.mod h1:4anqvV/w8Pcgu2tO0qr2XgsF3GXHowzryfQ5gOnVmWY= -cloud.google.com/go/pubsublite v1.10.0/go.mod h1:o9NVNBY4m8LubZqRCJtBdxpjP8DAsYizsxC6Z1vI7Dk= -cloud.google.com/go/recaptchaenterprise/v2 v2.26.0/go.mod h1:+ntF70/j7qBa6G/pwmYA0mkBcDeTCXV6WDqUL7GObfs= -cloud.google.com/go/recommendationengine v0.15.0/go.mod h1:Yx45rCF3A5fLSeXxSkXOCTXSBDBogrQnR7kUTJHwYxw= -cloud.google.com/go/recommender v1.19.0/go.mod h1:LRh+1HJjLx2kDE3S65AIlG/lvwA0llEFWYPD/QtgoaU= -cloud.google.com/go/redis v1.24.0/go.mod h1:ebtw9WLFKswecHO2ifNykuteNJNwoPqMCHz4UI11kF4= -cloud.google.com/go/resourcemanager v1.16.0/go.mod h1:Hn4HPkLRnTuiUhFEFJg736Brt7BwlS84xYU06sc3STc= -cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw= -cloud.google.com/go/retail v1.32.0/go.mod h1:t9w9mBarD59BnFHTST2LoiCP5608ZlEfniHLgA6OoH0= cloud.google.com/go/run v1.22.0 h1:U56fxJWdrT+yjo4S/Vrtw5m69NdNL11Cyv9jX2JOi1s= cloud.google.com/go/run v1.22.0/go.mod h1:Wo0aTNrqfftGmbxPPraeOxSUDUZ2c7IVNg2dk8Qm1Bs= -cloud.google.com/go/scheduler v1.16.0/go.mod h1:0hsZg0MZJADyke1lutI0FHAYJR8Dtm8oIivXkmpACkA= -cloud.google.com/go/secretmanager v1.20.0/go.mod h1:9OmSuOeiiUicANglrbdKWSnT3gYkRcXuUQDk7dDW0zU= -cloud.google.com/go/security v1.26.0/go.mod h1:nd0i5OHXtJduMt0n6UnEojy7fiTfnfj/PSDeD7LAD+c= -cloud.google.com/go/securitycenter v1.45.0/go.mod h1:7mAlzsCsKlEVmciAFORl431laDGpoKGFkSQndAzFs30= -cloud.google.com/go/servicedirectory v1.17.0/go.mod h1:CtgjXS1idj3s9Q6tB68021Rzk8Q6decV6+ldXC1BoBk= -cloud.google.com/go/shell v1.13.0/go.mod h1:9WWf3xHQUElP5fL/lB9IJ/MMMnN2W/T86cBp+pXFFWo= -cloud.google.com/go/spanner v1.91.0/go.mod h1:8NB5a7qgwIhGD19Ly+vkpKffPL78vIG9RcrgsuREha0= -cloud.google.com/go/speech v1.36.0/go.mod h1:tiSA8MiX49o1ngq5Ww2JFTvfjKxtAuBKY/UIH6coCPg= -cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU= -cloud.google.com/go/storagetransfer v1.19.0/go.mod h1:sy4ImXynHkm9CKmbILtmzLN36PHh7JOhUTpqXf5SvMs= -cloud.google.com/go/talent v1.14.0/go.mod h1:jieYQngp1YqRtqV2t92w3LTrjuLV05kMM4BZMUUneaw= -cloud.google.com/go/texttospeech v1.22.0/go.mod h1:bAksATiWPKaw8r8wVgANa4GkVdsyFE4y9ulRzKyuJec= -cloud.google.com/go/tpu v1.14.0/go.mod h1:1pggTTG5npfxea6vYjyl60Fg09VgbM7efBgVjnFZjpo= -cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= -cloud.google.com/go/translate v1.18.0/go.mod h1:aRVIE+P+7fngk8HwwFAgis5QA7wphGpKrFpdNoWtGCM= -cloud.google.com/go/video v1.33.0/go.mod h1:hEx8TNpQT6kdjMVsywePvT8BCb63Ee3F/R0GRa9wnzo= -cloud.google.com/go/videointelligence v1.17.0/go.mod h1:Phxz7AQpvXoOvz+KrrOZEJRo4CDgYXMDVDqhCtdF1jc= -cloud.google.com/go/vision/v2 v2.15.0/go.mod h1:DUdjdFkXqPvEoPC4WDYFvYCn0LlAZ4vVz29A0bXvW90= -cloud.google.com/go/vmmigration v1.16.0/go.mod h1:ILrSjXnHMpdamkkAU8fjMKKMsH27B6FLC5kv/6TkLy0= -cloud.google.com/go/vmwareengine v1.9.0/go.mod h1:zXXuUaIpvDhsV6sR+JdQfcQ4V5+pDarrp7FW7nOdS2I= -cloud.google.com/go/vpcaccess v1.14.0/go.mod h1:MxbVgr+2fpIFIEIdSmgnb8ykNWRPVtslpmWijp7an68= -cloud.google.com/go/webrisk v1.17.0/go.mod h1:ypwCZ+G/SXyUZ+x3ppxn1hu+6tDifGNd/OpwPtCdJHI= -cloud.google.com/go/websecurityscanner v1.12.0/go.mod h1:cZSc9HqoFdccL1mqZtPIInOd4R8PBGwI20wdnrz6AO8= -cloud.google.com/go/workflows v1.20.0/go.mod h1:TC9yx7VpjGdBBeKM8FG2EMtms5Q9nyTqI+2uV9bDNs4= -cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo= @@ -158,34 +38,21 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/ github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo= -github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= -github.com/Microsoft/hnslib v0.1.3/go.mod h1:5vTyBey4N/VI2ZTNh2gdWhkPMefSbCFYjpvVwye+qtI= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs= github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= -github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/andygrunwald/go-jira v1.17.0 h1:bbu5H676l6MaNcV6A7VDIAjIOQVgzNGEhNAwNI/Cjgo= github.com/andygrunwald/go-jira v1.17.0/go.mod h1:tiZsPUu9824bwcI2BUXatE4hJbs9rUOif0nv1lkq1hQ= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= @@ -239,33 +106,22 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bradleyfalzon/ghinstallation/v2 v2.0.3/go.mod h1:tlgi+JWCXnKFx/Y4WtnDbZEINo31N5bcvnCoqieefmk= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= -github.com/container-storage-interface/spec v1.12.1-0.20260720052920-cd9e7ad1ae09/go.mod h1:apAI5+IWKBXTKh293aCL8t3f2Z9qzjhwaI3ghwGoR94= -github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= -github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= -github.com/containerd/ttrpc v1.2.9/go.mod h1:jjtQRwXm4DL3KsHKW8vDiUOV6wO0hi6IPhmJhxU7aEs= -github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/containers/image/v5 v5.36.2 h1:GcxYQyAHRF/pLqR4p4RpvKllnNL8mOBn0eZnqJbfTwk= github.com/containers/image/v5 v5.36.2/go.mod h1:b4GMKH2z/5t6/09utbse2ZiLK/c72GuGLFdp7K69eA4= github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= @@ -274,18 +130,11 @@ github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpV github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ= github.com/containers/storage v1.59.1 h1:11Zu68MXsEQGBBd+GadPrHPpWeqjKS8hJDGiAHgIqDs= github.com/containers/storage v1.59.1/go.mod h1:KoAYHnAjP3/cTsRS+mmWZGkufSY2GACiKQ4V3ZLQnR0= -github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= -github.com/coredns/corefile-migration v1.0.34/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= -github.com/coreos/go-oidc v2.5.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= @@ -294,7 +143,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dgraph-io/badger/v4 v4.9.5 h1:zT46OMrF3ntqsfI3ynKp7hUkQrGlcK2CX5psQmH0iW0= @@ -317,7 +165,6 @@ github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= @@ -327,10 +174,8 @@ github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= -github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= @@ -375,7 +220,6 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY= github.com/go-git/go-git/v5 v5.19.2/go.mod h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk= -github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -437,22 +281,18 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/cadvisor/lib v0.60.5/go.mod h1:htHKT0OSYO6zaik+iLOo3Wj1mf/5l8uV5TJaqLz13oM= github.com/google/cel-go v0.29.2 h1:ZtDxkeiMmz0mxbKDYiNkE5Lk7V5edMRcaaDf2jX002k= github.com/google/cel-go v0.29.2/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= @@ -463,17 +303,11 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI= -github.com/google/go-github/v39 v39.0.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-github/v42 v42.0.0 h1:YNT0FwjPrEysRkLIiKuEfSvBPCGKphW5aS5PxwaoLec= github.com/google/go-github/v42 v42.0.0/go.mod h1:jgg/jvyI0YlDOM1/ps6XYh04HNQ3vKf0CVko62/EhRg= -github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= -github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -493,8 +327,6 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -503,25 +335,15 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE= -github.com/huandu/go-sqlbuilder v1.42.1/go.mod h1:BEm32AHl29lzKDeV3HAIkzrz9cgRyumkDohHeGYYBoM= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ishidawataru/sctp v0.0.0-20250521072954-ae8eb7fa7995/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= @@ -557,42 +379,32 @@ github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzH github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= -github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec/go.mod h1:TmwEoGCwIti7BCeJ9hescZgRtatxRE+A72pCoPfmcfk= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.14 h1:yUKzIgsCnosndOASY6/enly1EAuaXeFSQ7cdyA3OuYg= github.com/mattn/go-shellwords v1.0.14/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/maxcnunes/httpfake v1.2.4 h1:l7s/N7zuG6XpzG+5dUolg5SSoR3hANQxqzAkv+lREko= github.com/maxcnunes/httpfake v1.2.4/go.mod h1:rWVxb0bLKtOUM/5hN3UO1VEdEitz1hfcTXs7UyiK6r0= -github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA= -github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs/v3 v3.0.1/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/ipvs v1.1.0/go.mod h1:4VJMWuf098bsUMmZEiD4Tjk/O7mOn3l1PTD3s4OoYAs= github.com/moby/moby v28.5.2+incompatible h1:hIn6qcenb3JY1E3STwqEbBvJ8bha+u1LpqjX4CBvNCk= github.com/moby/moby v28.5.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= @@ -601,14 +413,12 @@ github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJ github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM= github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -619,30 +429,20 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= -github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc= github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/open-policy-agent/opa v1.20.1 h1:wSC3zjHbRyt7X3daV/DsjnhDywzB3l0m0gWhgX1W2vQ= github.com/open-policy-agent/opa v1.20.1/go.mod h1:pxxSP1noAirD8UJ7PgAjoRw39IE0Bk/JRFkUP3+51lU= -github.com/opencontainers/cgroups v0.0.7/go.mod h1:hPBRvnBhLZueEN0eJyozMeM3HeFGYlZW9KnO//px6G4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= @@ -668,8 +468,6 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= -github.com/proglottis/gpgme v0.1.4/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -680,41 +478,27 @@ github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+ github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/reeflective/readline v1.3.0/go.mod h1:bOpqx2/VqGlIoobyWR1Vgt/p5FiMfIHj4OicPuw6RfU= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= -github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/secure-systems-lab/go-securesystemslib v0.9.0/go.mod h1:DVHKMcZ+V4/woA/peqr+L0joiRXbPpQ042GgJckkFgw= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0= github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= -github.com/sigstore/fulcio v1.6.6/go.mod h1:BhQ22lwaebDgIxVBEYOOqLRcN5+xOV+C9bh/GUXRhOk= -github.com/sigstore/protobuf-specs v0.4.1/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= -github.com/sigstore/sigstore v1.9.5/go.mod h1:VtxgvGqCmEZN9X2zhFSOkfXxvKUjpy8RpUW39oCtoII= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -728,8 +512,6 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= -github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= @@ -743,7 +525,6 @@ github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWD github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/sylabs/sif/v2 v2.21.1/go.mod h1:YoqEGQnb5x/ItV653bawXHZJOXQaEWpGwHsSD3YePJI= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= @@ -756,8 +537,6 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= -github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= @@ -766,11 +545,8 @@ github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADT github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= -github.com/vbauerster/mpb/v8 v8.10.2/go.mod h1:+Ja4P92E3/CorSZgfDtK46D7AVbDqmBQRTmyTqPElo0= github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= -github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= -github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -783,47 +559,29 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeonx/timeago v1.0.0-rc5 h1:pwcQGpaH3eLfPtXeyPA4DmHWjoQt0Ea7/++FwpxqLxg= github.com/xeonx/timeago v1.0.0-rc5/go.mod h1:qDLrYEFynLO7y5Ho7w3GwgtYgpy5UfhcXIIQvMKVDkA= -github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= -github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24= gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= -go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= -go.etcd.io/etcd/api/v3 v3.7.0/go.mod h1:EcTihnwAQ0BQNh5dfAdaFVFdchuo7EP0HlX7TV3jz/A= -go.etcd.io/etcd/client/pkg/v3 v3.7.0/go.mod h1:cnzZGIUzSfjEwLC6UBVsSXlEK1eepS/JUD7wE6PLRT0= -go.etcd.io/etcd/client/v3 v3.7.0/go.mod h1:DJ382WuwjmbowjPDyaaQ0idWXy4dh91XRhe4FOrb9vM= -go.etcd.io/etcd/pkg/v3 v3.7.0/go.mod h1:fDQYyc8rOC1Yl4EZLh0O1OjHzwJW7jAly80+BpD1M9o= -go.etcd.io/etcd/server/v3 v3.7.0/go.mod h1:v7N1dPdSW2vzyxGsbDwK4X0sCe3SYNlQ+9eNIMGzdxQ= -go.etcd.io/raft/v3 v3.7.0/go.mod h1:6gX6T2X907DjnjsFLODnTxba77stjs84W9gTTI0GUNA= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/bridges/prometheus v0.69.0/go.mod h1:AAaS6xs5AyqMdR3Ir0nSWK+QudL2XM8Vbw5INzUxNc8= -go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s= -go.opentelemetry.io/contrib/instrumentation/github.com/emicklei/go-restful/otelrestful v0.69.0/go.mod h1:z5qVAACw5BI1yBkWfJRcU4qp+JDQ4fdFmdS4hTJVhH0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= @@ -836,7 +594,6 @@ go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6Tb go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= @@ -876,7 +633,6 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= @@ -891,19 +647,15 @@ golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= -golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= -golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.295.0 h1:SSqFeEVjnK5SKo6t7D0E0M7EfX8SP7K0+OJd2Ly5FzU= google.golang.org/api v0.295.0/go.mod h1:02qB8+Ox1ZFzcaKFMguy1nQLJmSIyvV6Ff4txJEXtl4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms= google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU= google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg= google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20260819154853-08b0e4226688/go.mod h1:832FQwEl9OKXy5rHqEY2U7uF7Bg+Hs7Zo72IIq+dYZ4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4= google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA= google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= @@ -917,11 +669,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= -gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -943,48 +692,28 @@ k8s.io/cli-runtime v0.37.0 h1:U3XakUeirBQJMz5688r04z74SIHSE7V5SIZ6Ho5JyBM= k8s.io/cli-runtime v0.37.0/go.mod h1:qiQMFkKwFFuPH6zy953On+nc3qfpEHAIDrJmAuRz5Vg= k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ= k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0= -k8s.io/cloud-provider v0.37.0/go.mod h1:q17nnGXsvnd9o24QY6Mtvs+pSitAMUKOWkx6lT7G9tY= -k8s.io/cluster-bootstrap v0.37.0/go.mod h1:nvYg1xT7nXPc5XTRmFYS/x617p5BXNviL8+d1mvMdMo= -k8s.io/code-generator v0.37.0/go.mod h1:qg7E/uDlyvevVRL1V8+h2z9UWmi/8gxaRka/lVXUBdk= k8s.io/component-base v0.37.0 h1:3SdSa4+itMdFTDFTeR8CxKGmSTSMXFlKL4ky8OqjguM= k8s.io/component-base v0.37.0/go.mod h1:LjOebp4R9y6LODWZQv102ZQxGheLcDO2ZJLAw6bbh4I= k8s.io/component-helpers v0.37.0 h1:tPz4goLftoiUBfYWZgPlP4Srkd/PciNz9q+0ReOfyxY= k8s.io/component-helpers v0.37.0/go.mod h1:wDAmi8hduKu3YGqKg5a0wxCpoql39DkTmdQ60bpLpkc= k8s.io/controller-manager v0.37.0 h1:0Wsb77PqGypDXtvSj423aKIq3JziipJFPU+qgUDaoRs= k8s.io/controller-manager v0.37.0/go.mod h1:x9OhyZqt8NfTN7CzeRHTgfpMzP43o/3PQwQgJhL5gsk= -k8s.io/cri-api v0.37.0/go.mod h1:6V8Gb6wznJYVPToj8CLxgbxsFeeGIswNZCE7cy+xT90= -k8s.io/cri-client v0.37.0/go.mod h1:x/nROOizpdhU/VGcokYhxtWioDPMhqinRRCHWOoVL2c= -k8s.io/csi-translation-lib v0.37.0/go.mod h1:1ug+iTy/0S+1VplKrTOslXamIH8+bVvmglmMT8VYq00= -k8s.io/dynamic-resource-allocation v0.37.0/go.mod h1:Nkono0X3H5tNnEf3IOK0Ml+I7HB4o0/UXaiiNCgB2Vw= -k8s.io/endpointslice v0.37.0/go.mod h1:PbotyEtd2DIpPHPp9ZgJsJHCBJuAEh2Oz21V5MyAYX4= -k8s.io/externaljwt v0.37.0/go.mod h1:MfUIWFM2xxGdIMdh3aOsMAZj8sFgHbngEd+ajFzD1qk= -k8s.io/gengo/v2 v2.0.0-20260408192533-25e2208e0dc3/go.mod h1:yvyl3l9E+UxlqOMUULdKTAYB0rEhsmjr7+2Vb/1pCSo= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kms v0.37.0/go.mod h1:0U6zfwLkfJr9O/jLgz2GARnKLh2uIhkq2EeaU66J5ow= -k8s.io/kube-aggregator v0.37.0/go.mod h1:Uy1F5FUItTfFfsx1T+cP+7uqKvV3J8/jjBxVzc/ICYU= -k8s.io/kube-controller-manager v0.37.0/go.mod h1:Nd/BuJWTyLB5BTm1oOZuH4qrD+uZ8rgmSaEgBM166k8= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A= k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I= -k8s.io/kube-proxy v0.37.0/go.mod h1:XKt9orRIUAjuZVddE7OAs1tpK8pHypiUGtVBwVR6cPQ= -k8s.io/kube-scheduler v0.37.0/go.mod h1:CPdW3moznhYQ68iyzhblQuVX+VDB6PX2ch9SJ7v1gBI= k8s.io/kubectl v0.37.0 h1:cici6hiofx93ASldmprDmZF55SfhVt4o3HniltVLjTc= k8s.io/kubectl v0.37.0/go.mod h1:RSeEl8e/yqDx6srG8Azr0uAtVPNIZljA0PNh9HCBcdg= k8s.io/kubelet v0.37.0 h1:VhaZanjlE5CkoAPAjKw0DH+Q0BYXVfYekZgzZDAJMMg= k8s.io/kubelet v0.37.0/go.mod h1:PHXfQuVqsTzFVqOeP67UUNv5Ajri+dLGoeaEnW6gGjE= k8s.io/kubernetes v1.37.0 h1:TDSD+Izz9inqs7SmtJCAu7L5/DuYW9hAZ3GBdF6Ddkw= k8s.io/kubernetes v1.37.0/go.mod h1:zAK7e3i5feyBL/bhucQnA5VRKuyrS8F+ijHJkZzQaWw= -k8s.io/metrics v0.37.0/go.mod h1:E14Jt50A9sFS2m3gLRdTA0ya7IliI0Sx3NxS5VKoCvM= -k8s.io/mount-utils v0.37.0/go.mod h1:uGJRC7u42QGYbHlHxt/5J2rXrfgv3XNYU3I0ZXVnAck= k8s.io/pod-security-admission v0.37.0 h1:5lx9eMh47oWJy2EQDHJ82yoYj6/KiTomzzcpDS1be14= k8s.io/pod-security-admission v0.37.0/go.mod h1:TaR1x79zQ3WBo2Avt49YiqJ55xwfak6KCdgVW30SHUc= -k8s.io/sample-apiserver v0.37.0/go.mod h1:1E7DVxhX3fKbVaaRzUxFcw1o3l6Gjo3wxsbTiKtD5sE= k8s.io/streaming v0.37.0 h1:iPBUZLZiKt5bV+lxJurASMOV07VuBhNpiwJt2//AWrM= k8s.io/streaming v0.37.0/go.mod h1:APlJR26ZWRcVy5bIEj0QRrKUXROtBHPcxl2NT7EAzPU= -k8s.io/system-validators v1.12.1/go.mod h1:awfSS706v9R12VC7u7K89FKfqVy44G+E0L1A0FX9Wmw= k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs= k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= -oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 h1:/YpDJ4vReG7ZmzSpBGxduXgywWkJU9zHubgJG03MT+Y= @@ -993,10 +722,8 @@ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5E sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kind v0.33.0 h1:AjvDv3vOygb/VKLVQW87lfktIBzkxR8Ump9DjxC8+Lk= sigs.k8s.io/kind v0.33.0/go.mod h1:FSqriGaoTPruiXWfRnUXNykF8r2t+fHtK0P0m1AbGF8= -sigs.k8s.io/knftables v0.0.22/go.mod h1:tig4GDnk1aG8sNsKW/iqid/PGgVLxJLEZkuUlO5C44Y= sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= -sigs.k8s.io/kustomize/kustomize/v5 v5.8.1/go.mod h1:0vFa5pQ/elNEQMyiAJuGku9rhAMzz7u9+61hRqFKiwY= sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= @@ -1005,4 +732,3 @@ sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3C sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= -tags.cncf.io/container-device-interface/specs-go v1.1.0/go.mod h1:u86hoFWqnh3hWz3esofRFKbI261bUlvUfLKGrDhJkgQ= From d1eab14c472862c4c893036adcc456b5fcb5bf63 Mon Sep 17 00:00:00 2001 From: Alex Kantor Date: Tue, 8 Sep 2026 21:57:14 +0100 Subject: [PATCH 7/7] refactor: make the credential policy a typed choice and pin the boundary A mutation audit of this branch killed 52 of 69 mutants. The survivors included the claim the branch exists to make: neither end of the credential boundary was pinned. OciSha256Anonymous could be rewired to pass a nil DockerAuthConfig, which re-enables credential discovery from auth files and helpers, and no test failed. The test asserted the fields of a helper that nothing had to call. The exported function cannot be driven end to end either, because it forces HTTPS and deliberately does not skip TLS verification, so a better test was not available. So the policy is now a typed choice rather than a caller-supplied SystemContext: ociSha256 takes a credentialSource, and credentialContext turns it into a context. noCredentials is the zero value, so a lookup that fails to state what it wants presents nothing rather than the host's credentials, and "hand it the wrong context" is no longer expressible. credentialContext has a table test covering all five combinations. The two-line delegation in each exported function is still not pinned; that needs a global seam or a trusted-cert registry. On the Azure side, anonymousFingerprint could be swapped for the credential-discovering OciSha256 because every test replaces it. Its production identity is now asserted by pointer. The ACR arm was never driven past credential construction, so a wrong domain reaching the registry client was invisible. It is now resolved end to end against a fake registry via a host-rewriting transport that records the host it was asked for, which also catches a hardcoded one. The pinned-digest cross-check was satisfied by comparing a single character, because the fixtures differed in every position; there is now a near-miss fixture and the check is exercised on both arms. fingerprintDockerService, the only production caller, had no coverage at all: inverting its digests-source condition and replacing the resolver with a constant both survived, and both now fail. Also: imageFingerprintSource and fingerprintPlan.source are deleted. Classification is a pure call on the domain, so storing the answer was derived state, and the fail-closed zero value added last round was guarding a path that cannot occur. Two comments that overstated what the code does are corrected. The manifest response body is closed. The app name is no longer repeated in wrapped errors. Test helpers parse srv.URL rather than trimming a prefix off it. Co-Authored-By: Claude Opus 5 --- internal/azure/azure_apps.go | 40 ++-- internal/azure/image_fingerprint_test.go | 230 ++++++++++++++++++----- internal/digest/digest.go | 129 ++++++++----- internal/digest/oci_anonymous_test.go | 111 +++++++++-- 4 files changed, 371 insertions(+), 139 deletions(-) diff --git a/internal/azure/azure_apps.go b/internal/azure/azure_apps.go index cce66924b..7658228bf 100644 --- a/internal/azure/azure_apps.go +++ b/internal/azure/azure_apps.go @@ -41,6 +41,9 @@ type AzureStaticCredentials struct { type AzureClient struct { Credentials AzureStaticCredentials AppServiceFactory *armappservice.ClientFactory + // acrClientOptions is nil in production. Tests set it so the ACR arm can be + // driven against a fake registry without package-level state. + acrClientOptions *azcontainerregistry.ClientOptions } // AppData represents the harvested Azure service app and function app data @@ -271,7 +274,7 @@ func (azureClient *AzureClient) fingerprintZipService(app *armappservice.Site, l destDir := filepath.Join(tmpDir, "extracted") err = unzip(packagePath, destDir, logger) if err != nil { - return AppData{}, fmt.Errorf("failed to unzip downloaded package for app [%s]: %v", *app.Name, err) + return AppData{}, fmt.Errorf("failed to unzip the downloaded package: %v", err) } // fingerprint the downloaded and unzipped package @@ -410,19 +413,6 @@ func (azureClient *AzureClient) fingerprintDockerService(app *armappservice.Site // the token audience per cloud, not the login-server suffix. var acrLoginServerSuffixes = []string{".azurecr.io", ".azurecr.cn", ".azurecr.us"} -// imageFingerprintSource is how an image reference is resolved to a fingerprint. -type imageFingerprintSource int - -const ( - // fingerprintFromAnonymousRegistry reads the fingerprint from a registry with - // no credential attached. It is the zero value deliberately: an unset or - // partially built plan must never select the credential-bearing arm. - fingerprintFromAnonymousRegistry imageFingerprintSource = iota - // fingerprintFromACR reads it from Azure Container Registry, authenticated - // with the Azure credential. - fingerprintFromACR -) - // isACRLoginServer reports whether domain is an Azure Container Registry login // server, matching on a whole label so that "azurecr.io.example.com" is not one. func isACRLoginServer(domain string) bool { @@ -442,7 +432,6 @@ func isACRLoginServer(domain string) bool { // before anything is contacted, so a test can assert every value that crosses // the boundary rather than only which resolver ran. type fingerprintPlan struct { - source imageFingerprintSource // domain is the registry the reference names, as the parser reports it. domain string // reference is the canonical form handed to a resolver. Classification and @@ -465,9 +454,9 @@ type fingerprintPlan struct { // registry component but resolves to attacker.example as a URL. The parser // rejects it. // -// Only fingerprintFromACR attaches the Azure credential, so the classification -// here is what keeps that credential away from a registry named in an app's own -// configuration. +// Only an Azure Container Registry login server gets the Azure credential, so +// the domain this reports is what keeps that credential away from a registry +// named in an app's own configuration. func planImageFingerprint(imageName string) (fingerprintPlan, error) { named, err := reference.ParseNormalizedNamed(imageName) if err != nil { @@ -503,10 +492,6 @@ func planImageFingerprint(imageName string) (fingerprintPlan, error) { plan.domain = reference.Domain(named) plan.repoPath = reference.Path(named) plan.reference = named.String() - plan.source = fingerprintFromAnonymousRegistry - if isACRLoginServer(plan.domain) { - plan.source = fingerprintFromACR - } return plan, nil } @@ -522,8 +507,8 @@ func (azureClient *AzureClient) GetImageFingerprint(imageName string, logger *lo } var fingerprint string - if plan.source == fingerprintFromACR { - fingerprint, err = azureClient.acrImageFingerprint(plan, nil, logger) + if isACRLoginServer(plan.domain) { + fingerprint, err = azureClient.acrImageFingerprint(plan, azureClient.acrClientOptions, logger) } else { fingerprint, err = anonymousImageFingerprint(plan, logger) } @@ -562,6 +547,13 @@ func (azureClient *AzureClient) acrImageFingerprint(plan fingerprintPlan, client if err != nil { return "", err } + if manifestRes.ManifestData != nil { + defer func() { + if err := manifestRes.ManifestData.Close(); err != nil { + logger.Warn("failed to close the manifest response for image %s: %v", plan.reference, err) + } + }() + } if manifestRes.DockerContentDigest == nil { return "", fmt.Errorf("no digest returned for image [%s]", plan.reference) } diff --git a/internal/azure/image_fingerprint_test.go b/internal/azure/image_fingerprint_test.go index 807437a28..0ba525733 100644 --- a/internal/azure/image_fingerprint_test.go +++ b/internal/azure/image_fingerprint_test.go @@ -4,12 +4,16 @@ import ( "errors" "net/http" "net/http/httptest" + "net/url" + "reflect" "strings" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + armappservice "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appservice/armappservice/v2" + "github.com/kosli-dev/cli/internal/digest" "github.com/kosli-dev/cli/internal/logger" "github.com/stretchr/testify/require" ) @@ -27,7 +31,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "acr image with a tag authenticates to acr", imageName: "myregistry.azurecr.io/myrepo/myapp:1.0", want: fingerprintPlan{ - source: fingerprintFromACR, domain: "myregistry.azurecr.io", + domain: "myregistry.azurecr.io", reference: "myregistry.azurecr.io/myrepo/myapp:1.0", repoPath: "myrepo/myapp", tagOrDigest: "1.0", }, @@ -38,7 +42,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "acr image pinned to a digest keeps the sha256 prefix", imageName: "myregistry.azurecr.io/myapp@sha256:" + sha, want: fingerprintPlan{ - source: fingerprintFromACR, domain: "myregistry.azurecr.io", + domain: "myregistry.azurecr.io", reference: "myregistry.azurecr.io/myapp@sha256:" + sha, repoPath: "myapp", tagOrDigest: "sha256:" + sha, pinnedFingerprint: sha, @@ -50,7 +54,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "tag and digest together drops the tag", imageName: "ghcr.io/owner/app:v1@sha256:" + sha, want: fingerprintPlan{ - source: fingerprintFromAnonymousRegistry, domain: "ghcr.io", + domain: "ghcr.io", reference: "ghcr.io/owner/app@sha256:" + sha, repoPath: "owner/app", tagOrDigest: "sha256:" + sha, pinnedFingerprint: sha, @@ -60,7 +64,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "image without a tag defaults to latest", imageName: "myregistry.azurecr.io/myapp", want: fingerprintPlan{ - source: fingerprintFromACR, domain: "myregistry.azurecr.io", + domain: "myregistry.azurecr.io", reference: "myregistry.azurecr.io/myapp:latest", repoPath: "myapp", tagOrDigest: "latest", }, @@ -69,7 +73,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "acr host with a port authenticates to acr", imageName: "myregistry.azurecr.io:443/myapp:v1", want: fingerprintPlan{ - source: fingerprintFromACR, domain: "myregistry.azurecr.io:443", + domain: "myregistry.azurecr.io:443", reference: "myregistry.azurecr.io:443/myapp:v1", repoPath: "myapp", tagOrDigest: "v1", }, @@ -78,7 +82,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "third party registry resolves anonymously", imageName: "ghcr.io/owner/app:v2", want: fingerprintPlan{ - source: fingerprintFromAnonymousRegistry, domain: "ghcr.io", + domain: "ghcr.io", reference: "ghcr.io/owner/app:v2", repoPath: "owner/app", tagOrDigest: "v2", }, }, @@ -86,7 +90,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "attacker controlled host resolves anonymously", imageName: "attacker.example/repo:latest", want: fingerprintPlan{ - source: fingerprintFromAnonymousRegistry, domain: "attacker.example", + domain: "attacker.example", reference: "attacker.example/repo:latest", repoPath: "repo", tagOrDigest: "latest", }, }, @@ -94,7 +98,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "acr lookalike host resolves anonymously", imageName: "azurecr.io.attacker.example/repo:latest", want: fingerprintPlan{ - source: fingerprintFromAnonymousRegistry, domain: "azurecr.io.attacker.example", + domain: "azurecr.io.attacker.example", reference: "azurecr.io.attacker.example/repo:latest", repoPath: "repo", tagOrDigest: "latest", }, }, @@ -102,7 +106,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "docker hub short form is normalised", imageName: "nginx:latest", want: fingerprintPlan{ - source: fingerprintFromAnonymousRegistry, domain: "docker.io", + domain: "docker.io", reference: "docker.io/library/nginx:latest", repoPath: "library/nginx", tagOrDigest: "latest", }, }, @@ -110,7 +114,7 @@ func TestPlanImageFingerprint(t *testing.T) { name: "docker hub user image is normalised", imageName: "myuser/myimage:tag", want: fingerprintPlan{ - source: fingerprintFromAnonymousRegistry, domain: "docker.io", + domain: "docker.io", reference: "docker.io/myuser/myimage:tag", repoPath: "myuser/myimage", tagOrDigest: "tag", }, }, @@ -282,6 +286,12 @@ func TestGetImageFingerprintHoldsTheRegistryToAPinnedDigest(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "is pinned to digest sha256:"+pinned) + // A near miss, so a comparison of only part of the digest cannot pass. + stubAnonymousFingerprint(t, strings.Repeat("a", 63)+"b", nil) + _, err = client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) + require.Error(t, err) + require.Contains(t, err.Error(), "is pinned to digest sha256:"+pinned) + stubAnonymousFingerprint(t, pinned, nil) fingerprint, err := client.GetImageFingerprint("ghcr.io/owner/app@sha256:"+pinned, logger.NewStandardLogger()) require.NoError(t, err) @@ -299,41 +309,136 @@ func TestAnonymousImageFingerprintWrapsTheUnderlyingError(t *testing.T) { require.Contains(t, err.Error(), "--digests-source logs") } -// fakeACR answers the manifest request with a chosen Docker-Content-Digest, or -// omits the header entirely when contentDigest is empty. -func fakeACR(t *testing.T, contentDigest string) (fingerprintPlan, *azcontainerregistry.ClientOptions) { +// hostRewritingTransport sends every request to addr regardless of the host in +// the URL, so a reference naming a real ACR login server can be resolved against +// a fake registry. +// It records the host it was asked for first, so a test can still assert which +// registry the client was pointed at even though the request is redirected. +type hostRewritingTransport struct { + addr string + inner http.RoundTripper + hostsAsked *[]string +} + +func (t hostRewritingTransport) RoundTrip(req *http.Request) (*http.Response, error) { + *t.hostsAsked = append(*t.hostsAsked, req.URL.Host) + rewritten := req.Clone(req.Context()) + rewritten.URL.Host = t.addr + return t.inner.RoundTrip(rewritten) +} + +// fakeACR stands up a registry that answers the manifest request with the given +// digest header and status, and records the paths it was asked for. +func fakeACR(t *testing.T, contentDigest string, status int) (*azcontainerregistry.ClientOptions, *[]string, *[]string) { t.Helper() + var paths []string + var hostsAsked []string srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // EscapedPath is what actually went on the wire; URL.Path is decoded. + paths = append(paths, r.URL.EscapedPath()) if contentDigest != "" { w.Header().Set("Docker-Content-Digest", contentDigest) } w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") - w.WriteHeader(http.StatusOK) + w.WriteHeader(status) _, _ = w.Write([]byte(`{"schemaVersion":2}`)) })) t.Cleanup(srv.Close) - plan := fingerprintPlan{ - source: fingerprintFromACR, domain: strings.TrimPrefix(srv.URL, "https://"), - reference: "fake/app:v1", repoPath: "app", tagOrDigest: "v1", - } + parsed, err := url.Parse(srv.URL) + require.NoError(t, err) options := &azcontainerregistry.ClientOptions{ - ClientOptions: azcore.ClientOptions{Transport: srv.Client()}, + ClientOptions: azcore.ClientOptions{ + Transport: &http.Client{Transport: hostRewritingTransport{ + addr: parsed.Host, inner: srv.Client().Transport, hostsAsked: &hostsAsked, + }}, + }, } - return plan, options + return options, &paths, &hostsAsked } -// TestACRImageFingerprintRejectsUnusableDigests covers the ACR arm's own error -// branches, which have no coverage otherwise because the client talks to a -// registry. The digest rule itself lives in internal/digest; this asserts the -// arm is actually wired to it. -func TestACRImageFingerprintRejectsUnusableDigests(t *testing.T) { - client := &AzureClient{Credentials: AzureStaticCredentials{ - TenantId: "00000000-0000-0000-0000-000000000000", - ClientId: "00000000-0000-0000-0000-000000000000", - ClientSecret: "not-a-real-secret", - }} +func acrTestClient(t *testing.T, options *azcontainerregistry.ClientOptions) *AzureClient { + t.Helper() + return &AzureClient{ + Credentials: AzureStaticCredentials{ + TenantId: "00000000-0000-0000-0000-000000000000", + ClientId: "00000000-0000-0000-0000-000000000000", + ClientSecret: "not-a-real-secret", + }, + acrClientOptions: options, + } +} + +// TestGetImageFingerprintDrivesTheACRArmEndToEnd resolves an ACR reference all +// the way through GetImageFingerprint, which is what proves the parsed domain, +// repo path and tag actually reach the registry client. Without it, replacing +// plan.domain inside the arm with a hand-rolled split of the image name goes +// unnoticed. +func TestGetImageFingerprintDrivesTheACRArmEndToEnd(t *testing.T) { + want := strings.Repeat("a", 64) + options, paths, hostsAsked := fakeACR(t, "sha256:"+want, http.StatusOK) + client := acrTestClient(t, options) + fingerprint, err := client.GetImageFingerprint("myregistry.azurecr.io/team/app:v1", logger.NewStandardLogger()) + + require.NoError(t, err) + require.Equal(t, want, fingerprint) + require.Contains(t, *paths, "/v2/team%2Fapp/manifests/v1", + "the parsed repo path and tag must reach the registry, in that order") + require.Contains(t, *hostsAsked, "myregistry.azurecr.io", + "the client must be pointed at the domain the parser reported") +} + +// TestGetImageFingerprintACRArmRequestsThePinnedDigest is the same for a pinned +// reference: the digest must reach the registry with its algorithm prefix. +func TestGetImageFingerprintACRArmRequestsThePinnedDigest(t *testing.T) { + pinned := strings.Repeat("a", 64) + options, paths, hostsAsked := fakeACR(t, "sha256:"+pinned, http.StatusOK) + client := acrTestClient(t, options) + + fingerprint, err := client.GetImageFingerprint("myregistry.azurecr.io/app@sha256:"+pinned, logger.NewStandardLogger()) + + require.NoError(t, err) + require.Equal(t, pinned, fingerprint) + require.Contains(t, *paths, "/v2/app/manifests/sha256:"+pinned, + "a bare hex digest would be read as a tag by the registry") + require.Contains(t, *hostsAsked, "myregistry.azurecr.io", + "the client must be pointed at the domain the parser reported") +} + +// TestGetImageFingerprintACRArmHoldsTheRegistryToAPinnedDigest exercises the +// cross-check on the credential-bearing arm, and with a digest differing in one +// character so a partial comparison cannot pass. +func TestGetImageFingerprintACRArmHoldsTheRegistryToAPinnedDigest(t *testing.T) { + pinned := strings.Repeat("a", 64) + nearMiss := strings.Repeat("a", 63) + "b" + options, _, _ := fakeACR(t, "sha256:"+nearMiss, http.StatusOK) + client := acrTestClient(t, options) + + _, err := client.GetImageFingerprint("myregistry.azurecr.io/app@sha256:"+pinned, logger.NewStandardLogger()) + + require.Error(t, err) + require.Contains(t, err.Error(), "is pinned to digest sha256:"+pinned) + require.Contains(t, err.Error(), "reported sha256:"+nearMiss) +} + +// TestGetImageFingerprintACRArmReportsRegistryErrors keeps the registry's own +// failure rather than degrading to the missing-header message. +func TestGetImageFingerprintACRArmReportsRegistryErrors(t *testing.T) { + options, _, _ := fakeACR(t, "", http.StatusNotFound) + client := acrTestClient(t, options) + + _, err := client.GetImageFingerprint("myregistry.azurecr.io/app:v1", logger.NewStandardLogger()) + + require.Error(t, err) + require.NotContains(t, err.Error(), "no digest returned", + "a 404 must surface as the registry error, not as a missing digest header") +} + +// TestGetImageFingerprintACRArmRejectsUnusableDigests covers the ACR arm's own +// error branches through the full path, which need a registry and so have no +// coverage otherwise. +func TestGetImageFingerprintACRArmRejectsUnusableDigests(t *testing.T) { for _, tc := range []struct { name string contentDigest string @@ -345,9 +450,10 @@ func TestACRImageFingerprintRejectsUnusableDigests(t *testing.T) { {name: "missing digest header", contentDigest: "", wantErrText: "no digest returned"}, } { t.Run(tc.name, func(t *testing.T) { - plan, options := fakeACR(t, tc.contentDigest) + options, _, _ := fakeACR(t, tc.contentDigest, http.StatusOK) + client := acrTestClient(t, options) - fingerprint, err := client.acrImageFingerprint(plan, options, logger.NewStandardLogger()) + fingerprint, err := client.GetImageFingerprint("myregistry.azurecr.io/app:v1", logger.NewStandardLogger()) require.Error(t, err) require.Empty(t, fingerprint) @@ -356,25 +462,55 @@ func TestACRImageFingerprintRejectsUnusableDigests(t *testing.T) { } } -func TestACRImageFingerprintReturnsTheSha256Hex(t *testing.T) { - client := &AzureClient{Credentials: AzureStaticCredentials{ - TenantId: "00000000-0000-0000-0000-000000000000", - ClientId: "00000000-0000-0000-0000-000000000000", - ClientSecret: "not-a-real-secret", - }} +// TestAnonymousFingerprintIsTheCredentialFreeResolver pins what the variable +// points at in production. Every other test replaces it, so swapping it for the +// credential-discovering OciSha256 would otherwise go unnoticed. +func TestAnonymousFingerprintIsTheCredentialFreeResolver(t *testing.T) { + require.Equal(t, + reflect.ValueOf(digest.OciSha256Anonymous).Pointer(), + reflect.ValueOf(anonymousFingerprint).Pointer(), + "anonymousFingerprint must be digest.OciSha256Anonymous, not a resolver that discovers host credentials") +} + +// TestFingerprintDockerServiceUsesTheACRSource covers the only production caller +// of GetImageFingerprint. Without it, inverting the digests-source condition, or +// replacing the resolver call with a constant, goes unnoticed. +func TestFingerprintDockerServiceUsesTheACRSource(t *testing.T) { want := strings.Repeat("a", 64) - plan, options := fakeACR(t, "sha256:"+want) + options, paths, _ := fakeACR(t, "sha256:"+want, http.StatusOK) + client := acrTestClient(t, options) + client.Credentials.DigestsSource = "acr" - fingerprint, err := client.acrImageFingerprint(plan, options, logger.NewStandardLogger()) + appName, appKind := "payments-api", "app" + imageName := "myregistry.azurecr.io/team/app:v1" + + appData, err := client.fingerprintDockerService( + &armappservice.Site{Name: &appName, Kind: &appKind}, logger.NewStandardLogger(), imageName) require.NoError(t, err) - require.Equal(t, want, fingerprint) + require.Equal(t, AppData{ + AppName: appName, + AppKind: appKind, + DigestsSource: "acr", + Digests: map[string]string{imageName: want}, + StartedAt: 0, + }, appData) + require.NotEmpty(t, *paths, "the acr source must actually contact the registry") } -// TestZeroValueSourceIsAnonymous is a fail-closed guarantee: the field that -// decides whether the Azure credential is sent must not default to sending it. -func TestZeroValueSourceIsAnonymous(t *testing.T) { - var unset fingerprintPlan - require.Equal(t, fingerprintFromAnonymousRegistry, unset.source, - "an unset plan must not select the credential-bearing arm") +// TestFingerprintDockerServicePropagatesResolverErrors keeps the resolver's error +// rather than reporting an app with no fingerprint. +func TestFingerprintDockerServicePropagatesResolverErrors(t *testing.T) { + options, _, _ := fakeACR(t, "sha512:"+strings.Repeat("c", 128), http.StatusOK) + client := acrTestClient(t, options) + client.Credentials.DigestsSource = "acr" + + appName, appKind := "payments-api", "app" + + _, err := client.fingerprintDockerService( + &armappservice.Site{Name: &appName, Kind: &appKind}, logger.NewStandardLogger(), + "myregistry.azurecr.io/team/app:v1") + + require.Error(t, err) + require.Contains(t, err.Error(), "algorithm is sha512") } diff --git a/internal/digest/digest.go b/internal/digest/digest.go index eda19674e..e0e2a1932 100644 --- a/internal/digest/digest.go +++ b/internal/digest/digest.go @@ -83,69 +83,62 @@ func DirSha256(dirPath string, excludePaths []string, logger *logger.Logger) (st return FileSha256(digestsFile.Name(), logger) } -// OciSha256 gets the digest of a docker/OCI image from its registry -func OciSha256(artifactName string, registryUsername string, registryPassword string) (string, error) { - sysCtx := &types.SystemContext{} - // Only set explicit credentials when provided. When DockerAuthConfig is nil, - // the containers/image library falls back to credential discovery from auth - // files (~/.docker/config.json, ~/.config/containers/auth.json) and credential - // helpers (e.g. docker-credential-ecr-login), which is needed when Docker is +// credentialSource says which credentials a registry lookup may present. +// +// It is a typed choice rather than a caller-supplied SystemContext so that a +// lookup cannot be handed the wrong credential policy by mistake: containers/image +// falls back to credential discovery whenever DockerAuthConfig is nil, and that +// fallback must not reach a registry named by an untrusted source. +type credentialSource int + +const ( + // noCredentials presents nothing. It is the zero value deliberately, so a + // lookup that forgets to say what it wants does not present host credentials. + noCredentials credentialSource = iota + // callerOrHostCredentials presents the caller's credentials when it supplied + // any, and otherwise lets containers/image discover them from auth files + // (~/.docker/config.json, ~/.config/containers/auth.json) and credential + // helpers such as docker-credential-ecr-login, which is needed when Docker is // not installed or when using Podman with a private registry like ECR. - if registryUsername != "" || registryPassword != "" { - sysCtx.DockerAuthConfig = &types.DockerAuthConfig{ - Username: registryUsername, - Password: registryPassword, - } - } - return ociSha256(artifactName, sysCtx) -} - -// OciSha256Anonymous gets the digest of a docker/OCI image from its registry -// without presenting any credential. A non-nil but empty DockerAuthConfig is -// what stops containers/image falling back to credential discovery, so no -// credential the host happens to hold is presented to the registry. -func OciSha256Anonymous(artifactName string) (string, error) { - return ociSha256(artifactName, anonymousSystemContext()) -} + callerOrHostCredentials +) -// Sha256FingerprintFromDigest turns a registry-supplied digest string into the -// hex fingerprint Kosli uses, rejecting any algorithm other than sha256. A -// registry chooses the algorithm it answers with, so this is the single place -// that rule is applied. -func Sha256FingerprintFromDigest(digestString string) (string, error) { - parsed, err := godigest.Parse(digestString) - if err != nil { - return "", fmt.Errorf("unparseable digest %q: %w", digestString, err) +// credentialContext builds the containers/image context for a credential source. +func credentialContext(source credentialSource, registryUsername, registryPassword string) *types.SystemContext { + sysCtx := &types.SystemContext{} + switch source { + case noCredentials: + // A non-nil but empty config is what stops the discovery fallback. A nil + // one silently enables it. + sysCtx.DockerAuthConfig = &types.DockerAuthConfig{} + case callerOrHostCredentials: + if registryUsername != "" || registryPassword != "" { + sysCtx.DockerAuthConfig = &types.DockerAuthConfig{ + Username: registryUsername, + Password: registryPassword, + } + } } - return Sha256Fingerprint(parsed) + return sysCtx } -// Sha256Fingerprint is the same rule for a digest that is already parsed, so a -// typed value does not have to be turned back into a string to be checked. -func Sha256Fingerprint(parsed godigest.Digest) (string, error) { - // godigest.Digest is a string type, so a caller can hand over an unvalidated - // one and Algorithm()/Encoded() would just split it on the colon. - if err := parsed.Validate(); err != nil { - return "", fmt.Errorf("invalid digest %q: %w", parsed.String(), err) - } - if parsed.Algorithm() != godigest.SHA256 { - return "", fmt.Errorf("digest algorithm is %s, but Kosli fingerprints are sha256", parsed.Algorithm()) - } - // godigest.Parse has already validated the charset and length, so the - // encoded portion is exactly 64 lowercase hex characters here. - return parsed.Encoded(), nil +// OciSha256 gets the digest of a docker/OCI image from its registry, presenting +// the given credentials, or those the host holds when none are given. +func OciSha256(artifactName string, registryUsername string, registryPassword string) (string, error) { + return ociSha256(artifactName, callerOrHostCredentials, registryUsername, registryPassword) } -// anonymousSystemContext presents no credential. The empty DockerAuthConfig is -// deliberately non-nil: a nil one makes containers/image fall back to credential -// discovery from auth files and credential helpers. -func anonymousSystemContext() *types.SystemContext { - return &types.SystemContext{DockerAuthConfig: &types.DockerAuthConfig{}} +// OciSha256Anonymous gets the digest of a docker/OCI image from its registry +// without presenting any credential, so no credential the host happens to hold +// is offered to a registry the caller does not control. +func OciSha256Anonymous(artifactName string) (string, error) { + return ociSha256(artifactName, noCredentials, "", "") } -func ociSha256(artifactName string, sysCtx *types.SystemContext) (string, error) { +func ociSha256(artifactName string, source credentialSource, registryUsername, registryPassword string) (string, error) { imageName := fmt.Sprintf("//%s", artifactName) ctx := context.Background() + sysCtx := credentialContext(source, registryUsername, registryPassword) // Parse image reference ref, err := docker.ParseReference(imageName) @@ -161,6 +154,7 @@ func ociSha256(artifactName string, sysCtx *types.SystemContext) (string, error) if err != nil { return "", fmt.Errorf("failed to get digest for %s: %w", imageName, err) } + fingerprint, err := Sha256Fingerprint(remoteDigest) if err != nil { return "", fmt.Errorf("registry reported a digest Kosli cannot use for %s: %w", imageName, err) @@ -168,6 +162,37 @@ func ociSha256(artifactName string, sysCtx *types.SystemContext) (string, error) return fingerprint, nil } +// Sha256FingerprintFromDigest turns a registry-supplied digest string into the +// hex fingerprint Kosli uses, rejecting any algorithm other than sha256, because +// a registry chooses the algorithm it answers with. +// +// This is the rule for the OCI and Azure Container Registry lookups. The older +// DockerImageSha256 and RemoteDockerImageSha256 paths still parse digests by +// hand and are not covered by it. +func Sha256FingerprintFromDigest(digestString string) (string, error) { + parsed, err := godigest.Parse(digestString) + if err != nil { + return "", fmt.Errorf("unparseable digest %q: %w", digestString, err) + } + return Sha256Fingerprint(parsed) +} + +// Sha256Fingerprint is the same rule for a digest that is already parsed, so a +// typed value does not have to be turned back into a string to be checked. +func Sha256Fingerprint(parsed godigest.Digest) (string, error) { + // godigest.Digest is a string type, so a caller can hand over an unvalidated + // one and Algorithm()/Encoded() would just split it on the colon. + if err := parsed.Validate(); err != nil { + return "", fmt.Errorf("invalid digest %q: %w", parsed.String(), err) + } + if parsed.Algorithm() != godigest.SHA256 { + return "", fmt.Errorf("digest algorithm is %s, but Kosli fingerprints are sha256", parsed.Algorithm()) + } + // Validate above has checked the charset and length, so the encoded portion + // is exactly 64 lowercase hex characters here. + return parsed.Encoded(), nil +} + // calculateDirContentSha256 calculates a sha256 digest for a directory content func calculateDirContentSha256(digestsFile *os.File, dirPath, tmpDir string, excludePaths []string, logger *logger.Logger) error { pathsToExclude := []string{} diff --git a/internal/digest/oci_anonymous_test.go b/internal/digest/oci_anonymous_test.go index 7c36e0a63..38f1e05f1 100644 --- a/internal/digest/oci_anonymous_test.go +++ b/internal/digest/oci_anonymous_test.go @@ -1,11 +1,16 @@ package digest import ( + "context" + "fmt" "net/http" "net/http/httptest" + "net/url" "strings" "testing" + "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/types" godigest "github.com/opencontainers/go-digest" "github.com/stretchr/testify/require" @@ -24,14 +29,27 @@ func fakeRegistry(t *testing.T, contentDigest string) string { w.WriteHeader(http.StatusOK) })) t.Cleanup(srv.Close) - return strings.TrimPrefix(srv.URL, "https://") + parsed, err := url.Parse(srv.URL) + require.NoError(t, err) + return parsed.Host } -func insecureAnonymousContext() *types.SystemContext { - return &types.SystemContext{ - DockerAuthConfig: &types.DockerAuthConfig{}, - DockerInsecureSkipTLSVerify: types.OptionalBoolTrue, +// insecureAnonymousLookup mirrors OciSha256Anonymous against a fake TLS +// registry. Production does not skip TLS verification, so the flag is set here +// rather than in credentialContext. +func insecureAnonymousLookup(artifactName string) (string, error) { + sysCtx := credentialContext(noCredentials, "", "") + sysCtx.DockerInsecureSkipTLSVerify = types.OptionalBoolTrue + + ref, err := docker.ParseReference("//" + artifactName) + if err != nil { + return "", err + } + remoteDigest, err := docker.GetDigest(context.Background(), sysCtx, ref) + if err != nil { + return "", fmt.Errorf("failed to get digest: %w", err) } + return Sha256Fingerprint(remoteDigest) } // TestOciSha256RejectsNonSha256RegistryDigest covers a registry answering with @@ -48,7 +66,7 @@ func TestOciSha256RejectsNonSha256RegistryDigest(t *testing.T) { t.Run(tc.algorithm, func(t *testing.T) { host := fakeRegistry(t, tc.contentDigest) - fingerprint, err := ociSha256(host+"/repo:tag", insecureAnonymousContext()) + fingerprint, err := insecureAnonymousLookup(host + "/repo:tag") require.Error(t, err) require.Empty(t, fingerprint) @@ -62,21 +80,82 @@ func TestOciSha256ReturnsTheSha256Fingerprint(t *testing.T) { want := strings.Repeat("a", 64) host := fakeRegistry(t, "sha256:"+want) - fingerprint, err := ociSha256(host+"/repo:tag", insecureAnonymousContext()) + fingerprint, err := insecureAnonymousLookup(host + "/repo:tag") require.NoError(t, err) require.Equal(t, want, fingerprint) } -// TestOciSha256AnonymousPresentsNoStoredCredential guards the credential -// boundary: a nil DockerAuthConfig makes containers/image fall back to -// credential discovery, so the anonymous helper must set a non-nil empty one. -func TestOciSha256AnonymousPresentsNoStoredCredential(t *testing.T) { - sysCtx := anonymousSystemContext() - require.NotNil(t, sysCtx.DockerAuthConfig, "a nil DockerAuthConfig falls back to credential discovery") - require.Empty(t, sysCtx.DockerAuthConfig.Username) - require.Empty(t, sysCtx.DockerAuthConfig.Password) - require.Empty(t, sysCtx.DockerAuthConfig.IdentityToken) +// TestCredentialContext pins the credential decision itself, which is the +// load-bearing property of the anonymous lookup: containers/image falls back to +// credential discovery from auth files and helpers whenever DockerAuthConfig is +// nil, so the anonymous source must produce a non-nil empty one. +func TestCredentialContext(t *testing.T) { + for _, tc := range []struct { + name string + source credentialSource + username string + password string + wantAuthConfig bool + wantUsername string + wantPassword string + }{ + { + name: "no credentials presents an empty config, not discovery", + source: noCredentials, + wantAuthConfig: true, + }, + { + name: "no credentials ignores any credentials passed alongside it", + source: noCredentials, + username: "user", + password: "pass", + wantAuthConfig: true, + }, + { + name: "caller credentials are presented", + source: callerOrHostCredentials, + username: "user", + password: "pass", + wantAuthConfig: true, + wantUsername: "user", + wantPassword: "pass", + }, + { + name: "caller credentials, password only", + source: callerOrHostCredentials, + password: "pass", + wantAuthConfig: true, + wantPassword: "pass", + }, + { + // A nil config is what enables discovery, and this path wants it. + name: "no caller credentials leaves discovery enabled", + source: callerOrHostCredentials, + wantAuthConfig: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + sysCtx := credentialContext(tc.source, tc.username, tc.password) + + if !tc.wantAuthConfig { + require.Nil(t, sysCtx.DockerAuthConfig, "a nil config enables credential discovery") + return + } + require.NotNil(t, sysCtx.DockerAuthConfig, "a nil config would enable credential discovery") + require.Equal(t, tc.wantUsername, sysCtx.DockerAuthConfig.Username) + require.Equal(t, tc.wantPassword, sysCtx.DockerAuthConfig.Password) + require.Empty(t, sysCtx.DockerAuthConfig.IdentityToken) + }) + } +} + +// TestNoCredentialsIsTheZeroValue means a lookup that fails to state its +// credential source presents nothing rather than the host's credentials. +func TestNoCredentialsIsTheZeroValue(t *testing.T) { + var unset credentialSource + require.Equal(t, noCredentials, unset) + require.NotNil(t, credentialContext(unset, "", "").DockerAuthConfig) } func TestSha256FingerprintFromDigest(t *testing.T) {