Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloudformation): inline ignore support for YAML templates #6358

Merged
merged 3 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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