Skip to content

Commit df024e8

Browse files
authored
feat(cloudformation): inline ignore support for YAML templates (aquasecurity#6358)
1 parent 29dee32 commit df024e8

File tree

22 files changed

+856
-468
lines changed

22 files changed

+856
-468
lines changed

docs/docs/scanner/misconfiguration/index.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ If multiple variables evaluate to the same hostname, Trivy will choose the envir
381381

382382
### Skipping resources by inline comments
383383

384-
Trivy supports ignoring misconfigured resources by inline comments for Terraform configuration files only.
384+
Trivy supports ignoring misconfigured resources by inline comments for Terraform and CloudFormation configuration files only.
385385

386386
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).
387387

@@ -422,6 +422,17 @@ As an example, consider the following check metadata:
422422

423423
Long ID would look like the following: `aws-s3-enable-logging`.
424424

425+
Example for CloudFromation:
426+
```yaml
427+
AWSTemplateFormatVersion: "2010-09-09"
428+
Resources:
429+
#trivy:ignore:*
430+
S3Bucket:
431+
Type: 'AWS::S3::Bucket'
432+
Properties:
433+
BucketName: test-bucket
434+
```
435+
425436
#### Expiration Date
426437
427438
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:

pkg/iac/ignore/parse.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package ignore
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"time"
7+
8+
"github.com/aquasecurity/trivy/pkg/iac/types"
9+
"github.com/aquasecurity/trivy/pkg/log"
10+
)
11+
12+
// RuleSectionParser defines the interface for parsing ignore rules.
13+
type RuleSectionParser interface {
14+
Key() string
15+
Parse(string) bool
16+
Param() any
17+
}
18+
19+
// Parse parses the configuration file and returns the Rules
20+
func Parse(src, path string, parsers ...RuleSectionParser) Rules {
21+
var rules Rules
22+
for i, line := range strings.Split(src, "\n") {
23+
line = strings.TrimSpace(line)
24+
rng := types.NewRange(path, i+1, i+1, "", nil)
25+
lineIgnores := parseLine(line, rng, parsers)
26+
for _, lineIgnore := range lineIgnores {
27+
rules = append(rules, lineIgnore)
28+
}
29+
}
30+
31+
rules.shift()
32+
33+
return rules
34+
}
35+
36+
func parseLine(line string, rng types.Range, parsers []RuleSectionParser) []Rule {
37+
var rules []Rule
38+
39+
sections := strings.Split(strings.TrimSpace(line), " ")
40+
for _, section := range sections {
41+
section := strings.TrimSpace(section)
42+
section = strings.TrimLeftFunc(section, func(r rune) bool {
43+
return r == '#' || r == '/' || r == '*'
44+
})
45+
46+
section, exists := hasIgnoreRulePrefix(section)
47+
if !exists {
48+
continue
49+
}
50+
51+
rule, err := parseComment(section, rng, parsers)
52+
if err != nil {
53+
log.Logger.Debugf("Failed to parse rule at %s: %s", rng.String(), err.Error())
54+
continue
55+
}
56+
rules = append(rules, rule)
57+
}
58+
59+
return rules
60+
}
61+
62+
func hasIgnoreRulePrefix(s string) (string, bool) {
63+
for _, prefix := range []string{"tfsec:", "trivy:"} {
64+
if after, found := strings.CutPrefix(s, prefix); found {
65+
return after, true
66+
}
67+
}
68+
69+
return "", false
70+
}
71+
72+
func parseComment(input string, rng types.Range, parsers []RuleSectionParser) (Rule, error) {
73+
rule := Rule{
74+
rng: rng,
75+
sections: make(map[string]any),
76+
}
77+
78+
parsers = append(parsers, &expiryDateParser{
79+
rng: rng,
80+
})
81+
82+
segments := strings.Split(input, ":")
83+
84+
for i := 0; i < len(segments)-1; i += 2 {
85+
key := segments[i]
86+
val := segments[i+1]
87+
if key == "ignore" {
88+
// special case, because id and parameters are in the same section
89+
idParser := &checkIDParser{
90+
StringMatchParser{SectionKey: "id"},
91+
}
92+
if idParser.Parse(val) {
93+
rule.sections[idParser.Key()] = idParser.Param()
94+
}
95+
}
96+
97+
for _, parser := range parsers {
98+
if parser.Key() != key {
99+
continue
100+
}
101+
102+
if parser.Parse(val) {
103+
rule.sections[parser.Key()] = parser.Param()
104+
}
105+
}
106+
}
107+
108+
if _, exists := rule.sections["id"]; !exists {
109+
return Rule{}, errors.New("rule section with the `ignore` key is required")
110+
}
111+
112+
return rule, nil
113+
}
114+
115+
type StringMatchParser struct {
116+
SectionKey string
117+
param string
118+
}
119+
120+
func (s *StringMatchParser) Key() string {
121+
return s.SectionKey
122+
}
123+
124+
func (s *StringMatchParser) Parse(str string) bool {
125+
s.param = str
126+
return str != ""
127+
}
128+
129+
func (s *StringMatchParser) Param() any {
130+
return s.param
131+
}
132+
133+
type checkIDParser struct {
134+
StringMatchParser
135+
}
136+
137+
func (s *checkIDParser) Parse(str string) bool {
138+
if idx := strings.Index(str, "["); idx != -1 {
139+
str = str[:idx]
140+
}
141+
return s.StringMatchParser.Parse(str)
142+
}
143+
144+
type expiryDateParser struct {
145+
rng types.Range
146+
expiry time.Time
147+
}
148+
149+
func (s *expiryDateParser) Key() string {
150+
return "exp"
151+
}
152+
153+
func (s *expiryDateParser) Parse(str string) bool {
154+
parsed, err := time.Parse("2006-01-02", str)
155+
if err != nil {
156+
log.Logger.Debugf("Incorrect time to ignore is specified: %s", str)
157+
parsed = time.Time{}
158+
} else if time.Now().After(parsed) {
159+
log.Logger.Debug("Ignore rule time has expired for location: %s", s.rng.String())
160+
}
161+
162+
s.expiry = parsed
163+
return true
164+
}
165+
166+
func (s *expiryDateParser) Param() any {
167+
return s.expiry
168+
}

pkg/iac/ignore/rule.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package ignore
2+
3+
import (
4+
"slices"
5+
"time"
6+
7+
"github.com/samber/lo"
8+
9+
"github.com/aquasecurity/trivy/pkg/iac/types"
10+
)
11+
12+
// Ignorer represents a function that checks if the rule should be ignored.
13+
type Ignorer func(resultMeta types.Metadata, param any) bool
14+
15+
type Rules []Rule
16+
17+
// Ignore checks if the rule should be ignored based on provided metadata, IDs, and ignorer functions.
18+
func (r Rules) Ignore(m types.Metadata, ids []string, ignorers map[string]Ignorer) bool {
19+
return slices.ContainsFunc(r, func(r Rule) bool {
20+
return r.ignore(m, ids, ignorers)
21+
})
22+
}
23+
24+
func (r Rules) shift() {
25+
var (
26+
currentRange *types.Range
27+
offset int
28+
)
29+
30+
for i := len(r) - 1; i > 0; i-- {
31+
currentIgnore, nextIgnore := r[i], r[i-1]
32+
if currentRange == nil {
33+
currentRange = &currentIgnore.rng
34+
}
35+
if nextIgnore.rng.GetStartLine()+1+offset == currentIgnore.rng.GetStartLine() {
36+
r[i-1].rng = *currentRange
37+
offset++
38+
} else {
39+
currentRange = nil
40+
offset = 0
41+
}
42+
}
43+
}
44+
45+
// Rule represents a rule for ignoring vulnerabilities.
46+
type Rule struct {
47+
rng types.Range
48+
sections map[string]any
49+
}
50+
51+
func (r Rule) ignore(m types.Metadata, ids []string, ignorers map[string]Ignorer) bool {
52+
matchMeta, ok := r.matchRange(&m)
53+
if !ok {
54+
return false
55+
}
56+
57+
ignorers = lo.Assign(defaultIgnorers(ids), ignorers)
58+
59+
for ignoreID, ignore := range ignorers {
60+
if param, exists := r.sections[ignoreID]; exists {
61+
if !ignore(*matchMeta, param) {
62+
return false
63+
}
64+
}
65+
}
66+
67+
return true
68+
}
69+
70+
func (r Rule) matchRange(m *types.Metadata) (*types.Metadata, bool) {
71+
metaHierarchy := m
72+
for metaHierarchy != nil {
73+
if r.rng.GetFilename() != metaHierarchy.Range().GetFilename() {
74+
metaHierarchy = metaHierarchy.Parent()
75+
continue
76+
}
77+
if metaHierarchy.Range().GetStartLine() == r.rng.GetStartLine()+1 ||
78+
metaHierarchy.Range().GetStartLine() == r.rng.GetStartLine() {
79+
return metaHierarchy, true
80+
}
81+
metaHierarchy = metaHierarchy.Parent()
82+
}
83+
84+
return nil, false
85+
}
86+
87+
func defaultIgnorers(ids []string) map[string]Ignorer {
88+
return map[string]Ignorer{
89+
"id": func(_ types.Metadata, param any) bool {
90+
id, ok := param.(string)
91+
return ok && (id == "*" || len(ids) == 0 || slices.Contains(ids, id))
92+
},
93+
"exp": func(_ types.Metadata, param any) bool {
94+
expiry, ok := param.(time.Time)
95+
return ok && time.Now().Before(expiry)
96+
},
97+
}
98+
}

0 commit comments

Comments
 (0)