github.com/khulnasoft-lab/defsec@v1.0.5-0.20230827010352-5e9f46893d95/pkg/scanners/terraform/parser/resolvers/registry.go (about)

     1  package resolvers
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/fs"
     8  	"net/http"
     9  	"os"
    10  	"sort"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/Masterminds/semver"
    15  )
    16  
    17  type registryResolver struct {
    18  	client *http.Client
    19  }
    20  
    21  var Registry = &registryResolver{
    22  	client: &http.Client{
    23  		// give it a maximum 5 seconds to resolve the module
    24  		Timeout: time.Second * 5,
    25  	},
    26  }
    27  
    28  type moduleVersions struct {
    29  	Modules []struct {
    30  		Versions []struct {
    31  			Version string `json:"version"`
    32  		} `json:"versions"`
    33  	} `json:"modules"`
    34  }
    35  
    36  const registryHostname = "registry.terraform.io"
    37  
    38  // nolint
    39  func (r *registryResolver) Resolve(ctx context.Context, target fs.FS, opt Options) (filesystem fs.FS, prefix string, downloadPath string, applies bool, err error) {
    40  
    41  	if !opt.AllowDownloads {
    42  		return
    43  	}
    44  
    45  	inputVersion := opt.Version
    46  	source, relativePath, _ := strings.Cut(opt.Source, "//")
    47  	parts := strings.Split(source, "/")
    48  	if len(parts) < 3 || len(parts) > 4 {
    49  		return
    50  	}
    51  
    52  	hostname := registryHostname
    53  	var token string
    54  	if len(parts) == 4 {
    55  		hostname = parts[0]
    56  		parts = parts[1:]
    57  
    58  		envVar := fmt.Sprintf("TF_TOKEN_%s", strings.ReplaceAll(hostname, ".", "_"))
    59  		token = os.Getenv(envVar)
    60  		if token != "" {
    61  			opt.Debug("Found a token for the registry at %s", hostname)
    62  		} else {
    63  			opt.Debug("No token was found for the registry at %s", hostname)
    64  		}
    65  	}
    66  
    67  	moduleName := strings.Join(parts, "/")
    68  
    69  	if opt.Version != "" {
    70  		versionUrl := fmt.Sprintf("https://%s/v1/modules/%s/versions", hostname, moduleName)
    71  		opt.Debug("Requesting module versions from registry using '%s'...", versionUrl)
    72  		req, err := http.NewRequestWithContext(ctx, http.MethodGet, versionUrl, nil)
    73  		if err != nil {
    74  			return nil, "", "", true, err
    75  		}
    76  		if token != "" {
    77  			req.Header.Set("Authorization", "Bearer "+token)
    78  		}
    79  		resp, err := r.client.Do(req)
    80  		if err != nil {
    81  			return nil, "", "", true, err
    82  		}
    83  		defer func() { _ = resp.Body.Close() }()
    84  		if resp.StatusCode != http.StatusOK {
    85  			return nil, "", "", true, fmt.Errorf("unexpected status code for versions endpoint: %d", resp.StatusCode)
    86  		}
    87  		var availableVersions moduleVersions
    88  		if err := json.NewDecoder(resp.Body).Decode(&availableVersions); err != nil {
    89  			return nil, "", "", true, err
    90  		}
    91  
    92  		opt.Version, err = resolveVersion(inputVersion, availableVersions)
    93  		if err != nil {
    94  			return nil, "", "", true, err
    95  		}
    96  		opt.Debug("Found version '%s' for constraint '%s'", opt.Version, inputVersion)
    97  	}
    98  
    99  	var url string
   100  	if opt.Version == "" {
   101  		url = fmt.Sprintf("https://%s/v1/modules/%s/download", hostname, moduleName)
   102  	} else {
   103  		url = fmt.Sprintf("https://%s/v1/modules/%s/%s/download", hostname, moduleName, opt.Version)
   104  	}
   105  
   106  	opt.Debug("Requesting module source from registry using '%s'...", url)
   107  
   108  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   109  	if err != nil {
   110  		return nil, "", "", true, err
   111  	}
   112  	if token != "" {
   113  		req.Header.Set("Authorization", "Bearer "+token)
   114  	}
   115  	if opt.Version != "" {
   116  		req.Header.Set("X-Terraform-Version", opt.Version)
   117  	}
   118  
   119  	resp, err := r.client.Do(req)
   120  	if err != nil {
   121  		return nil, "", "", true, err
   122  	}
   123  	defer func() { _ = resp.Body.Close() }()
   124  	if resp.StatusCode != http.StatusNoContent {
   125  		return nil, "", "", true, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
   126  	}
   127  
   128  	opt.Source = resp.Header.Get("X-Terraform-Get")
   129  	opt.Debug("Module '%s' resolved via registry to new source: '%s'", opt.Name, opt.Source)
   130  	opt.RelativePath = relativePath
   131  	filesystem, prefix, downloadPath, _, err = Remote.Resolve(ctx, target, opt)
   132  	if err != nil {
   133  		return nil, "", "", true, err
   134  	}
   135  
   136  	return filesystem, prefix, downloadPath, true, nil
   137  }
   138  
   139  func resolveVersion(input string, versions moduleVersions) (string, error) {
   140  	if len(versions.Modules) != 1 {
   141  		return "", fmt.Errorf("1 module expected, found %d", len(versions.Modules))
   142  	}
   143  	if len(versions.Modules[0].Versions) == 0 {
   144  		return "", fmt.Errorf("no available versions for module")
   145  	}
   146  	constraints, err := semver.NewConstraint(input)
   147  	if err != nil {
   148  		return "", err
   149  	}
   150  	var realVersions semver.Collection
   151  	for _, rawVersion := range versions.Modules[0].Versions {
   152  		realVersion, err := semver.NewVersion(rawVersion.Version)
   153  		if err != nil {
   154  			continue
   155  		}
   156  		realVersions = append(realVersions, realVersion)
   157  	}
   158  	sort.Sort(sort.Reverse(realVersions))
   159  	for _, realVersion := range realVersions {
   160  		if constraints.Check(realVersion) {
   161  			return realVersion.String(), nil
   162  		}
   163  	}
   164  	return "", fmt.Errorf("no available versions for module constraint '%s'", input)
   165  }