diff --git a/checks/cloud/aws/apigateway/no_public_access.rego b/checks/cloud/aws/apigateway/no_public_access.rego index fad776c7..d992d25a 100644 --- a/checks/cloud/aws/apigateway/no_public_access.rego +++ b/checks/cloud/aws/apigateway/no_public_access.rego @@ -35,11 +35,11 @@ deny contains res if { isManaged(api) some method in api.resources[_].methods not method_is_option(method) - not is_apikey_required(api) + not is_apikey_required(method) method.authorizationtype.value == authorization_none res := result.new("Authorization is not enabled for this method.", method.authorizationtype) } method_is_option(method) := method.httpmethod.value == "OPTION" -is_apikey_required(api) := api.apikeyrequired.value +is_apikey_required(method) := method.apikeyrequired.value diff --git a/checks/cloud/aws/apigateway/no_public_access_test.rego b/checks/cloud/aws/apigateway/no_public_access_test.rego index 1fad77fd..54a55d86 100644 --- a/checks/cloud/aws/apigateway/no_public_access_test.rego +++ b/checks/cloud/aws/apigateway/no_public_access_test.rego @@ -19,7 +19,7 @@ test_allow_get_method_with_auth if { } test_allow_if_api_required if { - test.assert_empty(check.deny) with input as input_with_method({"httpmethod": {"value": "GET"}, "authorizationtype": {"value": "AWS_IAM"}}) + test.assert_empty(check.deny) with input as input_with_method({"httpmethod": {"value": "GET"}, "authorizationtype": {"value": "AWS_IAM"}, "apikeyrequired": {"value": true}}) } input_with_method(method) = {"aws": {"apigateway": {"v1": {"apis": [{"resources": [{"methods": [method]}]}]}}}} diff --git a/checks/cloud/aws/cloudtrail/encryption_customer_key.tf.go b/checks/cloud/aws/cloudtrail/encryption_customer_key.tf.go index b4a950e5..16505f66 100644 --- a/checks/cloud/aws/cloudtrail/encryption_customer_key.tf.go +++ b/checks/cloud/aws/cloudtrail/encryption_customer_key.tf.go @@ -2,10 +2,19 @@ package cloudtrail var terraformEncryptionCustomerManagedKeyGoodExamples = []string{ ` +resource "aws_kms_key" "trail" { + enable_key_rotation = true +} + +resource "aws_kms_alias" "trail" { + name = "alias/trail" + target_key_id = aws_kms_key.trail.key_id +} + resource "aws_cloudtrail" "good_example" { is_multi_region_trail = true enable_log_file_validation = true - kms_key_id = var.kms_id + kms_key_id = aws_kms_alias.trail.arn event_selector { read_write_type = "All" diff --git a/checks/cloud/aws/cloudwatch/log_group_customer_key.tf.go b/checks/cloud/aws/cloudwatch/log_group_customer_key.tf.go index ddc0b7cb..714cd700 100644 --- a/checks/cloud/aws/cloudwatch/log_group_customer_key.tf.go +++ b/checks/cloud/aws/cloudwatch/log_group_customer_key.tf.go @@ -2,10 +2,19 @@ package cloudwatch var terraformLogGroupCustomerKeyGoodExamples = []string{ ` +resource "aws_kms_key" "cloudwatch" { + enable_key_rotation = true +} + +resource "aws_kms_alias" "cloudwatch" { + name = "alias/cloudwatch" + target_key_id = aws_kms_key.cloudwatch.key_id +} + resource "aws_cloudwatch_log_group" "good_example" { name = "good_example" - kms_key_id = aws_kms_key.log_key.arn + kms_key_id = aws_kms_alias.cloudwatch.arn } `, } diff --git a/checks/cloud/aws/ec2/no_public_ingress_sgr.tf.go b/checks/cloud/aws/ec2/no_public_ingress_sgr.tf.go index e5d68d5d..12d84703 100644 --- a/checks/cloud/aws/ec2/no_public_ingress_sgr.tf.go +++ b/checks/cloud/aws/ec2/no_public_ingress_sgr.tf.go @@ -8,9 +8,9 @@ var terraformNoPublicIngressSgrGoodExamples = []string{ } `, ` -resource "aws_security_group_rule" "allow_partner_rsync" { +resource "aws_security_group_rule" "example" { type = "ingress" - security_group_id = aws_security_group.….id + security_group_id = "sg-123456" from_port = 22 to_port = 22 protocol = "tcp" diff --git a/checks/cloud/nifcloud/computing/no_public_ingress_sgr.tf.go b/checks/cloud/nifcloud/computing/no_public_ingress_sgr.tf.go index 60206563..53a4a23e 100644 --- a/checks/cloud/nifcloud/computing/no_public_ingress_sgr.tf.go +++ b/checks/cloud/nifcloud/computing/no_public_ingress_sgr.tf.go @@ -8,9 +8,14 @@ var terraformNoPublicIngressSgrGoodExamples = []string{ } `, ` -resource "nifcloud_security_group_rule" "allow_partner_rsync" { +resource "nifcloud_security_group" "example" { + group_name = "allowtcp" + availability_zone = "east-11" +} + +resource "nifcloud_security_group_rule" "example" { type = "IN" - security_group_names = [nifcloud_security_group.….group_name] + security_group_names = [nifcloud_security_group.example.group_name] from_port = 22 to_port = 22 protocol = "TCP" diff --git a/checks/cloud/nifcloud/network/add_security_group_to_router.rego b/checks/cloud/nifcloud/network/add_security_group_to_router.rego index 0bf73643..91151f7a 100644 --- a/checks/cloud/nifcloud/network/add_security_group_to_router.rego +++ b/checks/cloud/nifcloud/network/add_security_group_to_router.rego @@ -32,6 +32,11 @@ import rego.v1 deny contains res if { some router in input.nifcloud.network.routers - router.securitygroup.value == "" - res := result.new("Router does not have a securiy group.", router.securitygroup) + not has_security_group(router) + res := result.new( + "Router does not have a securiy group.", + object.get(router, "securitygroup", router), + ) } + +has_security_group(router) if router.securitygroup.value != "" \ No newline at end of file diff --git a/internal/checks/checks.go b/internal/checks/checks.go new file mode 100644 index 00000000..51c02fa7 --- /dev/null +++ b/internal/checks/checks.go @@ -0,0 +1,30 @@ +package checks + +import ( + "sort" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/rules" + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +func LoadRegoChecks() []scan.Rule { + // Clean up all Go checks + rules.Reset() + + // Load Rego checks + rego.LoadAndRegister() + + var res []scan.Rule + + for _, metadata := range rules.GetRegistered(framework.ALL) { + res = append(res, metadata.Rule) + } + + sort.Slice(res, func(i, j int) bool { + return res[i].AVDID < res[j].AVDID + }) + + return res +} diff --git a/internal/checks/examples.go b/internal/checks/examples.go new file mode 100644 index 00000000..15cfc94f --- /dev/null +++ b/internal/checks/examples.go @@ -0,0 +1,117 @@ +package checks + +import ( + goast "go/ast" + "go/parser" + "go/token" + "strings" + + trivy_checks "github.com/aquasecurity/trivy-checks" + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +type Provider string + +const ( + TerraformProvider Provider = "Terraform" + CloudFormationProvider Provider = "CloudFormation" +) + +func providerByFileName(n string) Provider { + switch { + case strings.HasSuffix(n, "tf.go"): + return TerraformProvider + case strings.HasSuffix(n, "cf.go"): + return CloudFormationProvider + } + + panic("unreachable") +} + +type Example struct { + Path string + Provider Provider + GoodExample bool // bad example if false + Content string +} + +func GetCheckExamples(check scan.Rule) ([]*Example, error) { + var files []string + if check.Terraform != nil { + files = append(files, check.Terraform.BadExamples...) + // files = append(files, check.Terraform.GoodExamples...) + } + + if check.CloudFormation != nil { + files = append(files, check.CloudFormation.BadExamples...) + // files = append(files, check.CloudFormation.GoodExamples...) + } + + var res []*Example + + if check.RegoPackage != "" { + for _, path := range files { + exmpls, err := parseExamplesFromFile(path) + if err != nil { + return nil, err + } + + res = append(res, exmpls...) + } + } + + return res, nil +} + +func parseExamplesFromFile(filename string) ([]*Example, error) { + r, err := trivy_checks.EmbeddedPolicyFileSystem.Open(filename) + if err != nil { + return nil, err + } + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filename, r, parser.AllErrors) + if err != nil { + return nil, err + } + return extractExamples(f, filename), nil +} + +func extractExamples(f *goast.File, filename string) (res []*Example) { + goast.Inspect(f, func(n goast.Node) bool { + valueSpec, ok := n.(*goast.ValueSpec) + if !ok { + return true + } + + for _, id := range valueSpec.Names { + if !isExampleName(id.Name) { + continue + } + + if compositeLit, ok := valueSpec.Values[0].(*goast.CompositeLit); ok { + for _, e := range compositeLit.Elts { + if basicLit, ok := e.(*goast.BasicLit); ok { + res = append(res, &Example{ + Path: filename, + GoodExample: strings.HasSuffix(id.Name, "GoodExamples"), + Provider: providerByFileName(filename), + Content: cleanupExample(basicLit.Value), + }) + } + } + } + } + return true + }) + + return res +} + +func isExampleName(name string) bool { + return strings.HasSuffix(name, "GoodExamples") || strings.HasSuffix(name, "BadExamples") +} + +func cleanupExample(s string) string { + return strings.ReplaceAll(s, "`", "") +} diff --git a/test/examples_test.go b/test/examples_test.go index b818dc90..ecb5b327 100644 --- a/test/examples_test.go +++ b/test/examples_test.go @@ -3,75 +3,88 @@ package test import ( "context" "fmt" - goast "go/ast" - "go/parser" - "go/token" "io/fs" - "strings" "testing" "testing/fstest" - checks "github.com/aquasecurity/trivy-checks" - rules "github.com/aquasecurity/trivy/pkg/iac/rules" + "github.com/aquasecurity/trivy-checks/internal/checks" + "github.com/aquasecurity/trivy/pkg/iac/rego" "github.com/aquasecurity/trivy/pkg/iac/scan" "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform" "github.com/stretchr/testify/require" ) func TestCheckExamples(t *testing.T) { - tfScanner := terraform.New() - cfScanner := cloudformation.New() - - for _, registeredRule := range rules.GetRegistered() { - t.Run(registeredRule.AVDID, func(t *testing.T) { - tfexamples := getExamplesFromRule(t, registeredRule.Rule, registeredRule.Terraform) - for i, example := range tfexamples { - t.Run(fmt.Sprintf("terraform_%d", i), func(t *testing.T) { - scanExample(t, tfScanner, registeredRule.LongID(), example) - }) - } - - cfexamples := getExamplesFromRule(t, registeredRule.Rule, registeredRule.CloudFormation) - for i, example := range cfexamples { - t.Run(fmt.Sprintf("cloudformation_%d", i), func(t *testing.T) { - scanExample(t, cfScanner, registeredRule.LongID(), example) + opts := []options.ScannerOption{ + rego.WithEmbeddedLibraries(true), + rego.WithEmbeddedPolicies(true), + } + tfScanner := terraform.New(opts...) + cfScanner := cloudformation.New(opts...) + + for _, check := range checks.LoadRegoChecks() { + t.Run(check.AVDID, func(t *testing.T) { + exmpls, err := checks.GetCheckExamples(check) + require.NoError(t, err) + + for i, example := range exmpls { + s := getScannerForProvider(example.Provider, tfScanner, cfScanner) + require.NotNil(t, s) + t.Run(fmt.Sprintf("%s_%d", example.Provider, i), func(t *testing.T) { + scanExample(t, s, check.LongID(), example) }) } }) } } +func getScannerForProvider(provider checks.Provider, tfScanner, cfScanner scanner) scanner { + switch provider { + case checks.TerraformProvider: + return tfScanner + case checks.CloudFormationProvider: + return cfScanner + default: + return nil + } +} + type scanner interface { ScanFS(ctx context.Context, fsys fs.FS, dir string) (scan.Results, error) } -func scanExample(t *testing.T, s scanner, checkID string, example example) { - +func scanExample(t *testing.T, s scanner, checkID string, example *checks.Example) { var filename string - switch s.(type) { - case *terraform.Scanner: + switch example.Provider { + case checks.TerraformProvider: filename = fmt.Sprintf("%s.tf", checkID) - case *cloudformation.Scanner: + case checks.CloudFormationProvider: filename = fmt.Sprintf("%s.yaml", checkID) } fsys := fstest.MapFS{ filename: &fstest.MapFile{ - Data: []byte(example.content), + Data: []byte(example.Content), }, } res, err := s.ScanFS(context.TODO(), fsys, ".") require.NoError(t, err) - contains := resultsContainsCheck(res, checkID, example.good) + + assertResultContainsCheck(t, res, checkID, example) +} + +func assertResultContainsCheck(t *testing.T, results scan.Results, checkID string, example *checks.Example) { + contains := resultsContainsCheck(results, checkID, example.GoodExample) if !contains { exampleType := "good" - if !example.good { + if !example.GoodExample { exampleType = "bad" } - t.Fatalf("results does not contain check %q for %s example: %s", - checkID, exampleType, example.content) + t.Fatalf("results do not contain check %q for %s example: %s", + checkID, exampleType, example.Content) } } @@ -90,90 +103,3 @@ func resultsContainsCheck(results scan.Results, checkID string, good bool) bool return false } - -type example struct { - content string - good bool -} - -func getExamplesFromRule(t *testing.T, r scan.Rule, engine *scan.EngineMetadata) []example { - if engine == nil { - return nil - } - - examples := getExamplesForType(t, r, engine.GoodExamples, "GoodExamples") - examples = append(examples, getExamplesForType(t, r, engine.BadExamples, "BadExamples")...) - return examples -} - -func getExamplesForType(t *testing.T, r scan.Rule, files []string, exampleType string) []example { - var res []example - - if r.RegoPackage != "" { - for _, exampleFile := range files { - contents, err := getExampleValuesFromFile(exampleFile, exampleType) - if err != nil { - require.NoError(t, err) - } - - for _, content := range contents { - res = append(res, example{ - content: content, - good: exampleType == "GoodExamples", - }) - } - } - } else { - for _, exampleFile := range files { - res = append(res, example{ - content: exampleFile, - good: exampleType == "GoodExamples", - }) - } - } - - return res -} - -func getExampleValuesFromFile(filename string, exampleType string) ([]string, error) { - r, err := checks.EmbeddedPolicyFileSystem.Open(filename) - if err != nil { - return nil, err - } - f, err := parser.ParseFile(token.NewFileSet(), filename, r, parser.AllErrors) - if err != nil { - return nil, err - } - - res := []string{} - - for _, d := range f.Decls { - switch decl := d.(type) { - case *goast.GenDecl: - for _, spec := range decl.Specs { - switch spec := spec.(type) { - case *goast.ValueSpec: - for _, id := range spec.Names { - switch v := id.Obj.Decl.(*goast.ValueSpec).Values[0].(type) { - case *goast.CompositeLit: - for _, e := range v.Elts { - switch e := e.(type) { - case *goast.BasicLit: - if strings.HasSuffix(id.Name, exampleType) { - res = append(res, strings.ReplaceAll(e.Value, "`", "")) - } - } - } - } - } - } - } - } - } - - if len(res) == 0 { - return nil, fmt.Errorf("exampleType %s not found in file: %s", exampleType, filename) - } - - return res, nil -}