Skip to content

Commit

Permalink
Handle json nested value checks (#867)
Browse files Browse the repository at this point in the history
* Handle json nested value checks

custom checks written in json present as map[string]interface{} rather
than the yaml map[interface{}]interface{} so switch needs to cover both
options

* Fix up the rewriting of severity

Custom checks have a string severity which might be outdated, rewrite
before validating
  • Loading branch information
Owen Rumney committed Jul 14, 2021
1 parent 490358b commit ecbb3e4
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 28 deletions.
20 changes: 19 additions & 1 deletion example/custom_functions/.tfsec/functions_tfchecks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,22 @@ checks:
- resource
severity: HIGH
requiredLabels:
- aws_lb
- aws_lb
- code: TAG_Environment
description: Custom check to ensure the Environment tag is applied
errorMessage: The required Environment tag was missing
matchSpec:
action: contains
name: tags
value:
Environment:
action: isAny
value:
- production
- test
- development
requiredTypes:
- resource
severity: ERROR
requiredLabels:
- aws_instance
9 changes: 7 additions & 2 deletions example/custom_functions/main.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
resource "aws_lb" "test_lb" {
resource "aws_instance" "bastion" {
metadata_options {
http_endpoint = "enabled"
http_put_response_hop_limit = 1
http_tokens = "required"
}

tags = {
Environment = "uat"
Environment = "test"
}
}
13 changes: 11 additions & 2 deletions internal/app/tfsec/block/hclattribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,20 @@ func (attr *HCLAttribute) listContains(val cty.Value, stringToLookFor string, ig
}

func (attr *HCLAttribute) mapContains(checkValue interface{}, val cty.Value) bool {
valueMap := val.AsValueMap()
switch t := checkValue.(type) {
case map[interface{}]interface{}:
for k, v := range t {
valueMap := val.AsValueMap()
for key, value := range valueMap {
rawValue := getRawValue(value)
if key == k && evaluate(v, rawValue) {
return true
}
}
}
return false
case map[string]interface{}:
for k, v := range t {
for key, value := range valueMap {
rawValue := getRawValue(value)
if key == k && evaluate(v, rawValue) {
Expand All @@ -141,7 +151,6 @@ func (attr *HCLAttribute) mapContains(checkValue interface{}, val cty.Value) boo
}
return false
default:
valueMap := val.AsValueMap()
for key := range valueMap {
if key == checkValue {
return true
Expand Down
22 changes: 18 additions & 4 deletions internal/app/tfsec/block/value_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,25 @@ func evaluate(criteriaValue interface{}, testValue interface{}) bool {
switch t := criteriaValue.(type) {
case map[interface{}]interface{}:
if t[functionNameKey] != nil {
functionName := t[functionNameKey].(string)
if functions[functionName] != nil {
return functions[functionName](t[valueNameKey], testValue)
}
return executeFunction(t[functionNameKey].(string), t[valueNameKey], testValue)
}
case map[string]interface{}:
if t[functionNameKey] != nil {
return executeFunction(t[functionNameKey].(string), t[valueNameKey], testValue)
}
default:
return t == testValue
}
return false
}

func executeFunction(functionName string, criteriaValues, testValue interface{}) bool {
if functions[functionName] != nil {
return functions[functionName](criteriaValues, testValue)
}
return false
}

func isAny(criteriaValues interface{}, testValue interface{}) bool {
switch t := criteriaValues.(type) {
case []interface{}:
Expand All @@ -33,6 +41,12 @@ func isAny(criteriaValues interface{}, testValue interface{}) bool {
return true
}
}
case []string:
for _, v := range t {
if v == testValue.(string) {
return true
}
}
}
return false
}
Expand Down
14 changes: 2 additions & 12 deletions internal/app/tfsec/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"

"github.com/aquasecurity/tfsec/pkg/severity"
"gopkg.in/yaml.v2"
)

Expand Down Expand Up @@ -52,18 +53,7 @@ func LoadConfig(configFilePath string) (*Config, error) {
func rewriteSeverityOverrides(config *Config) error {

for k, s := range config.SeverityOverrides {
switch strings.ToUpper(s) {
case "CRITICAL", "HIGH", "MEDIUM", "LOW":
continue
case "ERROR":
config.SeverityOverrides[k] = "HIGH"
case "WARNING":
config.SeverityOverrides[k] = "MEDIUM"
case "INFO":
config.SeverityOverrides[k] = "LOW"
default:
return fmt.Errorf("could not rewrite the severity code [%s]", s)
}
config.SeverityOverrides[k] = string(severity.StringToSeverity(s))
}

return nil
Expand Down
7 changes: 6 additions & 1 deletion internal/app/tfsec/custom/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"encoding/json"
"errors"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"

"github.com/aquasecurity/tfsec/pkg/severity"
"gopkg.in/yaml.v2"
)

type ChecksFile struct {
Expand Down Expand Up @@ -78,6 +80,9 @@ func loadCheckFile(checkFilePath string) (ChecksFile, error) {
return checks, fmt.Errorf("couldn't process the file %s", checkFilePath)
}

for _, check := range checks.Checks {
check.Severity = severity.StringToSeverity(string(check.Severity))
}
return checks, nil
}

Expand Down
12 changes: 6 additions & 6 deletions internal/app/tfsec/custom/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

func Validate(checkFilePath string) error {
if _, err := os.Stat(checkFilePath); os.IsNotExist(err) {
return errors.New(fmt.Sprintf("check file could not be found at path %s", checkFilePath))
return fmt.Errorf("check file could not be found at path %s", checkFilePath)
}

checkFile, err := loadCheckFile(checkFilePath)
Expand All @@ -30,7 +30,7 @@ func Validate(checkFilePath string) error {
return errors.New("check json is not valid")
}
errorStrings := getErrorStrings(errs)
return errors.New(fmt.Sprintf("check failed with the following errors;\n\n - %s\n\n%s\n", errorStrings, jsonContent))
return fmt.Errorf("check failed with the following errors;\n\n - %s\n\n%s\n", errorStrings, jsonContent)
}
return nil
}(check); err != nil {
Expand Down Expand Up @@ -60,7 +60,7 @@ func validate(check *Check) []error {
checkErrors = append(checkErrors, errors.New("check.Description requires a value"))
}
if !check.Severity.IsValid() {
checkErrors = append(checkErrors, errors.New(fmt.Sprintf("check.Severity[%s] is not a recognised option. Should be %s", check.Severity, severity.ValidSeverity)))
checkErrors = append(checkErrors, fmt.Errorf("check.Severity[%s] is not a recognised option. Should be %s", check.Severity, severity.ValidSeverity))
}
if len(check.RequiredTypes) == 0 {
checkErrors = append(checkErrors, errors.New("check.RequiredTypes requires a value"))
Expand All @@ -73,7 +73,7 @@ func validate(check *Check) []error {

func validateMatchSpec(spec *MatchSpec, check *Check, checkErrors []error) []error {
if !spec.Action.isValid() {
checkErrors = append(checkErrors, errors.New(fmt.Sprintf("matchSpec.Action[%s] is not a recognised option. Should be %s", spec.Action, ValidCheckActions)))
checkErrors = append(checkErrors, fmt.Errorf("matchSpec.Action[%s] is not a recognised option. Should be %s", spec.Action, ValidCheckActions))
}
// if the check is one of `inModule`,`or`,`and`, `not`, no name is required
if len(spec.Name) == 0 && spec.Action != "inModule" && spec.Action != "or" && spec.Action != "and" && spec.Action != "not" {
Expand All @@ -83,14 +83,14 @@ func validateMatchSpec(spec *MatchSpec, check *Check, checkErrors []error) []err
// if the check is one of `or`, `and`, then all PredicateMatchSpec's must also be valid
if spec.Action == "or" || spec.Action == "and" {
for _, predicateMatchSpec := range spec.PredicateMatchSpec {
checkErrors = append(validateMatchSpec(&predicateMatchSpec, check, checkErrors))
checkErrors = append(checkErrors, validateMatchSpec(&predicateMatchSpec, check, checkErrors)...)
}
}

// `not` specification can only have a single predicateMatchSpec associated, which must be valid
if spec.Action == "not" {
if len(spec.PredicateMatchSpec) == 1 {
checkErrors = append(validateMatchSpec(&spec.PredicateMatchSpec[0], check, checkErrors))
checkErrors = append(checkErrors, validateMatchSpec(&spec.PredicateMatchSpec[0], check, checkErrors)...)
} else {
checkErrors = append(checkErrors, errors.New("`not` action must have a single predicate attached"))
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/severity/severity.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package severity

import (
"strings"
)

type Severity string

const (
Expand All @@ -26,3 +30,19 @@ func (s *Severity) IsValid() bool {
func (s *Severity) Valid() []Severity {
return ValidSeverity
}

func StringToSeverity(sev string) Severity {
s := strings.ToUpper(sev)
switch s {
case "CRITICAL", "HIGH", "MEDIUM", "LOW":
return Severity(s)
case "ERROR":
return High
case "WARNING":
return Medium
case "INFO":
return Low
default:
return None
}
}

0 comments on commit ecbb3e4

Please sign in to comment.