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(¶meters); err != nil { 233 return nil, err 234 } 235 return parameters, nil 236 }