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

     1  // Copied from github.com/hashicorp/terraform/internal/lang/funcs
     2  package funcs
     3  
     4  import (
     5  	"encoding/base64"
     6  	"fmt"
     7  	"io"
     8  	"io/fs"
     9  	"os"
    10  	"path/filepath"
    11  	"unicode/utf8"
    12  
    13  	"github.com/bmatcuk/doublestar/v4"
    14  	"github.com/hashicorp/hcl/v2"
    15  	"github.com/hashicorp/hcl/v2/hclsyntax"
    16  	"github.com/mitchellh/go-homedir"
    17  	"github.com/zclconf/go-cty/cty"
    18  	"github.com/zclconf/go-cty/cty/function"
    19  )
    20  
    21  // MakeFileFunc constructs a function that takes a file path and returns the
    22  // contents of that file, either directly as a string (where valid UTF-8 is
    23  // required) or as a string containing base64 bytes.
    24  func MakeFileFunc(target fs.FS, baseDir string, encBase64 bool) function.Function {
    25  	return function.New(&function.Spec{
    26  		Params: []function.Parameter{
    27  			{
    28  				Name: "path",
    29  				Type: cty.String,
    30  			},
    31  		},
    32  		Type: function.StaticReturnType(cty.String),
    33  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    34  			path := args[0].AsString()
    35  			src, err := readFileBytes(target, baseDir, path)
    36  			if err != nil {
    37  				err = function.NewArgError(0, err)
    38  				return cty.UnknownVal(cty.String), err
    39  			}
    40  
    41  			switch {
    42  			case encBase64:
    43  				enc := base64.StdEncoding.EncodeToString(src)
    44  				return cty.StringVal(enc), nil
    45  			default:
    46  				if !utf8.Valid(src) {
    47  					return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead", path)
    48  				}
    49  				return cty.StringVal(string(src)), nil
    50  			}
    51  		},
    52  	})
    53  }
    54  
    55  // MakeTemplateFileFunc constructs a function that takes a file path and
    56  // an arbitrary object of named values and attempts to render the referenced
    57  // file as a template using HCL template syntax.
    58  //
    59  // The template itself may recursively call other functions so a callback
    60  // must be provided to get access to those functions. The template cannot,
    61  // however, access any variables defined in the scope: it is restricted only to
    62  // those variables provided in the second function argument, to ensure that all
    63  // dependencies on other graph nodes can be seen before executing this function.
    64  //
    65  // As a special exception, a referenced template file may not recursively call
    66  // the templatefile function, since that would risk the same file being
    67  // included into itself indefinitely.
    68  func MakeTemplateFileFunc(target fs.FS, baseDir string, funcsCb func() map[string]function.Function) function.Function {
    69  
    70  	params := []function.Parameter{
    71  		{
    72  			Name: "path",
    73  			Type: cty.String,
    74  		},
    75  		{
    76  			Name: "vars",
    77  			Type: cty.DynamicPseudoType,
    78  		},
    79  	}
    80  
    81  	loadTmpl := func(fn string) (hcl.Expression, error) {
    82  		// We re-use File here to ensure the same filename interpretation
    83  		// as it does, along with its other safety checks.
    84  		tmplVal, err := File(target, baseDir, cty.StringVal(fn))
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  
    89  		expr, diags := hclsyntax.ParseTemplate([]byte(tmplVal.AsString()), fn, hcl.Pos{Line: 1, Column: 1})
    90  		if diags.HasErrors() {
    91  			return nil, diags
    92  		}
    93  
    94  		return expr, nil
    95  	}
    96  
    97  	renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) {
    98  		if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) {
    99  			return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time
   100  		}
   101  
   102  		ctx := &hcl.EvalContext{
   103  			Variables: varsVal.AsValueMap(),
   104  		}
   105  
   106  		// We require all of the variables to be valid HCL identifiers, because
   107  		// otherwise there would be no way to refer to them in the template
   108  		// anyway. Rejecting this here gives better feedback to the user
   109  		// than a syntax error somewhere in the template itself.
   110  		for n := range ctx.Variables {
   111  			if !hclsyntax.ValidIdentifier(n) {
   112  				// This error message intentionally doesn't describe _all_ of
   113  				// the different permutations that are technically valid as an
   114  				// HCL identifier, but rather focuses on what we might
   115  				// consider to be an "idiomatic" variable name.
   116  				return cty.DynamicVal, function.NewArgErrorf(1, "invalid template variable name %q: must start with a letter, followed by zero or more letters, digits, and underscores", n)
   117  			}
   118  		}
   119  
   120  		// We'll pre-check references in the template here so we can give a
   121  		// more specialized error message than HCL would by default, so it's
   122  		// clearer that this problem is coming from a templatefile call.
   123  		for _, traversal := range expr.Variables() {
   124  			root := traversal.RootName()
   125  			if _, ok := ctx.Variables[root]; !ok {
   126  				return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange())
   127  			}
   128  		}
   129  
   130  		givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems
   131  		funcs := make(map[string]function.Function, len(givenFuncs))
   132  		for name, fn := range givenFuncs {
   133  			if name == "templatefile" {
   134  				// We stub this one out to prevent recursive calls.
   135  				funcs[name] = function.New(&function.Spec{
   136  					Params: params,
   137  					Type: func(args []cty.Value) (cty.Type, error) {
   138  						return cty.NilType, fmt.Errorf("cannot recursively call templatefile from inside templatefile call")
   139  					},
   140  				})
   141  				continue
   142  			}
   143  			funcs[name] = fn
   144  		}
   145  		ctx.Functions = funcs
   146  
   147  		val, diags := expr.Value(ctx)
   148  		if diags.HasErrors() {
   149  			return cty.DynamicVal, diags
   150  		}
   151  		return val, nil
   152  	}
   153  
   154  	return function.New(&function.Spec{
   155  		Params: params,
   156  		Type: func(args []cty.Value) (cty.Type, error) {
   157  			if !(args[0].IsKnown() && args[1].IsKnown()) {
   158  				return cty.DynamicPseudoType, nil
   159  			}
   160  
   161  			// We'll render our template now to see what result type it produces.
   162  			// A template consisting only of a single interpolation an potentially
   163  			// return any type.
   164  			expr, err := loadTmpl(args[0].AsString())
   165  			if err != nil {
   166  				return cty.DynamicPseudoType, err
   167  			}
   168  
   169  			// This is safe even if args[1] contains unknowns because the HCL
   170  			// template renderer itself knows how to short-circuit those.
   171  			val, err := renderTmpl(expr, args[1])
   172  			return val.Type(), err
   173  		},
   174  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   175  			expr, err := loadTmpl(args[0].AsString())
   176  			if err != nil {
   177  				return cty.DynamicVal, err
   178  			}
   179  			return renderTmpl(expr, args[1])
   180  		},
   181  	})
   182  
   183  }
   184  
   185  // MakeFileExistsFunc constructs a function that takes a path
   186  // and determines whether a file exists at that path
   187  func MakeFileExistsFunc(baseDir string) function.Function {
   188  	return function.New(&function.Spec{
   189  		Params: []function.Parameter{
   190  			{
   191  				Name: "path",
   192  				Type: cty.String,
   193  			},
   194  		},
   195  		Type: function.StaticReturnType(cty.Bool),
   196  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   197  			path := args[0].AsString()
   198  			path, err := homedir.Expand(path)
   199  			if err != nil {
   200  				return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to expand ~: %s", err)
   201  			}
   202  
   203  			if !filepath.IsAbs(path) {
   204  				path = filepath.Join(baseDir, path)
   205  			}
   206  
   207  			// Ensure that the path is canonical for the host OS
   208  			path = filepath.Clean(path)
   209  
   210  			fi, err := os.Stat(path)
   211  			if err != nil {
   212  				if os.IsNotExist(err) {
   213  					return cty.False, nil
   214  				}
   215  				return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", path)
   216  			}
   217  
   218  			if fi.Mode().IsRegular() {
   219  				return cty.True, nil
   220  			}
   221  
   222  			return cty.False, fmt.Errorf("%s is not a regular file, but %q",
   223  				path, fi.Mode().String())
   224  		},
   225  	})
   226  }
   227  
   228  // MakeFileSetFunc constructs a function that takes a glob pattern
   229  // and enumerates a file set from that pattern
   230  func MakeFileSetFunc(baseDir string) function.Function {
   231  	return function.New(&function.Spec{
   232  		Params: []function.Parameter{
   233  			{
   234  				Name: "path",
   235  				Type: cty.String,
   236  			},
   237  			{
   238  				Name: "pattern",
   239  				Type: cty.String,
   240  			},
   241  		},
   242  		Type: function.StaticReturnType(cty.Set(cty.String)),
   243  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   244  			path := args[0].AsString()
   245  			pattern := args[1].AsString()
   246  
   247  			if !filepath.IsAbs(path) {
   248  				path = filepath.Join(baseDir, path)
   249  			}
   250  
   251  			// Join the path to the glob pattern, while ensuring the full
   252  			// pattern is canonical for the host OS. The joined path is
   253  			// automatically cleaned during this operation.
   254  			pattern = filepath.Join(path, pattern)
   255  
   256  			matches, err := doublestar.Glob(os.DirFS(path), pattern)
   257  			if err != nil {
   258  				return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err)
   259  			}
   260  
   261  			var matchVals []cty.Value
   262  			for _, match := range matches {
   263  				fi, err := os.Stat(match)
   264  
   265  				if err != nil {
   266  					return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat (%s): %s", match, err)
   267  				}
   268  
   269  				if !fi.Mode().IsRegular() {
   270  					continue
   271  				}
   272  
   273  				// Remove the path and file separator from matches.
   274  				match, err = filepath.Rel(path, match)
   275  
   276  				if err != nil {
   277  					return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to trim path of match (%s): %s", match, err)
   278  				}
   279  
   280  				// Replace any remaining file separators with forward slash (/)
   281  				// separators for cross-system compatibility.
   282  				match = filepath.ToSlash(match)
   283  
   284  				matchVals = append(matchVals, cty.StringVal(match))
   285  			}
   286  
   287  			if len(matchVals) == 0 {
   288  				return cty.SetValEmpty(cty.String), nil
   289  			}
   290  
   291  			return cty.SetVal(matchVals), nil
   292  		},
   293  	})
   294  }
   295  
   296  // BasenameFunc constructs a function that takes a string containing a filesystem path
   297  // and removes all except the last portion from it.
   298  var BasenameFunc = function.New(&function.Spec{
   299  	Params: []function.Parameter{
   300  		{
   301  			Name: "path",
   302  			Type: cty.String,
   303  		},
   304  	},
   305  	Type: function.StaticReturnType(cty.String),
   306  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   307  		return cty.StringVal(filepath.Base(args[0].AsString())), nil
   308  	},
   309  })
   310  
   311  // DirnameFunc constructs a function that takes a string containing a filesystem path
   312  // and removes the last portion from it.
   313  var DirnameFunc = function.New(&function.Spec{
   314  	Params: []function.Parameter{
   315  		{
   316  			Name: "path",
   317  			Type: cty.String,
   318  		},
   319  	},
   320  	Type: function.StaticReturnType(cty.String),
   321  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   322  		return cty.StringVal(filepath.Dir(args[0].AsString())), nil
   323  	},
   324  })
   325  
   326  // AbsPathFunc constructs a function that converts a filesystem path to an absolute path
   327  var AbsPathFunc = function.New(&function.Spec{
   328  	Params: []function.Parameter{
   329  		{
   330  			Name: "path",
   331  			Type: cty.String,
   332  		},
   333  	},
   334  	Type: function.StaticReturnType(cty.String),
   335  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   336  		absPath, err := filepath.Abs(args[0].AsString())
   337  		return cty.StringVal(filepath.ToSlash(absPath)), err
   338  	},
   339  })
   340  
   341  // PathExpandFunc constructs a function that expands a leading ~ character to the current user's home directory.
   342  var PathExpandFunc = function.New(&function.Spec{
   343  	Params: []function.Parameter{
   344  		{
   345  			Name: "path",
   346  			Type: cty.String,
   347  		},
   348  	},
   349  	Type: function.StaticReturnType(cty.String),
   350  	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   351  
   352  		homePath, err := homedir.Expand(args[0].AsString())
   353  		return cty.StringVal(homePath), err
   354  	},
   355  })
   356  
   357  func openFile(target fs.FS, baseDir, path string) (fs.File, error) {
   358  	path, err := homedir.Expand(path)
   359  	if err != nil {
   360  		return nil, fmt.Errorf("failed to expand ~: %s", err)
   361  	}
   362  
   363  	if !filepath.IsAbs(path) {
   364  		path = filepath.Join(baseDir, path)
   365  	}
   366  
   367  	// Ensure that the path is canonical for the host OS
   368  	path = filepath.Clean(path)
   369  
   370  	if target != nil {
   371  		return target.Open(path)
   372  	}
   373  	return os.Open(path)
   374  }
   375  
   376  func readFileBytes(target fs.FS, baseDir, path string) ([]byte, error) {
   377  	f, err := openFile(target, baseDir, path)
   378  	if err != nil {
   379  		if os.IsNotExist(err) {
   380  			// An extra Terraform-specific hint for this situation
   381  			return nil, fmt.Errorf("no file exists at %s; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource", path)
   382  		}
   383  		return nil, err
   384  	}
   385  
   386  	src, err := io.ReadAll(f)
   387  	if err != nil {
   388  		return nil, fmt.Errorf("failed to read %s", path)
   389  	}
   390  
   391  	return src, nil
   392  }
   393  
   394  // File reads the contents of the file at the given path.
   395  //
   396  // The file must contain valid UTF-8 bytes, or this function will return an error.
   397  //
   398  // The underlying function implementation works relative to a particular base
   399  // directory, so this wrapper takes a base directory string and uses it to
   400  // construct the underlying function before calling it.
   401  func File(target fs.FS, baseDir string, path cty.Value) (cty.Value, error) {
   402  	fn := MakeFileFunc(target, baseDir, false)
   403  	return fn.Call([]cty.Value{path})
   404  }
   405  
   406  // FileExists determines whether a file exists at the given path.
   407  //
   408  // The underlying function implementation works relative to a particular base
   409  // directory, so this wrapper takes a base directory string and uses it to
   410  // construct the underlying function before calling it.
   411  func FileExists(baseDir string, path cty.Value) (cty.Value, error) {
   412  	fn := MakeFileExistsFunc(baseDir)
   413  	return fn.Call([]cty.Value{path})
   414  }
   415  
   416  // FileSet enumerates a set of files given a glob pattern
   417  //
   418  // The underlying function implementation works relative to a particular base
   419  // directory, so this wrapper takes a base directory string and uses it to
   420  // construct the underlying function before calling it.
   421  func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) {
   422  	fn := MakeFileSetFunc(baseDir)
   423  	return fn.Call([]cty.Value{path, pattern})
   424  }
   425  
   426  // FileBase64 reads the contents of the file at the given path.
   427  //
   428  // The bytes from the file are encoded as base64 before returning.
   429  //
   430  // The underlying function implementation works relative to a particular base
   431  // directory, so this wrapper takes a base directory string and uses it to
   432  // construct the underlying function before calling it.
   433  func FileBase64(target fs.FS, baseDir string, path cty.Value) (cty.Value, error) {
   434  	fn := MakeFileFunc(target, baseDir, true)
   435  	return fn.Call([]cty.Value{path})
   436  }
   437  
   438  // Basename takes a string containing a filesystem path and removes all except the last portion from it.
   439  //
   440  // The underlying function implementation works only with the path string and does not access the filesystem itself.
   441  // It is therefore unable to take into account filesystem features such as symlinks.
   442  //
   443  // If the path is empty then the result is ".", representing the current working directory.
   444  func Basename(path cty.Value) (cty.Value, error) {
   445  	return BasenameFunc.Call([]cty.Value{path})
   446  }
   447  
   448  // Dirname takes a string containing a filesystem path and removes the last portion from it.
   449  //
   450  // The underlying function implementation works only with the path string and does not access the filesystem itself.
   451  // It is therefore unable to take into account filesystem features such as symlinks.
   452  //
   453  // If the path is empty then the result is ".", representing the current working directory.
   454  func Dirname(path cty.Value) (cty.Value, error) {
   455  	return DirnameFunc.Call([]cty.Value{path})
   456  }
   457  
   458  // Pathexpand takes a string that might begin with a `~` segment, and if so it replaces that segment with
   459  // the current user's home directory path.
   460  //
   461  // The underlying function implementation works only with the path string and does not access the filesystem itself.
   462  // It is therefore unable to take into account filesystem features such as symlinks.
   463  //
   464  // If the leading segment in the path is not `~` then the given path is returned unmodified.
   465  func Pathexpand(path cty.Value) (cty.Value, error) {
   466  	return PathExpandFunc.Call([]cty.Value{path})
   467  }