Skip to content

Commit

Permalink
feat(cloudformation): inline ignore support for YAML templates (#6358)
Browse files Browse the repository at this point in the history
  • Loading branch information
nikpivkin authored Mar 29, 2024
1 parent 29dee32 commit df024e8
Show file tree
Hide file tree
Showing 22 changed files with 856 additions and 468 deletions.
13 changes: 12 additions & 1 deletion docs/docs/scanner/misconfiguration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<rule>` immediately following the format-specific line-comment [token](https://developer.hashicorp.com/terraform/language/syntax/configuration#comments).

Expand Down Expand Up @@ -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:
Expand Down
168 changes: 168 additions & 0 deletions pkg/iac/ignore/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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 _, 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
}
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
}
98 changes: 98 additions & 0 deletions pkg/iac/ignore/rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 = &currentIgnore.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
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)
},
}
}
Loading

0 comments on commit df024e8

Please sign in to comment.