Files
trivy/pkg/iac/scanners/terraform/parser/parser.go
Nikita Pivkin 13190e92d9 fix(terraform): eval submodules (#6411)
Co-authored-by: William Reade <william@stacklet.io>
2024-04-04 03:40:40 +00:00

379 lines
8.8 KiB
Go

package parser
import (
"context"
"errors"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/zclconf/go-cty/cty"
"github.com/aquasecurity/trivy/pkg/extrafs"
"github.com/aquasecurity/trivy/pkg/iac/debug"
"github.com/aquasecurity/trivy/pkg/iac/ignore"
"github.com/aquasecurity/trivy/pkg/iac/scanners/options"
"github.com/aquasecurity/trivy/pkg/iac/terraform"
tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context"
)
type sourceFile struct {
file *hcl.File
path string
}
var _ ConfigurableTerraformParser = (*Parser)(nil)
// Parser is a tool for parsing terraform templates at a given file system location
type Parser struct {
projectRoot string
moduleName string
modulePath string
moduleSource string
moduleFS fs.FS
moduleBlock *terraform.Block
files []sourceFile
tfvarsPaths []string
stopOnHCLError bool
workspaceName string
underlying *hclparse.Parser
children []*Parser
options []options.ParserOption
debug debug.Logger
allowDownloads bool
skipCachedModules bool
fsMap map[string]fs.FS
skipRequired bool
configsFS fs.FS
}
func (p *Parser) SetDebugWriter(writer io.Writer) {
p.debug = debug.New(writer, "terraform", "parser", "<"+p.moduleName+">")
}
func (p *Parser) SetTFVarsPaths(s ...string) {
p.tfvarsPaths = s
}
func (p *Parser) SetStopOnHCLError(b bool) {
p.stopOnHCLError = b
}
func (p *Parser) SetWorkspaceName(s string) {
p.workspaceName = s
}
func (p *Parser) SetAllowDownloads(b bool) {
p.allowDownloads = b
}
func (p *Parser) SetSkipCachedModules(b bool) {
p.skipCachedModules = b
}
func (p *Parser) SetSkipRequiredCheck(b bool) {
p.skipRequired = b
}
func (p *Parser) SetConfigsFS(fsys fs.FS) {
p.configsFS = fsys
}
// New creates a new Parser
func New(moduleFS fs.FS, moduleSource string, opts ...options.ParserOption) *Parser {
p := &Parser{
workspaceName: "default",
underlying: hclparse.NewParser(),
options: opts,
moduleName: "root",
allowDownloads: true,
moduleFS: moduleFS,
moduleSource: moduleSource,
configsFS: moduleFS,
}
for _, option := range opts {
option(p)
}
return p
}
func (p *Parser) newModuleParser(moduleFS fs.FS, moduleSource, modulePath, moduleName string, moduleBlock *terraform.Block) *Parser {
mp := New(moduleFS, moduleSource)
mp.modulePath = modulePath
mp.moduleBlock = moduleBlock
mp.moduleName = moduleName
mp.projectRoot = p.projectRoot
p.children = append(p.children, mp)
for _, option := range p.options {
option(mp)
}
return mp
}
func (p *Parser) ParseFile(_ context.Context, fullPath string) error {
isJSON := strings.HasSuffix(fullPath, ".tf.json")
isHCL := strings.HasSuffix(fullPath, ".tf")
if !isJSON && !isHCL {
return nil
}
p.debug.Log("Parsing '%s'...", fullPath)
f, err := p.moduleFS.Open(filepath.ToSlash(fullPath))
if err != nil {
return err
}
defer func() { _ = f.Close() }()
data, err := io.ReadAll(f)
if err != nil {
return err
}
if dir := path.Dir(fullPath); p.projectRoot == "" {
p.debug.Log("Setting project/module root to '%s'", dir)
p.projectRoot = dir
p.modulePath = dir
}
var file *hcl.File
var diag hcl.Diagnostics
if isHCL {
file, diag = p.underlying.ParseHCL(data, fullPath)
} else {
file, diag = p.underlying.ParseJSON(data, fullPath)
}
if diag != nil && diag.HasErrors() {
return diag
}
p.files = append(p.files, sourceFile{
file: file,
path: fullPath,
})
p.debug.Log("Added file %s.", fullPath)
return nil
}
// ParseFS parses a root module, where it exists at the root of the provided filesystem
func (p *Parser) ParseFS(ctx context.Context, dir string) error {
dir = path.Clean(dir)
if p.projectRoot == "" {
p.debug.Log("Setting project/module root to '%s'", dir)
p.projectRoot = dir
p.modulePath = dir
}
slashed := filepath.ToSlash(dir)
p.debug.Log("Parsing FS from '%s'", slashed)
fileInfos, err := fs.ReadDir(p.moduleFS, slashed)
if err != nil {
return err
}
var paths []string
for _, info := range fileInfos {
realPath := path.Join(dir, info.Name())
if info.Type()&os.ModeSymlink != 0 {
extra, ok := p.moduleFS.(extrafs.FS)
if !ok {
// we can't handle symlinks in this fs type for now
p.debug.Log("Cannot resolve symlink '%s' in '%s' for this fs type", info.Name(), dir)
continue
}
realPath, err = extra.ResolveSymlink(info.Name(), dir)
if err != nil {
p.debug.Log("Failed to resolve symlink '%s' in '%s': %s", info.Name(), dir, err)
continue
}
info, err := extra.Stat(realPath)
if err != nil {
p.debug.Log("Failed to stat resolved symlink '%s': %s", realPath, err)
continue
}
if info.IsDir() {
continue
}
p.debug.Log("Resolved symlink '%s' in '%s' to '%s'", info.Name(), dir, realPath)
} else if info.IsDir() {
continue
}
paths = append(paths, realPath)
}
sort.Strings(paths)
for _, path := range paths {
if err := p.ParseFile(ctx, path); err != nil {
if p.stopOnHCLError {
return err
}
p.debug.Log("error parsing '%s': %s", path, err)
continue
}
}
return nil
}
var ErrNoFiles = errors.New("no files found")
func (p *Parser) Load(ctx context.Context) (*evaluator, error) {
p.debug.Log("Evaluating module...")
if len(p.files) == 0 {
p.debug.Log("No files found, nothing to do.")
return nil, ErrNoFiles
}
blocks, ignores, err := p.readBlocks(p.files)
if err != nil {
return nil, err
}
p.debug.Log("Read %d block(s) and %d ignore(s) for module '%s' (%d file[s])...", len(blocks), len(ignores), p.moduleName, len(p.files))
var inputVars map[string]cty.Value
if p.moduleBlock != nil {
inputVars = p.moduleBlock.Values().AsValueMap()
p.debug.Log("Added %d input variables from module definition.", len(inputVars))
} else {
inputVars, err = loadTFVars(p.configsFS, p.tfvarsPaths)
if err != nil {
return nil, err
}
p.debug.Log("Added %d variables from tfvars.", len(inputVars))
}
modulesMetadata, metadataPath, err := loadModuleMetadata(p.moduleFS, p.projectRoot)
if err != nil {
p.debug.Log("Error loading module metadata: %s.", err)
} else {
p.debug.Log("Loaded module metadata for %d module(s) from '%s'.", len(modulesMetadata.Modules), metadataPath)
}
workingDir, err := os.Getwd()
if err != nil {
return nil, err
}
p.debug.Log("Working directory for module evaluation is '%s'", workingDir)
return newEvaluator(
p.moduleFS,
p,
p.projectRoot,
p.modulePath,
workingDir,
p.moduleName,
blocks,
inputVars,
modulesMetadata,
p.workspaceName,
ignores,
p.debug.Extend("evaluator"),
p.allowDownloads,
p.skipCachedModules,
), nil
}
func (p *Parser) EvaluateAll(ctx context.Context) (terraform.Modules, cty.Value, error) {
e, err := p.Load(ctx)
if errors.Is(err, ErrNoFiles) {
return nil, cty.NilVal, nil
}
modules, fsMap := e.EvaluateAll(ctx)
p.debug.Log("Finished parsing module '%s'.", p.moduleName)
p.fsMap = fsMap
return modules, e.exportOutputs(), nil
}
func (p *Parser) GetFilesystemMap() map[string]fs.FS {
if p.fsMap == nil {
return make(map[string]fs.FS)
}
return p.fsMap
}
func (p *Parser) readBlocks(files []sourceFile) (terraform.Blocks, ignore.Rules, error) {
var blocks terraform.Blocks
var ignores ignore.Rules
moduleCtx := tfcontext.NewContext(&hcl.EvalContext{}, nil)
for _, file := range files {
fileBlocks, err := loadBlocksFromFile(file)
if err != nil {
if p.stopOnHCLError {
return nil, nil, err
}
p.debug.Log("Encountered HCL parse error: %s", err)
continue
}
for _, fileBlock := range fileBlocks {
blocks = append(blocks, terraform.NewBlock(fileBlock, moduleCtx, p.moduleBlock, nil, p.moduleSource, p.moduleFS))
}
fileIgnores := ignore.Parse(
string(file.file.Bytes),
file.path,
&ignore.StringMatchParser{
SectionKey: "ws",
},
&paramParser{},
)
ignores = append(ignores, fileIgnores...)
}
sortBlocksByHierarchy(blocks)
return blocks, ignores, nil
}
func loadBlocksFromFile(file sourceFile) (hcl.Blocks, error) {
contents, diagnostics := file.file.Body.Content(terraform.Schema)
if diagnostics != nil && diagnostics.HasErrors() {
return nil, diagnostics
}
if contents == nil {
return nil, nil
}
return contents.Blocks, nil
}
type paramParser struct {
params map[string]string
}
func (s *paramParser) Key() string {
return "ignore"
}
func (s *paramParser) Parse(str string) bool {
s.params = make(map[string]string)
idx := strings.Index(str, "[")
if idx == -1 {
return false
}
str = str[idx+1:]
paramStr := strings.TrimSuffix(str, "]")
for _, pair := range strings.Split(paramStr, ",") {
parts := strings.Split(pair, "=")
if len(parts) != 2 {
continue
}
s.params[parts[0]] = parts[1]
}
return true
}
func (s *paramParser) Param() any {
return s.params
}