diff --git a/avd_docs/aws/iam/AVD-AWS-0056/docs.md b/avd_docs/aws/iam/AVD-AWS-0056/docs.md index 5e4f6616..6ef28d6c 100644 --- a/avd_docs/aws/iam/AVD-AWS-0056/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0056/docs.md @@ -1,10 +1,11 @@ -IAM account password policies should prevent the reuse of passwords. +IAM account password policies should prevent the reuse of passwords. The account password policy should be set to prevent using any of the last five used passwords. + ### Impact -Password reuse increase the risk of compromised passwords being abused + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0057/docs.md b/avd_docs/aws/iam/AVD-AWS-0057/docs.md index 6aa472c9..a216e3da 100644 --- a/avd_docs/aws/iam/AVD-AWS-0057/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0057/docs.md @@ -1,8 +1,9 @@ You should use the principle of least privilege when defining your IAM policies. This means you should specify each exact permission required without using wildcards, as this could cause the granting of access to certain undesired actions, resources and principals. + ### Impact -Overly permissive policies may grant access to sensitive resources + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0058/docs.md b/avd_docs/aws/iam/AVD-AWS-0058/docs.md index dc348ac7..d8d62d81 100644 --- a/avd_docs/aws/iam/AVD-AWS-0058/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0058/docs.md @@ -1,8 +1,9 @@ IAM account password policies should ensure that passwords content including at least one lowercase character. + ### Impact -Short, simple passwords are easier to compromise + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0059/docs.md b/avd_docs/aws/iam/AVD-AWS-0059/docs.md index c05bb09b..c6dfcd89 100644 --- a/avd_docs/aws/iam/AVD-AWS-0059/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0059/docs.md @@ -1,8 +1,9 @@ IAM account password policies should ensure that passwords content including at least one number. + ### Impact -Short, simple passwords are easier to compromise + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0060/docs.md b/avd_docs/aws/iam/AVD-AWS-0060/docs.md index 59c8fd41..ecda9e2c 100644 --- a/avd_docs/aws/iam/AVD-AWS-0060/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0060/docs.md @@ -1,8 +1,9 @@ IAM account password policies should ensure that passwords content including a symbol. + ### Impact -Short, simple passwords are easier to compromise + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0061/docs.md b/avd_docs/aws/iam/AVD-AWS-0061/docs.md index 5060939a..08c95295 100644 --- a/avd_docs/aws/iam/AVD-AWS-0061/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0061/docs.md @@ -1,9 +1,11 @@ , + IAM account password policies should ensure that passwords content including at least one uppercase character. + ### Impact -Short, simple passwords are easier to compromise + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0062/docs.md b/avd_docs/aws/iam/AVD-AWS-0062/docs.md index 605e4c56..d79275fa 100644 --- a/avd_docs/aws/iam/AVD-AWS-0062/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0062/docs.md @@ -1,10 +1,11 @@ -IAM account password policies should have a maximum age specified. - +IAM account password policies should have a maximum age specified. + The account password policy should be set to expire passwords after 90 days or less. + ### Impact -Long life password increase the likelihood of a password eventually being compromised + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0063/docs.md b/avd_docs/aws/iam/AVD-AWS-0063/docs.md index 747afab0..bfbacb74 100644 --- a/avd_docs/aws/iam/AVD-AWS-0063/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0063/docs.md @@ -1,10 +1,11 @@ -IAM account password policies should ensure that passwords have a minimum length. +IAM account password policies should ensure that passwords have a minimum length. The account password policy should be set to enforce minimum password length of at least 14 characters. + ### Impact -Short, simple passwords are easier to compromise + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0123/Terraform.md b/avd_docs/aws/iam/AVD-AWS-0123/Terraform.md index a2d54374..3011c48e 100644 --- a/avd_docs/aws/iam/AVD-AWS-0123/Terraform.md +++ b/avd_docs/aws/iam/AVD-AWS-0123/Terraform.md @@ -28,97 +28,6 @@ resource "aws_iam_group_policy" "mfa" { EOF } -``` -```hcl -resource "aws_iam_group" "support" { - name = "support" -} -resource "aws_iam_policy" "mfa" { - - name = "something" - policy = < {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0140/docs.md b/avd_docs/aws/iam/AVD-AWS-0140/docs.md index d030b51f..d7ddddc2 100644 --- a/avd_docs/aws/iam/AVD-AWS-0140/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0140/docs.md @@ -1,10 +1,9 @@ - The root user has unrestricted access to all services and resources in an AWS account. We highly recommend that you avoid using the root user for daily tasks. Minimizing the use of the root user and adopting the principle of least privilege for access management reduce the risk of accidental changes and unintended disclosure of highly privileged credentials. - + ### Impact -Compromise of the root account compromises the entire AWS account and all resources within it. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0141/docs.md b/avd_docs/aws/iam/AVD-AWS-0141/docs.md index 8ce07d08..9b26eaf7 100644 --- a/avd_docs/aws/iam/AVD-AWS-0141/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0141/docs.md @@ -1,10 +1,9 @@ - CIS recommends that all access keys be associated with the root user be removed. Removing access keys associated with the root user limits vectors that the account can be compromised by. Removing the root user access keys also encourages the creation and use of role-based accounts that are least privileged. - + ### Impact -Compromise of the root account compromises the entire AWS account and all resources within it. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0142/docs.md b/avd_docs/aws/iam/AVD-AWS-0142/docs.md index 564eb976..4751ef00 100644 --- a/avd_docs/aws/iam/AVD-AWS-0142/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0142/docs.md @@ -1,12 +1,11 @@ - MFA adds an extra layer of protection on top of a user name and password. With MFA enabled, when a user signs in to an AWS website, they're prompted for their user name and password and for an authentication code from their AWS MFA device. When you use virtual MFA for the root user, CIS recommends that the device used is not a personal device. Instead, use a dedicated mobile device (tablet or phone) that you manage to keep charged and secured independent of any individual personal devices. This lessens the risks of losing access to the MFA due to device loss, device trade-in, or if the individual owning the device is no longer employed at the company. - + ### Impact -Compromise of the root account compromises the entire AWS account and all resources within it. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0143/docs.md b/avd_docs/aws/iam/AVD-AWS-0143/docs.md index 36035931..b27790bd 100644 --- a/avd_docs/aws/iam/AVD-AWS-0143/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0143/docs.md @@ -1,10 +1,9 @@ - CIS recommends that you apply IAM policies directly to groups and roles but not users. Assigning privileges at the group or role level reduces the complexity of access management as the number of users grow. Reducing access management complexity might in turn reduce opportunity for a principal to inadvertently receive or retain excessive privileges. - + ### Impact -Complex access control is difficult to manage and maintain. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0144/docs.md b/avd_docs/aws/iam/AVD-AWS-0144/docs.md index a8c7b46b..7aed9d24 100644 --- a/avd_docs/aws/iam/AVD-AWS-0144/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0144/docs.md @@ -1,10 +1,9 @@ - CIS recommends that you remove or deactivate all credentials that have been unused in 90 days or more. Disabling or removing unnecessary credentials reduces the window of opportunity for credentials associated with a compromised or abandoned account to be used. - + ### Impact -Leaving unused credentials active widens the scope for compromise. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0145/docs.md b/avd_docs/aws/iam/AVD-AWS-0145/docs.md index 7296acd3..0d967e43 100644 --- a/avd_docs/aws/iam/AVD-AWS-0145/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0145/docs.md @@ -1,10 +1,9 @@ - IAM user accounts should be protected with multi factor authentication to add safe guards to password compromise. - + ### Impact -User accounts are more vulnerable to compromise without multi factor authentication activated + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0146/docs.md b/avd_docs/aws/iam/AVD-AWS-0146/docs.md index cd5c026a..25597e1b 100644 --- a/avd_docs/aws/iam/AVD-AWS-0146/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0146/docs.md @@ -1,10 +1,9 @@ - Regularly rotating your IAM credentials helps prevent a compromised set of IAM access keys from accessing components in your AWS account. - + ### Impact -Compromised keys are more likely to be used to compromise the account + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0165/docs.md b/avd_docs/aws/iam/AVD-AWS-0165/docs.md index d8c6497e..dcd80f1b 100644 --- a/avd_docs/aws/iam/AVD-AWS-0165/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0165/docs.md @@ -1,10 +1,9 @@ - Hardware MFA adds an extra layer of protection on top of a user name and password. With MFA enabled, when a user signs in to an AWS website, they're prompted for their user name and password and for an authentication code from their AWS MFA device. - + ### Impact -Compromise of the root account compromises the entire AWS account and all resources within it. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0166/docs.md b/avd_docs/aws/iam/AVD-AWS-0166/docs.md index cd7ff838..12269eab 100644 --- a/avd_docs/aws/iam/AVD-AWS-0166/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0166/docs.md @@ -1,10 +1,9 @@ +AWS IAM users can access AWS resources using different types of credentials, such as passwords or access keys. It is recommended that all credentials that have been unused in45 or greater days be deactivated or removed. -Disabling or removing unnecessary credentials will reduce the window of opportunity for credentials associated with a compromised or abandoned account to be used. - ### Impact -Leaving unused credentials active widens the scope for compromise. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0167/docs.md b/avd_docs/aws/iam/AVD-AWS-0167/docs.md index 12d6f6ca..2ba10a1c 100644 --- a/avd_docs/aws/iam/AVD-AWS-0167/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0167/docs.md @@ -1,10 +1,9 @@ - Multiple active access keys widens the scope for compromise. - + ### Impact -Widened scope for compromise. + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0168/docs.md b/avd_docs/aws/iam/AVD-AWS-0168/docs.md index 503c7d0e..08a65a3a 100644 --- a/avd_docs/aws/iam/AVD-AWS-0168/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0168/docs.md @@ -1,13 +1,15 @@ - Removing expired SSL/TLS certificates eliminates the risk that an invalid certificate will be + deployed accidentally to a resource such as AWS Elastic Load Balancer (ELB), which can + damage the credibility of the application/website behind the ELB. As a best practice, it is + recommended to delete expired certificates. - + ### Impact -Risk of misconfiguration and damage to credibility + {{ remediationActions }} diff --git a/avd_docs/aws/iam/AVD-AWS-0169/docs.md b/avd_docs/aws/iam/AVD-AWS-0169/docs.md index e65af623..22004491 100644 --- a/avd_docs/aws/iam/AVD-AWS-0169/docs.md +++ b/avd_docs/aws/iam/AVD-AWS-0169/docs.md @@ -1,11 +1,11 @@ - By implementing least privilege for access control, an IAM Role will require an appropriate + IAM Policy to allow Support Center Access in order to manage Incidents with AWS Support. - + ### Impact -Incident management is not possible without a support role. + {{ remediationActions }} diff --git a/checks/cloud/aws/iam/disable_unused_credentials.go b/checks/cloud/aws/iam/disable_unused_credentials.go index c78f3b79..9d69e24a 100644 --- a/checks/cloud/aws/iam/disable_unused_credentials.go +++ b/checks/cloud/aws/iam/disable_unused_credentials.go @@ -35,7 +35,8 @@ CIS recommends that you remove or deactivate all credentials that have been unus Links: []string{ "https://console.aws.amazon.com/iam/", }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { diff --git a/checks/cloud/aws/iam/disable_unused_credentials.rego b/checks/cloud/aws/iam/disable_unused_credentials.rego new file mode 100644 index 00000000..eafa7505 --- /dev/null +++ b/checks/cloud/aws/iam/disable_unused_credentials.rego @@ -0,0 +1,47 @@ +# METADATA +# title: Credentials which are no longer used should be disabled. +# description: | +# CIS recommends that you remove or deactivate all credentials that have been unused in 90 days or more. Disabling or removing unnecessary credentials reduces the window of opportunity for credentials associated with a compromised or abandoned account to be used. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://console.aws.amazon.com/iam/ +# custom: +# id: AVD-AWS-0144 +# avd_id: AVD-AWS-0144 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: disable-unused-credentials +# recommended_action: Disable credentials which are no longer used. +# frameworks: +# cis-aws-1.2: +# - "1.3" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0144 + +import rego.v1 + +import data.lib.iam + +days_to_check = 90 + +deny contains res if { + some user in input.aws.iam.users + iam.user_is_inactive(user, days_to_check) + res := result.new("User has not logged in for >90 days.", user) +} + +deny contains res if { + some user in input.aws.iam.users + not iam.user_is_inactive(user, days_to_check) + some key in user.accesskeys + iam.key_is_unused(key, days_to_check) + res := result.new(sprintf("User access key %q has not been used in >90 days", [key.accesskeyid.value]), user) +} diff --git a/checks/cloud/aws/iam/disable_unused_credentials_45.go b/checks/cloud/aws/iam/disable_unused_credentials_45.go index 4683b644..abf88843 100644 --- a/checks/cloud/aws/iam/disable_unused_credentials_45.go +++ b/checks/cloud/aws/iam/disable_unused_credentials_45.go @@ -35,7 +35,8 @@ Disabling or removing unnecessary credentials will reduce the window of opportun Links: []string{ "https://console.aws.amazon.com/iam/", }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { diff --git a/checks/cloud/aws/iam/disable_unused_credentials_45.rego b/checks/cloud/aws/iam/disable_unused_credentials_45.rego new file mode 100644 index 00000000..ad726846 --- /dev/null +++ b/checks/cloud/aws/iam/disable_unused_credentials_45.rego @@ -0,0 +1,46 @@ +# METADATA +# title: Disabling or removing unnecessary credentials will reduce the window of opportunity for credentials associated with a compromised or abandoned account to be used. +# description: | +# AWS IAM users can access AWS resources using different types of credentials, such as passwords or access keys. It is recommended that all credentials that have been unused in45 or greater days be deactivated or removed. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://console.aws.amazon.com/iam/ +# custom: +# id: AVD-AWS-0166 +# avd_id: AVD-AWS-0166 +# provider: aws +# service: iam +# severity: LOW +# short_code: disable-unused-credentials-45-days +# recommended_action: Disable credentials which are no longer used. +# frameworks: +# cis-aws-1.4: +# - "1.12" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0166 + +import data.lib.iam +import rego.v1 + +days_to_check = 45 + +deny contains res if { + some user in input.aws.iam.users + iam.user_is_inactive(user, days_to_check) + res := result.new("User has not logged in for >45 days.", user) +} + +deny contains res if { + some user in input.aws.iam.users + not iam.user_is_inactive(user, days_to_check) + some key in user.accesskeys + iam.key_is_unused(key, days_to_check) + res := result.new(sprintf("User access key %q has not been used in >45 days", [key.accesskeyid.value]), user) +} diff --git a/checks/cloud/aws/iam/disable_unused_credentials_45_test.go b/checks/cloud/aws/iam/disable_unused_credentials_45_test.go deleted file mode 100644 index 4b856ce0..00000000 --- a/checks/cloud/aws/iam/disable_unused_credentials_45_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package iam - -import ( - "testing" - "time" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckUnusedCredentialsDisabled45Days(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "User logged in today", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "User never logged in, but used access key today", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "User logged in 50 days ago", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour*24*50), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "User last used access key 50 days ago but it is no longer active", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*120), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour*24*50), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "User last used access key 50 days ago and it is active", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*120), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour*24*50), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckUnusedCredentialsDisabled45Days.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckUnusedCredentialsDisabled45Days.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/disable_unused_credentials_45_test.rego b/checks/cloud/aws/iam/disable_unused_credentials_45_test.rego new file mode 100644 index 00000000..5321cccd --- /dev/null +++ b/checks/cloud/aws/iam/disable_unused_credentials_45_test.rego @@ -0,0 +1,66 @@ +package builtin.aws.iam.aws0166_test + +import rego.v1 + +import data.builtin.aws.iam.aws0166 as check +import data.lib.datetime +import data.lib.test + +test_allow_user_logged_in_today if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": time.format(time.now_ns())}, + }) +} + +test_allow_user_never_logged_in if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": datetime.zero_time_string}, + }) +} + +test_disallow_user_logged_in_100_days_ago if { + test.assert_equal_message("User has not logged in for >45 days.", check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + }) +} + +test_disallow_user_access_key_not_used_100_days if { + test.assert_equal_message(`User access key "AKIACKCEVSQ6C2EXAMPLE" has not been used in >45 days`, check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": time.format(time.now_ns())}, + "accesskeys": [{ + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": true}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + }], + }) +} + +test_allow_nonactive_user_access_key_not_used_100_days if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": time.format(time.now_ns())}, + "accesskeys": [{ + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": false}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + }], + }) +} + +test_allow_user_access_key_used_today if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": time.format(time.now_ns())}, + "accesskeys": [{ + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": true}, + "lastaccess": {"value": time.format(time.now_ns())}, + }], + }) +} + +build_input(body) = {"aws": {"iam": {"users": [body]}}} diff --git a/checks/cloud/aws/iam/disable_unused_credentials_test.go b/checks/cloud/aws/iam/disable_unused_credentials_test.go deleted file mode 100644 index 9a5f6164..00000000 --- a/checks/cloud/aws/iam/disable_unused_credentials_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package iam - -import ( - "testing" - "time" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckUnusedCredentialsDisabled(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "User logged in today", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "User never logged in, but used access key today", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "User logged in 100 days ago", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour*24*100), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "User last used access key 100 days ago but it is no longer active", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*120), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour*24*100), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "User last used access key 100 days ago and it is active", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*120), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour*24*100), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckUnusedCredentialsDisabled.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckUnusedCredentialsDisabled.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/disable_unused_credentials_test.rego b/checks/cloud/aws/iam/disable_unused_credentials_test.rego new file mode 100644 index 00000000..087344a9 --- /dev/null +++ b/checks/cloud/aws/iam/disable_unused_credentials_test.rego @@ -0,0 +1,85 @@ +package builtin.aws.iam.aws0144_test + +import rego.v1 + +import data.builtin.aws.iam.aws0144 as check +import data.lib.datetime +import data.lib.test + +test_allow_user_logged_in_today if { + test.assert_empty(check.deny) with input as build_input({ + "name": "test", + "lastaccess": {"value": time.format(time.now_ns())}, + }) +} + +test_allow_user_never_logged_in if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": datetime.zero_time_string}, + }) +} + +test_disallow_user_logged_in_100_days_ago if { + test.assert_equal_message("User has not logged in for >90 days.", check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + }) +} + +test_disallow_user_access_key_not_used_100_days if { + test.assert_equal_message(`User access key "AKIACKCEVSQ6C2EXAMPLE" has not been used in >90 days`, check.deny) with input as build_input({ + "name": {"value": "test"}, + "lastaccess": {"value": time.format(time.now_ns())}, + "accesskeys": [{ + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": true}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + }], + }) +} + +test_allow_nonactive_user_access_key_not_used_100_days if { + test.assert_empty(check.deny) with input as build_input({ + "name": "test", + "lastaccess": {"value": time.format(time.now_ns())}, + "accesskeys": [{ + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": false}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + }], + }) +} + +test_allow_user_access_key_used_today if { + test.assert_empty(check.deny) with input as build_input({ + "name": "test", + "lastaccess": {"value": time.format(time.now_ns())}, + "accesskeys": [{ + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": true}, + "lastaccess": {"value": time.format(time.now_ns())}, + }], + }) +} + +test_disallow_one_of_the_user_access_key_used_100_days if { + test.assert_equal_message(`User access key "AKIACKCEVSQ6C2EXAMPLE" has not been used in >90 days`, check.deny) with input as build_input({ + "name": "test", + "lastaccess": {"value": time.format(time.now_ns())}, + "accesskeys": [ + { + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": true}, + "lastaccess": {"value": time.format(time.now_ns())}, + }, + { + "accesskeyid": {"value": "AKIACKCEVSQ6C2EXAMPLE"}, + "active": {"value": true}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + }, + ], + }) +} + +build_input(body) = {"aws": {"iam": {"users": [body]}}} diff --git a/checks/cloud/aws/iam/enforce_group_mfa.go b/checks/cloud/aws/iam/enforce_group_mfa.go index f9e3c2a0..dd73a7d2 100644 --- a/checks/cloud/aws/iam/enforce_group_mfa.go +++ b/checks/cloud/aws/iam/enforce_group_mfa.go @@ -38,7 +38,8 @@ IAM groups should be protected with multi factor authentication to add safe guar Links: terraformEnforceMfaLinks, RemediationMarkdown: terraformEnforceMfaRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { diff --git a/checks/cloud/aws/iam/enforce_group_mfa.rego b/checks/cloud/aws/iam/enforce_group_mfa.rego new file mode 100644 index 00000000..2e8eb861 --- /dev/null +++ b/checks/cloud/aws/iam/enforce_group_mfa.rego @@ -0,0 +1,46 @@ +# METADATA +# title: IAM groups should have MFA enforcement activated. +# description: | +# IAM groups should be protected with multi factor authentication to add safe guards to password compromise. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0123 +# avd_id: AVD-AWS-0123 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: enforce-group-mfa +# recommended_action: Use terraform-module/enforce-mfa/aws to ensure that MFA is enforced +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/modules/terraform-module/enforce-mfa/aws/latest +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# good_examples: checks/cloud/aws/iam/enforce_group_mfa.tf.go +# bad_examples: checks/cloud/aws/iam/enforce_group_mfa.tf.go +package builtin.aws.iam.aws0123 + +import rego.v1 + +deny contains res if { + some group in input.aws.iam.groups + not is_group_mfa_enforced(group) + res := result.new("Multi-Factor authentication is not enforced for group", group) +} + +is_group_mfa_enforced(group) if { + some policy in group.policies + value := json.unmarshal(policy.document.value) + some condition in value.Statement[_].Condition + some key, _ in condition + key == "aws:MultiFactorAuthPresent" +} diff --git a/checks/cloud/aws/iam/enforce_group_mfa_test.go b/checks/cloud/aws/iam/enforce_group_mfa_test.go deleted file mode 100644 index b053bc7a..00000000 --- a/checks/cloud/aws/iam/enforce_group_mfa_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package iam - -import ( - "testing" - - "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/liamg/iamgo" - - "github.com/stretchr/testify/assert" -) - -func TestCheckEnforceGroupMFA(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "IAM policy with no MFA required", - input: iam.IAM{ - Groups: []iam.Group{ - { - Metadata: types.NewTestMetadata(), - Policies: []iam.Policy{ - { - Metadata: types.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"ec2:*"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - } - }(), - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "IAM policy with MFA required", - input: iam.IAM{ - Groups: []iam.Group{ - { - Metadata: types.NewTestMetadata(), - Policies: []iam.Policy{ - { - Metadata: types.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"ec2:*"}) - sb.WithCondition("Bool", "aws:MultiFactorAuthPresent", []string{"true"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - } - }(), - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckEnforceGroupMFA.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckEnforceGroupMFA.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/enforce_group_mfa_test.rego b/checks/cloud/aws/iam/enforce_group_mfa_test.rego new file mode 100644 index 00000000..448a3c1d --- /dev/null +++ b/checks/cloud/aws/iam/enforce_group_mfa_test.rego @@ -0,0 +1,19 @@ +package builtin.aws.iam.aws0123_test + +import rego.v1 + +import data.builtin.aws.iam.aws0123 as check +import data.lib.test + +test_allow_group_with_mfa if { + test.assert_empty(check.deny) with input as build_condition({ + "StringLike": {"kms:ViaService": "timestream.*.amazonaws.com"}, + "Bool": {"aws:MultiFactorAuthPresent": "true"}, + }) +} + +test_disallow_group_without_mfa if { + test.assert_equal_message("Multi-Factor authentication is not enforced for group", check.deny) with input as build_condition({}) +} + +build_condition(body) = {"aws": {"iam": {"groups": [{"policies": [{"document": {"value": json.marshal({"Statement": [{"Condition": body}]})}}]}]}}} diff --git a/checks/cloud/aws/iam/enforce_root_hardware_mfa.go b/checks/cloud/aws/iam/enforce_root_hardware_mfa.go index cec62071..68ad925e 100644 --- a/checks/cloud/aws/iam/enforce_root_hardware_mfa.go +++ b/checks/cloud/aws/iam/enforce_root_hardware_mfa.go @@ -9,7 +9,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/state" ) -var checkRootHardwareMFAEnabled = rules.Register( +var CheckRootHardwareMFAEnabled = rules.Register( scan.Rule{ AVDID: "AVD-AWS-0165", Provider: providers.AWSProvider, @@ -27,7 +27,8 @@ Hardware MFA adds an extra layer of protection on top of a user name and passwor Links: []string{ "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable_physical.html", }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, user := range s.AWS.IAM.Users { diff --git a/checks/cloud/aws/iam/enforce_root_hardware_mfa.rego b/checks/cloud/aws/iam/enforce_root_hardware_mfa.rego new file mode 100644 index 00000000..4b84c64a --- /dev/null +++ b/checks/cloud/aws/iam/enforce_root_hardware_mfa.rego @@ -0,0 +1,43 @@ +# METADATA +# title: The "root" account has unrestricted access to all resources in the AWS account. It is highly recommended that this account have hardware MFA enabled. +# description: | +# Hardware MFA adds an extra layer of protection on top of a user name and password. With MFA enabled, when a user signs in to an AWS website, they're prompted for their user name and password and for an authentication code from their AWS MFA device. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_enable_physical.html +# custom: +# id: AVD-AWS-0165 +# avd_id: AVD-AWS-0165 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: enforce-root-hardware-mfa +# recommended_action: Enable hardware MFA on the root user account. +# frameworks: +# cis-aws-1.4: +# - "1.6" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0165 + +import rego.v1 + +deny contains res if { + some user in input.aws.iam.users + user.name.value == "root" + not is_user_have_hardware_mfa(user) + res := result.new("Root user does not have a hardware MFA device", user) +} + +# is_user_have_hardware_mfa(user) if + +is_user_have_hardware_mfa(user) if { + some device in user.mfadevices + device.isvirtual.value == false +} diff --git a/checks/cloud/aws/iam/enforce_root_hardware_mfa_test.go b/checks/cloud/aws/iam/enforce_root_hardware_mfa_test.go deleted file mode 100644 index 1f7e74eb..00000000 --- a/checks/cloud/aws/iam/enforce_root_hardware_mfa_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package iam - -import ( - "testing" - - "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRootHardwareMFAEnabled(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "root user without mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: types.NewTestMetadata(), - Name: types.String("root", types.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "root user with virtual MFA mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: types.NewTestMetadata(), - Name: types.String("root", types.NewTestMetadata()), - MFADevices: []iam.MFADevice{ - { - Metadata: types.NewTestMetadata(), - IsVirtual: types.Bool(true, types.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "other user without mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: types.NewTestMetadata(), - Name: types.String("other", types.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "root user with hardware mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: types.NewTestMetadata(), - Name: types.String("root", types.NewTestMetadata()), - MFADevices: []iam.MFADevice{ - { - Metadata: types.NewTestMetadata(), - IsVirtual: types.Bool(false, types.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := checkRootHardwareMFAEnabled.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == checkRootHardwareMFAEnabled.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/enforce_root_hardware_mfa_test.rego b/checks/cloud/aws/iam/enforce_root_hardware_mfa_test.rego new file mode 100644 index 00000000..942806d9 --- /dev/null +++ b/checks/cloud/aws/iam/enforce_root_hardware_mfa_test.rego @@ -0,0 +1,44 @@ +package builtin.aws.iam.aws0165_test + +import rego.v1 + +import data.builtin.aws.iam.aws0165 as check +import data.lib.test + +test_disallow_root_user_without_mfa if { + test.assert_equal_message("Root user does not have a hardware MFA device", check.deny) with input as build_input({"name": {"value": "root"}}) +} + +test_disallow_root_user_with_virtual_mfa if { + test.assert_equal_message("Root user does not have a hardware MFA device", check.deny) with input as build_input({ + "name": {"value": "root"}, + "mfadevices": [{"isvirtual": {"value": true}}], + }) +} + +test_allow_non_root_user_without_mfa if { + test.assert_empty(check.deny) with input as build_input({"name": {"value": "other"}}) +} + +test_allow_root_user_with_hardware_mfa if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": {"value": "root"}}, + "mfadevices": [{"isvirtual": {"value": false}}], + }) +} + +test_allow_root_user_with_different_mfa if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "root"}, + "mfadevices": [ + {"isvirtual": {"value": true}}, + {"isvirtual": {"value": false}}, + ], + }) +} + +test_allow_without_user if { + test.assert_empty(check.deny) with input as build_input({}) +} + +build_input(body) = {"aws": {"iam": {"users": [body]}}} diff --git a/checks/cloud/aws/iam/enforce_root_mfa.go b/checks/cloud/aws/iam/enforce_root_mfa.go index 60196752..59bd1cdb 100644 --- a/checks/cloud/aws/iam/enforce_root_mfa.go +++ b/checks/cloud/aws/iam/enforce_root_mfa.go @@ -35,7 +35,8 @@ When you use virtual MFA for the root user, CIS recommends that the device used Links: []string{ "https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.14", }, - Severity: severity.Critical, + Severity: severity.Critical, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, user := range s.AWS.IAM.Users { diff --git a/checks/cloud/aws/iam/enforce_root_mfa.rego b/checks/cloud/aws/iam/enforce_root_mfa.rego new file mode 100644 index 00000000..e3c442b7 --- /dev/null +++ b/checks/cloud/aws/iam/enforce_root_mfa.rego @@ -0,0 +1,42 @@ +# METADATA +# title: The "root" account has unrestricted access to all resources in the AWS account. It is highly recommended that this account have MFA enabled. +# description: | +# MFA adds an extra layer of protection on top of a user name and password. With MFA enabled, when a user signs in to an AWS website, they're prompted for their user name and password and for an authentication code from their AWS MFA device. +# +# When you use virtual MFA for the root user, CIS recommends that the device used is not a personal device. Instead, use a dedicated mobile device (tablet or phone) that you manage to keep charged and secured independent of any individual personal devices. This lessens the risks of losing access to the MFA due to device loss, device trade-in, or if the individual owning the device is no longer employed at the company. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cis-controls.html#securityhub-cis-controls-1.14 +# custom: +# id: AVD-AWS-0142 +# avd_id: AVD-AWS-0142 +# provider: aws +# service: iam +# severity: CRITICAL +# short_code: enforce-root-mfa +# recommended_action: Enable MFA on the root user account. +# frameworks: +# cis-aws-1.4: +# - "1.5" +# cis-aws-1.2: +# - "1.13" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0142 + +import rego.v1 + +import data.lib.iam + +deny contains res if { + some user in input.aws.iam.users + iam.is_root_user(user) + not iam.user_has_mfa_devices(user) + res := result.new("Root user does not have an MFA device", user) +} diff --git a/checks/cloud/aws/iam/enforce_root_mfa_test.go b/checks/cloud/aws/iam/enforce_root_mfa_test.go deleted file mode 100644 index 95a38c7b..00000000 --- a/checks/cloud/aws/iam/enforce_root_mfa_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRootMFAEnabled(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "root user without mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "other user without mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("other", trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "root user with mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - MFADevices: []iam.MFADevice{ - { - Metadata: trivyTypes.NewTestMetadata(), - IsVirtual: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := checkRootMFAEnabled.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == checkRootMFAEnabled.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/enforce_root_mfa_test.rego b/checks/cloud/aws/iam/enforce_root_mfa_test.rego new file mode 100644 index 00000000..646bf848 --- /dev/null +++ b/checks/cloud/aws/iam/enforce_root_mfa_test.rego @@ -0,0 +1,26 @@ +package builtin.aws.iam.aws0142_test + +import rego.v1 + +import data.builtin.aws.iam.aws0142 as check +import data.lib.test + +test_disallow_root_user_without_mfa if { + test.assert_equal_message("Root user does not have an MFA device", check.deny) with input as build_input({"name": {"value": "root"}}) +} + +test_allow_non_root_user_without_mfa if { + test.assert_empty(check.deny) with input as build_input({"name": {"value": "other"}}) +} + +test_allow_root_user_with_mfa if { + test.assert_empty(check.deny) with input as build_input({ + "name": "root", + "mfadevices": [ + {"isvirtual": {"value": false}}, + {"isvirtual": {"value": true}}, + ], + }) +} + +build_input(body) = {"aws": {"iam": {"users": [body]}}} diff --git a/checks/cloud/aws/iam/enforce_user_mfa.go b/checks/cloud/aws/iam/enforce_user_mfa.go index 029077fb..9823baae 100644 --- a/checks/cloud/aws/iam/enforce_user_mfa.go +++ b/checks/cloud/aws/iam/enforce_user_mfa.go @@ -33,7 +33,8 @@ IAM user accounts should be protected with multi factor authentication to add sa Links: []string{ "https://console.aws.amazon.com/iam/", }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { diff --git a/checks/cloud/aws/iam/enforce_user_mfa.rego b/checks/cloud/aws/iam/enforce_user_mfa.rego new file mode 100644 index 00000000..f6a21418 --- /dev/null +++ b/checks/cloud/aws/iam/enforce_user_mfa.rego @@ -0,0 +1,40 @@ +# METADATA +# title: IAM Users should have MFA enforcement activated. +# description: | +# IAM user accounts should be protected with multi factor authentication to add safe guards to password compromise. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://console.aws.amazon.com/iam/ +# custom: +# id: AVD-AWS-0145 +# avd_id: AVD-AWS-0145 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: enforce-user-mfa +# recommended_action: Enable MFA for the user account +# frameworks: +# cis-aws-1.2: +# - "1.2" +# cis-aws-1.4: +# - "1.4" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0145 + +import rego.v1 + +import data.lib.iam + +deny contains res if { + some user in input.aws.iam.users + not iam.user_has_mfa_devices(user) + iam.is_user_logged_in(user) + res := result.new("User account does not have MFA", user) +} diff --git a/checks/cloud/aws/iam/enforce_user_mfa_test.go b/checks/cloud/aws/iam/enforce_user_mfa_test.go deleted file mode 100644 index 96e760b7..00000000 --- a/checks/cloud/aws/iam/enforce_user_mfa_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package iam - -import ( - "testing" - "time" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckEnforceUserMFA(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "user logged in without mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("other", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "user without mfa never logged in", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("other", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "user with mfa", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - MFADevices: []iam.MFADevice{ - { - Metadata: trivyTypes.NewTestMetadata(), - IsVirtual: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckEnforceUserMFA.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckEnforceUserMFA.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/enforce_user_mfa_test.rego b/checks/cloud/aws/iam/enforce_user_mfa_test.rego new file mode 100644 index 00000000..52aeb0ee --- /dev/null +++ b/checks/cloud/aws/iam/enforce_user_mfa_test.rego @@ -0,0 +1,31 @@ +package builtin.aws.iam.aws0145_test + +import rego.v1 + +import data.builtin.aws.iam.aws0145 as check +import data.lib.datetime +import data.lib.test + +test_disallow_user_logged_in_without_mfa if { + test.assert_equal_message("User account does not have MFA", check.deny) with input as build_input({ + "name": {"value": "other"}, + "lastaccess": {"value": time.format(time.now_ns())}, + }) +} + +test_allow_user_never_logged_in_with_mfa if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "other"}, + "lastaccess": {"value": datetime.zero_time_string}, + }) +} + +test_allow_user_logged_in_with_mfa if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "other"}, + "lastaccess": {"value": time.format(time.now_ns())}, + "mfadevices": [{"isvirtual": {"value": false}}], + }) +} + +build_input(body) = {"aws": {"iam": {"users": [body]}}} diff --git a/checks/cloud/aws/iam/limit_root_account_usage.go b/checks/cloud/aws/iam/limit_root_account_usage.go index 0ab670c4..ef55b032 100644 --- a/checks/cloud/aws/iam/limit_root_account_usage.go +++ b/checks/cloud/aws/iam/limit_root_account_usage.go @@ -16,7 +16,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/providers" ) -var checkLimitRootAccountUsage = rules.Register( +var CheckLimitRootAccountUsage = rules.Register( scan.Rule{ AVDID: "AVD-AWS-0140", Provider: providers.AWSProvider, @@ -36,7 +36,8 @@ The root user has unrestricted access to all services and resources in an AWS ac Links: []string{ "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html", }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, user := range s.AWS.IAM.Users { diff --git a/checks/cloud/aws/iam/limit_root_account_usage.rego b/checks/cloud/aws/iam/limit_root_account_usage.rego new file mode 100644 index 00000000..db4bb11e --- /dev/null +++ b/checks/cloud/aws/iam/limit_root_account_usage.rego @@ -0,0 +1,40 @@ +# METADATA +# title: The "root" account has unrestricted access to all resources in the AWS account. It is highly recommended that the use of this account be avoided. +# description: | +# The root user has unrestricted access to all services and resources in an AWS account. We highly recommend that you avoid using the root user for daily tasks. Minimizing the use of the root user and adopting the principle of least privilege for access management reduce the risk of accidental changes and unintended disclosure of highly privileged credentials. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html +# custom: +# id: AVD-AWS-0140 +# avd_id: AVD-AWS-0140 +# provider: aws +# service: iam +# severity: LOW +# short_code: limit-root-account-usage +# recommended_action: Use lower privileged accounts instead, so only required privileges are available. +# frameworks: +# cis-aws-1.2: +# - "1.1" +# cis-aws-1.4: +# - "1.7" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0140 + +import data.lib.datetime +import data.lib.iam +import rego.v1 + +deny contains res if { + some user in input.aws.iam.users + iam.is_root_user(user) + datetime.time_diff_lt_days(user.lastaccess.value, 1) + res := result.new("The root user logged in within the last 24 hours", user) +} diff --git a/checks/cloud/aws/iam/limit_root_account_usage_test.go b/checks/cloud/aws/iam/limit_root_account_usage_test.go deleted file mode 100644 index 7ba21d2c..00000000 --- a/checks/cloud/aws/iam/limit_root_account_usage_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package iam - -import ( - "testing" - "time" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckLimitRootAccountUsage(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "root user, never logged in", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "root user, logged in months ago", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("other", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour*24*90), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "root user, logged in today", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "other user, logged in today", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("other", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now().Add(-time.Hour), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := checkLimitRootAccountUsage.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == checkLimitRootAccountUsage.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/limit_root_account_usage_test.rego b/checks/cloud/aws/iam/limit_root_account_usage_test.rego new file mode 100644 index 00000000..945b1122 --- /dev/null +++ b/checks/cloud/aws/iam/limit_root_account_usage_test.rego @@ -0,0 +1,37 @@ +package builtin.aws.iam.aws0140_test + +import rego.v1 + +import data.builtin.aws.iam.aws0140 as check +import data.lib.datetime +import data.lib.test + +test_allow_root_user_never_logged_in if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "root"}, + "lastaccess": {"value": datetime.zero_time_string}, + }) +} + +test_allow_root_user_logged_in_over_24_hours if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "root"}, + "lastaccess": {"value": time.format(time.now_ns() - datetime.days_to_ns(7))}, + }) +} + +test_disallow_root_user_logged_in_within_24_hours if { + test.assert_equal_message("The root user logged in within the last 24 hours", check.deny) with input as build_input({ + "name": {"value": "root"}, + "lastaccess": {"value": time.format(time.now_ns())}, + }) +} + +test_allow_nonroot_user_logged_in_within_24_hours if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "other"}, + "lastaccess": {"value": time.format(time.now_ns())}, + }) +} + +build_input(body) = {"aws": {"iam": {"users": [body]}}} diff --git a/checks/cloud/aws/iam/limit_user_access_keys.go b/checks/cloud/aws/iam/limit_user_access_keys.go index 192ac786..65593d4e 100644 --- a/checks/cloud/aws/iam/limit_user_access_keys.go +++ b/checks/cloud/aws/iam/limit_user_access_keys.go @@ -32,7 +32,8 @@ Multiple active access keys widens the scope for compromise. Links: []string{ "https://console.aws.amazon.com/iam/", }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, user := range s.AWS.IAM.Users { diff --git a/checks/cloud/aws/iam/limit_user_access_keys.rego b/checks/cloud/aws/iam/limit_user_access_keys.rego new file mode 100644 index 00000000..a3fcd6b5 --- /dev/null +++ b/checks/cloud/aws/iam/limit_user_access_keys.rego @@ -0,0 +1,35 @@ +# METADATA +# title: No user should have more than one active access key. +# description: | +# Multiple active access keys widens the scope for compromise. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://console.aws.amazon.com/iam/ +# custom: +# id: AVD-AWS-0167 +# avd_id: AVD-AWS-0167 +# provider: aws +# service: iam +# severity: LOW +# short_code: limit-user-access-keys +# recommended_action: Limit the number of active access keys to one key per user. +# frameworks: +# cis-aws-1.4: +# - "1.13" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0167 + +import rego.v1 + +deny contains res if { + some user in input.aws.iam.users + count([key | some key in user.accesskeys; key.active.value]) > 1 + res := result.new("User has more than one active access key", user) +} diff --git a/checks/cloud/aws/iam/limit_user_access_keys_test.go b/checks/cloud/aws/iam/limit_user_access_keys_test.go deleted file mode 100644 index 76405660..00000000 --- a/checks/cloud/aws/iam/limit_user_access_keys_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package iam - -import ( - "testing" - "time" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckLimitUserAccessKeys(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "Single active access key", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "One active, one inactive access key", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "Two inactive keys", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "Two active keys", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckLimitUserAccessKeys.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckLimitUserAccessKeys.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/limit_user_access_keys_test.rego b/checks/cloud/aws/iam/limit_user_access_keys_test.rego new file mode 100644 index 00000000..4b04b7d3 --- /dev/null +++ b/checks/cloud/aws/iam/limit_user_access_keys_test.rego @@ -0,0 +1,29 @@ +package builtin.aws.iam.aws0167_test + +import rego.v1 + +import data.builtin.aws.iam.aws0167 as check +import data.lib.test + +test_allow_one_key_is_active if { + test.assert_empty(check.deny) with input as build_input([{"active": {"value": true}}]) +} + +test_allow_two_keys_but_one_non_active if { + test.assert_empty(check.deny) with input as build_input([ + {"active": {"value": false}}, + {"active": {"value": true}}, + ]) +} + +test_disallow_two_active_keys if { + test.assert_equal_message("User has more than one active access key", check.deny) with input as build_input([ + {"active": {"value": true}}, + {"active": {"value": true}}, + ]) +} + +build_input(keys) = {"aws": {"iam": {"users": [{ + "name": {"value": "test"}, + "accesskeys": keys, +}]}}} diff --git a/checks/cloud/aws/iam/no_password_reuse.go b/checks/cloud/aws/iam/no_password_reuse.go index a45fcd0b..0bd06ed8 100755 --- a/checks/cloud/aws/iam/no_password_reuse.go +++ b/checks/cloud/aws/iam/no_password_reuse.go @@ -35,7 +35,8 @@ The account password policy should be set to prevent using any of the last five Links: terraformNoPasswordReuseLinks, RemediationMarkdown: terraformNoPasswordReuseRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { diff --git a/checks/cloud/aws/iam/no_password_reuse.rego b/checks/cloud/aws/iam/no_password_reuse.rego new file mode 100644 index 00000000..58629d0d --- /dev/null +++ b/checks/cloud/aws/iam/no_password_reuse.rego @@ -0,0 +1,45 @@ +# METADATA +# title: IAM Password policy should prevent password reuse. +# description: | +# IAM account password policies should prevent the reuse of passwords. +# +# The account password policy should be set to prevent using any of the last five used passwords. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0056 +# avd_id: AVD-AWS-0056 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: no-password-reuse +# recommended_action: Prevent password reuse in the policy +# frameworks: +# cis-aws-1.2: +# - "1.10" +# cis-aws-1.4: +# - "1.9" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy +# good_examples: checks/cloud/aws/iam/no_password_reuse.tf.go +# bad_examples: checks/cloud/aws/iam/no_password_reuse.tf.go +package builtin.aws.iam.aws0056 + +import rego.v1 + +deny contains res if { + policy := input.aws.iam.passwordpolicy + policy.__defsec_metadata.managed + policy.reusepreventioncount.value < 5 + res := result.new("Password policy allows reuse of recent passwords.", policy) +} diff --git a/checks/cloud/aws/iam/no_password_reuse_test.go b/checks/cloud/aws/iam/no_password_reuse_test.go deleted file mode 100644 index 53bf316c..00000000 --- a/checks/cloud/aws/iam/no_password_reuse_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckNoPasswordReuse(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "IAM with 1 password that can't be reused (min)", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - ReusePreventionCount: trivyTypes.Int(1, trivyTypes.NewTestMetadata()), - }, - }, - expected: true, - }, - { - name: "IAM with 5 passwords that can't be reused", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - ReusePreventionCount: trivyTypes.Int(5, trivyTypes.NewTestMetadata()), - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckNoPasswordReuse.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckNoPasswordReuse.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/no_password_reuse_test.rego b/checks/cloud/aws/iam/no_password_reuse_test.rego new file mode 100644 index 00000000..0bf1a862 --- /dev/null +++ b/checks/cloud/aws/iam/no_password_reuse_test.rego @@ -0,0 +1,16 @@ +package builtin.aws.iam.aws0056_test + +import rego.v1 + +import data.builtin.aws.iam.aws0056 as check +import data.lib.test + +test_disallow_policy_with_less_than_5_password_reuse if { + inp = {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "reusepreventioncount": {"value": 1}}}}} + test.assert_equal_message("Password policy allows reuse of recent passwords.", check.deny) with input as inp +} + +test_allow_policy_with_5_password_reuse if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "reusepreventioncount": {"value": 5}}}}} + test.assert_empty(check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/no_policy_wildcards.go b/checks/cloud/aws/iam/no_policy_wildcards.go index 7019e6a0..af9ee4dc 100755 --- a/checks/cloud/aws/iam/no_policy_wildcards.go +++ b/checks/cloud/aws/iam/no_policy_wildcards.go @@ -49,7 +49,8 @@ var CheckNoPolicyWildcards = rules.Register( Links: cloudFormationNoPolicyWildcardsLinks, RemediationMarkdown: cloudFormationNoPolicyWildcardsRemediationMarkdown, }, - Severity: severity.High, + Severity: severity.High, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, policy := range s.AWS.IAM.Policies { diff --git a/checks/cloud/aws/iam/no_policy_wildcards.rego b/checks/cloud/aws/iam/no_policy_wildcards.rego new file mode 100644 index 00000000..e60e4ab1 --- /dev/null +++ b/checks/cloud/aws/iam/no_policy_wildcards.rego @@ -0,0 +1,95 @@ +# METADATA +# title: IAM policy should avoid use of wildcards and instead apply the principle of least privilege +# description: | +# You should use the principle of least privilege when defining your IAM policies. This means you should specify each exact permission required without using wildcards, as this could cause the granting of access to certain undesired actions, resources and principals. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html +# custom: +# id: AVD-AWS-0057 +# avd_id: AVD-AWS-0057 +# provider: aws +# service: iam +# severity: HIGH +# short_code: no-policy-wildcards +# recommended_action: Specify the exact permissions required, and to which resources they should apply instead of using wildcards. +# frameworks: +# cis-aws-1.4: +# - "1.16" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +# good_examples: checks/cloud/aws/iam/no_policy_wildcards.tf.go +# bad_examples: checks/cloud/aws/iam/no_policy_wildcards.tf.go +# cloudformation: +# good_examples: checks/cloud/aws/iam/no_policy_wildcards.cf.go +# bad_examples: checks/cloud/aws/iam/no_policy_wildcards.cf.go +package builtin.aws.iam.aws0057 + +import rego.v1 + +cloudwatch_log_stream_resource_pattern := `^arn:aws:logs:.*:.+:log-group:.+:\*` + +deny contains res if { + some policy in input.aws.iam.policies + statement := parse_statement(policy) + some action in statement.Action + contains(action, "*") + message := sprintf("IAM policy document uses wildcarded action %q", [action]) + res := result.new(message, {}) # TODO: MetadataFromIamGo +} + +deny contains res if { + some policy in input.aws.iam.policies + statement := parse_statement(policy) + some resource in statement.Resource + contains(resource, "*") + some action in statement.Action + not data.aws.iam.allowed_actions[action] + not is_object_key_contains_wildcard(resource) + not regex.match(cloudwatch_log_stream_resource_pattern, resource) + message := sprintf("IAM policy document uses sensitive action %q on wildcarded resource %q", [action, resource]) + res := result.new(message, {}) # TODO: MetadataFromIamGo +} + +deny contains res if { + some policy in input.aws.iam.policies + statement := parse_statement(policy) + statement.Principal.All == true # TODO: check if it's exported to Rego + res := result.new("IAM policy document uses wildcarded principal.", {}) # TODO: MetadataFromIamGo +} + +deny contains res if { + some policy in input.aws.iam.policies + statement := parse_statement(policy) + some principal in statement.Principal.AWS + contains(principal, "*") + res := result.new("IAM policy document uses wildcarded principal.", {}) # TODO: MetadataFromIamGo +} + +parse_statement(policy) := statement if { + policy.builtin.value == false + document := json.unmarshal(policy.document.value) + some statement in document.Statement + statement.Effect == "Allow" +} + +is_object_key_contains_wildcard(key) if { + arn_parts := split(key, ":") + count(arn_parts) == 6 + arn_parts[2] == "s3" + + resource_parts := split(arn_parts[5], "/") + count(resource_parts) == 2 + + not contains(resource_parts[0], "*") + contains(resource_parts[1], "*") +} diff --git a/checks/cloud/aws/iam/no_policy_wildcards_test.go b/checks/cloud/aws/iam/no_policy_wildcards_test.go deleted file mode 100644 index 76112498..00000000 --- a/checks/cloud/aws/iam/no_policy_wildcards_test.go +++ /dev/null @@ -1,357 +0,0 @@ -package iam - -import ( - "testing" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - "github.com/aquasecurity/trivy/pkg/iac/state" - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - "github.com/liamg/iamgo" - "github.com/stretchr/testify/assert" -) - -func TestCheckNoPolicyWildcards(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "IAM policy with wildcard resource", - input: iam.IAM{ - Roles: []iam.Role{ - { - Metadata: trivyTypes.NewTestMetadata(), - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithSid("ListYourObjects") - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"s3:ListBucket"}) - sb.WithResources([]string{"arn:aws:s3:::*"}) - sb.WithAWSPrincipals([]string{"arn:aws:iam::1234567890:root"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "Builtin IAM policy with wildcard resource", - input: iam.IAM{ - Roles: []iam.Role{ - { - Metadata: trivyTypes.NewTestMetadata(), - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithSid("ListYourObjects") - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"s3:ListBucket"}) - sb.WithResources([]string{"arn:aws:s3:::*"}) - sb.WithAWSPrincipals([]string{"arn:aws:iam::1234567890:root"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "IAM policy with wildcard action", - input: iam.IAM{ - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithSid("ListYourObjects") - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"s3:*"}) - sb.WithResources([]string{"arn:aws:s3:::bucket-name"}) - sb.WithAWSPrincipals([]string{"arn:aws:iam::1234567890:root"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "IAM policies without wildcards", - input: iam.IAM{ - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"s3:GetObject"}) - sb.WithResources([]string{"arn:aws:s3:::bucket-name"}) - sb.WithAWSPrincipals([]string{"arn:aws:iam::1234567890:root"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - Roles: []iam.Role{ - { - Metadata: trivyTypes.NewTestMetadata(), - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"sts:AssumeRole"}) - sb.WithServicePrincipals([]string{"s3.amazonaws.com"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "IAM policy with wildcard resource for cloudwatch log stream", - input: iam.IAM{ - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"logs:CreateLogStream"}) - sb.WithResources([]string{"arn:aws:logs:us-west-2:123456789012:log-group:SampleLogGroupName:*"}) - sb.WithAWSPrincipals([]string{"arn:aws:iam::1234567890:root"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - Roles: []iam.Role{ - { - Metadata: trivyTypes.NewTestMetadata(), - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"sts:AssumeRole"}) - sb.WithServicePrincipals([]string{"logs.amazonaws.com"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "IAM policy with wildcard resource for cloudwatch log stream", - input: iam.IAM{ - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"logs:CreateLogStream"}) - sb.WithResources([]string{"*"}) - sb.WithAWSPrincipals([]string{"arn:aws:iam::1234567890:root"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - Roles: []iam.Role{ - { - Metadata: trivyTypes.NewTestMetadata(), - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Document: func() iam.Document { - - builder := iamgo.NewPolicyBuilder() - builder.WithVersion("2012-10-17") - - sb := iamgo.NewStatementBuilder() - sb.WithEffect(iamgo.EffectAllow) - sb.WithActions([]string{"sts:AssumeRole"}) - sb.WithServicePrincipals([]string{"logs.amazonaws.com"}) - - builder.WithStatement(sb.Build()) - - return iam.Document{ - Parsed: builder.Build(), - Metadata: trivyTypes.NewTestMetadata(), - } - }(), - Builtin: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckNoPolicyWildcards.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckNoPolicyWildcards.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} - -func TestIsObjectKeyContainsWildcard(t *testing.T) { - tests := []struct { - name string - resource string - expected bool - }{ - { - name: "all s3 objects", - resource: "arn:aws:s3:::examplebucket/*", - expected: true, - }, - { - name: "wildcard in object key", - resource: "arn:aws:s3:::examplebucket/*/test.log", - expected: true, - }, - { - name: "Not S3 ARN", - resource: "arn:aws:cloudwatch:*:123456789012:alarm/*", - expected: false, - }, - { - name: "all S3 buckets and objects", - resource: "arn:aws:s3:::*", - expected: false, - }, - { - name: "non-valid ARN", - resource: "test", - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := isObjectKeyContainsWildcard(tt.resource) - assert.Equal(t, tt.expected, got) - }) - } -} diff --git a/checks/cloud/aws/iam/no_policy_wildcards_test.rego b/checks/cloud/aws/iam/no_policy_wildcards_test.rego new file mode 100644 index 00000000..5596dd07 --- /dev/null +++ b/checks/cloud/aws/iam/no_policy_wildcards_test.rego @@ -0,0 +1,109 @@ +package builtin.aws.iam.aws0057_test + +import rego.v1 + +import data.builtin.aws.iam.aws0057 as check +import data.lib.test + +allowed_actions := {"sqs:ListQueues": {}} + +test_deny_wildcard_resource if { + inp := build_input(false, { + "Effect": "Allow", + "Action": ["s3:ListBucket"], + "Resource": ["arn:aws:s3:::*"], + "Principal": {"AWS": ["arn:aws:iam::1234567890:root"]}, + }) + + test.assert_equal_message(`IAM policy document uses sensitive action "" on wildcarded resource "arn:aws:s3:::*"`, check.deny) with input as inp with data.aws.iam.allowed_actions as allowed_actions +} + +test_allow_wildcard_resource_with_allowed_action if { + inp := build_input(false, { + "Effect": "Allow", + "Action": ["sqs:ListQueues"], + "Resource": ["arn:aws:sqs:*:123456789012:alice_queue_*"], + "Principal": {"AWS": ["arn:aws:iam::1234567890:root"]}, + }) + + test.assert_empty(check.deny) with input as inp with data.aws.iam.allowed_actions as allowed_actions +} + +test_allow_builtin_policy_with_wildcard_resource if { + inp := build_input(true, { + "Effect": "Allow", + "Action": ["s3:ListBucket"], + "Resource": ["arn:aws:s3:::*"], + "Principal": {"AWS": ["arn:aws:iam::1234567890:root"]}, + }) + + test.assert_empty(check.deny) with input as inp with data.aws.iam.allowed_actions as allowed_actions +} + +test_deny_wildcard_action if { + inp := build_input(false, { + "Effect": "Allow", + "Action": ["s3:*"], + "Resource": ["arn:aws:s3:::bucket-name"], + "Principal": {"AWS": ["arn:aws:iam::1234567890:root"]}, + }) + test.assert_equal_message(`IAM policy document uses wildcarded action "s3:*"`, check.deny) with input as inp with data.aws.iam.allowed_actions as allowed_actions +} + +test_allow_policy_without_wildcards if { + inp := build_input(false, { + "Effect": "Allow", + "Action": ["s3:GetObject"], + "Resource": ["arn:aws:s3:::bucket-name"], + "Principal": {"AWS": ["arn:aws:iam::1234567890:root"]}, + }) +} + +test_allow_wildcard_resource_for_cloudwatch_log_group if { + inp := build_input(false, { + "Effect": "Allow", + "Action": ["logs:CreateLogStream"], + "Resource": ["arn:aws:logs:us-west-2:123456789012:log-group:SampleLogGroupName:*"], + }) + test.assert_empty(check.deny) with input as inp with data.aws.iam.allowed_actions as allowed_actions +} + +test_deny_wildcard_resource_for_cloudwatch_log_stream if { + inp := build_input(false, { + "Effect": "Allow", + "Action": ["logs:CreateLogStream"], + "Resource": ["*"], + }) + + test.assert_equal_message("IAM policy document uses sensitive action \"logs:CreateLogStream\" on wildcarded resource \"arn:aws:logs:us-west-2:123456789012:log-group:SampleLogGroupName:*\"", check.deny) with input as inp with data.aws.iam.allowed_actions as allowed_actions +} + +test_deny_issues_with_multiple_policies if { + inp := {"aws": {"iam": {"policies": [ + { + "builtin": {"value": false}, + "document": {"value": json.marshal({"Statement": [{ + "Effect": "Allow", + "Action": ["s3:*"], + "Resource": ["arn:aws:s3:::bucket-name"], + "Principal": {"AWS": ["arn:aws:iam::1234567890:root"]}, + }]})}, + }, + { + "builtin": {"value": false}, + "document": {"value": json.marshal({"Statement": [{ + "Effect": "Allow", + "Action": ["s3:ListBucket"], + "Resource": ["arn:aws:s3:::*"], + "Principal": {"AWS": ["arn:aws:iam::1234567890:root"]}, + }]})}, + }, + ]}}} + + test.assert_count(check.deny, 2) with input as inp with data.aws.iam.allowed_actions as allowed_actions +} + +build_input(builtin, statement) := {"aws": {"iam": {"policies": [{ + "builtin": {"value": builtin}, + "document": {"value": json.marshal({"Statement": [statement]})}, +}]}}} diff --git a/checks/cloud/aws/iam/no_root_access_keys.go b/checks/cloud/aws/iam/no_root_access_keys.go index 8a3bdfba..08a906ae 100644 --- a/checks/cloud/aws/iam/no_root_access_keys.go +++ b/checks/cloud/aws/iam/no_root_access_keys.go @@ -14,7 +14,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/providers" ) -var checkNoRootAccessKeys = rules.Register( +var CheckNoRootAccessKeys = rules.Register( scan.Rule{ AVDID: "AVD-AWS-0141", Provider: providers.AWSProvider, @@ -40,7 +40,8 @@ CIS recommends that all access keys be associated with the root user be removed. Links: terraformNoRootAccessKeysLinks, RemediationMarkdown: terraformNoRootAccessKeysRemediationMarkdown, }, - Severity: severity.Critical, + Severity: severity.Critical, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, user := range s.AWS.IAM.Users { diff --git a/checks/cloud/aws/iam/no_root_access_keys.rego b/checks/cloud/aws/iam/no_root_access_keys.rego new file mode 100644 index 00000000..190b0aba --- /dev/null +++ b/checks/cloud/aws/iam/no_root_access_keys.rego @@ -0,0 +1,47 @@ +# METADATA +# title: The root user has complete access to all services and resources in an AWS account. AWS Access Keys provide programmatic access to a given account. +# description: | +# CIS recommends that all access keys be associated with the root user be removed. Removing access keys associated with the root user limits vectors that the account can be compromised by. Removing the root user access keys also encourages the creation and use of role-based accounts that are least privileged. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html +# custom: +# id: AVD-AWS-0141 +# avd_id: AVD-AWS-0141 +# provider: aws +# service: iam +# severity: CRITICAL +# short_code: no-root-access-keys +# recommended_action: Use lower privileged accounts instead, so only required privileges are available. +# frameworks: +# cis-aws-1.2: +# - "1.12" +# cis-aws-1.4: +# - "1.4" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key +# good_examples: checks/cloud/aws/iam/no_root_access_keys.tf.go +# bad_examples: checks/cloud/aws/iam/no_root_access_keys.tf.go +package builtin.aws.iam.aws0141 + +import data.lib.iam +import rego.v1 + +deny contains res if { + some user in input.aws.iam.users + iam.is_root_user(user) + + some key in user.accesskeys + key.active.value + + res := result.new("Access key exists for root user", key) +} diff --git a/checks/cloud/aws/iam/no_root_access_keys_test.go b/checks/cloud/aws/iam/no_root_access_keys_test.go deleted file mode 100644 index dbd5c634..00000000 --- a/checks/cloud/aws/iam/no_root_access_keys_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckNoRootAccessKeys(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "root user without access key", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - AccessKeys: nil, - }, - }, - }, - expected: false, - }, - { - name: "other user without access key", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("other", trivyTypes.NewTestMetadata()), - AccessKeys: nil, - }, - }, - }, - expected: false, - }, - { - name: "other user with access key", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("other", trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("BLAH", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "root user with inactive access key", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("BLAH", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "root user with active access key", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("root", trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("BLAH", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := checkNoRootAccessKeys.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == checkNoRootAccessKeys.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/no_root_access_keys_test.rego b/checks/cloud/aws/iam/no_root_access_keys_test.rego new file mode 100644 index 00000000..c4de9d6d --- /dev/null +++ b/checks/cloud/aws/iam/no_root_access_keys_test.rego @@ -0,0 +1,40 @@ +package builtin.aws.iam.aws0141_test + +import rego.v1 + +import data.builtin.aws.iam.aws0141 as check +import data.lib.test + +test_allow_root_user_without_access_keys if { + test.assert_empty(check.deny) with input as build_input({"name": {"value": "root"}}) +} + +test_allow_non_root_user_without_access_keys if { + test.assert_empty(check.deny) with input as build_input({"name": {"value": "user"}}) +} + +test_allow_non_root_user_with_access_keys if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "user"}, + "accesskeys": [{"active": {"value": true}}], + }) +} + +test_allow_root_user_with_inactive_access_keys if { + test.assert_empty(check.deny) with input as build_input({ + "name": {"value": "root"}, + "accesskeys": [{"active": {"value": false}}], + }) +} + +test_disallow_root_user_with_active_access_keys if { + test.assert_equal_message("Access key exists for root user", check.deny) with input as build_input({ + "name": {"value": "root"}, + "accesskeys": [ + {"active": {"value": false}}, + {"active": {"value": true}}, + ], + }) +} + +build_input(body) := {"aws": {"iam": {"users": [body]}}} diff --git a/checks/cloud/aws/iam/no_user_attached_policies.go b/checks/cloud/aws/iam/no_user_attached_policies.go index 8c8674d0..9b2a995a 100644 --- a/checks/cloud/aws/iam/no_user_attached_policies.go +++ b/checks/cloud/aws/iam/no_user_attached_policies.go @@ -14,7 +14,7 @@ import ( "github.com/aquasecurity/trivy/pkg/iac/providers" ) -var checkNoUserAttachedPolicies = rules.Register( +var CheckNoUserAttachedPolicies = rules.Register( scan.Rule{ AVDID: "AVD-AWS-0143", Provider: providers.AWSProvider, @@ -40,7 +40,8 @@ CIS recommends that you apply IAM policies directly to groups and roles but not Links: terraformNoUserAttachedPoliciesLinks, RemediationMarkdown: terraformNoUserAttachedPoliciesRemediationMarkdown, }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, user := range s.AWS.IAM.Users { diff --git a/checks/cloud/aws/iam/no_user_attached_policies.rego b/checks/cloud/aws/iam/no_user_attached_policies.rego new file mode 100644 index 00000000..3bd23eb0 --- /dev/null +++ b/checks/cloud/aws/iam/no_user_attached_policies.rego @@ -0,0 +1,43 @@ +# METADATA +# title: IAM policies should not be granted directly to users. +# description: | +# CIS recommends that you apply IAM policies directly to groups and roles but not users. Assigning privileges at the group or role level reduces the complexity of access management as the number of users grow. Reducing access management complexity might in turn reduce opportunity for a principal to inadvertently receive or retain excessive privileges. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://console.aws.amazon.com/iam/ +# custom: +# id: AVD-AWS-0143 +# avd_id: AVD-AWS-0143 +# provider: aws +# service: iam +# severity: LOW +# short_code: no-user-attached-policies +# recommended_action: Grant policies at the group level instead. +# frameworks: +# cis-aws-1.4: +# - "1.15" +# cis-aws-1.2: +# - "1.16" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user +# good_examples: checks/cloud/aws/iam/no_user_attached_policies.tf.go +# bad_examples: checks/cloud/aws/iam/no_user_attached_policies.tf.go +package builtin.aws.iam.aws0143 + +import rego.v1 + +deny contains res if { + some user in input.aws.iam.users + count(user.policies) > 0 + + res := result.new("One or more policies are attached directly to a user", user) +} diff --git a/checks/cloud/aws/iam/no_user_attached_policies_test.go b/checks/cloud/aws/iam/no_user_attached_policies_test.go deleted file mode 100644 index 7f142617..00000000 --- a/checks/cloud/aws/iam/no_user_attached_policies_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckNoUserAttachedPolicies(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "user without policies attached", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("example", trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "user with a policy attached", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("example", trivyTypes.NewTestMetadata()), - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("another.policy", trivyTypes.NewTestMetadata()), - Document: iam.Document{ - Metadata: trivyTypes.NewTestMetadata(), - }, - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := checkNoUserAttachedPolicies.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == checkNoUserAttachedPolicies.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/no_user_attached_policies_test.rego b/checks/cloud/aws/iam/no_user_attached_policies_test.rego new file mode 100644 index 00000000..5d70f92c --- /dev/null +++ b/checks/cloud/aws/iam/no_user_attached_policies_test.rego @@ -0,0 +1,18 @@ +package builtin.aws.iam.aws0143_test + +import rego.v1 + +import data.builtin.aws.iam.aws0143 as check +import data.lib.test + +test_allow_user_without_attached_policies if { + inp := {"aws": {"iam": {"users": [{"policies": []}]}}} + + test.assert_empty(check.deny) with input as inp +} + +test_disallow_user_with_attached_policies if { + inp := {"aws": {"iam": {"users": [{"policies": [{"name": {"value": "policy_name"}}]}]}}} + + test.assert_equal_message("One or more policies are attached directly to a user", check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/remove_expired_certificates.go b/checks/cloud/aws/iam/remove_expired_certificates.go index fcd9fc61..1a6311d1 100644 --- a/checks/cloud/aws/iam/remove_expired_certificates.go +++ b/checks/cloud/aws/iam/remove_expired_certificates.go @@ -37,7 +37,8 @@ recommended to delete expired certificates. Links: []string{ "https://console.aws.amazon.com/iam/", }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, certificate := range s.AWS.IAM.ServerCertificates { diff --git a/checks/cloud/aws/iam/remove_expired_certificates.rego b/checks/cloud/aws/iam/remove_expired_certificates.rego new file mode 100644 index 00000000..d733ce7a --- /dev/null +++ b/checks/cloud/aws/iam/remove_expired_certificates.rego @@ -0,0 +1,42 @@ +# METADATA +# title: Delete expired TLS certificates +# description: | +# Removing expired SSL/TLS certificates eliminates the risk that an invalid certificate will be +# +# deployed accidentally to a resource such as AWS Elastic Load Balancer (ELB), which can +# +# damage the credibility of the application/website behind the ELB. As a best practice, it is +# +# recommended to delete expired certificates. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://console.aws.amazon.com/iam/ +# custom: +# id: AVD-AWS-0168 +# avd_id: AVD-AWS-0168 +# provider: aws +# service: iam +# severity: LOW +# short_code: remove-expired-certificates +# recommended_action: Remove expired certificates +# frameworks: +# cis-aws-1.4: +# - "1.19" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0168 + +import rego.v1 + +deny contains res if { + some certificate in input.aws.iam.servercertificates + time.parse_rfc3339_ns(certificate.expiration.value) < time.now_ns() + + res := result.new("Certificate has expired", certificate) +} diff --git a/checks/cloud/aws/iam/remove_expired_certificates_test.go b/checks/cloud/aws/iam/remove_expired_certificates_test.go deleted file mode 100644 index 4ab10e95..00000000 --- a/checks/cloud/aws/iam/remove_expired_certificates_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package iam - -import ( - "testing" - "time" - - "github.com/aquasecurity/trivy/pkg/iac/state" - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRemoveExpiredCertificates(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "No certs", - input: iam.IAM{}, - expected: false, - }, - { - name: "Valid cert", - input: iam.IAM{ - ServerCertificates: []iam.ServerCertificate{ - { - Metadata: trivyTypes.NewTestMetadata(), - Expiration: trivyTypes.Time(time.Now().Add(time.Hour), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - { - name: "Expired cert", - input: iam.IAM{ - ServerCertificates: []iam.ServerCertificate{ - { - Metadata: trivyTypes.NewTestMetadata(), - Expiration: trivyTypes.Time(time.Now().Add(-time.Hour), trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckRemoveExpiredCertificates.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckRemoveExpiredCertificates.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/remove_expired_certificates_test.rego b/checks/cloud/aws/iam/remove_expired_certificates_test.rego new file mode 100644 index 00000000..75b8af01 --- /dev/null +++ b/checks/cloud/aws/iam/remove_expired_certificates_test.rego @@ -0,0 +1,19 @@ +package builtin.aws.iam.aws0168_test + +import rego.v1 + +import data.builtin.aws.iam.aws0168 as check +import data.lib.datetime +import data.lib.test + +test_disallow_expired_certificate if { + inp := {"aws": {"iam": {"servercertificates": [{"expiration": {"value": time.format(time.now_ns() - datetime.days_to_ns(10))}}]}}} + + test.assert_equal_message("Certificate has expired", check.deny) with input as inp +} + +test_allow_non_expired_certificate if { + inp := {"aws": {"iam": {"servercertificates": [{"expiration": {"value": time.format(time.now_ns() + datetime.days_to_ns(10))}}]}}} + + test.assert_empty(check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/require_lowercase_in_passwords.go b/checks/cloud/aws/iam/require_lowercase_in_passwords.go index 1a3dba10..0d8b19d2 100755 --- a/checks/cloud/aws/iam/require_lowercase_in_passwords.go +++ b/checks/cloud/aws/iam/require_lowercase_in_passwords.go @@ -32,7 +32,8 @@ var CheckRequireLowercaseInPasswords = rules.Register( Links: terraformRequireLowercaseInPasswordsLinks, RemediationMarkdown: terraformRequireLowercaseInPasswordsRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { policy := s.AWS.IAM.PasswordPolicy diff --git a/checks/cloud/aws/iam/require_lowercase_in_passwords.rego b/checks/cloud/aws/iam/require_lowercase_in_passwords.rego new file mode 100644 index 00000000..f096c49a --- /dev/null +++ b/checks/cloud/aws/iam/require_lowercase_in_passwords.rego @@ -0,0 +1,42 @@ +# METADATA +# title: IAM Password policy should have requirement for at least one lowercase character. +# description: | +# IAM account password policies should ensure that passwords content including at least one lowercase character. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0058 +# avd_id: AVD-AWS-0058 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: require-lowercase-in-passwords +# recommended_action: Enforce longer, more complex passwords in the policy +# frameworks: +# cis-aws-1.2: +# - "1.6" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy +# good_examples: checks/cloud/aws/iam/require_lowercase_in_passwords.tf.go +# bad_examples: checks/cloud/aws/iam/require_lowercase_in_passwords.tf.go +package builtin.aws.iam.aws0058 + +import rego.v1 + +deny contains res if { + policy := input.aws.iam.passwordpolicy + policy.__defsec_metadata.managed + not policy.requirelowercase.value + + res := result.new("Password policy does not require lowercase characters", policy.requirelowercase) +} diff --git a/checks/cloud/aws/iam/require_lowercase_in_passwords_test.go b/checks/cloud/aws/iam/require_lowercase_in_passwords_test.go deleted file mode 100644 index c4b499c3..00000000 --- a/checks/cloud/aws/iam/require_lowercase_in_passwords_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRequireLowercaseInPasswords(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "IAM password policy lowercase not required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireLowercase: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - expected: true, - }, - { - name: "IAM password policy lowercase required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireLowercase: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckRequireLowercaseInPasswords.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckRequireLowercaseInPasswords.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/require_lowercase_in_passwords_test.rego b/checks/cloud/aws/iam/require_lowercase_in_passwords_test.rego new file mode 100644 index 00000000..475687b8 --- /dev/null +++ b/checks/cloud/aws/iam/require_lowercase_in_passwords_test.rego @@ -0,0 +1,18 @@ +package builtin.aws.iam.aws0058_test + +import rego.v1 + +import data.builtin.aws.iam.aws0058 as check +import data.lib.test + +test_allow_policy_require_lowercase_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requirelowercase": {"value": true}}}}} + + test.assert_empty(check.deny) with input as inp +} + +test_disallow_policy_no_require_lowercase_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requirelowercase": {"value": false}}}}} + + test.assert_equal_message("Password policy does not require lowercase characters", check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/require_numbers_in_passwords.go b/checks/cloud/aws/iam/require_numbers_in_passwords.go index 1a9ae8b1..11fcf4be 100755 --- a/checks/cloud/aws/iam/require_numbers_in_passwords.go +++ b/checks/cloud/aws/iam/require_numbers_in_passwords.go @@ -32,7 +32,8 @@ var CheckRequireNumbersInPasswords = rules.Register( Links: terraformRequireNumbersInPasswordsLinks, RemediationMarkdown: terraformRequireNumbersInPasswordsRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { policy := s.AWS.IAM.PasswordPolicy diff --git a/checks/cloud/aws/iam/require_numbers_in_passwords.rego b/checks/cloud/aws/iam/require_numbers_in_passwords.rego new file mode 100644 index 00000000..0eb0207a --- /dev/null +++ b/checks/cloud/aws/iam/require_numbers_in_passwords.rego @@ -0,0 +1,42 @@ +# METADATA +# title: IAM Password policy should have requirement for at least one number in the password. +# description: | +# IAM account password policies should ensure that passwords content including at least one number. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0059 +# avd_id: AVD-AWS-0059 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: require-numbers-in-passwords +# recommended_action: Enforce longer, more complex passwords in the policy +# frameworks: +# cis-aws-1.2: +# - "1.8" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy +# good_examples: checks/cloud/aws/iam/require_numbers_in_passwords.tf.go +# bad_examples: checks/cloud/aws/iam/require_numbers_in_passwords.tf.go +package builtin.aws.iam.aws0059 + +import rego.v1 + +deny contains res if { + policy := input.aws.iam.passwordpolicy + policy.__defsec_metadata.managed + not policy.requirenumbers.value + + res := result.new("Password policy does not require numbers.", policy.requirenumbers) +} diff --git a/checks/cloud/aws/iam/require_numbers_in_passwords_test.go b/checks/cloud/aws/iam/require_numbers_in_passwords_test.go deleted file mode 100644 index 20b8fcdf..00000000 --- a/checks/cloud/aws/iam/require_numbers_in_passwords_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRequireNumbersInPasswords(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "IAM password policy numbers not required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireNumbers: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - expected: true, - }, - { - name: "IAM password policy numbers required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireNumbers: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckRequireNumbersInPasswords.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckRequireNumbersInPasswords.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/require_numbers_in_passwords_test.rego b/checks/cloud/aws/iam/require_numbers_in_passwords_test.rego new file mode 100644 index 00000000..dd617165 --- /dev/null +++ b/checks/cloud/aws/iam/require_numbers_in_passwords_test.rego @@ -0,0 +1,16 @@ +package builtin.aws.iam.aws0059_test + +import rego.v1 + +import data.builtin.aws.iam.aws0059 as check +import data.lib.test + +test_allow_policy_require_numbers_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requirenumbers": {"value": true}}}}} + test.assert_empty(check.deny) with input as inp +} + +test_disallow_policy_no_require_numbers_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requirenumbers": {"value": false}}}}} + test.assert_equal_message("Password policy does not require numbers.", check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/require_support_role.go b/checks/cloud/aws/iam/require_support_role.go index bb41ccc9..5bb56051 100644 --- a/checks/cloud/aws/iam/require_support_role.go +++ b/checks/cloud/aws/iam/require_support_role.go @@ -34,7 +34,8 @@ IAM Policy to allow Support Center Access in order to manage Incidents with AWS Links: []string{ "https://console.aws.amazon.com/iam/", }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { diff --git a/checks/cloud/aws/iam/require_support_role.rego b/checks/cloud/aws/iam/require_support_role.rego new file mode 100644 index 00000000..1d561fd5 --- /dev/null +++ b/checks/cloud/aws/iam/require_support_role.rego @@ -0,0 +1,43 @@ +# METADATA +# title: Missing IAM Role to allow authorized users to manage incidents with AWS Support. +# description: | +# By implementing least privilege for access control, an IAM Role will require an appropriate +# +# IAM Policy to allow Support Center Access in order to manage Incidents with AWS Support. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://console.aws.amazon.com/iam/ +# custom: +# id: AVD-AWS-0169 +# avd_id: AVD-AWS-0169 +# provider: aws +# service: iam +# severity: LOW +# short_code: require-support-role +# recommended_action: Create an IAM role with the necessary permissions to manage incidents with AWS Support. +# frameworks: +# cis-aws-1.4: +# - "1.17" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0169 + +import rego.v1 + +deny contains res if { + some role in input.aws.iam.roles + not has_iam_support_role(role) + res := result.new("Missing IAM support role.", role) +} + +has_iam_support_role(role) if { + some policy in role.policies + policy.builtin.value + policy.name.value == "AWSSupportAccess" +} diff --git a/checks/cloud/aws/iam/require_support_role_test.go b/checks/cloud/aws/iam/require_support_role_test.go deleted file mode 100644 index a6e4dbdd..00000000 --- a/checks/cloud/aws/iam/require_support_role_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRequireSupportRole(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "No support role", - input: iam.IAM{}, - expected: true, - }, - { - name: "Has support role", - input: iam.IAM{ - Roles: []iam.Role{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("example", trivyTypes.NewTestMetadata()), - Policies: []iam.Policy{ - { - Metadata: trivyTypes.NewTestMetadata(), - Builtin: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Name: trivyTypes.String("AWSSupportRole", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckRequireSupportRole.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckRequireSupportRole.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/require_support_role_test.rego b/checks/cloud/aws/iam/require_support_role_test.rego new file mode 100644 index 00000000..c0c1281e --- /dev/null +++ b/checks/cloud/aws/iam/require_support_role_test.rego @@ -0,0 +1,39 @@ +package builtin.aws.iam.aws0169_test + +import rego.v1 + +import data.builtin.aws.iam.aws0169 as check +import data.lib.test + +test_disallow_no_support_role if { + inp := {"aws": {"iam": {"roles": [{"policies": [{ + "name": {"value": "roleName"}, + "builtin": {"value": true}, + }]}]}}} + + test.assert_equal_message("Missing IAM support role.", check.deny) with input as inp +} + +test_disallow_non_built_in_support_role if { + inp := {"aws": {"iam": {"roles": [{"policies": [{ + "name": {"value": "AWSSupportAccess"}, + "builtin": {"value": false}, + }]}]}}} + + test.assert_equal_message("Missing IAM support role.", check.deny) with input as inp +} + +test_allow_has_support_role if { + inp := {"aws": {"iam": {"roles": [{"policies": [ + { + "name": {"value": "AWSSupplyChainFederationAdminAccess"}, + "builtin": {"value": true}, + }, + { + "name": {"value": "AWSSupportAccess"}, + "builtin": {"value": true}, + }, + ]}]}}} + + test.assert_empty(check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/require_symbols_in_passwords.go b/checks/cloud/aws/iam/require_symbols_in_passwords.go index 73c49464..6624fabf 100755 --- a/checks/cloud/aws/iam/require_symbols_in_passwords.go +++ b/checks/cloud/aws/iam/require_symbols_in_passwords.go @@ -32,7 +32,8 @@ var CheckRequireSymbolsInPasswords = rules.Register( Links: terraformRequireSymbolsInPasswordsLinks, RemediationMarkdown: terraformRequireSymbolsInPasswordsRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { policy := s.AWS.IAM.PasswordPolicy diff --git a/checks/cloud/aws/iam/require_symbols_in_passwords.rego b/checks/cloud/aws/iam/require_symbols_in_passwords.rego new file mode 100644 index 00000000..b2d5048c --- /dev/null +++ b/checks/cloud/aws/iam/require_symbols_in_passwords.rego @@ -0,0 +1,42 @@ +# METADATA +# title: IAM Password policy should have requirement for at least one symbol in the password. +# description: | +# IAM account password policies should ensure that passwords content including a symbol. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0060 +# avd_id: AVD-AWS-0060 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: require-symbols-in-passwords +# recommended_action: Enforce longer, more complex passwords in the policy +# frameworks: +# cis-aws-1.2: +# - "1.7" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy +# good_examples: checks/cloud/aws/iam/require_symbols_in_passwords.tf.go +# bad_examples: checks/cloud/aws/iam/require_symbols_in_passwords.tf.go +package builtin.aws.iam.aws0060 + +import rego.v1 + +deny contains res if { + policy := input.aws.iam.passwordpolicy + policy.__defsec_metadata.managed + not policy.requiresymbols.value + + res := result.new("Password policy does not require symbols.", policy.requiresymbols) +} diff --git a/checks/cloud/aws/iam/require_symbols_in_passwords_test.go b/checks/cloud/aws/iam/require_symbols_in_passwords_test.go deleted file mode 100644 index 44900017..00000000 --- a/checks/cloud/aws/iam/require_symbols_in_passwords_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRequireSymbolsInPasswords(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "IAM password policy symbols not required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireSymbols: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - expected: true, - }, - { - name: "IAM password policy symbols required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireSymbols: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckRequireSymbolsInPasswords.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckRequireSymbolsInPasswords.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/require_symbols_in_passwords_test.rego b/checks/cloud/aws/iam/require_symbols_in_passwords_test.rego new file mode 100644 index 00000000..14a54f3f --- /dev/null +++ b/checks/cloud/aws/iam/require_symbols_in_passwords_test.rego @@ -0,0 +1,16 @@ +package builtin.aws.iam.aws0060_test + +import rego.v1 + +import data.builtin.aws.iam.aws0060 as check +import data.lib.test + +test_allow_policy_require_symbols_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requiresymbols": {"value": true}}}}} + test.assert_empty(check.deny) with input as inp +} + +test_disallow_policy_no_require_symbols_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requiresymbols": {"value": false}}}}} + test.assert_equal_message("Password policy does not require symbols.", check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/require_uppercase_in_passwords.go b/checks/cloud/aws/iam/require_uppercase_in_passwords.go index a2f70600..7c8e1470 100755 --- a/checks/cloud/aws/iam/require_uppercase_in_passwords.go +++ b/checks/cloud/aws/iam/require_uppercase_in_passwords.go @@ -33,7 +33,8 @@ IAM account password policies should ensure that passwords content including at Links: terraformRequireUppercaseInPasswordsLinks, RemediationMarkdown: terraformRequireUppercaseInPasswordsRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { policy := s.AWS.IAM.PasswordPolicy diff --git a/checks/cloud/aws/iam/require_uppercase_in_passwords.rego b/checks/cloud/aws/iam/require_uppercase_in_passwords.rego new file mode 100644 index 00000000..34eb799f --- /dev/null +++ b/checks/cloud/aws/iam/require_uppercase_in_passwords.rego @@ -0,0 +1,44 @@ +# METADATA +# title: IAM Password policy should have requirement for at least one uppercase character. +# description: | +# , +# +# IAM account password policies should ensure that passwords content including at least one uppercase character. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0061 +# avd_id: AVD-AWS-0061 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: require-uppercase-in-passwords +# recommended_action: Enforce longer, more complex passwords in the policy +# frameworks: +# cis-aws-1.2: +# - "1.5" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy +# good_examples: checks/cloud/aws/iam/require_uppercase_in_passwords.tf.go +# bad_examples: checks/cloud/aws/iam/require_uppercase_in_passwords.tf.go +package builtin.aws.iam.aws0061 + +import rego.v1 + +deny contains res if { + policy := input.aws.iam.passwordpolicy + policy.__defsec_metadata.managed + not policy.requireuppercase.value + + res := result.new("Password policy does not require uppercase characters.", policy.requireuppercase) +} diff --git a/checks/cloud/aws/iam/require_uppercase_in_passwords_test.go b/checks/cloud/aws/iam/require_uppercase_in_passwords_test.go deleted file mode 100644 index e60be0f4..00000000 --- a/checks/cloud/aws/iam/require_uppercase_in_passwords_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRequireUppercaseInPasswords(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "IAM password policy uppercase not required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireUppercase: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - expected: true, - }, - { - name: "IAM password policy uppercase required", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - RequireUppercase: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckRequireUppercaseInPasswords.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckRequireUppercaseInPasswords.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/require_uppercase_in_passwords_test.rego b/checks/cloud/aws/iam/require_uppercase_in_passwords_test.rego new file mode 100644 index 00000000..853795ad --- /dev/null +++ b/checks/cloud/aws/iam/require_uppercase_in_passwords_test.rego @@ -0,0 +1,16 @@ +package builtin.aws.iam.aws0061_test + +import rego.v1 + +import data.builtin.aws.iam.aws0061 as check +import data.lib.test + +test_allow_policy_require_uppercase_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requireuppercase": {"value": true}}}}} + test.assert_empty(check.deny) with input as inp +} + +test_disallow_policy_no_require_uppercase_in_passwords if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "requireuppercase": {"value": false}}}}} + test.assert_equal_message("Password policy does not require uppercase characters.", check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/rotate_access_keys.go b/checks/cloud/aws/iam/rotate_access_keys.go index 909d25f1..f8c4c097 100644 --- a/checks/cloud/aws/iam/rotate_access_keys.go +++ b/checks/cloud/aws/iam/rotate_access_keys.go @@ -36,7 +36,8 @@ Regularly rotating your IAM credentials helps prevent a compromised set of IAM a Links: []string{ "https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/automatically-rotate-iam-user-access-keys-at-scale-with-aws-organizations-and-aws-secrets-manager.html", }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { diff --git a/checks/cloud/aws/iam/rotate_access_keys.rego b/checks/cloud/aws/iam/rotate_access_keys.rego new file mode 100644 index 00000000..4b2ae935 --- /dev/null +++ b/checks/cloud/aws/iam/rotate_access_keys.rego @@ -0,0 +1,47 @@ +# METADATA +# title: Access keys should be rotated at least every 90 days +# description: | +# Regularly rotating your IAM credentials helps prevent a compromised set of IAM access keys from accessing components in your AWS account. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/automatically-rotate-iam-user-access-keys-at-scale-with-aws-organizations-and-aws-secrets-manager.html +# custom: +# id: AVD-AWS-0146 +# avd_id: AVD-AWS-0146 +# provider: aws +# service: iam +# severity: LOW +# short_code: rotate-access-keys +# recommended_action: Rotate keys every 90 days or less +# frameworks: +# cis-aws-1.2: +# - "1.4" +# cis-aws-1.4: +# - "1.14" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +package builtin.aws.iam.aws0146 + +import data.lib.datetime +import rego.v1 + +deny contains res if { + some user in input.aws.iam.users + + some key in user.accesskeys + key.active.value + + ns := time.parse_rfc3339_ns(key.creationdate.value) + diff := time.now_ns() - ns + diff > datetime.days_to_ns(90) + days := ceil((diff - datetime.days_to_ns(90)) / datetime.ns_in_day) + + msg := sprintf("User access key %q should have been rotated %d day(s) ago", [key.accesskeyid.value, days]) + res := result.new(msg, user) +} diff --git a/checks/cloud/aws/iam/rotate_access_keys_test.go b/checks/cloud/aws/iam/rotate_access_keys_test.go deleted file mode 100644 index d56d9fc8..00000000 --- a/checks/cloud/aws/iam/rotate_access_keys_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package iam - -import ( - "testing" - "time" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckAccessKeysRotated(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "Access key created a month ago", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "Access key created 4 months ago", - input: iam.IAM{ - Users: []iam.User{ - { - Metadata: trivyTypes.NewTestMetadata(), - Name: trivyTypes.String("user", trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.TimeUnresolvable(trivyTypes.NewTestMetadata()), - AccessKeys: []iam.AccessKey{ - { - Metadata: trivyTypes.NewTestMetadata(), - AccessKeyId: trivyTypes.String("AKIACKCEVSQ6C2EXAMPLE", trivyTypes.NewTestMetadata()), - Active: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - CreationDate: trivyTypes.Time(time.Now().Add(-time.Hour*24*30*4), trivyTypes.NewTestMetadata()), - LastAccess: trivyTypes.Time(time.Now(), trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckAccessKeysRotated.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckAccessKeysRotated.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/rotate_access_keys_test.rego b/checks/cloud/aws/iam/rotate_access_keys_test.rego new file mode 100644 index 00000000..53136a9b --- /dev/null +++ b/checks/cloud/aws/iam/rotate_access_keys_test.rego @@ -0,0 +1,25 @@ +package builtin.aws.iam.aws0146_test + +import rego.v1 + +import data.builtin.aws.iam.aws0146 as check +import data.lib.datetime +import data.lib.test + +test_allow_access_key_created_within_90_days if { + inp := {"aws": {"iam": {"users": [{"accesskeys": [{ + "creationdate": {"value": time.format(time.now_ns() - datetime.days_to_ns(10))}, + "accesskeyid": {"value": "keyid"}, + "active": {"value": true}, + }]}]}}} + test.assert_empty(check.deny) with input as inp +} + +test_disallow_access_key_created_more_than_90_days_ago if { + inp := {"aws": {"iam": {"users": [{"accesskeys": [{ + "creationdate": {"value": time.format(time.now_ns() - datetime.days_to_ns(100))}, + "accesskeyid": {"value": "keyid"}, + "active": {"value": true}, + }]}]}}} + test.assert_equal_message(`User access key "keyid" should have been rotated 10 day(s) ago`, check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/set_max_password_age.go b/checks/cloud/aws/iam/set_max_password_age.go index 3c045242..2dd98dbe 100755 --- a/checks/cloud/aws/iam/set_max_password_age.go +++ b/checks/cloud/aws/iam/set_max_password_age.go @@ -34,7 +34,8 @@ The account password policy should be set to expire passwords after 90 days or l Links: terraformSetMaxPasswordAgeLinks, RemediationMarkdown: terraformSetMaxPasswordAgeRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { policy := s.AWS.IAM.PasswordPolicy diff --git a/checks/cloud/aws/iam/set_max_password_age.rego b/checks/cloud/aws/iam/set_max_password_age.rego new file mode 100644 index 00000000..fab7c9a2 --- /dev/null +++ b/checks/cloud/aws/iam/set_max_password_age.rego @@ -0,0 +1,43 @@ +# METADATA +# title: IAM Password policy should have expiry less than or equal to 90 days. +# description: | +# IAM account password policies should have a maximum age specified. +# +# The account password policy should be set to expire passwords after 90 days or less. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0062 +# avd_id: AVD-AWS-0062 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: set-max-password-age +# recommended_action: Limit the password duration with an expiry in the policy +# frameworks: +# cis-aws-1.2: +# - "1.11" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy +# good_examples: checks/cloud/aws/iam/set_max_password_age.tf.go +# bad_examples: checks/cloud/aws/iam/set_max_password_age.tf.go +package builtin.aws.iam.aws0062 + +import rego.v1 + +deny contains res if { + policy := input.aws.iam.passwordpolicy + policy.__defsec_metadata.managed + policy.maxagedays.value < 90 + res := result.new("Password policy allows a maximum password age of greater than 90 days.", policy.maxagedays) +} diff --git a/checks/cloud/aws/iam/set_max_password_age_test.go b/checks/cloud/aws/iam/set_max_password_age_test.go deleted file mode 100644 index 2cb93d81..00000000 --- a/checks/cloud/aws/iam/set_max_password_age_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckSetMaxPasswordAge(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "Password expires in 99 days", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - MaxAgeDays: trivyTypes.Int(99, trivyTypes.NewTestMetadata()), - }, - }, - expected: true, - }, - { - name: "Password expires in 60 days", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - MaxAgeDays: trivyTypes.Int(60, trivyTypes.NewTestMetadata()), - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckSetMaxPasswordAge.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckSetMaxPasswordAge.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/set_max_password_age_test.rego b/checks/cloud/aws/iam/set_max_password_age_test.rego new file mode 100644 index 00000000..dfa42d9b --- /dev/null +++ b/checks/cloud/aws/iam/set_max_password_age_test.rego @@ -0,0 +1,16 @@ +package builtin.aws.iam.aws0062_test + +import rego.v1 + +import data.builtin.aws.iam.aws0062 as check +import data.lib.test + +test_allow_password_with_max_age_days_over_90 if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "maxagedays": {"value": 91}}}}} + test.assert_empty(check.deny) with input as inp +} + +test_disallow_password_with_max_age_days_within_90 if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "maxagedays": {"value": 60}}}}} + test.assert_equal_message("Password policy allows a maximum password age of greater than 90 days.", check.deny) with input as inp +} diff --git a/checks/cloud/aws/iam/set_minimum_password_length.go b/checks/cloud/aws/iam/set_minimum_password_length.go index 8fc8e31a..aefa7f03 100755 --- a/checks/cloud/aws/iam/set_minimum_password_length.go +++ b/checks/cloud/aws/iam/set_minimum_password_length.go @@ -35,7 +35,8 @@ The account password policy should be set to enforce minimum password length of Links: terraformSetMinimumPasswordLengthLinks, RemediationMarkdown: terraformSetMinimumPasswordLengthRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { policy := s.AWS.IAM.PasswordPolicy diff --git a/checks/cloud/aws/iam/set_minimum_password_length.rego b/checks/cloud/aws/iam/set_minimum_password_length.rego new file mode 100644 index 00000000..743d0dcd --- /dev/null +++ b/checks/cloud/aws/iam/set_minimum_password_length.rego @@ -0,0 +1,47 @@ +# METADATA +# title: IAM Password policy should have minimum password length of 14 or more characters. +# description: | +# IAM account password policies should ensure that passwords have a minimum length. +# +# The account password policy should be set to enforce minimum password length of at least 14 characters. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html#password-policy-details +# custom: +# id: AVD-AWS-0063 +# avd_id: AVD-AWS-0063 +# provider: aws +# service: iam +# severity: MEDIUM +# short_code: set-minimum-password-length +# recommended_action: Enforce longer, more complex passwords in the policy +# frameworks: +# cis-aws-1.2: +# - "1.9" +# cis-aws-1.4: +# - "1.8" +# input: +# selector: +# - type: cloud +# subtypes: +# - service: iam +# provider: aws +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy +# good_examples: checks/cloud/aws/iam/set_minimum_password_length.tf.go +# bad_examples: checks/cloud/aws/iam/set_minimum_password_length.tf.go +package builtin.aws.iam.aws0063 + +import rego.v1 + +msg := "Password policy allows a maximum password age of greater than 90 days" + +deny contains res if { + policy := input.aws.iam.passwordpolicy + policy.__defsec_metadata.managed + policy.minimumlength.value < 14 + res := result.new("Password policy allows a maximum password age of greater than 90 days", policy.minimumlength) +} diff --git a/checks/cloud/aws/iam/set_minimum_password_length_test.go b/checks/cloud/aws/iam/set_minimum_password_length_test.go deleted file mode 100644 index cfdc8764..00000000 --- a/checks/cloud/aws/iam/set_minimum_password_length_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package iam - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckSetMinimumPasswordLength(t *testing.T) { - tests := []struct { - name string - input iam.IAM - expected bool - }{ - { - name: "Minimum password length set to 8", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - MinimumLength: trivyTypes.Int(8, trivyTypes.NewTestMetadata()), - }, - }, - expected: true, - }, - { - name: "Minimum password length set to 15", - input: iam.IAM{ - PasswordPolicy: iam.PasswordPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - MinimumLength: trivyTypes.Int(15, trivyTypes.NewTestMetadata()), - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.AWS.IAM = test.input - results := CheckSetMinimumPasswordLength.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckSetMinimumPasswordLength.LongID() { - found = true - } - } - if test.expected { - assert.True(t, found, "Rule should have been found") - } else { - assert.False(t, found, "Rule should not have been found") - } - }) - } -} diff --git a/checks/cloud/aws/iam/set_minimum_password_length_test.rego b/checks/cloud/aws/iam/set_minimum_password_length_test.rego new file mode 100644 index 00000000..b687ec1e --- /dev/null +++ b/checks/cloud/aws/iam/set_minimum_password_length_test.rego @@ -0,0 +1,16 @@ +package builtin.aws.iam.aws0063_test + +import rego.v1 + +import data.builtin.aws.iam.aws0063 as check +import data.lib.test + +test_allow_password_length_over_14 if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "minimumlength": {"value": 15}}}}} + test.assert_empty(check.deny) with input as inp +} + +test_disallow_password_length_under_14 if { + inp := {"aws": {"iam": {"passwordpolicy": {"__defsec_metadata": {"managed": true}, "minimumlength": {"value": 13}}}}} + test.assert_equal_message("Password policy allows a maximum password age of greater than 90 days", check.deny) with input as inp +} diff --git a/lib/datetime.rego b/lib/datetime.rego new file mode 100644 index 00000000..77cf7b74 --- /dev/null +++ b/lib/datetime.rego @@ -0,0 +1,15 @@ +package lib.datetime + +import rego.v1 + +ns_in_day := 86400000000000 + +zero_time_string := "0001-01-01T00:00:00Z" + +time_is_never(string_value) := string_value == zero_time_string + +time_diff_gt_days(value, days) := (time.now_ns() - time.parse_rfc3339_ns(value)) > days_to_ns(days) + +time_diff_lt_days(value, days) := (time.now_ns() - time.parse_rfc3339_ns(value)) < days_to_ns(days) + +days_to_ns(days) := days * ns_in_day diff --git a/lib/iam.rego b/lib/iam.rego new file mode 100644 index 00000000..c96caa60 --- /dev/null +++ b/lib/iam.rego @@ -0,0 +1,24 @@ +package lib.iam + +import rego.v1 + +import data.lib.datetime + +is_user_logged_in(user) if { + # user.lastaccess.is_resolvable + not datetime.time_is_never(user.lastaccess.value) +} + +user_has_mfa_devices(user) if count(user.mfadevices) > 0 + +user_is_inactive(user, days) if { + is_user_logged_in(user) + datetime.time_diff_gt_days(user.lastaccess.value, days) +} + +key_is_unused(key, days) if { + key.active.value + datetime.time_diff_gt_days(key.lastaccess.value, days) +} + +is_root_user(user) := user.name.value == "root"