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 }