mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-23 07:29:00 -08:00
214 lines
5.8 KiB
Go
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 = ®istryResolver{
|
|
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)
|
|
}
|