github.com/aquasecurity/trivy-iac@v0.8.1-0.20240127024015-3d8e412cf0ab/pkg/scanners/cloudformation/parser/parser.go (about)

     1  package parser
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/aquasecurity/defsec/pkg/debug"
    15  	"github.com/aquasecurity/defsec/pkg/scanners/options"
    16  	"github.com/liamg/jfather"
    17  	"gopkg.in/yaml.v3"
    18  
    19  	"github.com/aquasecurity/trivy-iac/pkg/detection"
    20  )
    21  
    22  var _ options.ConfigurableParser = (*Parser)(nil)
    23  
    24  type Parser struct {
    25  	debug               debug.Logger
    26  	skipRequired        bool
    27  	parameterFiles      []string
    28  	parameters          map[string]any
    29  	overridedParameters Parameters
    30  	configsFS           fs.FS
    31  }
    32  
    33  func WithParameters(params map[string]any) options.ParserOption {
    34  	return func(cp options.ConfigurableParser) {
    35  		if p, ok := cp.(*Parser); ok {
    36  			p.parameters = params
    37  		}
    38  	}
    39  }
    40  
    41  func WithParameterFiles(files ...string) options.ParserOption {
    42  	return func(cp options.ConfigurableParser) {
    43  		if p, ok := cp.(*Parser); ok {
    44  			p.parameterFiles = files
    45  		}
    46  	}
    47  }
    48  
    49  func WithConfigsFS(fsys fs.FS) options.ParserOption {
    50  	return func(cp options.ConfigurableParser) {
    51  		if p, ok := cp.(*Parser); ok {
    52  			p.configsFS = fsys
    53  		}
    54  	}
    55  }
    56  
    57  func (p *Parser) SetDebugWriter(writer io.Writer) {
    58  	p.debug = debug.New(writer, "cloudformation", "parser")
    59  }
    60  
    61  func (p *Parser) SetSkipRequiredCheck(b bool) {
    62  	p.skipRequired = b
    63  }
    64  
    65  func New(options ...options.ParserOption) *Parser {
    66  	p := &Parser{}
    67  	for _, option := range options {
    68  		option(p)
    69  	}
    70  	return p
    71  }
    72  
    73  func (p *Parser) ParseFS(ctx context.Context, fsys fs.FS, dir string) (FileContexts, error) {
    74  	var contexts FileContexts
    75  	if err := fs.WalkDir(fsys, filepath.ToSlash(dir), func(path string, entry fs.DirEntry, err error) error {
    76  		select {
    77  		case <-ctx.Done():
    78  			return ctx.Err()
    79  		default:
    80  		}
    81  		if err != nil {
    82  			return err
    83  		}
    84  		if entry.IsDir() {
    85  			return nil
    86  		}
    87  
    88  		if !p.Required(fsys, path) {
    89  			p.debug.Log("not a CloudFormation file, skipping %s", path)
    90  			return nil
    91  		}
    92  
    93  		c, err := p.ParseFile(ctx, fsys, path)
    94  		if err != nil {
    95  			p.debug.Log("Error parsing file '%s': %s", path, err)
    96  			return nil
    97  		}
    98  		contexts = append(contexts, c)
    99  		return nil
   100  	}); err != nil {
   101  		return nil, err
   102  	}
   103  	return contexts, nil
   104  }
   105  
   106  func (p *Parser) Required(fs fs.FS, path string) bool {
   107  	if p.skipRequired {
   108  		return true
   109  	}
   110  
   111  	f, err := fs.Open(filepath.ToSlash(path))
   112  	if err != nil {
   113  		return false
   114  	}
   115  	defer func() { _ = f.Close() }()
   116  	if data, err := io.ReadAll(f); err == nil {
   117  		return detection.IsType(path, bytes.NewReader(data), detection.FileTypeCloudFormation)
   118  	}
   119  	return false
   120  
   121  }
   122  
   123  func (p *Parser) ParseFile(ctx context.Context, fsys fs.FS, path string) (context *FileContext, err error) {
   124  	defer func() {
   125  		if e := recover(); e != nil {
   126  			err = fmt.Errorf("panic during parse: %s", e)
   127  		}
   128  	}()
   129  
   130  	select {
   131  	case <-ctx.Done():
   132  		return nil, ctx.Err()
   133  	default:
   134  	}
   135  
   136  	if p.configsFS == nil {
   137  		p.configsFS = fsys
   138  	}
   139  
   140  	if err := p.parseParams(); err != nil {
   141  		return nil, fmt.Errorf("failed to parse parameters file: %w", err)
   142  	}
   143  
   144  	sourceFmt := YamlSourceFormat
   145  	if strings.HasSuffix(strings.ToLower(path), ".json") {
   146  		sourceFmt = JsonSourceFormat
   147  	}
   148  
   149  	f, err := fsys.Open(filepath.ToSlash(path))
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	defer func() { _ = f.Close() }()
   154  
   155  	content, err := io.ReadAll(f)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	lines := strings.Split(string(content), "\n")
   161  
   162  	context = &FileContext{
   163  		filepath:     path,
   164  		lines:        lines,
   165  		SourceFormat: sourceFmt,
   166  	}
   167  
   168  	if strings.HasSuffix(strings.ToLower(path), ".json") {
   169  		if err := jfather.Unmarshal(content, context); err != nil {
   170  			return nil, NewErrInvalidContent(path, err)
   171  		}
   172  	} else {
   173  		if err := yaml.Unmarshal(content, context); err != nil {
   174  			return nil, NewErrInvalidContent(path, err)
   175  		}
   176  	}
   177  
   178  	context.OverrideParameters(p.overridedParameters)
   179  
   180  	context.lines = lines
   181  	context.SourceFormat = sourceFmt
   182  	context.filepath = path
   183  
   184  	p.debug.Log("Context loaded from source %s", path)
   185  
   186  	// the context must be set to conditions before resources
   187  	for _, c := range context.Conditions {
   188  		c.setContext(context)
   189  	}
   190  
   191  	for name, r := range context.Resources {
   192  		r.ConfigureResource(name, fsys, path, context)
   193  	}
   194  
   195  	return context, nil
   196  }
   197  
   198  func (p *Parser) parseParams() error {
   199  	if p.overridedParameters != nil { // parameters have already been parsed
   200  		return nil
   201  	}
   202  
   203  	params := make(Parameters)
   204  
   205  	var errs []error
   206  
   207  	for _, path := range p.parameterFiles {
   208  		if parameters, err := p.parseParametersFile(path); err != nil {
   209  			errs = append(errs, err)
   210  		} else {
   211  			params.Merge(parameters)
   212  		}
   213  	}
   214  
   215  	if len(errs) != 0 {
   216  		return errors.Join(errs...)
   217  	}
   218  
   219  	params.Merge(p.parameters)
   220  
   221  	p.overridedParameters = params
   222  	return nil
   223  }
   224  
   225  func (p *Parser) parseParametersFile(path string) (Parameters, error) {
   226  	f, err := p.configsFS.Open(path)
   227  	if err != nil {
   228  		return nil, fmt.Errorf("parameters file %q open error: %w", path, err)
   229  	}
   230  
   231  	var parameters Parameters
   232  	if err := json.NewDecoder(f).Decode(&parameters); err != nil {
   233  		return nil, err
   234  	}
   235  	return parameters, nil
   236  }