Files
trivy/pkg/iac/scanners/terraform/parser/resolvers/registry.go
Nikita Pivkin bfdf5cfc30 refactor(misconf): use slog (#7295)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
2024-08-23 04:27:17 +00:00

214 lines
5.8 KiB
Go

package resolvers
import (
"context"
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"sort"
"strings"
"time"
"golang.org/x/net/idna"
"github.com/aquasecurity/go-version/pkg/version"
"github.com/aquasecurity/trivy/pkg/log"
)
type registryResolver struct {
client *http.Client
}
var Registry = &registryResolver{
client: &http.Client{
// give it a maximum 5 seconds to resolve the module
Timeout: time.Second * 5,
},
}
type moduleVersions struct {
Modules []struct {
Versions []struct {
Version string `json:"version"`
} `json:"versions"`
} `json:"modules"`
}
const registryHostname = "registry.terraform.io"
// nolint
func (r *registryResolver) Resolve(ctx context.Context, target fs.FS, opt Options) (filesystem fs.FS, prefix string, downloadPath string, applies bool, err error) {
if !opt.AllowDownloads {
return
}
inputVersion := opt.Version
source, _ := splitPackageSubdirRaw(opt.Source)
parts := strings.Split(source, "/")
if len(parts) < 3 || len(parts) > 4 {
return
}
hostname := registryHostname
var token string
if len(parts) == 4 {
hostname = parts[0]
parts = parts[1:]
token, err = getPrivateRegistryTokenFromEnvVars(hostname)
if err == nil {
opt.Logger.Debug("Found a token for the registry", log.String("hostname", hostname))
} else {
opt.Logger.Error(
"Failed to find a token for the registry",
log.String("hostname", hostname), log.Err(err))
}
}
moduleName := strings.Join(parts, "/")
if opt.Version != "" {
versionUrl := fmt.Sprintf("https://%s/v1/modules/%s/versions", hostname, moduleName)
opt.Logger.Debug("Requesting module versions from registry using",
log.String("url", versionUrl))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, versionUrl, nil)
if err != nil {
return nil, "", "", true, err
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := r.client.Do(req)
if err != nil {
return nil, "", "", true, err
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return nil, "", "", true, fmt.Errorf("unexpected status code for versions endpoint: %d", resp.StatusCode)
}
var availableVersions moduleVersions
if err := json.NewDecoder(resp.Body).Decode(&availableVersions); err != nil {
return nil, "", "", true, err
}
opt.Version, err = resolveVersion(inputVersion, availableVersions)
if err != nil {
return nil, "", "", true, err
}
opt.Logger.Debug("Found module version",
log.String("version", opt.Version), log.String("constraint", inputVersion))
}
var url string
if opt.Version == "" {
url = fmt.Sprintf("https://%s/v1/modules/%s/download", hostname, moduleName)
} else {
url = fmt.Sprintf("https://%s/v1/modules/%s/%s/download", hostname, moduleName, opt.Version)
}
opt.Logger.Debug("Requesting module source from registry", log.String("url", url))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, "", "", true, err
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
if opt.Version != "" {
req.Header.Set("X-Terraform-Version", opt.Version)
}
resp, err := r.client.Do(req)
if err != nil {
return nil, "", "", true, err
}
defer func() { _ = resp.Body.Close() }()
// OpenTofu may return 200 with body
switch resp.StatusCode {
case http.StatusOK:
// https://opentofu.org/docs/internals/module-registry-protocol/#sample-response-1
var downloadResponse struct {
Location string `json:"location"`
}
if err := json.NewDecoder(resp.Body).Decode(&downloadResponse); err != nil {
return nil, "", "", true, fmt.Errorf("failed to decode download response: %w", err)
}
opt.Source = downloadResponse.Location
case http.StatusNoContent:
opt.Source = resp.Header.Get("X-Terraform-Get")
default:
return nil, "", "", true, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
if opt.Source == "" {
return nil, "", "", true, fmt.Errorf("no source was found for the registry at %s", hostname)
}
opt.Logger.Debug("Module resolved via registry to new source",
log.String("source", opt.Source), log.String("name", moduleName))
filesystem, prefix, downloadPath, _, err = Remote.Resolve(ctx, target, opt)
if err != nil {
return nil, "", "", true, err
}
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))
}
if len(versions.Modules[0].Versions) == 0 {
return "", fmt.Errorf("no available versions for module")
}
constraints, err := version.NewConstraints(input)
if err != nil {
return "", err
}
var realVersions version.Collection
for _, rawVersion := range versions.Modules[0].Versions {
realVersion, err := version.Parse(rawVersion.Version)
if err != nil {
continue
}
realVersions = append(realVersions, realVersion)
}
sort.Sort(sort.Reverse(realVersions))
for _, realVersion := range realVersions {
if constraints.Check(realVersion) {
return realVersion.String(), nil
}
}
return "", fmt.Errorf("no available versions for module constraint '%s'", input)
}