diff --git a/docs/docs/scanner/misconfiguration/index.md b/docs/docs/scanner/misconfiguration/index.md index 026d86dc51f7..49cb97ef3636 100644 --- a/docs/docs/scanner/misconfiguration/index.md +++ b/docs/docs/scanner/misconfiguration/index.md @@ -360,6 +360,25 @@ This can be repeated for specifying multiple packages. trivy conf --policy ./policy --namespaces main --namespaces user ./configs ``` +### Private terraform registries +Trivy can download terraform code from private registries. +To pass credentials you must use the `TF_TOKEN_` environment variables. +You cannot use a `.terraformrc` or `terraform.rc` file, these are not supported by trivy yet. + +From the terraform docs: + +> Environment variable names should have the prefix TF_TOKEN_ added to the domain name, with periods encoded as underscores. +> For example, the value of a variable named `TF_TOKEN_app_terraform_io` will be used as a bearer authorization token when the CLI makes service requests to the hostname `app.terraform.io`. +> +> You must convert domain names containing non-ASCII characters to their punycode equivalent with an ACE prefix. +> For example, token credentials for `例えば.com` must be set in a variable called `TF_TOKEN_xn--r8j3dr99h_com`. +> +> Hyphens are also valid within host names but usually invalid as variable names and may be encoded as double underscores. +> For example, you can set a token for the domain name café.fr as TF_TOKEN_xn--caf-dma_fr or TF_TOKEN_xn____caf__dma_fr. + +If multiple variables evaluate to the same hostname, Trivy will choose the environment variable name where the dashes have not been encoded as double underscores. + + ### Skipping resources by inline comments Some configuration file formats (e.g. Terraform) support inline comments. diff --git a/pkg/iac/scanners/terraform/parser/resolvers/registry.go b/pkg/iac/scanners/terraform/parser/resolvers/registry.go index a64fba4804da..30ceb1a6bceb 100644 --- a/pkg/iac/scanners/terraform/parser/resolvers/registry.go +++ b/pkg/iac/scanners/terraform/parser/resolvers/registry.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "golang.org/x/net/idna" + "github.com/aquasecurity/go-version/pkg/semver" ) @@ -55,12 +57,11 @@ func (r *registryResolver) Resolve(ctx context.Context, target fs.FS, opt Option hostname = parts[0] parts = parts[1:] - envVar := fmt.Sprintf("TF_TOKEN_%s", strings.ReplaceAll(hostname, ".", "_")) - token = os.Getenv(envVar) - if token != "" { + token, err = getPrivateRegistryTokenFromEnvVars(hostname) + if err == nil { opt.Debug("Found a token for the registry at %s", hostname) } else { - opt.Debug("No token was found for the registry at %s", hostname) + opt.Debug(err.Error()) } } @@ -136,6 +137,28 @@ func (r *registryResolver) Resolve(ctx context.Context, target fs.FS, opt Option return filesystem, prefix, downloadPath, true, nil } +func getPrivateRegistryTokenFromEnvVars(hostname string) (string, error) { + token := "" + asciiHostname, err := idna.ToASCII(hostname) + if err != nil { + return "", fmt.Errorf("could not convert hostname %s to a punycode encoded ASCII string so cannot find token for this registry", hostname) + } + + envVar := fmt.Sprintf("TF_TOKEN_%s", strings.ReplaceAll(asciiHostname, ".", "_")) + token = os.Getenv(envVar) + + // Dashes in the hostname can optionally be converted to double underscores + if token == "" { + envVar = strings.ReplaceAll(envVar, "-", "__") + token = os.Getenv(envVar) + } + + if token == "" { + return "", fmt.Errorf("no token was found for the registry at %s", hostname) + } + return token, nil +} + func resolveVersion(input string, versions moduleVersions) (string, error) { if len(versions.Modules) != 1 { return "", fmt.Errorf("1 module expected, found %d", len(versions.Modules)) diff --git a/pkg/iac/scanners/terraform/parser/resolvers/registry_test.go b/pkg/iac/scanners/terraform/parser/resolvers/registry_test.go new file mode 100644 index 000000000000..a36c19ae4c1e --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/registry_test.go @@ -0,0 +1,56 @@ +package resolvers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_getPrivateRegistryTokenFromEnvVars_ErrorsWithNoEnvVarSet(t *testing.T) { + token, err := getPrivateRegistryTokenFromEnvVars("registry.example.com") + assert.Equal(t, "", token) + assert.Equal(t, "no token was found for the registry at registry.example.com", err.Error()) +} + +func Test_getPrivateRegistryTokenFromEnvVars_ConvertsSiteNameToEnvVar(t *testing.T) { + tests := []struct { + name string + siteName string + tokenName string + }{ + { + name: "returns string when simple env var set", + siteName: "registry.example.com", + tokenName: "TF_TOKEN_registry_example_com", + }, + { + name: "allows dashes in hostname to be dashes", + siteName: "my-registry.example.com", + tokenName: "TF_TOKEN_my-registry_example_com", + }, + { + name: "allows dashes in hostname to be double underscores", + siteName: "my-registry.example.com", + tokenName: "TF_TOKEN_my__registry_example_com", + }, + { + name: "handles utf8 to punycode correctly", + siteName: "例えば.com", + tokenName: "TF_TOKEN_xn--r8j3dr99h_com", + }, + { + name: "handles punycode with dash to underscore conversion", + siteName: "café.fr", + tokenName: "TF_TOKEN_xn____caf__dma_fr", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(tt.tokenName, "abcd") + token, err := getPrivateRegistryTokenFromEnvVars(tt.siteName) + assert.Equal(t, "abcd", token) + assert.Equal(t, nil, err) + }) + } +}