diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml index 50d6f96e62acd..a1015cd79aa17 100644 --- a/.github/actions/setup-go/action.yaml +++ b/.github/actions/setup-go/action.yaml @@ -4,7 +4,7 @@ description: | inputs: version: description: "The Go version to use." - default: "1.25.9" + default: "1.25.10" use-cache: description: "Whether to use the cache." default: "true" diff --git a/coderd/azureidentity/azureidentity.go b/coderd/azureidentity/azureidentity.go index e4da9e54fc27c..820d856aa42b6 100644 --- a/coderd/azureidentity/azureidentity.go +++ b/coderd/azureidentity/azureidentity.go @@ -6,14 +6,15 @@ import ( "encoding/base64" "encoding/json" "encoding/pem" - "errors" "io" + "net" "net/http" + "net/url" "regexp" "sync" "time" - "go.mozilla.org/pkcs7" + "github.com/smallstep/pkcs7" "golang.org/x/xerrors" ) @@ -25,17 +26,188 @@ var allowedSigners = regexp.MustCompile(`^(.*\.)?metadata\.(azure\.(com|us|cn)|m // each time a parse occurs. var pkcs7Mutex sync.Mutex +// allowedCertHosts contains the hosts Azure intermediate +// certificates are served from. Only these hosts are permitted +// when fetching issuing certificates referenced in the signer +// certificate. This prevents SSRF via crafted +// IssuingCertificateURL values. +// +// Source: https://learn.microsoft.com/en-us/azure/security/fundamentals/azure-ca-details +var allowedCertHosts = map[string]bool{ + "www.microsoft.com": true, + "cacerts.digicert.com": true, +} + +// maxCertResponseBytes is the maximum size of a certificate +// response body we will read. Azure intermediate certificates +// are typically under 4 KiB; 1 MiB is a generous upper bound +// that prevents memory exhaustion from malicious responses. +const maxCertResponseBytes = 1 << 20 // 1 MiB + +// extraBlockedNetworks lists special-use CIDR ranges that the +// stdlib classification methods (IsLoopback, IsPrivate, etc.) do +// not cover. Blocking these prevents SSRF against carrier-grade +// NAT, network-benchmarking, documentation, discard-only, and +// the all-zeros "this network" range. +// +// IPv6 ranges already handled by stdlib: +// - ::1/128 (IsLoopback) +// - fc00::/7 (IsPrivate, ULA) +// - fe80::/10 (IsLinkLocalUnicast) +// - ff00::/8 (IsMulticast) +// - ::/128 (IsUnspecified) +var extraBlockedNetworks []*net.IPNet + +func init() { + for _, cidr := range []string{ + // IPv4 special-use ranges. + "0.0.0.0/8", // RFC 1122 "this network". + "100.64.0.0/10", // RFC 6598 carrier-grade NAT. + "198.18.0.0/15", // RFC 2544 benchmarking. + + // IPv6 special-use ranges not covered by stdlib. + "64:ff9b:1::/48", // RFC 8215 IPv4/IPv6 translation. + "100::/64", // RFC 6666 discard-only. + "2001:2::/48", // RFC 5180 benchmarking. + "2001:db8::/32", // RFC 3849 documentation. + } { + _, network, _ := net.ParseCIDR(cidr) + extraBlockedNetworks = append(extraBlockedNetworks, network) + } +} + +// isPrivateIP reports whether the IP is on a network that must +// not be reachable when fetching certificates. IPv4-mapped IPv6 +// addresses are canonicalized to IPv4 first so a literal like +// ::ffff:169.254.169.254 cannot bypass the IPv4 ranges. +func isPrivateIP(ip net.IP) bool { + if v4 := ip.To4(); v4 != nil { + ip = v4 + } + if ip.IsLoopback() || + ip.IsPrivate() || + ip.IsLinkLocalUnicast() || + ip.IsLinkLocalMulticast() || + ip.IsMulticast() || + ip.IsUnspecified() || + ip.IsInterfaceLocalMulticast() { + return true + } + for _, network := range extraBlockedNetworks { + if network.Contains(ip) { + return true + } + } + return false +} + +// certFetchClient is an HTTP client that refuses to connect +// to private or link-local IP addresses. This provides +// defense-in-depth against SSRF even if the host allowlist is +// somehow bypassed (e.g. via DNS rebinding). +var certFetchClient = &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, xerrors.Errorf("split host/port: %w", err) + } + ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, xerrors.Errorf("resolve host: %w", err) + } + if len(ips) == 0 { + return nil, xerrors.Errorf("no addresses for %q", host) + } + // Reject up front so a single tainted answer + // short-circuits the dial rather than racing it. + for _, ip := range ips { + if isPrivateIP(ip.IP) { + return nil, xerrors.Errorf( + "certificate fetch blocked: %q resolved to private IP %s", + host, ip.IP, + ) + } + } + // Dial the validated IP directly. If we dialed by + // hostname here, Go's stdlib would re-resolve and a + // hostile resolver could swap in a private IP after + // validation (DNS rebinding). TLS verification still + // uses the URL host via the Transport's TLS config. + var d net.Dialer + var firstErr error + for _, ip := range ips { + conn, derr := d.DialContext(ctx, network, net.JoinHostPort(ip.IP.String(), port)) + if derr == nil { + return conn, nil + } + if firstErr == nil { + firstErr = derr + } + } + return nil, firstErr + }, + }, +} + +// IsAllowedCertificateURL reports whether rawURL points to a +// host on the allowlist, uses http or https, and targets a +// standard PKI distribution port. Microsoft and DigiCert serve +// these artifacts on 80/443 only; any other port is rejected to +// keep the SSRF surface as narrow as the hostname itself. +func IsAllowedCertificateURL(rawURL string) bool { + if rawURL == "" { + return false + } + u, err := url.Parse(rawURL) + if err != nil { + return false + } + if u.Scheme != "http" && u.Scheme != "https" { + return false + } + if !allowedCertHosts[u.Hostname()] { + return false + } + switch u.Port() { + case "", "80", "443": + return true + default: + return false + } +} + type metadata struct { VMID string `json:"vmId"` } type Options struct { - x509.VerifyOptions + // Roots is the trusted root certificate pool. If nil, + // the embedded root certificate pool is used. + Roots *x509.CertPool + // Intermediates are additional intermediate certificates to + // inject into the PKCS7 object for chain verification. Azure + // PKCS7 envelopes typically only contain the signing cert, so + // intermediates must be supplied externally. When nil, the + // hardcoded Azure intermediate certificates are used. + Intermediates []*x509.Certificate + // CurrentTime, if non-zero, overrides the verification + // timestamp for certificate chain validation. + CurrentTime time.Time + // Offline disables fetching of issuing certificates when + // chain verification fails. Offline bool } // Validate ensures the signature was signed by an Azure certificate. // It returns the associated VM ID if successful. +// +// Verification has two parts, both handled by VerifyWithChainAtTime: +// 1. PKCS7 signature check: proves the content was signed by the +// private key corresponding to the certificate in the envelope. +// 2. Certificate chain check: proves the signing certificate +// chains to a trusted root through known intermediates. func Validate(ctx context.Context, signature string, options Options) (string, error) { data, err := base64.StdEncoding.DecodeString(signature) if err != nil { @@ -54,56 +226,87 @@ func Validate(ctx context.Context, signature string, options Options) (string, e if !allowedSigners.MatchString(signer.Subject.CommonName) { return "", xerrors.Errorf("unmatched common name of signer: %q", signer.Subject.CommonName) } - if options.Intermediates == nil { - options.Intermediates = x509.NewCertPool() - for _, cert := range Certificates { - block, rest := pem.Decode([]byte(cert)) - if len(rest) != 0 { - return "", xerrors.Errorf("invalid certificate. %d bytes remain", len(rest)) - } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return "", xerrors.Errorf("parse certificate: %w", err) - } - options.Intermediates.AddCert(cert) + // Azure PKCS7 envelopes typically contain only the signing + // certificate. Inject intermediate certificates so the + // library can build a chain from signer to trusted root. + intermediates := options.Intermediates + if intermediates == nil { + intermediates, err = ParseCertificates() + if err != nil { + return "", xerrors.Errorf("parse hardcoded certificates: %w", err) } } - _, err = signer.Verify(options.VerifyOptions) - if err != nil { - if !errors.As(err, &x509.UnknownAuthorityError{}) { - return "", xerrors.Errorf("verify signature: %w", err) + pkcs7Data.Certificates = append(pkcs7Data.Certificates, intermediates...) + // Resolve root trust store. VerifyWithChainAtTime skips + // chain verification when the trust store is nil, so we + // must always provide one. + roots := options.Roots + if roots == nil { + roots, err = x509.SystemCertPool() + if err != nil { + return "", xerrors.Errorf("load roots: %w", err) } + } + + currentTime := options.CurrentTime + if currentTime.IsZero() { + currentTime = time.Now() + } + + // VerifyWithChainAtTime validates both the PKCS7 signature + // (proving the content was signed by the certificate's + // private key) and the certificate chain (proving the signer + // chains to a trusted root). + err = pkcs7Data.VerifyWithChainAtTime(roots, currentTime) + if err != nil { if options.Offline { - return "", xerrors.Errorf("certificate from %v is not cached: %w", signer.IssuingCertificateURL, err) + return "", xerrors.Errorf("verify pkcs7: %w", err) } + // The chain verification may fail when the signing + // certificate was issued by an intermediate not yet in + // our hardcoded list. Fetch the issuing certificates + // and retry. ctx, cancelFunc := context.WithTimeout(ctx, 5*time.Second) defer cancelFunc() for _, certURL := range signer.IssuingCertificateURL { + if !IsAllowedCertificateURL(certURL) { + return "", xerrors.New("issuing certificate URL not on allowlist") + } req, err := http.NewRequestWithContext(ctx, "GET", certURL, nil) if err != nil { - return "", xerrors.Errorf("new request %q: %w", certURL, err) + return "", xerrors.New("construct certificate request") } - res, err := http.DefaultClient.Do(req) + res, err := certFetchClient.Do(req) if err != nil { - return "", xerrors.Errorf("no cached certificate for %q found. error fetching: %w", certURL, err) + return "", xerrors.New("certificate fetch unsuccessful") } - data, err := io.ReadAll(res.Body) + limited := io.LimitReader(res.Body, maxCertResponseBytes+1) + certData, err := io.ReadAll(limited) + _ = res.Body.Close() if err != nil { - _ = res.Body.Close() - return "", xerrors.Errorf("read body %q: %w", certURL, err) + return "", xerrors.New("read certificate response body") } - _ = res.Body.Close() - cert, err := x509.ParseCertificate(data) + if int64(len(certData)) > maxCertResponseBytes { + return "", xerrors.New( + "certificate response exceeds maximum size", + ) + } + cert, err := x509.ParseCertificate(certData) if err != nil { - return "", xerrors.Errorf("parse certificate %q: %w", certURL, err) + // Do not wrap the parse error; it may contain + // fragments of the HTTP response body, which + // could leak internal data to the caller. + return "", xerrors.New( + "fetched data is not a valid certificate", + ) } - options.Intermediates.AddCert(cert) + pkcs7Data.Certificates = append(pkcs7Data.Certificates, cert) } - _, err = signer.Verify(options.VerifyOptions) + err = pkcs7Data.VerifyWithChainAtTime(roots, currentTime) if err != nil { - return "", err + return "", xerrors.New("signature verification failed after fetching issuing certificates") } } @@ -115,6 +318,24 @@ func Validate(ctx context.Context, signature string, options Options) (string, e return metadata.VMID, nil } +// ParseCertificates parses the hardcoded Azure intermediate +// certificates and returns them as x509.Certificate values. +func ParseCertificates() ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for _, certPEM := range Certificates { + block, rest := pem.Decode([]byte(certPEM)) + if len(rest) != 0 { + return nil, xerrors.Errorf("invalid certificate. %d bytes remain", len(rest)) + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, xerrors.Errorf("parse certificate: %w", err) + } + certs = append(certs, cert) + } + return certs, nil +} + // Certificates are manually downloaded from Azure, then processed with OpenSSL // and added here. See: https://learn.microsoft.com/en-us/azure/security/fundamentals/azure-ca-details // diff --git a/coderd/azureidentity/azureidentity_internal_test.go b/coderd/azureidentity/azureidentity_internal_test.go new file mode 100644 index 0000000000000..a4b9ddcdb4d93 --- /dev/null +++ b/coderd/azureidentity/azureidentity_internal_test.go @@ -0,0 +1,76 @@ +package azureidentity + +import ( + "context" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsPrivateIP(t *testing.T) { + t.Parallel() + cases := []struct { + name string + ip string + blocked bool + }{ + {"loopback v4", "127.0.0.1", true}, + {"loopback v6", "::1", true}, + {"link local v4 (azure metadata)", "169.254.169.254", true}, + {"link local v6", "fe80::1", true}, + {"rfc1918 10/8", "10.0.0.1", true}, + {"rfc1918 172.16/12", "172.16.0.1", true}, + {"rfc1918 192.168/16", "192.168.0.1", true}, + {"ipv6 ula", "fc00::1", true}, + {"unspecified v4", "0.0.0.0", true}, + {"unspecified v6", "::", true}, + {"this-network 0.0.0.0/8", "0.1.2.3", true}, + {"cgnat 100.64/10", "100.64.0.1", true}, + {"benchmarking 198.18/15", "198.18.0.1", true}, + {"multicast v4", "224.0.0.1", true}, + {"ipv6 nat64 well-known", "64:ff9b:1::1", true}, + {"ipv6 discard-only", "100::1", true}, + {"ipv6 benchmarking", "2001:2::1", true}, + {"ipv6 documentation", "2001:db8::1", true}, + // IPv4-mapped IPv6: must canonicalize to v4 before + // classification, otherwise an attacker could bypass + // the metadata block via ::ffff:169.254.169.254. + {"ipv4-mapped metadata", "::ffff:169.254.169.254", true}, + {"ipv4-mapped rfc1918", "::ffff:10.0.0.1", true}, + + {"public v4", "8.8.8.8", false}, + {"public v6", "2606:4700:4700::1111", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ip := net.ParseIP(tc.ip) + require.NotNil(t, ip, "parse %q", tc.ip) + require.Equal(t, tc.blocked, isPrivateIP(ip)) + }) + } +} + +// TestCertFetchClientRejectsLoopback proves the dialer refuses +// to connect even when the URL itself would have passed an +// allowlist (httptest.Server always binds to 127.0.0.1, so a +// successful fetch here would mean the SSRF guard had failed). +func TestCertFetchClientRejectsLoopback(t *testing.T) { + t.Parallel() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("should never be reached")) + })) + t.Cleanup(srv.Close) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, nil) + require.NoError(t, err) + resp, err := certFetchClient.Do(req) + if resp != nil { + defer resp.Body.Close() + } + require.Error(t, err) + require.Contains(t, err.Error(), "private IP") +} diff --git a/coderd/azureidentity/azureidentity_test.go b/coderd/azureidentity/azureidentity_test.go index bd94f836beb3b..9ed2750f4541e 100644 --- a/coderd/azureidentity/azureidentity_test.go +++ b/coderd/azureidentity/azureidentity_test.go @@ -1,13 +1,19 @@ package azureidentity_test import ( + "bytes" "context" + "crypto/rand" + "crypto/rsa" "crypto/x509" - "encoding/pem" + "crypto/x509/pkix" + "encoding/base64" + "math/big" "runtime" "testing" "time" + "github.com/smallstep/pkcs7" "github.com/stretchr/testify/require" "github.com/coder/coder/v2/coderd/azureidentity" @@ -50,10 +56,8 @@ func TestValidate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() vm, err := azureidentity.Validate(context.Background(), tc.payload, azureidentity.Options{ - VerifyOptions: x509.VerifyOptions{ - CurrentTime: tc.date, - }, - Offline: true, + CurrentTime: tc.date, + Offline: true, }) require.NoError(t, err) require.Equal(t, tc.vmID, vm) @@ -69,12 +73,10 @@ func TestExpiresSoon(t *testing.T) { t.Skip() const threshold = 1 - for _, c := range azureidentity.Certificates { - block, rest := pem.Decode([]byte(c)) - require.Zero(t, len(rest)) - cert, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) + certs, err := azureidentity.ParseCertificates() + require.NoError(t, err) + for _, cert := range certs { expiresSoon := cert.NotAfter.Before(time.Now().AddDate(0, threshold, 0)) if expiresSoon { t.Errorf("certificate expires within %d months %s: %s", threshold, cert.NotAfter, cert.Subject.CommonName) @@ -87,3 +89,206 @@ func TestExpiresSoon(t *testing.T) { } } } + +func TestIsAllowedCertificateURL(t *testing.T) { + t.Parallel() + tests := []struct { + name string + url string + allowed bool + }{ + {"microsoft http", "http://www.microsoft.com/pki/mscorp/cert.crt", true}, + {"microsoft https", "https://www.microsoft.com/pkiops/certs/cert.crt", true}, + {"digicert http", "http://cacerts.digicert.com/DigiCertGlobalRootG2.crt", true}, + {"digicert https", "https://cacerts.digicert.com/DigiCertGlobalRootG3.crt", true}, + {"evil domain", "http://evil.example.com/cert.crt", false}, + {"metadata endpoint", "http://169.254.169.254/latest/meta-data/", false}, + {"localhost", "http://localhost/secret", false}, + {"subdomain trick", "http://www.microsoft.com.evil.com/cert.crt", false}, + {"empty string", "", false}, + {"ftp scheme", "ftp://www.microsoft.com/cert.crt", false}, + {"no scheme", "www.microsoft.com/cert.crt", false}, + {"javascript scheme", "javascript:alert(1)", false}, + {"microsoft with path", "http://www.microsoft.com/pkiops/certs/cert.crt", true}, + {"microsoft explicit port 80", "http://www.microsoft.com:80/cert.crt", true}, + {"microsoft explicit port 443", "https://www.microsoft.com:443/cert.crt", true}, + {"microsoft non-standard port", "http://www.microsoft.com:8080/cert.crt", false}, + {"microsoft port 22", "http://www.microsoft.com:22/cert.crt", false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result := azureidentity.IsAllowedCertificateURL(tc.url) + require.Equal(t, tc.allowed, result, "URL: %s", tc.url) + }) + } +} + +// testCertChain holds a three-level certificate hierarchy (Root CA, +// Intermediate CA, Signing/leaf) together with their private keys. +type testCertChain struct { + RootCert *x509.Certificate + RootKey *rsa.PrivateKey + IntermediateCert *x509.Certificate + IntermediateKey *rsa.PrivateKey + SigningCert *x509.Certificate + SigningKey *rsa.PrivateKey +} + +// newTestCertChain creates a fresh three-level certificate chain for +// testing. All certificates are valid at time.Now(). +func newTestCertChain(t *testing.T) testCertChain { + t.Helper() + + // Smaller key sizes are fine for tests; keeps them fast. + const keyBits = 2048 + + // ---- Root CA ---- + rootKey, err := rsa.GenerateKey(rand.Reader, keyBits) + require.NoError(t, err) + rootTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Test Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + rootDER, err := x509.CreateCertificate(rand.Reader, rootTmpl, rootTmpl, &rootKey.PublicKey, rootKey) + require.NoError(t, err) + rootCert, err := x509.ParseCertificate(rootDER) + require.NoError(t, err) + + // ---- Intermediate CA ---- + intermediateKey, err := rsa.GenerateKey(rand.Reader, keyBits) + require.NoError(t, err) + intermediateTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "Test Intermediate CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + intermediateDER, err := x509.CreateCertificate(rand.Reader, intermediateTmpl, rootCert, &intermediateKey.PublicKey, rootKey) + require.NoError(t, err) + intermediateCert, err := x509.ParseCertificate(intermediateDER) + require.NoError(t, err) + + // ---- Signing (leaf) certificate ---- + signingKey, err := rsa.GenerateKey(rand.Reader, keyBits) + require.NoError(t, err) + signingTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: "metadata.azure.com"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + } + signingDER, err := x509.CreateCertificate(rand.Reader, signingTmpl, intermediateCert, &signingKey.PublicKey, intermediateKey) + require.NoError(t, err) + signingCert, err := x509.ParseCertificate(signingDER) + require.NoError(t, err) + + return testCertChain{ + RootCert: rootCert, + RootKey: rootKey, + IntermediateCert: intermediateCert, + IntermediateKey: intermediateKey, + SigningCert: signingCert, + SigningKey: signingKey, + } +} + +// createSignedPKCS7 produces a base64-encoded PKCS7 SignedData +// envelope over content, signed by the chain's leaf certificate. +func (tc *testCertChain) createSignedPKCS7(t *testing.T, content []byte) string { + t.Helper() + + sd, err := pkcs7.NewSignedData(content) + require.NoError(t, err) + err = sd.AddSignerChain(tc.SigningCert, tc.SigningKey, []*x509.Certificate{tc.IntermediateCert}, pkcs7.SignerInfoConfig{}) + require.NoError(t, err) + der, err := sd.Finish() + require.NoError(t, err) + return base64.StdEncoding.EncodeToString(der) +} + +// validationOptions returns azureidentity.Options that trust only this +// chain's Root CA. +func (tc *testCertChain) validationOptions() azureidentity.Options { + roots := x509.NewCertPool() + roots.AddCert(tc.RootCert) + return azureidentity.Options{ + Roots: roots, + Intermediates: []*x509.Certificate{tc.IntermediateCert}, + Offline: true, + } +} + +func TestValidate_TamperedContent(t *testing.T) { + t.Parallel() + if runtime.GOOS == "darwin" { + t.Skip("pkcs7 signing uses SHA1 which may be restricted on macOS") + } + + chain := newTestCertChain(t) + + // Build a valid PKCS7 envelope. + original := []byte(`{"vmId":"tamper-test-vm"}`) + signed := chain.createSignedPKCS7(t, original) + + // Decode, tamper with the content, re-encode. + raw, err := base64.StdEncoding.DecodeString(signed) + require.NoError(t, err) + tampered := bytes.Replace(raw, []byte("tamper-test-vm"), []byte("tampered!!!!!!"), 1) + require.NotEqual(t, raw, tampered, "payload should have changed") + tamperedB64 := base64.StdEncoding.EncodeToString(tampered) + + opts := chain.validationOptions() + _, err = azureidentity.Validate(context.Background(), tamperedB64, opts) + require.Error(t, err, "tampered content must not pass validation") +} + +func TestValidate_UntrustedCertWithValidSignature(t *testing.T) { + t.Parallel() + if runtime.GOOS == "darwin" { + t.Skip("pkcs7 signing uses SHA1 which may be restricted on macOS") + } + + chain := newTestCertChain(t) + + content := []byte(`{"vmId":"untrusted-test-vm"}`) + signed := chain.createSignedPKCS7(t, content) + + // Build options that trust a DIFFERENT root, so the chain + // should not verify. + otherRoot, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + otherRootTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(99), + Subject: pkix.Name{CommonName: "Other Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + otherRootDER, err := x509.CreateCertificate(rand.Reader, otherRootTmpl, otherRootTmpl, &otherRoot.PublicKey, otherRoot) + require.NoError(t, err) + otherRootCert, err := x509.ParseCertificate(otherRootDER) + require.NoError(t, err) + + untrustedRoots := x509.NewCertPool() + untrustedRoots.AddCert(otherRootCert) + opts := azureidentity.Options{ + Roots: untrustedRoots, + Intermediates: []*x509.Certificate{chain.IntermediateCert}, + Offline: true, + } + + _, err = azureidentity.Validate(context.Background(), signed, opts) + require.Error(t, err, "signature from untrusted CA must not pass validation") +} diff --git a/coderd/coderd.go b/coderd/coderd.go index aaabf5873b555..eb3cff6da1c6c 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -3,7 +3,6 @@ package coderd import ( "context" "crypto/tls" - "crypto/x509" "database/sql" "errors" "expvar" @@ -50,6 +49,7 @@ import ( "github.com/coder/coder/v2/coderd/appearance" "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/awsidentity" + "github.com/coder/coder/v2/coderd/azureidentity" "github.com/coder/coder/v2/coderd/boundaryusage" "github.com/coder/coder/v2/coderd/connectionlog" "github.com/coder/coder/v2/coderd/cryptokeys" @@ -171,7 +171,7 @@ type Options struct { ChatdInstructionLookupTimeout time.Duration AWSCertificates awsidentity.Certificates Authorizer rbac.Authorizer - AzureCertificates x509.VerifyOptions + AzureCertificates azureidentity.Options GoogleTokenValidator *idtoken.Validator GithubOAuth2Config *GithubOAuth2Config OIDCConfig *OIDCConfig @@ -1982,29 +1982,50 @@ func New(options *Options) *API { // Add CSP headers to all static assets and pages. CSP headers only affect // browsers, so these don't make sense on api routes. - cspMW := httpmw.CSPHeaders( - options.Telemetry.Enabled(), func() []*proxyhealth.ProxyHost { - if api.DeploymentValues.Dangerous.AllowAllCors { - // In this mode, allow all external requests. - return []*proxyhealth.ProxyHost{ - { - Host: "*", - AppHost: "*", - }, - } - } - // Always add the primary, since the app host may be on a sub-domain. - proxies := []*proxyhealth.ProxyHost{ + cspProxyHosts := func() []*proxyhealth.ProxyHost { + if api.DeploymentValues.Dangerous.AllowAllCors { + // In this mode, allow all external requests. + return []*proxyhealth.ProxyHost{ { - Host: api.AccessURL.Host, - AppHost: appurl.ConvertAppHostForCSP(api.AccessURL.Host, api.AppHostname), + Host: "*", + AppHost: "*", }, } - if f := api.WorkspaceProxyHostsFn.Load(); f != nil { - proxies = append(proxies, (*f)()...) - } - return proxies - }, additionalCSPHeaders) + } + // Always add the primary, since the app host may be on a sub-domain. + proxies := []*proxyhealth.ProxyHost{ + { + Host: api.AccessURL.Host, + AppHost: appurl.ConvertAppHostForCSP(api.AccessURL.Host, api.AppHostname), + }, + } + if f := api.WorkspaceProxyHostsFn.Load(); f != nil { + proxies = append(proxies, (*f)()...) + } + return proxies + } + cspMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), cspProxyHosts, additionalCSPHeaders) + + // Embed routes (e.g. VS Code extension chat) are designed to be + // loaded inside iframes, so they must not include frame-ancestors + // in their CSP. The CSP wildcard '*' only matches network schemes + // (http, https, ws, wss) and cannot cover custom schemes like + // vscode-webview://, so the only way to allow all embedders is + // to omit the directive entirely. If the operator explicitly + // configured frame-ancestors via CODER_ADDITIONAL_CSP_POLICY, + // respect that setting. + + embedCSPHeaders := make(map[httpmw.CSPFetchDirective][]string, len(additionalCSPHeaders)) + for k, v := range additionalCSPHeaders { + embedCSPHeaders[k] = v + } + if _, ok := additionalCSPHeaders[httpmw.CSPFrameAncestors]; !ok { + embedCSPHeaders[httpmw.CSPFrameAncestors] = []string{} + } + embedCSPMW := httpmw.CSPHeaders(options.Telemetry.Enabled(), cspProxyHosts, embedCSPHeaders) + embedHandler := embedCSPMW(compressHandler(httpmw.HSTS(api.SiteHandler, options.StrictTransportSecurityCfg))) + r.Get("/agents/{agentId}/embed", embedHandler.ServeHTTP) + r.Get("/agents/{agentId}/embed/*", embedHandler.ServeHTTP) // Static file handler must be wrapped with HSTS handler if the // StrictTransportSecurityAge is set. We only need to set this header on diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index 81c7884627b43..7625e725fa0dd 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -32,11 +32,11 @@ import ( "time" "cloud.google.com/go/compute/metadata" - "github.com/fullsailor/pkcs7" "github.com/go-chi/chi/v5" "github.com/golang-jwt/jwt/v4" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" + "github.com/smallstep/pkcs7" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/cases" @@ -59,6 +59,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/autobuild" "github.com/coder/coder/v2/coderd/awsidentity" + "github.com/coder/coder/v2/coderd/azureidentity" "github.com/coder/coder/v2/coderd/connectionlog" "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/database" @@ -118,7 +119,7 @@ type Options struct { AppHostname string AWSCertificates awsidentity.Certificates Authorizer rbac.Authorizer - AzureCertificates x509.VerifyOptions + AzureCertificates azureidentity.Options GithubOAuth2Config *coderd.GithubOAuth2Config RealIPConfig *httpmw.RealIPConfig OIDCConfig *coderd.OIDCConfig @@ -1583,27 +1584,63 @@ func NewAWSInstanceIdentity(t testing.TB, instanceID string) (awsidentity.Certif } } -// NewAzureInstanceIdentity returns a metadata client and ID token validator for faking -// instance authentication for Azure. -func NewAzureInstanceIdentity(t testing.TB, instanceID string) (x509.VerifyOptions, *http.Client) { - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) +// NewAzureInstanceIdentity returns a metadata client and ID token +// validator for faking instance authentication for Azure. It builds +// a realistic 3-level certificate chain (Root CA -> Intermediate -> +// Signing Cert) to match the real Azure trust hierarchy. +func NewAzureInstanceIdentity(t testing.TB, instanceID string) (azureidentity.Options, *http.Client) { + // Root CA (self-signed, trusted). + rootKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + rootTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Test Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + rootDER, err := x509.CreateCertificate(rand.Reader, rootTmpl, rootTmpl, &rootKey.PublicKey, rootKey) + require.NoError(t, err) + rootCert, err := x509.ParseCertificate(rootDER) require.NoError(t, err) - rawCertificate, err := x509.CreateCertificate(rand.Reader, &x509.Certificate{ - SerialNumber: big.NewInt(2022), - NotAfter: time.Now().AddDate(1, 0, 0), - Subject: pkix.Name{ - CommonName: "metadata.azure.com", - }, - }, &x509.Certificate{}, &privateKey.PublicKey, privateKey) + // Intermediate CA (signed by root). + interKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + interTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "Test Intermediate CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(5, 0, 0), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + interDER, err := x509.CreateCertificate(rand.Reader, interTmpl, rootCert, &interKey.PublicKey, rootKey) + require.NoError(t, err) + interCert, err := x509.ParseCertificate(interDER) require.NoError(t, err) - certificate, err := x509.ParseCertificate(rawCertificate) + // Signing cert (leaf, signed by intermediate). + signKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + signTmpl := &x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{CommonName: "metadata.azure.com"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().AddDate(1, 0, 0), + } + signDER, err := x509.CreateCertificate(rand.Reader, signTmpl, interCert, &signKey.PublicKey, interKey) + require.NoError(t, err) + signCert, err := x509.ParseCertificate(signDER) require.NoError(t, err) + // Build PKCS7 signed data with only the signing cert. signed, err := pkcs7.NewSignedData([]byte(`{"vmId":"` + instanceID + `"}`)) require.NoError(t, err) - err = signed.AddSigner(certificate, privateKey, pkcs7.SignerInfoConfig{}) + err = signed.AddSigner(signCert, signKey, pkcs7.SignerInfoConfig{}) require.NoError(t, err) signatureRaw, err := signed.Finish() require.NoError(t, err) @@ -1616,12 +1653,12 @@ func NewAzureInstanceIdentity(t testing.TB, instanceID string) (x509.VerifyOptio }) require.NoError(t, err) - certPool := x509.NewCertPool() - certPool.AddCert(certificate) + roots := x509.NewCertPool() + roots.AddCert(rootCert) - return x509.VerifyOptions{ - Intermediates: certPool, - Roots: certPool, + return azureidentity.Options{ + Roots: roots, + Intermediates: []*x509.Certificate{interCert}, }, &http.Client{ Transport: roundTripper(func(r *http.Request) (*http.Response, error) { // Only handle metadata server requests. diff --git a/coderd/httpmw/csp.go b/coderd/httpmw/csp.go index f39781ad51b03..1395d9ccdb705 100644 --- a/coderd/httpmw/csp.go +++ b/coderd/httpmw/csp.go @@ -142,6 +142,22 @@ func CSPHeaders(telemetry bool, proxyHosts func() []*proxyhealth.ProxyHost, stat cspSrcs.Append(directive, values...) } + // Default to 'self' to prevent clickjacking unless + // explicitly overridden via staticAdditions (e.g. for + // embeddable routes). + // + // An explicit empty value means "omit frame-ancestors + // entirely", which is needed for embed routes where + // non-network-scheme parents (e.g. vscode-webview://) + // must be able to frame the page. The CSP wildcard '*' + // only matches network schemes (http, https, ws, wss) + // so it cannot cover custom schemes. + if vals, ok := cspSrcs[CSPFrameAncestors]; !ok { + cspSrcs[CSPFrameAncestors] = []string{"'self'"} + } else if len(vals) == 0 { + delete(cspSrcs, CSPFrameAncestors) + } + var csp strings.Builder for src, vals := range cspSrcs { _, _ = fmt.Fprintf(&csp, "%s %s; ", src, strings.Join(vals, " ")) diff --git a/coderd/httpmw/csp_test.go b/coderd/httpmw/csp_test.go index ba88320e6fac9..105abd0df18f1 100644 --- a/coderd/httpmw/csp_test.go +++ b/coderd/httpmw/csp_test.go @@ -12,6 +12,63 @@ import ( "github.com/coder/coder/v2/coderd/proxyhealth" ) +func TestCSPFrameAncestors(t *testing.T) { + t.Parallel() + + t.Run("DefaultSelf", func(t *testing.T) { + t.Parallel() + + r := httptest.NewRequest(http.MethodGet, "/", nil) + rw := httptest.NewRecorder() + + httpmw.CSPHeaders(false, func() []*proxyhealth.ProxyHost { + return nil + }, nil)(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + })).ServeHTTP(rw, r) + + csp := rw.Header().Get("Content-Security-Policy") + require.Contains(t, csp, "frame-ancestors 'self'") + }) + + t.Run("OverrideViaStaticAdditions", func(t *testing.T) { + t.Parallel() + + r := httptest.NewRequest(http.MethodGet, "/", nil) + rw := httptest.NewRecorder() + + httpmw.CSPHeaders(false, func() []*proxyhealth.ProxyHost { + return nil + }, map[httpmw.CSPFetchDirective][]string{ + httpmw.CSPFrameAncestors: {"https://example.com"}, + })(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + })).ServeHTTP(rw, r) + + csp := rw.Header().Get("Content-Security-Policy") + require.Contains(t, csp, "frame-ancestors https://example.com") + require.NotContains(t, csp, "frame-ancestors 'self'") + }) + + t.Run("OmitWhenEmpty", func(t *testing.T) { + t.Parallel() + + r := httptest.NewRequest(http.MethodGet, "/", nil) + rw := httptest.NewRecorder() + + httpmw.CSPHeaders(false, func() []*proxyhealth.ProxyHost { + return nil + }, map[httpmw.CSPFetchDirective][]string{ + httpmw.CSPFrameAncestors: {}, + })(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + })).ServeHTTP(rw, r) + + csp := rw.Header().Get("Content-Security-Policy") + require.NotContains(t, csp, "frame-ancestors") + }) +} + func TestCSP(t *testing.T) { t.Parallel() diff --git a/coderd/workspaceresourceauth.go b/coderd/workspaceresourceauth.go index c8608ea03c087..4fabf62bc911c 100644 --- a/coderd/workspaceresourceauth.go +++ b/coderd/workspaceresourceauth.go @@ -7,6 +7,7 @@ import ( "github.com/mitchellh/mapstructure" + "cdr.dev/slog/v3" "github.com/coder/coder/v2/coderd/awsidentity" "github.com/coder/coder/v2/coderd/azureidentity" "github.com/coder/coder/v2/coderd/database" @@ -35,13 +36,19 @@ func (api *API) postWorkspaceAuthAzureInstanceIdentity(rw http.ResponseWriter, r if !httpapi.Read(ctx, rw, r, &req) { return } - instanceID, err := azureidentity.Validate(r.Context(), req.Signature, azureidentity.Options{ - VerifyOptions: api.AzureCertificates, - }) + instanceID, err := azureidentity.Validate(r.Context(), req.Signature, api.AzureCertificates) if err != nil { + // Log the full error for operators but return only a + // generic message to the caller. Errors from the + // certificate fetch path may contain fragments of + // internal HTTP responses, so exposing them would be + // an information disclosure risk. + api.Logger.Warn(ctx, "azure identity validation failed", + slog.Error(err), + ) httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{ Message: "Invalid Azure identity.", - Detail: err.Error(), + Detail: "Signature verification failed.", }) return } diff --git a/dogfood/coder/Dockerfile b/dogfood/coder/Dockerfile index 53ac087ba5090..8b4a0ce31dcea 100644 --- a/dogfood/coder/Dockerfile +++ b/dogfood/coder/Dockerfile @@ -11,8 +11,8 @@ RUN cargo install jj-cli typos-cli watchexec-cli FROM ubuntu:jammy@sha256:eb29ed27b0821dca09c2e28b39135e185fc1302036427d5f4d70a41ce8fd7659 AS go # Install Go manually, so that we can control the version -ARG GO_VERSION=1.25.9 -ARG GO_CHECKSUM="00859d7bd6defe8bf84d9db9e57b9a4467b2887c18cd93ae7460e713db774bc1" +ARG GO_VERSION=1.25.10 +ARG GO_CHECKSUM="42d4f7a32316aa66591eca7e89867256057a4264451aca10570a715b3637ba70" # Boring Go is needed to build FIPS-compliant binaries. RUN apt-get update && \ diff --git a/go.mod b/go.mod index 79df56ee9bb8f..6970083aa25cb 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/coder/coder/v2 -go 1.25.9 +go 1.25.10 // Required until a v3 of chroma is created to lazily initialize all XML files. // None of our dependencies seem to use the registries anyways, so this @@ -143,7 +143,6 @@ require ( github.com/fatih/structs v1.1.0 github.com/fatih/structtag v1.2.0 github.com/fergusstrange/embedded-postgres v1.34.0 - github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa github.com/gen2brain/beeep v0.11.1 github.com/gliderlabs/ssh v0.3.8 github.com/go-chi/chi/v5 v5.2.4 @@ -156,7 +155,7 @@ require ( github.com/gohugoio/hugo v0.160.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/golang-migrate/migrate/v4 v4.19.0 - github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 + github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207 github.com/google/go-cmp v0.7.0 github.com/google/go-github/v43 v43.0.1-0.20220414155304-00e42332e405 github.com/google/go-github/v61 v61.0.0 @@ -209,27 +208,26 @@ require ( github.com/valyala/fasthttp v1.70.0 github.com/wagslane/go-password-validator v0.3.0 github.com/zclconf/go-cty-yaml v1.2.0 - go.mozilla.org/pkcs7 v0.9.0 go.nhat.io/otelsql v0.16.0 - go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 - go.opentelemetry.io/otel/sdk v1.42.0 - go.opentelemetry.io/otel/trace v1.42.0 + go.opentelemetry.io/otel/sdk v1.43.0 + go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/atomic v1.11.0 go.uber.org/goleak v1.3.1-0.20240429205332-517bace7cc29 go.uber.org/mock v0.6.0 go4.org/netipx v0.0.0-20230728180743-ad4cb58a6516 - golang.org/x/crypto v0.49.0 - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa + golang.org/x/crypto v0.50.0 + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f golang.org/x/mod v0.35.0 - golang.org/x/net v0.52.0 + golang.org/x/net v0.53.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.42.0 - golang.org/x/term v0.41.0 - golang.org/x/text v0.35.0 - golang.org/x/tools v0.43.0 + golang.org/x/sys v0.43.0 + golang.org/x/term v0.42.0 + golang.org/x/text v0.36.0 + golang.org/x/tools v0.44.0 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da google.golang.org/api v0.274.0 google.golang.org/grpc v1.80.0 @@ -458,8 +456,8 @@ require ( go.opentelemetry.io/collector/pdata/pprofile v0.121.0 // indirect go.opentelemetry.io/collector/semconv v0.123.0 // indirect go.opentelemetry.io/contrib v1.19.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 - go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 + go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect @@ -504,9 +502,10 @@ require ( github.com/dgraph-io/ristretto/v2 v2.4.0 github.com/elazarl/goproxy v1.8.0 github.com/fsnotify/fsnotify v1.9.0 - github.com/go-git/go-git/v5 v5.18.0 + github.com/go-git/go-git/v5 v5.19.0 github.com/mark3labs/mcp-go v0.38.0 github.com/shopspring/decimal v1.4.0 + github.com/smallstep/pkcs7 v0.2.1 gonum.org/v1/gonum v0.17.0 ) @@ -562,7 +561,7 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/esiqveland/notify v0.13.3 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/jsonname v0.25.4 // indirect github.com/go-openapi/swag/jsonutils v0.25.4 // indirect @@ -586,7 +585,7 @@ require ( github.com/kaptinlin/jsonpointer v0.4.10 // indirect github.com/kaptinlin/jsonschema v0.6.10 // indirect github.com/kaptinlin/messageformat-go v0.4.10 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/landlock-lsm/go-landlock v0.0.0-20251103212306-430f8e5cd97c // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/dsig v1.0.0 // indirect @@ -612,7 +611,7 @@ require ( github.com/segmentio/asm v1.2.1 // indirect github.com/sergeymakinen/go-bmp v1.0.0 // indirect github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect - github.com/sony/gobreaker/v2 v2.3.0 // indirect + github.com/sony/gobreaker/v2 v2.4.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect @@ -628,11 +627,11 @@ require ( github.com/zeebo/xxh3 v1.0.2 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.40.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect - golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect google.golang.org/genai v1.51.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect diff --git a/go.sum b/go.sum index 3e6b05b79c879..d9c674820e52f 100644 --- a/go.sum +++ b/go.sum @@ -389,8 +389,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 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/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= -github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= @@ -492,8 +492,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= @@ -516,10 +514,10 @@ github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5 github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= -github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= -github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= -github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= +github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= @@ -638,8 +636,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8 h1:4txT5G2kqVAKMjzidIabL/8KqjIK71yj30YOeuxLn10= -github.com/gomarkdown/markdown v0.0.0-20240930133441-72d49d9543d8/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207 h1:p7t34F7K4OCRQblcDhNJnP46Uaarz3z2cLcvOZYxWn8= +github.com/gomarkdown/markdown v0.0.0-20260411013819-759bbc3e3207/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 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/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= @@ -800,8 +798,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -1008,8 +1006,8 @@ github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1 github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= github.com/pion/udp v0.1.4 h1:OowsTmu1Od3sD6i3fQUJxJn2fEvJO6L1TidgadtbTI8= github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= -github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= -github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= @@ -1083,8 +1081,10 @@ github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnB github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= -github.com/sony/gobreaker/v2 v2.3.0 h1:7VYxZ69QXRQ2Q4eEawHn6eU4FiuwovzJwsUMA03Lu4I= -github.com/sony/gobreaker/v2 v2.3.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= +github.com/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg= +github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s= github.com/sosedoff/gitkit v0.4.0 h1:opyQJ/h9xMRLsz2ca/2CRXtstePcpldiZN8DpLLF8Os= github.com/sosedoff/gitkit v0.4.0/go.mod h1:V3EpGZ0nvCBhXerPsbDeqtyReNb48cwP9KtkUYTKT5I= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= @@ -1270,8 +1270,6 @@ github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM= github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= -go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.nhat.io/otelsql v0.16.0 h1:MUKhNSl7Vk1FGyopy04FBDimyYogpRFs0DBB9frQal0= go.nhat.io/otelsql v0.16.0/go.mod h1:YB2ocf0Q8+kK4kxzXYUOHj7P2Km8tNmE2QlRS0frUtc= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -1311,11 +1309,11 @@ go.opentelemetry.io/contrib/detectors/gcp v1.40.0 h1:Awaf8gmW99tZTOWqkLCOl6aw1/r go.opentelemetry.io/contrib/detectors/gcp v1.40.0/go.mod h1:99OY9ZCqyLkzJLTh5XhECpLRSxcZl+ZDKBEO+jMBFR4= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= @@ -1326,16 +1324,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3A go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -1371,10 +1369,11 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1401,8 +1400,8 @@ golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1416,6 +1415,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1455,11 +1455,12 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c h1:6a8FdnNk6bTXBjR4AGKFgUKuo+7GnR3FX5L7CbveeZc= -golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= +golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1471,8 +1472,9 @@ golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -1485,8 +1487,9 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= 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= @@ -1498,8 +1501,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/scripts/ironbank/Dockerfile b/scripts/ironbank/Dockerfile index 8aa0a9eac831b..97c710fc7ee5f 100644 --- a/scripts/ironbank/Dockerfile +++ b/scripts/ironbank/Dockerfile @@ -1,6 +1,6 @@ ARG BASE_REGISTRY=registry1.dso.mil -ARG BASE_IMAGE=ironbank/redhat/ubi/ubi8-minimal -ARG BASE_TAG=8.7 +ARG BASE_IMAGE=ironbank/redhat/ubi/ubi9-minimal +ARG BASE_TAG=9.6 FROM ${BASE_REGISTRY}/${BASE_IMAGE}:${BASE_TAG} @@ -16,6 +16,9 @@ RUN microdnf update --assumeyes && \ shadow-utils \ tar \ unzip && \ + # Remove python3-urllib3 if present to address CVE-2026-44431. + # Coder is a Go binary and does not use Python at runtime. + microdnf remove --assumeyes python3-urllib3 2>/dev/null || true && \ microdnf clean all # Configure the cryptography policy manually. These policies likely diff --git a/scripts/ironbank/build_ironbank.sh b/scripts/ironbank/build_ironbank.sh index 8af8431d93376..902c9d1dbc965 100755 --- a/scripts/ironbank/build_ironbank.sh +++ b/scripts/ironbank/build_ironbank.sh @@ -96,8 +96,8 @@ fi pushd "$tmpdir" docker build \ --build-arg BASE_REGISTRY=registry.access.redhat.com \ - --build-arg BASE_IMAGE=ubi8/ubi-minimal \ - --build-arg BASE_TAG=8.7 \ + --build-arg BASE_IMAGE=ubi9/ubi-minimal \ + --build-arg BASE_TAG=9.6 \ --build-arg TERRAFORM_CODER_PROVIDER_VERSION="$terraform_coder_provider_version" \ -t "$image_tag" \ . >&2 diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 5bad8cee66333..42e1988cfd254 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -211,7 +211,7 @@ async function findMatchWorkspace(q: string): Promise { } } -function workspacesKey(req: WorkspacesRequest = {}) { +export function workspacesKey(req: WorkspacesRequest = {}) { return ["workspaces", req] as const; } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx index 53aff3701f8eb..a50bbe0ed1502 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.stories.tsx @@ -14,6 +14,7 @@ import { getTemplatesQueryKey, templateVersionsQueryKey, } from "#/api/queries/templates"; +import { workspacesKey } from "#/api/queries/workspaces"; import type { Workspace } from "#/api/typesGenerated"; import { workspaceChecks } from "#/modules/workspaces/permissions"; import { @@ -22,6 +23,7 @@ import { MockTemplate, MockTemplateVersion, MockUserOwner, + MockWorkspace, } from "#/testHelpers/entities"; import { withAuthProvider, @@ -55,7 +57,7 @@ const deletingWorkspace: Workspace = { }, }; -const meta: Meta = { +const meta = { title: "pages/WorkspacesPage/WorkspacesPage", component: WorkspacesPage, decorators: [withAuthProvider, withDashboardProvider, withProxyProvider()], @@ -108,10 +110,10 @@ const meta: Meta = { spyOn(API, "getOrganizations").mockResolvedValue([MockDefaultOrganization]); spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]); }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const DeleteWorkspaceShowsDeletingStateImmediately: Story = { beforeEach: () => { @@ -170,3 +172,47 @@ export const DeleteWorkspaceShowsDeletingStateImmediately: Story = { ); }, }; + +const makePage = (prefix: string) => + Array.from({ length: 25 }, (_, i) => ({ + ...MockWorkspace, + id: `${prefix}-workspace-${i}`, + name: `${prefix}-workspace-${i}`, + })); + +export const PaginationChangesQueryKey: Story = { + parameters: { + chromatic: { disableSnapshot: true }, + queries: [ + ...meta.parameters.queries, + { + key: workspacesKey({ q: "owner:me", limit: 25, offset: 0 }), + data: { workspaces: makePage("page1"), count: 50 }, + }, + { + key: workspacesKey({ q: "owner:me", limit: 25, offset: 25 }), + data: { workspaces: makePage("page2"), count: 50 }, + }, + ], + }, + play: async ({ canvasElement, step }) => { + const canvas = within(canvasElement); + const user = userEvent.setup(); + + await step("Page 1 renders from cache", async () => { + await canvas.findByText("page1-workspace-0"); + }); + + await step("Clicking next page shows page 2 data", async () => { + const nextButton = await canvas.findByRole("button", { + name: /next page/i, + }); + await user.click(nextButton); + + await canvas.findByText("page2-workspace-0"); + await waitFor(() => { + expect(canvas.queryByText("page1-workspace-0")).not.toBeInTheDocument(); + }); + }); + }, +}; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx index 896f09742b329..6dae88d1ffba5 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.test.tsx @@ -296,67 +296,6 @@ describe("WorkspacesPage", () => { MockStoppedWorkspace.latest_build.template_version_id, ); }); - - it("correctly handles pagination by including pagination parameters in query key", async () => { - const totalWorkspaces = 50; - const workspacesPage1 = Array.from({ length: 25 }, (_, i) => ({ - ...MockWorkspace, - id: `page1-workspace-${i}`, - name: `page1-workspace-${i}`, - })); - const workspacesPage2 = Array.from({ length: 25 }, (_, i) => ({ - ...MockWorkspace, - id: `page2-workspace-${i}`, - name: `page2-workspace-${i}`, - })); - - const getWorkspacesSpy = vi.spyOn(API, "getWorkspaces"); - - getWorkspacesSpy.mockImplementation(({ offset }) => { - switch (offset) { - case 0: - return Promise.resolve({ - workspaces: workspacesPage1, - count: totalWorkspaces, - }); - case 25: - return Promise.resolve({ - workspaces: workspacesPage2, - count: totalWorkspaces, - }); - default: - return Promise.reject(new Error("Unexpected offset")); - } - }); - - const user = userEvent.setup(); - renderWithAuth(); - - await waitFor(() => { - expect(screen.getByText("page1-workspace-0")).toBeInTheDocument(); - }); - - expect(getWorkspacesSpy).toHaveBeenLastCalledWith({ - q: "owner:me", - offset: 0, - limit: 25, - }); - - const nextPageButton = screen.getByRole("button", { name: /next page/i }); - await user.click(nextPageButton); - - await waitFor(() => { - expect(screen.getByText("page2-workspace-0")).toBeInTheDocument(); - }); - - expect(getWorkspacesSpy).toHaveBeenLastCalledWith({ - q: "owner:me", - offset: 25, - limit: 25, - }); - - expect(screen.queryByText("page1-workspace-0")).not.toBeInTheDocument(); - }); }); const getWorkspaceCheckbox = (workspace: Workspace) => {