From 5b80a0ee903f696c3c2c62e77ee5ec1001f93c87 Mon Sep 17 00:00:00 2001 From: nikpivkin Date: Wed, 20 Mar 2024 12:43:12 +0700 Subject: [PATCH] feat(cloudformation): support inline ignores --- docs/docs/scanner/misconfiguration/index.md | 13 +- pkg/iac/ignore/parse.go | 169 ++++++++++++ pkg/iac/ignore/rule.go | 99 +++++++ pkg/iac/ignore/rule_test.go | 241 ++++++++++++++++++ pkg/iac/scan/code_test.go | 6 +- pkg/iac/scan/result.go | 17 ++ .../cloudformation/parser/file_context.go | 2 + .../scanners/cloudformation/parser/parser.go | 11 +- .../cloudformation/parser/property.go | 15 +- .../cloudformation/parser/reference.go | 37 --- pkg/iac/scanners/cloudformation/scanner.go | 24 +- .../scanners/cloudformation/scanner_test.go | 128 ++++++++++ .../scanners/terraform/executor/executor.go | 172 +++++++------ pkg/iac/scanners/terraform/executor/option.go | 12 - pkg/iac/scanners/terraform/ignore_test.go | 2 +- pkg/iac/scanners/terraform/options.go | 16 -- .../scanners/terraform/parser/evaluator.go | 5 +- .../scanners/terraform/parser/load_blocks.go | 131 ---------- .../terraform/parser/load_blocks_test.go | 13 - pkg/iac/scanners/terraform/parser/parser.go | 59 ++++- pkg/iac/scanners/terraform/scanner_test.go | 36 --- pkg/iac/terraform/ignore.go | 100 -------- pkg/iac/terraform/module.go | 8 +- 23 files changed, 847 insertions(+), 469 deletions(-) create mode 100644 pkg/iac/ignore/parse.go create mode 100644 pkg/iac/ignore/rule.go create mode 100644 pkg/iac/ignore/rule_test.go delete mode 100644 pkg/iac/scanners/terraform/parser/load_blocks.go delete mode 100644 pkg/iac/scanners/terraform/parser/load_blocks_test.go delete mode 100644 pkg/iac/terraform/ignore.go diff --git a/docs/docs/scanner/misconfiguration/index.md b/docs/docs/scanner/misconfiguration/index.md index b1107a530718..2c1bcbf49ce7 100644 --- a/docs/docs/scanner/misconfiguration/index.md +++ b/docs/docs/scanner/misconfiguration/index.md @@ -381,7 +381,7 @@ If multiple variables evaluate to the same hostname, Trivy will choose the envir ### Skipping resources by inline comments -Trivy supports ignoring misconfigured resources by inline comments for Terraform configuration files only. +Trivy supports ignoring misconfigured resources by inline comments for Terraform and CloudFormation configuration files only. In cases where Trivy can detect comments of a specific format immediately adjacent to resource definitions, it is possible to ignore findings from a single source of resource definition (in contrast to `.trivyignore`, which has a directory-wide scope on all of the files scanned). The format for these comments is `trivy:ignore:` immediately following the format-specific line-comment [token](https://developer.hashicorp.com/terraform/language/syntax/configuration#comments). @@ -422,6 +422,17 @@ As an example, consider the following check metadata: Long ID would look like the following: `aws-s3-enable-logging`. +Example for CloudFromation: +```yaml +AWSTemplateFormatVersion: "2010-09-09" +Resources: +#trivy:ignore:* + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket +``` + #### Expiration Date You can specify the expiration date of the ignore rule in `yyyy-mm-dd` format. This is a useful feature when you want to make sure that an ignored issue is not forgotten and worth revisiting in the future. For example: diff --git a/pkg/iac/ignore/parse.go b/pkg/iac/ignore/parse.go new file mode 100644 index 000000000000..0b634638825d --- /dev/null +++ b/pkg/iac/ignore/parse.go @@ -0,0 +1,169 @@ +package ignore + +import ( + "errors" + "strings" + "time" + + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// RuleSectionParser defines the interface for parsing ignore rules. +type RuleSectionParser interface { + Key() string + Parse(string) bool + Param() any +} + +// Parse parses the configuration file and returns the Rules +func Parse(src, path string, parsers ...RuleSectionParser) Rules { + var rules Rules + for i, line := range strings.Split(src, "\n") { + line = strings.TrimSpace(line) + rng := types.NewRange(path, i+1, i+1, "", nil) + lineIgnores := parseLine(line, rng, parsers) + for _, lineIgnore := range lineIgnores { + rules = append(rules, lineIgnore) + } + } + + rules.shift() + + return rules +} + +func parseLine(line string, rng types.Range, parsers []RuleSectionParser) []Rule { + var rules []Rule + + sections := strings.Split(strings.TrimSpace(line), " ") + for i, section := range sections { + section := strings.TrimSpace(section) + section = strings.TrimLeftFunc(section, func(r rune) bool { + return r == '#' || r == '/' || r == '*' + }) + + section, exists := hasIgnoreRulePrefix(section) + if !exists { + continue + } + + rule, err := parseComment(section, rng, parsers) + if err != nil { + log.Logger.Debugf("Failed to parse rule at %s: %s", rng.String(), err.Error()) + continue + } + rule.block = i == 0 + rules = append(rules, rule) + } + + return rules +} + +func hasIgnoreRulePrefix(s string) (string, bool) { + for _, prefix := range []string{"tfsec:", "trivy:"} { + if after, found := strings.CutPrefix(s, prefix); found { + return after, true + } + } + + return "", false +} + +func parseComment(input string, rng types.Range, parsers []RuleSectionParser) (Rule, error) { + rule := Rule{ + rng: rng, + sections: make(map[string]any), + } + + parsers = append(parsers, &expiryDateParser{ + rng: rng, + }) + + segments := strings.Split(input, ":") + + for i := 0; i < len(segments)-1; i += 2 { + key := segments[i] + val := segments[i+1] + if key == "ignore" { + // special case, because id and parameters are in the same section + idParser := &checkIDParser{ + StringMatchParser{SectionKey: "id"}, + } + if idParser.Parse(val) { + rule.sections[idParser.Key()] = idParser.Param() + } + } + + for _, parser := range parsers { + if parser.Key() != key { + continue + } + + if parser.Parse(val) { + rule.sections[parser.Key()] = parser.Param() + } + } + } + + if _, exists := rule.sections["id"]; !exists { + return Rule{}, errors.New("rule section with the `ignore` key is required") + } + + return rule, nil +} + +type StringMatchParser struct { + SectionKey string + param string +} + +func (s *StringMatchParser) Key() string { + return s.SectionKey +} + +func (s *StringMatchParser) Parse(str string) bool { + s.param = str + return str != "" +} + +func (s *StringMatchParser) Param() any { + return s.param +} + +type checkIDParser struct { + StringMatchParser +} + +func (s *checkIDParser) Parse(str string) bool { + if idx := strings.Index(str, "["); idx != -1 { + str = str[:idx] + } + return s.StringMatchParser.Parse(str) +} + +type expiryDateParser struct { + rng types.Range + expiry time.Time +} + +func (s *expiryDateParser) Key() string { + return "exp" +} + +func (s *expiryDateParser) Parse(str string) bool { + parsed, err := time.Parse("2006-01-02", str) + if err != nil { + log.Logger.Debugf("Incorrect time to ignore is specified: %s", str) + parsed = time.Time{} + } else if time.Now().After(parsed) { + log.Logger.Debug("Ignore rule time has expired for location: %s", s.rng.String()) + } + + s.expiry = parsed + return true +} + +func (s *expiryDateParser) Param() any { + return s.expiry +} diff --git a/pkg/iac/ignore/rule.go b/pkg/iac/ignore/rule.go new file mode 100644 index 000000000000..8e6fac20b5d1 --- /dev/null +++ b/pkg/iac/ignore/rule.go @@ -0,0 +1,99 @@ +package ignore + +import ( + "slices" + "time" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// Ignorer represents a function that checks if the rule should be ignored. +type Ignorer func(resultMeta types.Metadata, param any) bool + +type Rules []Rule + +// Ignore checks if the rule should be ignored based on provided metadata, IDs, and ignorer functions. +func (r Rules) Ignore(m types.Metadata, ids []string, ignorers map[string]Ignorer) bool { + return slices.ContainsFunc(r, func(r Rule) bool { + return r.ignore(m, ids, ignorers) + }) +} + +func (r Rules) shift() { + var ( + currentRange *types.Range + offset int + ) + + for i := len(r) - 1; i > 0; i-- { + currentIgnore, nextIgnore := r[i], r[i-1] + if currentRange == nil { + currentRange = ¤tIgnore.rng + } + if nextIgnore.rng.GetStartLine()+1+offset == currentIgnore.rng.GetStartLine() { + r[i-1].rng = *currentRange + offset++ + } else { + currentRange = nil + offset = 0 + } + } +} + +// Rule represents a rule for ignoring vulnerabilities. +type Rule struct { + rng types.Range + block bool + sections map[string]any +} + +func (r Rule) ignore(m types.Metadata, ids []string, ignorers map[string]Ignorer) bool { + matchMeta, ok := r.matchRange(&m) + if !ok { + return false + } + + ignorers = lo.Assign(defaultIgnorers(ids), ignorers) + + for ignoreID, ignore := range ignorers { + if param, exists := r.sections[ignoreID]; exists { + if !ignore(*matchMeta, param) { + return false + } + } + } + + return true +} + +func (r Rule) matchRange(m *types.Metadata) (*types.Metadata, bool) { + metaHierarchy := m + for metaHierarchy != nil { + if r.rng.GetFilename() != metaHierarchy.Range().GetFilename() { + metaHierarchy = metaHierarchy.Parent() + continue + } + if metaHierarchy.Range().GetStartLine() == r.rng.GetStartLine()+1 || + metaHierarchy.Range().GetStartLine() == r.rng.GetStartLine() { + return metaHierarchy, true + } + metaHierarchy = metaHierarchy.Parent() + } + + return nil, false +} + +func defaultIgnorers(ids []string) map[string]Ignorer { + return map[string]Ignorer{ + "id": func(_ types.Metadata, param any) bool { + id, ok := param.(string) + return ok && (id == "*" || len(ids) == 0 || slices.Contains(ids, id)) + }, + "exp": func(_ types.Metadata, param any) bool { + expiry, ok := param.(time.Time) + return ok && time.Now().Before(expiry) + }, + } +} diff --git a/pkg/iac/ignore/rule_test.go b/pkg/iac/ignore/rule_test.go new file mode 100644 index 000000000000..262d9795d2bc --- /dev/null +++ b/pkg/iac/ignore/rule_test.go @@ -0,0 +1,241 @@ +package ignore_test + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/stretchr/testify/assert" +) + +func metadataWithLine(path string, line int) types.Metadata { + return types.NewMetadata(types.NewRange(path, line, line, "", nil), "") +} + +func TestRules_Ignore(t *testing.T) { + + const filename = "test" + + type args struct { + metadata types.Metadata + ids []string + } + + tests := []struct { + name string + src string + args args + shouldIgnore bool + }{ + { + name: "no ignore", + src: `#test`, + shouldIgnore: false, + }, + { + name: "one ignore rule", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "blank line between rule and finding", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 3), + ids: []string{"rule-1"}, + }, + shouldIgnore: false, + }, + { + name: "rule and a finding on the same line", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "rule and a finding on the same line", + src: `test #trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "multiple rules on one line", + src: `test #trivy:ignore:rule-1 #trivy:ignore:rule-2`, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-2"}, + }, + shouldIgnore: true, + }, + { + name: "rule and find from different files", + src: `test #trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine("another-file", 1), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "multiple ignore rule", + src: `#trivy:ignore:rule-1 +#trivy:ignore:rule-2 +`, + args: args{ + metadata: metadataWithLine(filename, 3), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "ignore section with params", + src: `#trivy:ignore:rule-1[param1=1]`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "id's don't match", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "without ignore section", + src: `#trivy:exp:2022-01-01`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "non valid ignore section", + src: `#trivy:ignore`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "ignore rule with expiry date passed", + src: `#trivy:ignore:rule-1:exp:2022-01-01`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: false, + }, + { + name: "ignore rule with expiry date not passed", + src: `#trivy:ignore:rule-1:exp:2026-01-01`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "ignore rule with invalid expiry date", + src: `#trivy:ignore:rule-1:exp:2026-99-01`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := ignore.Parse(tt.src, filename) + got := rules.Ignore(tt.args.metadata, tt.args.ids, nil) + assert.Equal(t, tt.shouldIgnore, got) + }) + } +} + +func TestRules_IgnoreWithCustomIgnorer(t *testing.T) { + const filename = "test" + + type args struct { + metadata types.Metadata + ids []string + ignorers map[string]ignore.Ignorer + } + + tests := []struct { + name string + src string + parser ignore.RuleSectionParser + args args + shouldIgnore bool + }{ + { + name: "happy", + src: `#trivy:ignore:rule-1:ws:dev`, + parser: &ignore.StringMatchParser{ + SectionKey: "ws", + }, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + ignorers: map[string]ignore.Ignorer{ + "ws": func(_ types.Metadata, param any) bool { + ws, ok := param.(string) + if !ok { + return false + } + return ws == "dev" + }, + }, + }, + shouldIgnore: true, + }, + { + name: "bad", + src: `#trivy:ignore:rule-1:ws:prod`, + parser: &ignore.StringMatchParser{ + SectionKey: "ws", + }, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + ignorers: map[string]ignore.Ignorer{ + "ws": func(_ types.Metadata, param any) bool { + ws, ok := param.(string) + if !ok { + return false + } + return ws == "dev" + }, + }, + }, + shouldIgnore: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := ignore.Parse(tt.src, filename, tt.parser) + got := rules.Ignore(tt.args.metadata, tt.args.ids, tt.args.ignorers) + assert.Equal(t, tt.shouldIgnore, got) + }) + } +} diff --git a/pkg/iac/scan/code_test.go b/pkg/iac/scan/code_test.go index e0591ed23c85..c3ffe3725ef1 100644 --- a/pkg/iac/scan/code_test.go +++ b/pkg/iac/scan/code_test.go @@ -5,13 +5,11 @@ import ( "strings" "testing" - iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" - + "github.com/liamg/memoryfs" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/liamg/memoryfs" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" ) func TestResult_GetCode(t *testing.T) { diff --git a/pkg/iac/scan/result.go b/pkg/iac/scan/result.go index 861171e2dcc0..d9924c6aaeef 100644 --- a/pkg/iac/scan/result.go +++ b/pkg/iac/scan/result.go @@ -7,6 +7,7 @@ import ( "reflect" "strings" + "github.com/aquasecurity/trivy/pkg/iac/ignore" "github.com/aquasecurity/trivy/pkg/iac/severity" iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" ) @@ -261,6 +262,22 @@ func (r *Results) AddIgnored(source interface{}, descriptions ...string) { *r = append(*r, res) } +func (r *Results) Ignore(ignoreRules ignore.Rules, ignores map[string]ignore.Ignorer) { + for i, result := range *r { + allIDs := []string{ + result.Rule().LongID(), + result.Rule().AVDID, + strings.ToLower(result.Rule().AVDID), + result.Rule().ShortCode, + } + allIDs = append(allIDs, result.Rule().Aliases...) + + if ignoreRules.Ignore(result.Metadata(), allIDs, ignores) { + (*r)[i].OverrideStatus(StatusIgnored) + } + } +} + func (r *Results) SetRule(rule Rule) { for i := range *r { (*r)[i].rule = rule diff --git a/pkg/iac/scanners/cloudformation/parser/file_context.go b/pkg/iac/scanners/cloudformation/parser/file_context.go index 4904d13f29d0..746dae7e024b 100644 --- a/pkg/iac/scanners/cloudformation/parser/file_context.go +++ b/pkg/iac/scanners/cloudformation/parser/file_context.go @@ -1,6 +1,7 @@ package parser import ( + "github.com/aquasecurity/trivy/pkg/iac/ignore" iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" ) @@ -17,6 +18,7 @@ type FileContext struct { filepath string lines []string SourceFormat SourceFormat + Ignores ignore.Rules Parameters map[string]*Parameter `json:"Parameters" yaml:"Parameters"` Resources map[string]*Resource `json:"Resources" yaml:"Resources"` Globals map[string]*Resource `json:"Globals" yaml:"Globals"` diff --git a/pkg/iac/scanners/cloudformation/parser/parser.go b/pkg/iac/scanners/cloudformation/parser/parser.go index 43e4099289c5..65bf1440432d 100644 --- a/pkg/iac/scanners/cloudformation/parser/parser.go +++ b/pkg/iac/scanners/cloudformation/parser/parser.go @@ -16,6 +16,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/debug" "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/iac/ignore" "github.com/aquasecurity/trivy/pkg/iac/scanners/options" ) @@ -165,12 +166,14 @@ func (p *Parser) ParseFile(ctx context.Context, fsys fs.FS, path string) (fctx * SourceFormat: sourceFmt, } - if strings.HasSuffix(strings.ToLower(path), ".json") { - if err := jfather.Unmarshal(content, fctx); err != nil { + switch sourceFmt { + case YamlSourceFormat: + if err := yaml.Unmarshal(content, fctx); err != nil { return nil, NewErrInvalidContent(path, err) } - } else { - if err := yaml.Unmarshal(content, fctx); err != nil { + fctx.Ignores = ignore.Parse(string(content), path) + case JsonSourceFormat: + if err := jfather.Unmarshal(content, fctx); err != nil { return nil, NewErrInvalidContent(path, err) } } diff --git a/pkg/iac/scanners/cloudformation/parser/property.go b/pkg/iac/scanners/cloudformation/parser/property.go index 3cdbbb36b58a..cc39f4838bc0 100644 --- a/pkg/iac/scanners/cloudformation/parser/property.go +++ b/pkg/iac/scanners/cloudformation/parser/property.go @@ -113,19 +113,8 @@ func (p *Property) Range() iacTypes.Range { } func (p *Property) Metadata() iacTypes.Metadata { - base := p - if p.isFunction() { - if resolved, ok := p.resolveValue(); ok { - base = resolved - } - } - ref := NewCFReferenceWithValue(p.parentRange, *base, p.logicalId) - return iacTypes.NewMetadata(p.Range(), ref.String()) -} - -func (p *Property) MetadataWithValue(resolvedValue *Property) iacTypes.Metadata { - ref := NewCFReferenceWithValue(p.parentRange, *resolvedValue, p.logicalId) - return iacTypes.NewMetadata(p.Range(), ref.String()) + return iacTypes.NewMetadata(p.Range(), p.name). + WithParent(iacTypes.NewMetadata(p.parentRange, p.logicalId)) } func (p *Property) isFunction() bool { diff --git a/pkg/iac/scanners/cloudformation/parser/reference.go b/pkg/iac/scanners/cloudformation/parser/reference.go index 59cbf583c8cf..705eef2747af 100644 --- a/pkg/iac/scanners/cloudformation/parser/reference.go +++ b/pkg/iac/scanners/cloudformation/parser/reference.go @@ -1,15 +1,12 @@ package parser import ( - "fmt" - iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" ) type CFReference struct { logicalId string resourceRange iacTypes.Range - resolvedValue Property } func NewCFReference(id string, resourceRange iacTypes.Range) CFReference { @@ -19,40 +16,6 @@ func NewCFReference(id string, resourceRange iacTypes.Range) CFReference { } } -func NewCFReferenceWithValue(resourceRange iacTypes.Range, resolvedValue Property, logicalId string) CFReference { - return CFReference{ - resourceRange: resourceRange, - resolvedValue: resolvedValue, - logicalId: logicalId, - } -} - func (cf CFReference) String() string { return cf.resourceRange.String() } - -func (cf CFReference) LogicalID() string { - return cf.logicalId -} - -func (cf CFReference) ResourceRange() iacTypes.Range { - return cf.resourceRange -} - -func (cf CFReference) PropertyRange() iacTypes.Range { - if cf.resolvedValue.IsNotNil() { - return cf.resolvedValue.Range() - } - return iacTypes.Range{} -} - -func (cf CFReference) DisplayValue() string { - if cf.resolvedValue.IsNotNil() { - return fmt.Sprintf("%v", cf.resolvedValue.RawValue()) - } - return "" -} - -func (cf *CFReference) Comment() string { - return cf.resolvedValue.Comment() -} diff --git a/pkg/iac/scanners/cloudformation/scanner.go b/pkg/iac/scanners/cloudformation/scanner.go index 4c0cbbc4216d..0920f4425fdb 100644 --- a/pkg/iac/scanners/cloudformation/scanner.go +++ b/pkg/iac/scanners/cloudformation/scanner.go @@ -15,7 +15,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/rules" "github.com/aquasecurity/trivy/pkg/iac/scan" "github.com/aquasecurity/trivy/pkg/iac/scanners" - parser2 "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" "github.com/aquasecurity/trivy/pkg/iac/scanners/options" "github.com/aquasecurity/trivy/pkg/iac/types" ) @@ -23,7 +23,7 @@ import ( func WithParameters(params map[string]any) options.ScannerOption { return func(cs options.ConfigurableScanner) { if s, ok := cs.(*Scanner); ok { - s.addParserOptions(parser2.WithParameters(params)) + s.addParserOptions(parser.WithParameters(params)) } } } @@ -31,7 +31,7 @@ func WithParameters(params map[string]any) options.ScannerOption { func WithParameterFiles(files ...string) options.ScannerOption { return func(cs options.ConfigurableScanner) { if s, ok := cs.(*Scanner); ok { - s.addParserOptions(parser2.WithParameterFiles(files...)) + s.addParserOptions(parser.WithParameterFiles(files...)) } } } @@ -39,7 +39,7 @@ func WithParameterFiles(files ...string) options.ScannerOption { func WithConfigsFS(fsys fs.FS) options.ScannerOption { return func(cs options.ConfigurableScanner) { if s, ok := cs.(*Scanner); ok { - s.addParserOptions(parser2.WithConfigsFS(fsys)) + s.addParserOptions(parser.WithConfigsFS(fsys)) } } } @@ -51,7 +51,7 @@ type Scanner struct { // nolint: gocritic debug debug.Logger policyDirs []string policyReaders []io.Reader - parser *parser2.Parser + parser *parser.Parser regoScanner *rego.Scanner skipRequired bool regoOnly bool @@ -131,7 +131,7 @@ func New(opts ...options.ScannerOption) *Scanner { opt(s) } s.addParserOptions(options.ParserWithSkipRequiredCheck(s.skipRequired)) - s.parser = parser2.New(s.parserOptions...) + s.parser = parser.New(s.parserOptions...) return s } @@ -206,7 +206,7 @@ func (s *Scanner) ScanFile(ctx context.Context, fsys fs.FS, path string) (scan.R return results, nil } -func (s *Scanner) scanFileContext(ctx context.Context, regoScanner *rego.Scanner, cfCtx *parser2.FileContext, fsys fs.FS) (results scan.Results, err error) { +func (s *Scanner) scanFileContext(ctx context.Context, regoScanner *rego.Scanner, cfCtx *parser.FileContext, fsys fs.FS) (results scan.Results, err error) { state := adapter.Adapt(*cfCtx) if state == nil { return nil, nil @@ -247,7 +247,15 @@ func (s *Scanner) scanFileContext(ctx context.Context, regoScanner *rego.Scanner if err != nil { return nil, fmt.Errorf("rego scan error: %w", err) } - return append(results, regoResults...), nil + results = append(results, regoResults...) + + results.Ignore(cfCtx.Ignores, nil) + + for _, ignored := range results.GetIgnored() { + s.debug.Log("Ignored '%s' at '%s'.", ignored.Rule().LongID(), ignored.Range()) + } + + return results, nil } func getDescription(scanResult scan.Result, ref string) string { diff --git a/pkg/iac/scanners/cloudformation/scanner_test.go b/pkg/iac/scanners/cloudformation/scanner_test.go index 6aea88abc1af..0f8299325b82 100644 --- a/pkg/iac/scanners/cloudformation/scanner_test.go +++ b/pkg/iac/scanners/cloudformation/scanner_test.go @@ -2,6 +2,7 @@ package cloudformation import ( "context" + "strings" "testing" "github.com/aquasecurity/trivy/internal/testutil" @@ -101,3 +102,130 @@ deny[res] { }, }, actualCode.Lines) } + +const bucketNameCheck = `# METADATA +# title: "test rego" +# scope: package +# schemas: +# - input: schema["cloud"] +# custom: +# id: AVD-AWS-001 +# avd_id: AVD-AWS-001 +# provider: aws +# service: s3 +# severity: LOW +# input: +# selector: +# - type: cloud +# subtypes: +# - service: s3 +# provider: aws +package user.aws.aws001 + +deny[res] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "test-bucket" + res := result.new("Denied", bucket.name) +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + algo := bucket.encryption.algorithm + algo.value == "AES256" + res := result.new("Denied", algo) +} +` + +func TestIgnore(t *testing.T) { + tests := []struct { + name string + src string + ignored int + }{ + { + name: "without ignored", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket +`, + ignored: 0, + }, + { + name: "rule before resource", + src: `--- +Resources: +#trivy:ignore:AVD-AWS-001 + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket +`, + ignored: 1, + }, + { + name: "rule before property", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: +#trivy:ignore:AVD-AWS-001 + BucketName: test-bucket +`, + ignored: 1, + }, + { + name: "rule on the same line with the property", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket #trivy:ignore:AVD-AWS-001 +`, + ignored: 1, + }, + { + name: "rule on the same line with the nested property", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 #trivy:ignore:AVD-AWS-001 +`, + ignored: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(t, map[string]string{ + "/code/main.yaml": tt.src, + }) + + scanner := New( + options.ScannerWithRegoOnly(true), + options.ScannerWithEmbeddedPolicies(false), + options.ScannerWithPolicyReader(strings.NewReader(bucketNameCheck)), + options.ScannerWithPolicyNamespaces("user"), + ) + + results, err := scanner.ScanFS(context.TODO(), fsys, "code") + require.NoError(t, err) + + if tt.ignored == 0 { + require.Len(t, results.GetFailed(), 1) + } else { + assert.Len(t, results.GetIgnored(), tt.ignored) + } + }) + } +} diff --git a/pkg/iac/scanners/terraform/executor/executor.go b/pkg/iac/scanners/terraform/executor/executor.go index 003b5b7f4db2..e296775b4380 100644 --- a/pkg/iac/scanners/terraform/executor/executor.go +++ b/pkg/iac/scanners/terraform/executor/executor.go @@ -1,39 +1,41 @@ package executor import ( + "fmt" "runtime" "sort" - "strings" "time" + "github.com/zclconf/go-cty/cty" + adapter "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform" "github.com/aquasecurity/trivy/pkg/iac/debug" "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/ignore" "github.com/aquasecurity/trivy/pkg/iac/rego" "github.com/aquasecurity/trivy/pkg/iac/rules" "github.com/aquasecurity/trivy/pkg/iac/scan" "github.com/aquasecurity/trivy/pkg/iac/severity" "github.com/aquasecurity/trivy/pkg/iac/state" "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" ) // Executor scans HCL blocks by running all registered rules against them type Executor struct { - enableIgnores bool - excludedRuleIDs []string - excludeIgnoresIDs []string - includedRuleIDs []string - ignoreCheckErrors bool - workspaceName string - useSingleThread bool - debug debug.Logger - resultsFilters []func(scan.Results) scan.Results - alternativeIDProviderFunc func(string) []string - severityOverrides map[string]string - regoScanner *rego.Scanner - regoOnly bool - stateFuncs []func(*state.State) - frameworks []framework.Framework + enableIgnores bool + excludedRuleIDs []string + includedRuleIDs []string + ignoreCheckErrors bool + workspaceName string + useSingleThread bool + debug debug.Logger + resultsFilters []func(scan.Results) scan.Results + severityOverrides map[string]string + regoScanner *rego.Scanner + regoOnly bool + stateFuncs []func(*state.State) + frameworks []framework.Framework } type Metrics struct { @@ -66,16 +68,11 @@ func New(options ...Option) *Executor { } // Find element in list -func checkInList(id string, altIDs, list []string) bool { +func checkInList(id string, list []string) bool { for _, codeIgnored := range list { if codeIgnored == id { return true } - for _, alt := range altIDs { - if alt == codeIgnored { - return true - } - } } return false } @@ -119,35 +116,36 @@ func (e *Executor) Execute(modules terraform.Modules) (scan.Results, Metrics, er if e.enableIgnores { e.debug.Log("Applying ignores...") - var ignores terraform.Ignores + var ignores ignore.Rules for _, module := range modules { ignores = append(ignores, module.Ignores()...) } - ignores = e.removeExcludedIgnores(ignores) + ignorers := map[string]ignore.Ignorer{ + "ws": func(_ types.Metadata, param any) bool { + ws, ok := param.(string) + if !ok { + return false + } - for i, result := range results { - allIDs := []string{ - result.Rule().LongID(), - result.Rule().AVDID, - strings.ToLower(result.Rule().AVDID), - result.Rule().ShortCode, - } - allIDs = append(allIDs, result.Rule().Aliases...) + return ws == e.workspaceName + }, + "ignore": func(resultMeta types.Metadata, param any) bool { + params, ok := param.(map[string]string) + if !ok { + return false + } - if e.alternativeIDProviderFunc != nil { - allIDs = append(allIDs, e.alternativeIDProviderFunc(result.Rule().LongID())...) - } - if ignores.Covering( - modules, - result.Metadata(), - e.workspaceName, - allIDs..., - ) != nil { - e.debug.Log("Ignored '%s' at '%s'.", result.Rule().LongID(), result.Range()) - results[i].OverrideStatus(scan.StatusIgnored) - } + return ignoreByParams(params, modules, &resultMeta) + }, + } + + results.Ignore(ignores, ignorers) + + for _, ignored := range results.GetIgnored() { + e.debug.Log("Ignored '%s' at '%s'.", ignored.Rule().LongID(), ignored.Range()) } + } else { e.debug.Log("Ignores are disabled.") } @@ -175,25 +173,6 @@ func (e *Executor) Execute(modules terraform.Modules) (scan.Results, Metrics, er return results, metrics, nil } -func (e *Executor) removeExcludedIgnores(ignores terraform.Ignores) terraform.Ignores { - var filteredIgnores terraform.Ignores - for _, ignore := range ignores { - if !contains(e.excludeIgnoresIDs, ignore.RuleID) { - filteredIgnores = append(filteredIgnores, ignore) - } - } - return filteredIgnores -} - -func contains(arr []string, s string) bool { - for _, elem := range arr { - if elem == s { - return true - } - } - return false -} - func (e *Executor) updateSeverity(results []scan.Result) scan.Results { if len(e.severityOverrides) == 0 { return results @@ -202,25 +181,15 @@ func (e *Executor) updateSeverity(results []scan.Result) scan.Results { var overriddenResults scan.Results for _, res := range results { for code, sev := range e.severityOverrides { - - var altMatch bool - if e.alternativeIDProviderFunc != nil { - alts := e.alternativeIDProviderFunc(res.Rule().LongID()) - for _, alt := range alts { - if alt == code { - altMatch = true - break - } - } + if res.Rule().LongID() != code { + continue } - if altMatch || res.Rule().LongID() == code { - overrides := scan.Results([]scan.Result{res}) - override := res.Rule() - override.Severity = severity.Severity(sev) - overrides.SetRule(override) - res = overrides[0] - } + overrides := scan.Results([]scan.Result{res}) + override := res.Rule() + override.Severity = severity.Severity(sev) + overrides.SetRule(override) + res = overrides[0] } overriddenResults = append(overriddenResults, res) } @@ -232,11 +201,7 @@ func (e *Executor) filterResults(results scan.Results) scan.Results { includedOnly := len(e.includedRuleIDs) > 0 for i, result := range results { id := result.Rule().LongID() - var altIDs []string - if e.alternativeIDProviderFunc != nil { - altIDs = e.alternativeIDProviderFunc(id) - } - if (includedOnly && !checkInList(id, altIDs, e.includedRuleIDs)) || checkInList(id, altIDs, e.excludedRuleIDs) { + if (includedOnly && !checkInList(id, e.includedRuleIDs)) || checkInList(id, e.excludedRuleIDs) { e.debug.Log("Excluding '%s' at '%s'.", result.Rule().LongID(), result.Range()) results[i].OverrideStatus(scan.StatusIgnored) } @@ -266,3 +231,40 @@ func (e *Executor) sortResults(results []scan.Result) { } }) } + +func ignoreByParams(params map[string]string, modules terraform.Modules, m *types.Metadata) bool { + if len(params) == 0 { + return true + } + block := modules.GetBlockByIgnoreRange(m) + if block == nil { + return true + } + for key, val := range params { + attr := block.GetAttribute(key) + if attr.IsNil() || !attr.Value().IsKnown() { + return false + } + switch attr.Type() { + case cty.String: + if !attr.Equals(val) { + return false + } + case cty.Number: + bf := attr.Value().AsBigFloat() + f64, _ := bf.Float64() + comparableInt := fmt.Sprintf("%d", int(f64)) + comparableFloat := fmt.Sprintf("%f", f64) + if val != comparableInt && val != comparableFloat { + return false + } + case cty.Bool: + if fmt.Sprintf("%t", attr.IsTrue()) != val { + return false + } + default: + return false + } + } + return true +} diff --git a/pkg/iac/scanners/terraform/executor/option.go b/pkg/iac/scanners/terraform/executor/option.go index d32abb7afdcb..1e9ab5b9d998 100644 --- a/pkg/iac/scanners/terraform/executor/option.go +++ b/pkg/iac/scanners/terraform/executor/option.go @@ -18,12 +18,6 @@ func OptionWithFrameworks(frameworks ...framework.Framework) Option { } } -func OptionWithAlternativeIDProvider(f func(string) []string) Option { - return func(s *Executor) { - s.alternativeIDProviderFunc = f - } -} - func OptionWithResultsFilter(f func(scan.Results) scan.Results) Option { return func(s *Executor) { s.resultsFilters = append(s.resultsFilters, f) @@ -54,12 +48,6 @@ func OptionExcludeRules(ruleIDs []string) Option { } } -func OptionExcludeIgnores(ruleIDs []string) Option { - return func(s *Executor) { - s.excludeIgnoresIDs = ruleIDs - } -} - func OptionIncludeRules(ruleIDs []string) Option { return func(s *Executor) { s.includedRuleIDs = ruleIDs diff --git a/pkg/iac/scanners/terraform/ignore_test.go b/pkg/iac/scanners/terraform/ignore_test.go index 6e561d256653..5cd3f2cdfd89 100644 --- a/pkg/iac/scanners/terraform/ignore_test.go +++ b/pkg/iac/scanners/terraform/ignore_test.go @@ -56,7 +56,7 @@ resource "bad" "my-rule" { } `, assertLength: 0}, {name: "IgnoreLineAboveTheBlockMatchingParamBool", inputOptions: ` -// tfsec:ignore:*[secure=false] +// trivy:ignore:*[secure=false] resource "bad" "my-rule" { secure = false } diff --git a/pkg/iac/scanners/terraform/options.go b/pkg/iac/scanners/terraform/options.go index 2dddb856c049..d78c1f0cf897 100644 --- a/pkg/iac/scanners/terraform/options.go +++ b/pkg/iac/scanners/terraform/options.go @@ -27,14 +27,6 @@ func ScannerWithTFVarsPaths(paths ...string) options.ScannerOption { } } -func ScannerWithAlternativeIDProvider(f func(string) []string) options.ScannerOption { - return func(s options.ConfigurableScanner) { - if tf, ok := s.(ConfigurableTerraformScanner); ok { - tf.AddExecutorOptions(executor.OptionWithAlternativeIDProvider(f)) - } - } -} - func ScannerWithSeverityOverrides(overrides map[string]string) options.ScannerOption { return func(s options.ConfigurableScanner) { if tf, ok := s.(ConfigurableTerraformScanner); ok { @@ -59,14 +51,6 @@ func ScannerWithExcludedRules(ruleIDs []string) options.ScannerOption { } } -func ScannerWithExcludeIgnores(ruleIDs []string) options.ScannerOption { - return func(s options.ConfigurableScanner) { - if tf, ok := s.(ConfigurableTerraformScanner); ok { - tf.AddExecutorOptions(executor.OptionExcludeIgnores(ruleIDs)) - } - } -} - func ScannerWithIncludedRules(ruleIDs []string) options.ScannerOption { return func(s options.ConfigurableScanner) { if tf, ok := s.(ConfigurableTerraformScanner); ok { diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index 1fe9a72fdcac..e7e3415e1b52 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -14,6 +14,7 @@ import ( "golang.org/x/exp/slices" "github.com/aquasecurity/trivy/pkg/iac/debug" + "github.com/aquasecurity/trivy/pkg/iac/ignore" "github.com/aquasecurity/trivy/pkg/iac/terraform" tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context" "github.com/aquasecurity/trivy/pkg/iac/types" @@ -32,7 +33,7 @@ type evaluator struct { projectRootPath string // root of the current scan modulePath string moduleName string - ignores terraform.Ignores + ignores ignore.Rules parentParser *Parser debug debug.Logger allowDownloads bool @@ -50,7 +51,7 @@ func newEvaluator( inputVars map[string]cty.Value, moduleMetadata *modulesMetadata, workspace string, - ignores []terraform.Ignore, + ignores ignore.Rules, logger debug.Logger, allowDownloads bool, skipCachedModules bool, diff --git a/pkg/iac/scanners/terraform/parser/load_blocks.go b/pkg/iac/scanners/terraform/parser/load_blocks.go deleted file mode 100644 index c5409d42f27b..000000000000 --- a/pkg/iac/scanners/terraform/parser/load_blocks.go +++ /dev/null @@ -1,131 +0,0 @@ -package parser - -import ( - "fmt" - "regexp" - "strings" - "time" - - "github.com/hashicorp/hcl/v2" - - "github.com/aquasecurity/trivy/pkg/iac/terraform" - "github.com/aquasecurity/trivy/pkg/iac/types" -) - -func loadBlocksFromFile(file sourceFile, moduleSource string) (hcl.Blocks, []terraform.Ignore, error) { - ignores := parseIgnores(file.file.Bytes, file.path, moduleSource) - contents, diagnostics := file.file.Body.Content(terraform.Schema) - if diagnostics != nil && diagnostics.HasErrors() { - return nil, nil, diagnostics - } - if contents == nil { - return nil, nil, nil - } - return contents.Blocks, ignores, nil -} - -func parseIgnores(data []byte, path, moduleSource string) []terraform.Ignore { - var ignores []terraform.Ignore - for i, line := range strings.Split(string(data), "\n") { - line = strings.TrimSpace(line) - lineIgnores := parseIgnoresFromLine(line) - for _, lineIgnore := range lineIgnores { - lineIgnore.Range = types.NewRange(path, i+1, i+1, moduleSource, nil) - ignores = append(ignores, lineIgnore) - } - } - for a, ignoreA := range ignores { - if !ignoreA.Block { - continue - } - for _, ignoreB := range ignores { - if !ignoreB.Block { - continue - } - if ignoreA.Range.GetStartLine()+1 == ignoreB.Range.GetStartLine() { - ignoreA.Range = ignoreB.Range - ignores[a] = ignoreA - } - } - } - return ignores - -} - -var commentPattern = regexp.MustCompile(`^\s*([/]+|/\*|#)+\s*tfsec:`) -var trivyCommentPattern = regexp.MustCompile(`^\s*([/]+|/\*|#)+\s*trivy:`) - -func parseIgnoresFromLine(input string) []terraform.Ignore { - - var ignores []terraform.Ignore - - input = commentPattern.ReplaceAllString(input, "tfsec:") - input = trivyCommentPattern.ReplaceAllString(input, "trivy:") - - bits := strings.Split(strings.TrimSpace(input), " ") - for i, bit := range bits { - bit := strings.TrimSpace(bit) - bit = strings.TrimPrefix(bit, "#") - bit = strings.TrimPrefix(bit, "//") - bit = strings.TrimPrefix(bit, "/*") - - if strings.HasPrefix(bit, "tfsec:") || strings.HasPrefix(bit, "trivy:") { - ignore, err := parseIgnoreFromComment(bit) - if err != nil { - continue - } - ignore.Block = i == 0 - ignores = append(ignores, *ignore) - } - } - - return ignores -} - -func parseIgnoreFromComment(input string) (*terraform.Ignore, error) { - var ignore terraform.Ignore - if !strings.HasPrefix(input, "tfsec:") && !strings.HasPrefix(input, "trivy:") { - return nil, fmt.Errorf("invalid ignore") - } - - input = input[6:] - - segments := strings.Split(input, ":") - - for i := 0; i < len(segments)-1; i += 2 { - key := segments[i] - val := segments[i+1] - switch key { - case "ignore": - ignore.RuleID, ignore.Params = parseIDWithParams(val) - case "exp": - parsed, err := time.Parse("2006-01-02", val) - if err != nil { - return &ignore, err - } - ignore.Expiry = &parsed - case "ws": - ignore.Workspace = val - } - } - - return &ignore, nil -} - -func parseIDWithParams(input string) (string, map[string]string) { - params := make(map[string]string) - if !strings.Contains(input, "[") { - return input, params - } - parts := strings.Split(input, "[") - id := parts[0] - paramStr := strings.TrimSuffix(parts[1], "]") - for _, pair := range strings.Split(paramStr, ",") { - parts := strings.Split(pair, "=") - if len(parts) != 2 { - continue - } - params[parts[0]] = parts[1] - } - return id, params -} diff --git a/pkg/iac/scanners/terraform/parser/load_blocks_test.go b/pkg/iac/scanners/terraform/parser/load_blocks_test.go deleted file mode 100644 index e32d19a75044..000000000000 --- a/pkg/iac/scanners/terraform/parser/load_blocks_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package parser - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParsingDoubleComment(t *testing.T) { - ignores := parseIgnoresFromLine("## tfsec:ignore:abc") - assert.Equal(t, 1, len(ignores)) - assert.Truef(t, ignores[0].Block, "Expected ignore to be a block") -} diff --git a/pkg/iac/scanners/terraform/parser/parser.go b/pkg/iac/scanners/terraform/parser/parser.go index e09e9e621ef4..35a8b454f00f 100644 --- a/pkg/iac/scanners/terraform/parser/parser.go +++ b/pkg/iac/scanners/terraform/parser/parser.go @@ -17,6 +17,7 @@ import ( "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" @@ -326,12 +327,12 @@ func (p *Parser) GetFilesystemMap() map[string]fs.FS { return p.fsMap } -func (p *Parser) readBlocks(files []sourceFile) (terraform.Blocks, terraform.Ignores, error) { +func (p *Parser) readBlocks(files []sourceFile) (terraform.Blocks, ignore.Rules, error) { var blocks terraform.Blocks - var ignores terraform.Ignores + var ignores ignore.Rules moduleCtx := tfcontext.NewContext(&hcl.EvalContext{}, nil) for _, file := range files { - fileBlocks, fileIgnores, err := loadBlocksFromFile(file, p.moduleSource) + fileBlocks, err := loadBlocksFromFile(file) if err != nil { if p.stopOnHCLError { return nil, nil, err @@ -342,9 +343,61 @@ func (p *Parser) readBlocks(files []sourceFile) (terraform.Blocks, terraform.Ign 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", + }, + ¶mParser{}, + ) 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 +} diff --git a/pkg/iac/scanners/terraform/scanner_test.go b/pkg/iac/scanners/terraform/scanner_test.go index 9e44893e0ff7..dbc2d67c3c64 100644 --- a/pkg/iac/scanners/terraform/scanner_test.go +++ b/pkg/iac/scanners/terraform/scanner_test.go @@ -68,42 +68,6 @@ func scanWithOptions(t *testing.T, code string, opt ...options.ScannerOption) sc return results } -func Test_OptionWithAlternativeIDProvider(t *testing.T) { - reg := rules.Register(alwaysFailRule) - defer rules.Deregister(reg) - - options := []options.ScannerOption{ - ScannerWithAlternativeIDProvider(func(s string) []string { - return []string{"something", "altid", "blah"} - }), - } - results := scanWithOptions(t, ` -//tfsec:ignore:altid -resource "something" "else" {} -`, options...) - require.Len(t, results.GetFailed(), 0) - require.Len(t, results.GetIgnored(), 1) - -} - -func Test_TrivyOptionWithAlternativeIDProvider(t *testing.T) { - reg := rules.Register(alwaysFailRule) - defer rules.Deregister(reg) - - options := []options.ScannerOption{ - ScannerWithAlternativeIDProvider(func(s string) []string { - return []string{"something", "altid", "blah"} - }), - } - results := scanWithOptions(t, ` -//trivy:ignore:altid -resource "something" "else" {} -`, options...) - require.Len(t, results.GetFailed(), 0) - require.Len(t, results.GetIgnored(), 1) - -} - func Test_OptionWithSeverityOverrides(t *testing.T) { reg := rules.Register(alwaysFailRule) defer rules.Deregister(reg) diff --git a/pkg/iac/terraform/ignore.go b/pkg/iac/terraform/ignore.go deleted file mode 100644 index e52fbf202be5..000000000000 --- a/pkg/iac/terraform/ignore.go +++ /dev/null @@ -1,100 +0,0 @@ -package terraform - -import ( - "fmt" - "time" - - "github.com/zclconf/go-cty/cty" - - iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" -) - -type Ignore struct { - Range iacTypes.Range - RuleID string - Expiry *time.Time - Workspace string - Block bool - Params map[string]string -} - -type Ignores []Ignore - -func (ignores Ignores) Covering(modules Modules, m iacTypes.Metadata, workspace string, ids ...string) *Ignore { - for _, ignore := range ignores { - if ignore.Covering(modules, m, workspace, ids...) { - return &ignore - } - } - return nil -} - -func (ignore Ignore) Covering(modules Modules, m iacTypes.Metadata, workspace string, ids ...string) bool { - if ignore.Expiry != nil && time.Now().After(*ignore.Expiry) { - return false - } - if ignore.Workspace != "" && ignore.Workspace != workspace { - return false - } - idMatch := ignore.RuleID == "*" || len(ids) == 0 - for _, id := range ids { - if id == ignore.RuleID { - idMatch = true - break - } - } - if !idMatch { - return false - } - - metaHierarchy := &m - for metaHierarchy != nil { - if ignore.Range.GetFilename() != metaHierarchy.Range().GetFilename() { - metaHierarchy = metaHierarchy.Parent() - continue - } - if metaHierarchy.Range().GetStartLine() == ignore.Range.GetStartLine()+1 || metaHierarchy.Range().GetStartLine() == ignore.Range.GetStartLine() { - return ignore.MatchParams(modules, metaHierarchy) - } - metaHierarchy = metaHierarchy.Parent() - } - return false - -} - -func (ignore Ignore) MatchParams(modules Modules, blockMetadata *iacTypes.Metadata) bool { - if len(ignore.Params) == 0 { - return true - } - block := modules.GetBlockByIgnoreRange(blockMetadata) - if block == nil { - return true - } - for key, val := range ignore.Params { - attr := block.GetAttribute(key) - if attr.IsNil() || !attr.Value().IsKnown() { - return false - } - switch attr.Type() { - case cty.String: - if !attr.Equals(val) { - return false - } - case cty.Number: - bf := attr.Value().AsBigFloat() - f64, _ := bf.Float64() - comparableInt := fmt.Sprintf("%d", int(f64)) - comparableFloat := fmt.Sprintf("%f", f64) - if val != comparableInt && val != comparableFloat { - return false - } - case cty.Bool: - if fmt.Sprintf("%t", attr.IsTrue()) != val { - return false - } - default: - return false - } - } - return true -} diff --git a/pkg/iac/terraform/module.go b/pkg/iac/terraform/module.go index dd89fa2bd40d..fec6ad7c8d0e 100644 --- a/pkg/iac/terraform/module.go +++ b/pkg/iac/terraform/module.go @@ -3,6 +3,8 @@ package terraform import ( "fmt" "strings" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" ) type Module struct { @@ -10,11 +12,11 @@ type Module struct { blockMap map[string]Blocks rootPath string modulePath string - ignores Ignores + ignores ignore.Rules parent *Module } -func NewModule(rootPath, modulePath string, blocks Blocks, ignores Ignores) *Module { +func NewModule(rootPath, modulePath string, blocks Blocks, ignores ignore.Rules) *Module { blockMap := make(map[string]Blocks) @@ -41,7 +43,7 @@ func (c *Module) RootPath() string { return c.rootPath } -func (c *Module) Ignores() Ignores { +func (c *Module) Ignores() ignore.Rules { return c.ignores }