Skip to content

Commit

Permalink
feat(cloudformation): support inline ignores
Browse files Browse the repository at this point in the history
  • Loading branch information
nikpivkin committed Mar 20, 2024
1 parent 8ec3938 commit 239e279
Show file tree
Hide file tree
Showing 21 changed files with 603 additions and 467 deletions.
145 changes: 145 additions & 0 deletions pkg/iac/ignore/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package ignore

import (
"strings"
"time"

"github.com/aquasecurity/trivy/pkg/iac/types"
"github.com/aquasecurity/trivy/pkg/log"
)

// Parse parses the configuration file and returns the Rules
func Parse(src, path string, parsers ...RuleFragmentParser) 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 []RuleFragmentParser) []Rule {
var ignores []Rule

fragments := strings.Split(strings.TrimSpace(line), " ")
for i, fragment := range fragments {
fragment := strings.TrimSpace(fragment)
fragment = strings.TrimLeftFunc(fragment, func(r rune) bool {
return r == '#' || r == '/' || r == '*'
})

fragment, exists := hasIgnoreRulePrefix(fragment)
if !exists {
continue
}

ignore, err := parseComment(fragment, rng, parsers)
if err != nil {
continue
}
ignore.block = i == 0
ignores = append(ignores, ignore)
}

return ignores
}

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 []RuleFragmentParser) (Rule, error) {
rule := Rule{
rng: rng,
parsers: make(map[string]RuleFragmentParser),
}

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 := new(checkIDParser)
if idParser.Parse(val) {
rule.parsers["id"] = idParser
}
}

for _, parser := range parsers {
if parser.ID() != key {
continue
}

if parser.Parse(val) {
rule.parsers[parser.ID()] = parser
}
}
}

return rule, nil
}

type checkIDParser struct {
id string
}

func (s *checkIDParser) ID() string {
return "id"
}

func (s *checkIDParser) Parse(str string) bool {
if idx := strings.Index(str, "["); idx != -1 {
str = str[:idx]
}
s.id = str
return str != ""
}

func (s *checkIDParser) Param() any {
return s.id
}

type expiryDateParser struct {
rng types.Range
expiry time.Time
}

func (s *expiryDateParser) ID() 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
}
109 changes: 109 additions & 0 deletions pkg/iac/ignore/rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package ignore

import (
"slices"
"time"

"github.com/samber/lo"

"github.com/aquasecurity/trivy/pkg/iac/types"
)

// RuleFragmentParser defines the interface for parsing ignore rules.
type RuleFragmentParser interface {
ID() string
Parse(string) bool
Param() any
}

// 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
block bool
parsers map[string]RuleFragmentParser
}

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 spec, exists := r.parsers[ignoreID]; exists {
if !ignore(*matchMeta, spec.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)
if !ok {
return false
}
return 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)
},
}
}
6 changes: 2 additions & 4 deletions pkg/iac/scan/code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions pkg/iac/scan/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/iac/scanners/cloudformation/parser/file_context.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package parser

import (
"github.com/aquasecurity/trivy/pkg/iac/ignore"
iacTypes "github.com/aquasecurity/trivy/pkg/iac/types"
)

Expand All @@ -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"`
Expand Down
11 changes: 7 additions & 4 deletions pkg/iac/scanners/cloudformation/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
}
Expand Down
22 changes: 10 additions & 12 deletions pkg/iac/scanners/cloudformation/parser/property.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,17 @@ 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())
}
// base := p
// if p.isFunction() {
// if resolved, ok := p.resolveValue(); ok {
// base = resolved
// }
// }

// ref := NewCFReferenceWithValue(p.parentRange, *base, p.logicalId)

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 {
Expand Down
Loading

0 comments on commit 239e279

Please sign in to comment.