diff --git a/avd_docs/azure/monitor/AVD-AZU-0031/docs.md b/avd_docs/azure/monitor/AVD-AZU-0031/docs.md index 2e4c39a7..5839c392 100644 --- a/avd_docs/azure/monitor/AVD-AZU-0031/docs.md +++ b/avd_docs/azure/monitor/AVD-AZU-0031/docs.md @@ -1,8 +1,9 @@ The average time to detect a breach is up to 210 days, to ensure that all the information required for an effective investigation is available, the retention period should allow for delayed starts to investigating. + ### Impact -Short life activity logs can lead to missing records when investigating a breach + {{ remediationActions }} diff --git a/avd_docs/azure/monitor/AVD-AZU-0032/docs.md b/avd_docs/azure/monitor/AVD-AZU-0032/docs.md index ec736a9b..47af42ff 100644 --- a/avd_docs/azure/monitor/AVD-AZU-0032/docs.md +++ b/avd_docs/azure/monitor/AVD-AZU-0032/docs.md @@ -1,8 +1,9 @@ Log profiles should capture all regions to ensure that all events are logged + ### Impact -Activity may be occurring in locations that aren't being monitored + {{ remediationActions }} diff --git a/avd_docs/azure/monitor/AVD-AZU-0033/docs.md b/avd_docs/azure/monitor/AVD-AZU-0033/docs.md index 16196826..38cee9ff 100644 --- a/avd_docs/azure/monitor/AVD-AZU-0033/docs.md +++ b/avd_docs/azure/monitor/AVD-AZU-0033/docs.md @@ -1,8 +1,9 @@ Log profiles should capture all categories to ensure that all events are logged + ### Impact -Log profile must capture all activity to be able to ensure that all relevant information possible is available for an investigation + {{ remediationActions }} diff --git a/avd_docs/azure/network/AVD-AZU-0047/docs.md b/avd_docs/azure/network/AVD-AZU-0047/docs.md index 41376117..420dcd15 100644 --- a/avd_docs/azure/network/AVD-AZU-0047/docs.md +++ b/avd_docs/azure/network/AVD-AZU-0047/docs.md @@ -1,10 +1,10 @@ Network security rules should not use very broad subnets. - Where possible, segments should be broken into smaller subnets. + ### Impact -The port is exposed for ingress from the internet + {{ remediationActions }} diff --git a/avd_docs/azure/network/AVD-AZU-0048/docs.md b/avd_docs/azure/network/AVD-AZU-0048/docs.md index eef047ce..d1268a1b 100644 --- a/avd_docs/azure/network/AVD-AZU-0048/docs.md +++ b/avd_docs/azure/network/AVD-AZU-0048/docs.md @@ -1,10 +1,10 @@ RDP access can be configured on either the network security group or in the network security group rule. - RDP access should not be permitted from the internet (*, 0.0.0.0, /0, internet, any). Consider using the Azure Bastion Service. + ### Impact -Anyone from the internet can potentially RDP onto an instance + {{ remediationActions }} diff --git a/avd_docs/azure/network/AVD-AZU-0049/docs.md b/avd_docs/azure/network/AVD-AZU-0049/docs.md index d5e28c3e..b758ec9c 100644 --- a/avd_docs/azure/network/AVD-AZU-0049/docs.md +++ b/avd_docs/azure/network/AVD-AZU-0049/docs.md @@ -1,11 +1,13 @@ -Flow logs are the source of truth for all network activity in your cloud environment. -To enable analysis in security event that was detected late, you need to have the logs available. - +Flow logs are the source of truth for all network activity in your cloud environment. + +To enable analysis in security event that was detected late, you need to have the logs available. + Setting an retention policy will help ensure as much information is available for review. + ### Impact -Not enabling retention or having short expiry on flow logs could lead to compromise being undetected limiting time for analysis + {{ remediationActions }} diff --git a/avd_docs/azure/network/AVD-AZU-0050/docs.md b/avd_docs/azure/network/AVD-AZU-0050/docs.md index e42590c0..31fccd28 100644 --- a/avd_docs/azure/network/AVD-AZU-0050/docs.md +++ b/avd_docs/azure/network/AVD-AZU-0050/docs.md @@ -1,10 +1,10 @@ -SSH access can be configured on either the network security group or in the network security group rule. - +SSH access can be configured on either the network security group or in the network security group rule. SSH access should not be permitted from the internet (*, 0.0.0.0, /0, internet, any) + ### Impact -Its dangerous to allow SSH access from the internet + {{ remediationActions }} diff --git a/avd_docs/azure/network/AVD-AZU-0051/docs.md b/avd_docs/azure/network/AVD-AZU-0051/docs.md index b5834eb0..420dcd15 100644 --- a/avd_docs/azure/network/AVD-AZU-0051/docs.md +++ b/avd_docs/azure/network/AVD-AZU-0051/docs.md @@ -1,10 +1,10 @@ Network security rules should not use very broad subnets. - Where possible, segments should be broken into smaller subnets. + ### Impact -The port is exposed for egress to the internet + {{ remediationActions }} diff --git a/avd_docs/azure/securitycenter/AVD-AZU-0044/Terraform.md b/avd_docs/azure/securitycenter/AVD-AZU-0044/Terraform.md index ddd8c460..440ed302 100644 --- a/avd_docs/azure/securitycenter/AVD-AZU-0044/Terraform.md +++ b/avd_docs/azure/securitycenter/AVD-AZU-0044/Terraform.md @@ -1,5 +1,5 @@ - Set alert notifications to be on +Set alert notifications to be on ```hcl resource "azurerm_security_center_contact" "good_example" { diff --git a/avd_docs/azure/securitycenter/AVD-AZU-0044/docs.md b/avd_docs/azure/securitycenter/AVD-AZU-0044/docs.md index b1a5bf93..bf1edd70 100644 --- a/avd_docs/azure/securitycenter/AVD-AZU-0044/docs.md +++ b/avd_docs/azure/securitycenter/AVD-AZU-0044/docs.md @@ -1,9 +1,11 @@ -It is recommended that at least one valid contact is configured for the security center. +It is recommended that at least one valid contact is configured for the security center. + Microsoft will notify the security contact directly in the event of a security incident using email and require alerting to be turned on. + ### Impact -The ability to react to high severity notifications could be delayed + {{ remediationActions }} diff --git a/avd_docs/azure/securitycenter/AVD-AZU-0045/docs.md b/avd_docs/azure/securitycenter/AVD-AZU-0045/docs.md index 62c1ab4b..2f3a3e44 100644 --- a/avd_docs/azure/securitycenter/AVD-AZU-0045/docs.md +++ b/avd_docs/azure/securitycenter/AVD-AZU-0045/docs.md @@ -1,10 +1,11 @@ To benefit from Azure Defender you should use the Standard subscription tier. - - Enabling Azure Defender extends the capabilities of the free mode to workloads running in private and other public clouds, providing unified security management and threat protection across your hybrid cloud workloads. + +Enabling Azure Defender extends the capabilities of the free mode to workloads running in private and other public clouds, providing unified security management and threat protection across your hybrid cloud workloads. + ### Impact -Using free subscription does not enable Azure Defender for the resource type + {{ remediationActions }} diff --git a/avd_docs/azure/securitycenter/AVD-AZU-0046/docs.md b/avd_docs/azure/securitycenter/AVD-AZU-0046/docs.md index 12095552..4a9ca988 100644 --- a/avd_docs/azure/securitycenter/AVD-AZU-0046/docs.md +++ b/avd_docs/azure/securitycenter/AVD-AZU-0046/docs.md @@ -1,9 +1,11 @@ -It is recommended that at least one valid contact is configured for the security center. +It is recommended that at least one valid contact is configured for the security center. + Microsoft will notify the security contact directly in the event of a security incident and will look to use a telephone number in cases where a prompt response is required. + ### Impact -Without a telephone number set, Azure support can't contact + {{ remediationActions }} diff --git a/avd_docs/azure/synapse/AVD-AZU-0034/docs.md b/avd_docs/azure/synapse/AVD-AZU-0034/docs.md index 9e31b5d5..0fdf511e 100644 --- a/avd_docs/azure/synapse/AVD-AZU-0034/docs.md +++ b/avd_docs/azure/synapse/AVD-AZU-0034/docs.md @@ -2,10 +2,12 @@ Synapse Workspace does not have managed virtual network enabled by default. When you create your Azure Synapse workspace, you can choose to associate it to a Microsoft Azure Virtual Network. The Virtual Network associated with your workspace is managed by Azure Synapse. This Virtual Network is called a Managed workspace Virtual Network. + Managed private endpoints are private endpoints created in a Managed Virtual Network associated with your Azure Synapse workspace. Managed private endpoints establish a private link to Azure resources. You can only use private links in a workspace that has a Managed workspace Virtual Network. + ### Impact -Your Synapse workspace is not using the private endpoints + {{ remediationActions }} diff --git a/checks/cloud/azure/monitor/activity_log_retention_set.go b/checks/cloud/azure/monitor/activity_log_retention_set.go index 94f9a613..8df1de96 100755 --- a/checks/cloud/azure/monitor/activity_log_retention_set.go +++ b/checks/cloud/azure/monitor/activity_log_retention_set.go @@ -27,7 +27,8 @@ var CheckActivityLogRetentionSet = rules.Register( Links: terraformActivityLogRetentionSetLinks, RemediationMarkdown: terraformActivityLogRetentionSetRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, profile := range s.Azure.Monitor.LogProfiles { diff --git a/checks/cloud/azure/monitor/activity_log_retention_set.rego b/checks/cloud/azure/monitor/activity_log_retention_set.rego new file mode 100644 index 00000000..6b45478e --- /dev/null +++ b/checks/cloud/azure/monitor/activity_log_retention_set.rego @@ -0,0 +1,54 @@ +# METADATA +# title: Ensure the activity retention log is set to at least a year +# description: | +# The average time to detect a breach is up to 210 days, to ensure that all the information required for an effective investigation is available, the retention period should allow for delayed starts to investigating. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/platform-logs-overview +# custom: +# id: AVD-AZU-0031 +# avd_id: AVD-AZU-0031 +# provider: azure +# service: monitor +# severity: MEDIUM +# short_code: activity-log-retention-set +# recommended_action: Set a retention period that will allow for delayed investigation +# input: +# selector: +# - type: cloud +# subtypes: +# - service: monitor +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_log_profile#retention_policy +# good_examples: checks/cloud/azure/monitor/activity_log_retention_set.tf.go +# bad_examples: checks/cloud/azure/monitor/activity_log_retention_set.tf.go +package builtin.azure.monitor.azure0031 + +import rego.v1 + +deny contains res if { + some profile in input.azure.monitor.logprofiles + isManaged(profile) + not profile.retentionpolicy.enabled.value + res := result.new( + "Profile does not enable the log retention policy.", + object.get(profile, ["retentionpolicy", "enabled"], profile), + ) +} + +deny contains res if { + some profile in input.azure.monitor.logprofiles + isManaged(profile) + profile.retentionpolicy.enabled.value + not is_recommended_retention_policy(profile) + res := result.new( + "Profile has a log retention policy of less than 1 year.", + object.get(profile, ["retentionpolicy", "days"], profile), + ) +} + +is_recommended_retention_policy(profile) := profile.retentionpolicy.days.value >= 365 diff --git a/checks/cloud/azure/monitor/activity_log_retention_set_test.go b/checks/cloud/azure/monitor/activity_log_retention_set_test.go deleted file mode 100644 index a9499a9b..00000000 --- a/checks/cloud/azure/monitor/activity_log_retention_set_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package monitor - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckActivityLogRetentionSet(t *testing.T) { - tests := []struct { - name string - input monitor.Monitor - expected bool - }{ - { - name: "Log retention policy disabled", - input: monitor.Monitor{ - LogProfiles: []monitor.LogProfile{ - { - Metadata: trivyTypes.NewTestMetadata(), - RetentionPolicy: monitor.RetentionPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - Enabled: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - Days: trivyTypes.Int(365, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: true, - }, - { - name: "Log retention policy enabled for 90 days", - input: monitor.Monitor{ - LogProfiles: []monitor.LogProfile{ - { - Metadata: trivyTypes.NewTestMetadata(), - RetentionPolicy: monitor.RetentionPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Days: trivyTypes.Int(90, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: true, - }, - { - name: "Log retention policy enabled for 365 days", - input: monitor.Monitor{ - LogProfiles: []monitor.LogProfile{ - { - Metadata: trivyTypes.NewTestMetadata(), - RetentionPolicy: monitor.RetentionPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Days: trivyTypes.Int(365, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Monitor = test.input - results := CheckActivityLogRetentionSet.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckActivityLogRetentionSet.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/azure/monitor/activity_log_retention_set_test.rego b/checks/cloud/azure/monitor/activity_log_retention_set_test.rego new file mode 100644 index 00000000..076efa59 --- /dev/null +++ b/checks/cloud/azure/monitor/activity_log_retention_set_test.rego @@ -0,0 +1,33 @@ +package builtin.azure.monitor.azure0031_test + +import rego.v1 + +import data.builtin.azure.monitor.azure0031 as check +import data.lib.test + +test_deny_retention_policy_disabled if { + inp := {"azure": {"monitor": {"logprofiles": [{"retentionpolicy": {"enabled": {"value": false}}}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_deny_retention_policy_enabled_but_days_lt_365 if { + inp := {"azure": {"monitor": {"logprofiles": [{"retentionpolicy": { + "enabled": {"value": true}, + "days": {"value": 30}, + }}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_retention_policy_enabled_and_days_gt_365 if { + inp := {"azure": {"monitor": {"logprofiles": [{"retentionpolicy": { + "enabled": {"value": true}, + "days": {"value": 365}, + }}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/monitor/capture_all_activities.go b/checks/cloud/azure/monitor/capture_all_activities.go index 9ca8c7a1..28eb4616 100755 --- a/checks/cloud/azure/monitor/capture_all_activities.go +++ b/checks/cloud/azure/monitor/capture_all_activities.go @@ -35,7 +35,8 @@ var CheckCaptureAllActivities = rules.Register( Links: terraformCaptureAllActivitiesLinks, RemediationMarkdown: terraformCaptureAllActivitiesRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { required := []string{ diff --git a/checks/cloud/azure/monitor/capture_all_activities.rego b/checks/cloud/azure/monitor/capture_all_activities.rego new file mode 100644 index 00000000..823aa7d6 --- /dev/null +++ b/checks/cloud/azure/monitor/capture_all_activities.rego @@ -0,0 +1,55 @@ +# METADATA +# title: Ensure log profile captures all activities +# description: | +# Log profiles should capture all categories to ensure that all events are logged +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/azure-monitor/essentials/activity-log +# - https://docs.microsoft.com/en-us/cli/azure/monitor/log-profiles?view=azure-cli-latest#az_monitor_log_profiles_create-required-parameters +# custom: +# id: AVD-AZU-0033 +# avd_id: AVD-AZU-0033 +# provider: azure +# service: monitor +# severity: MEDIUM +# short_code: capture-all-activities +# recommended_action: Configure log profile to capture all activities +# input: +# selector: +# - type: cloud +# subtypes: +# - service: monitor +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_log_profile#categories +# good_examples: checks/cloud/azure/monitor/capture_all_activities.tf.go +# bad_examples: checks/cloud/azure/monitor/capture_all_activities.tf.go +package builtin.azure.monitor.azure0033 + +import rego.v1 + +required_categories := {"Action", "Write", "Delete"} + +deny contains res if { + some profile in input.azure.monitor.logprofiles + isManaged(profile) + missing := missing_required_categories(profile) + count(missing) > 0 + res := result.new( + sprintf("Log profile does not require categories: %v", [missing]), + profile, + ) +} + +missing_required_categories(profile) := missing if { + categories := { + val | + some category in profile.categories + val := category.value + } + + missing := required_categories - categories +} else := {} diff --git a/checks/cloud/azure/monitor/capture_all_activities_test.go b/checks/cloud/azure/monitor/capture_all_activities_test.go deleted file mode 100644 index bc90c2b6..00000000 --- a/checks/cloud/azure/monitor/capture_all_activities_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package monitor - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckCaptureAllActivities(t *testing.T) { - tests := []struct { - name string - input monitor.Monitor - expected bool - }{ - { - name: "Log profile captures only write activities", - input: monitor.Monitor{ - LogProfiles: []monitor.LogProfile{ - { - Metadata: trivyTypes.NewTestMetadata(), - Categories: []trivyTypes.StringValue{ - trivyTypes.String("Write", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: true, - }, - { - name: "Log profile captures action, write, delete activities", - input: monitor.Monitor{ - LogProfiles: []monitor.LogProfile{ - { - Metadata: trivyTypes.NewTestMetadata(), - Categories: []trivyTypes.StringValue{ - trivyTypes.String("Action", trivyTypes.NewTestMetadata()), - trivyTypes.String("Write", trivyTypes.NewTestMetadata()), - trivyTypes.String("Delete", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Monitor = test.input - results := CheckCaptureAllActivities.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckCaptureAllActivities.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/azure/monitor/capture_all_activities_test.rego b/checks/cloud/azure/monitor/capture_all_activities_test.rego new file mode 100644 index 00000000..0046b024 --- /dev/null +++ b/checks/cloud/azure/monitor/capture_all_activities_test.rego @@ -0,0 +1,31 @@ +package builtin.azure.monitor.azure0033_test + +import rego.v1 + +import data.builtin.azure.monitor.azure0033 as check +import data.lib.test + +test_deny_log_profile_captures_only_write_activities if { + inp := {"azure": {"monitor": {"logprofiles": [{"categories": [{"value": "Write"}]}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_deny_log_profile_without_categories if { + inp := {"azure": {"monitor": {"logprofiles": [{}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_log_profile_with_all_required_categories if { + inp := {"azure": {"monitor": {"logprofiles": [{"categories": [ + {"value": "Write"}, + {"value": "Delete"}, + {"value": "Action"}, + ]}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/monitor/capture_all_regions.go b/checks/cloud/azure/monitor/capture_all_regions.go index 3b05031e..6208c55c 100755 --- a/checks/cloud/azure/monitor/capture_all_regions.go +++ b/checks/cloud/azure/monitor/capture_all_regions.go @@ -35,7 +35,8 @@ var CheckCaptureAllRegions = rules.Register( Links: terraformCaptureAllRegionsLinks, RemediationMarkdown: terraformCaptureAllRegionsRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, profile := range s.Azure.Monitor.LogProfiles { diff --git a/checks/cloud/azure/monitor/capture_all_regions.rego b/checks/cloud/azure/monitor/capture_all_regions.rego new file mode 100644 index 00000000..0da1c598 --- /dev/null +++ b/checks/cloud/azure/monitor/capture_all_regions.rego @@ -0,0 +1,128 @@ +# METADATA +# title: Ensure activitys are captured for all locations +# description: | +# Log profiles should capture all regions to ensure that all events are logged +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/cli/azure/monitor/log-profiles?view=azure-cli-latest#az_monitor_log_profiles_create-required-parameters +# custom: +# id: AVD-AZU-0032 +# avd_id: AVD-AZU-0032 +# provider: azure +# service: monitor +# severity: MEDIUM +# short_code: capture-all-regions +# recommended_action: Enable capture for all locations +# input: +# selector: +# - type: cloud +# subtypes: +# - service: monitor +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_log_profile#locations +# good_examples: checks/cloud/azure/monitor/capture_all_regions.tf.go +# bad_examples: checks/cloud/azure/monitor/capture_all_regions.tf.go +package builtin.azure.monitor.azure0032 + +import rego.v1 + +deny contains res if { + some profile in input.azure.monitor.logprofiles + missing := missing_regions(profile) + count(missing) > 0 + det := details(missing) + res := result.new( + sprintf("Log profile does not log to all regions (%s).", [det]), + profile, + ) +} + +details(missing) := msg if { + count(missing) < 10 + msg := sprintf("missing: %v", [missing]) +} else := sprintf("%d regions are missing", [count(missing)]) + +missing_regions(profile) := missing if { + regions := { + val | + some region in profile.locations + val := region.value + } + + missing := required_regions - regions +} + +required_regions := { + "eastus", + "eastus2", + "southcentralus", + "westus2", + "westus3", + "australiaeast", + "southeastasia", + "northeurope", + "swedencentral", + "uksouth", + "westeurope", + "centralus", + "northcentralus", + "westus", + "southafricanorth", + "centralindia", + "eastasia", + "japaneast", + "jioindiawest", + "koreacentral", + "canadacentral", + "francecentral", + "germanywestcentral", + "norwayeast", + "switzerlandnorth", + "uaenorth", + "brazilsouth", + "centralusstage", + "eastusstage", + "eastus2stage", + "northcentralusstage", + "southcentralusstage", + "westusstage", + "westus2stage", + "asia", + "asiapacific", + "australia", + "brazil", + "canada", + "europe", + "global", + "india", + "japan", + "uk", + "unitedstates", + "eastasiastage", + "southeastasiastage", + "centraluseuap", + "eastus2euap", + "westcentralus", + "southafricawest", + "australiacentral", + "australiacentral2", + "australiasoutheast", + "japanwest", + "jioindiacentral", + "koreasouth", + "southindia", + "westindia", + "canadaeast", + "francesouth", + "germanynorth", + "norwaywest", + "swedensouth", + "switzerlandwest", + "ukwest", + "uaecentral", + "brazilsoutheast", +} diff --git a/checks/cloud/azure/monitor/capture_all_regions_test.rego b/checks/cloud/azure/monitor/capture_all_regions_test.rego new file mode 100644 index 00000000..e7600f1f --- /dev/null +++ b/checks/cloud/azure/monitor/capture_all_regions_test.rego @@ -0,0 +1,34 @@ +package builtin.azure.monitor.azure0032_test + +import rego.v1 + +import data.builtin.azure.monitor.azure0032 as check +import data.lib.test + +test_deny_profile_captures_only_one_region if { + inp := {"azure": {"monitor": {"logprofiles": [{"locations": [{"value": "eastus"}]}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_deny_profiles_without_locations if { + inp := {"azure": {"monitor": {"logprofiles": [{}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_profiles_with_all_locations if { + inp := {"azure": {"monitor": {"logprofiles": [{"locations": all_locations}]}}} + + res := check.deny with input as inp + count(res) == 0 +} + +all_locations := locations if { + locations := [ + {"value": region} | + some region in check.required_regions + ] +} diff --git a/checks/cloud/azure/network/disable_rdp_from_internet.go b/checks/cloud/azure/network/disable_rdp_from_internet.go index b8ecdede..6ac26642 100755 --- a/checks/cloud/azure/network/disable_rdp_from_internet.go +++ b/checks/cloud/azure/network/disable_rdp_from_internet.go @@ -31,7 +31,8 @@ RDP access should not be permitted from the internet (*, 0.0.0.0, /0, internet, Links: terraformDisableRdpFromInternetLinks, RemediationMarkdown: terraformDisableRdpFromInternetRemediationMarkdown, }, - Severity: severity.Critical, + Severity: severity.Critical, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, group := range s.Azure.Network.SecurityGroups { diff --git a/checks/cloud/azure/network/disable_rdp_from_internet.rego b/checks/cloud/azure/network/disable_rdp_from_internet.rego new file mode 100644 index 00000000..7e01ea8a --- /dev/null +++ b/checks/cloud/azure/network/disable_rdp_from_internet.rego @@ -0,0 +1,52 @@ +# METADATA +# title: RDP access should not be accessible from the Internet, should be blocked on port 3389 +# description: | +# RDP access can be configured on either the network security group or in the network security group rule. +# RDP access should not be permitted from the internet (*, 0.0.0.0, /0, internet, any). Consider using the Azure Bastion Service. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/bastion/tutorial-create-host-portal +# custom: +# id: AVD-AZU-0048 +# avd_id: AVD-AZU-0048 +# provider: azure +# service: network +# severity: CRITICAL +# short_code: disable-rdp-from-internet +# recommended_action: Block RDP port from internet +# input: +# selector: +# - type: cloud +# subtypes: +# - service: network +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/network_security_group#security_rule +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule#source_port_ranges +# good_examples: checks/cloud/azure/network/disable_rdp_from_internet.tf.go +# bad_examples: checks/cloud/azure/network/disable_rdp_from_internet.tf.go +package builtin.azure.network.azure0048 + +import rego.v1 + +deny contains res if { + some group in input.azure.network.securitygroups + some rule in group.rules + rule.allow.value + not rule.outbound.value + lower(rule.protocol.value) != "icmp" + some ports in rule.destinationports + port_range_includes(ports.start, ports.end, 3389) + some ip in rule.sourceaddresses + cidr.is_public(ip.value) + cidr.count_addresses(ip.value) > 1 + res := result.new( + "Security group rule allows ingress to RDP port from multiple public internet addresses.", + ip, + ) +} + +port_range_includes(from, to, port) if from <= port <= to diff --git a/checks/cloud/azure/network/disable_rdp_from_internet_test.go b/checks/cloud/azure/network/disable_rdp_from_internet_test.go deleted file mode 100644 index c3ef9db8..00000000 --- a/checks/cloud/azure/network/disable_rdp_from_internet_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package network - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckDisableRdpFromInternet(t *testing.T) { - tests := []struct { - name string - input network.Network - expected bool - }{ - { - name: "Security group inbound rule allowing RDP access from the Internet", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("*", trivyTypes.NewTestMetadata()), - }, - SourcePorts: nil, - DestinationAddresses: nil, - DestinationPorts: []network.PortRange{ - { - Metadata: trivyTypes.NewTestMetadata(), - Start: trivyTypes.IntTest(3310), - End: trivyTypes.IntTest(3390), - }, - }, - Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "Security group inbound rule allowing RDP access from a specific address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - DestinationPorts: []network.PortRange{ - { - Metadata: trivyTypes.NewTestMetadata(), - Start: trivyTypes.IntTest(3310), - End: trivyTypes.IntTest(3390), - }, - }, - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("4.53.160.75", trivyTypes.NewTestMetadata()), - }, - Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "Security group inbound rule allowing only ICMP", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("*", trivyTypes.NewTestMetadata()), - }, - SourcePorts: nil, - DestinationAddresses: nil, - DestinationPorts: []network.PortRange{ - { - Metadata: trivyTypes.NewTestMetadata(), - Start: trivyTypes.IntTest(3310), - End: trivyTypes.IntTest(3390), - }, - }, - Protocol: trivyTypes.String("Icmp", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Network = test.input - results := CheckDisableRdpFromInternet.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckDisableRdpFromInternet.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/azure/network/disable_rdp_from_internet_test.rego b/checks/cloud/azure/network/disable_rdp_from_internet_test.rego new file mode 100644 index 00000000..862a4d51 --- /dev/null +++ b/checks/cloud/azure/network/disable_rdp_from_internet_test.rego @@ -0,0 +1,54 @@ +package builtin.azure.network.azure0048_test + +import rego.v1 + +import data.builtin.azure.network.azure0048 as check +import data.lib.test + +test_deny_inbound_rule_allows_rdp_access_from_internet if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "outbound": {"value": false}, + "allow": {"value": true}, + "protocol": {"value": "Tcp"}, + "sourceaddresses": [{"value": "*"}], + "destinationports": [{ + "start": 3310, + "end": 3390, + }], + }]}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_inbound_rule_allow_rdp_access_from_specific_address if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "outbound": {"value": false}, + "allow": {"value": true}, + "protocol": {"value": "Tcp"}, + "sourceaddresses": [{"value": "237.84.2.178"}], + "destinationports": [{ + "start": 3310, + "end": 3390, + }], + }]}]}}} + + res := check.deny with input as inp + res == set() +} + +test_allow_inbound_rule_allow_access_for_icmp if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "outbound": {"value": false}, + "allow": {"value": true}, + "protocol": {"value": "Icmp"}, + "sourceaddresses": [{"value": "*"}], + "destinationports": [{ + "start": 3310, + "end": 3390, + }], + }]}]}}} + + res := check.deny with input as inp + res == set() +} diff --git a/checks/cloud/azure/network/no_public_egress.go b/checks/cloud/azure/network/no_public_egress.go index d598f44f..d526138e 100755 --- a/checks/cloud/azure/network/no_public_egress.go +++ b/checks/cloud/azure/network/no_public_egress.go @@ -30,7 +30,8 @@ Where possible, segments should be broken into smaller subnets.`, Links: terraformNoPublicEgressLinks, RemediationMarkdown: terraformNoPublicEgressRemediationMarkdown, }, - Severity: severity.Critical, + Severity: severity.Critical, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, group := range s.Azure.Network.SecurityGroups { diff --git a/checks/cloud/azure/network/no_public_egress.rego b/checks/cloud/azure/network/no_public_egress.rego new file mode 100644 index 00000000..470bce51 --- /dev/null +++ b/checks/cloud/azure/network/no_public_egress.rego @@ -0,0 +1,42 @@ +# METADATA +# title: An outbound network security rule allows traffic to /0. +# description: | +# Network security rules should not use very broad subnets. +# Where possible, segments should be broken into smaller subnets. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/security/fundamentals/network-best-practices +# custom: +# id: AVD-AZU-0051 +# avd_id: AVD-AZU-0051 +# provider: azure +# service: network +# severity: CRITICAL +# short_code: no-public-egress +# recommended_action: Set a more restrictive cidr range +# input: +# selector: +# - type: cloud +# subtypes: +# - service: network +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule +# good_examples: checks/cloud/azure/network/no_public_egress.tf.go +# bad_examples: checks/cloud/azure/network/no_public_egress.tf.go +package builtin.azure.network.azure0051 + +import rego.v1 + +deny contains res if { + some group in input.azure.network.securitygroups + some rule in group.rules + rule.outbound.value + rule.allow.value + some addr in rule.destinationaddresses + cidr.is_public(addr.value) + res := result.new("Security group rule allows egress to public internet.", addr) +} diff --git a/checks/cloud/azure/network/no_public_egress_test.go b/checks/cloud/azure/network/no_public_egress_test.go deleted file mode 100644 index 3abd1357..00000000 --- a/checks/cloud/azure/network/no_public_egress_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package network - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckNoPublicEgress(t *testing.T) { - tests := []struct { - name string - input network.Network - expected bool - }{ - { - name: "Security group outbound rule with wildcard destination address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - DestinationAddresses: []trivyTypes.StringValue{ - trivyTypes.String("*", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "Security group outbound rule with private destination address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - DestinationAddresses: []trivyTypes.StringValue{ - trivyTypes.String("10.0.0.0/16", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Network = test.input - results := CheckNoPublicEgress.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckNoPublicEgress.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/azure/network/no_public_egress_test.rego b/checks/cloud/azure/network/no_public_egress_test.rego new file mode 100644 index 00000000..cb61d036 --- /dev/null +++ b/checks/cloud/azure/network/no_public_egress_test.rego @@ -0,0 +1,28 @@ +package builtin.azure.network.azure0051_test + +import rego.v1 + +import data.builtin.azure.network.azure0051 as check +import data.lib.test + +test_deny_outbound_rule_with_wildcard_destination_address if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "allow": {"value": true}, + "outbound": {"value": true}, + "destinationaddresses": [{"value": "*"}], + }]}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_outbound_rule_with_private_destination_address if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "allow": {"value": true}, + "outbound": {"value": true}, + "destinationaddresses": [{"value": "10.0.0.0/16"}], + }]}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/network/no_public_ingress.go b/checks/cloud/azure/network/no_public_ingress.go index efa59ff0..c99a0a43 100755 --- a/checks/cloud/azure/network/no_public_ingress.go +++ b/checks/cloud/azure/network/no_public_ingress.go @@ -30,7 +30,8 @@ Where possible, segments should be broken into smaller subnets.`, Links: terraformNoPublicIngressLinks, RemediationMarkdown: terraformNoPublicIngressRemediationMarkdown, }, - Severity: severity.Critical, + Severity: severity.Critical, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, group := range s.Azure.Network.SecurityGroups { diff --git a/checks/cloud/azure/network/no_public_ingress.rego b/checks/cloud/azure/network/no_public_ingress.rego new file mode 100644 index 00000000..4a632f66 --- /dev/null +++ b/checks/cloud/azure/network/no_public_ingress.rego @@ -0,0 +1,43 @@ +# METADATA +# title: An inbound network security rule allows traffic from /0. +# description: | +# Network security rules should not use very broad subnets. +# Where possible, segments should be broken into smaller subnets. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/security/fundamentals/network-best-practices +# custom: +# id: AVD-AZU-0047 +# avd_id: AVD-AZU-0047 +# provider: azure +# service: network +# severity: CRITICAL +# short_code: no-public-ingress +# recommended_action: Set a more restrictive cidr range +# input: +# selector: +# - type: cloud +# subtypes: +# - service: network +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule +# good_examples: checks/cloud/azure/network/no_public_ingress.tf.go +# bad_examples: checks/cloud/azure/network/no_public_ingress.tf.go +package builtin.azure.network.azure0047 + +import rego.v1 + +deny contains res if { + some group in input.azure.network.securitygroups + some rule in group.rules + not rule.outbound.value + rule.allow.value + some addr in rule.sourceaddresses + cidr.is_public(addr.value) + cidr.count_addresses(addr.value) > 1 + res := result.new("Security group rule allows ingress from public internet.", addr) +} diff --git a/checks/cloud/azure/network/no_public_ingress_test.go b/checks/cloud/azure/network/no_public_ingress_test.go deleted file mode 100644 index dbbe0e0c..00000000 --- a/checks/cloud/azure/network/no_public_ingress_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package network - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckNoPublicIngress(t *testing.T) { - tests := []struct { - name string - input network.Network - expected bool - }{ - { - name: "Security group inbound rule with asterisk wildcard source address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("*", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "Security group inbound rule with lower case internet wildcard source address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("internet", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "Security group inbound rule with upper case internet wildcard source address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("Internet", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "Security group inbound rule with private source address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("10.0.0.0/16", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Network = test.input - results := CheckNoPublicIngress.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckNoPublicIngress.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/azure/network/no_public_ingress_test.rego b/checks/cloud/azure/network/no_public_ingress_test.rego new file mode 100644 index 00000000..958c5ac0 --- /dev/null +++ b/checks/cloud/azure/network/no_public_ingress_test.rego @@ -0,0 +1,28 @@ +package builtin.azure.network.azure0047_test + +import rego.v1 + +import data.builtin.azure.network.azure0047 as check +import data.lib.test + +test_deny_inbound_rule_with_wildcard_source_address if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "allow": {"value": true}, + "outbound": {"value": false}, + "sourceaddresses": [{"value": "*"}], + }]}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_inbound_rule_with_private_source_address if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "allow": {"value": true}, + "outbound": {"value": false}, + "sourceaddresses": [{"value": "10.0.0.0/16"}], + }]}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/network/retention_policy_set.go b/checks/cloud/azure/network/retention_policy_set.go index 39025ea1..7b7b088a 100755 --- a/checks/cloud/azure/network/retention_policy_set.go +++ b/checks/cloud/azure/network/retention_policy_set.go @@ -30,7 +30,8 @@ Setting an retention policy will help ensure as much information is available fo Links: terraformRetentionPolicySetLinks, RemediationMarkdown: terraformRetentionPolicySetRemediationMarkdown, }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, flowLog := range s.Azure.Network.NetworkWatcherFlowLogs { diff --git a/checks/cloud/azure/network/retention_policy_set.rego b/checks/cloud/azure/network/retention_policy_set.rego new file mode 100644 index 00000000..6d1658e7 --- /dev/null +++ b/checks/cloud/azure/network/retention_policy_set.rego @@ -0,0 +1,60 @@ +# METADATA +# title: Retention policy for flow logs should be enabled and set to greater than 90 days +# description: | +# Flow logs are the source of truth for all network activity in your cloud environment. +# +# To enable analysis in security event that was detected late, you need to have the logs available. +# +# Setting an retention policy will help ensure as much information is available for review. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-monitoring-overview +# custom: +# id: AVD-AZU-0049 +# avd_id: AVD-AZU-0049 +# provider: azure +# service: network +# severity: LOW +# short_code: retention-policy-set +# recommended_action: Ensure flow log retention is turned on with an expiry of >90 days +# input: +# selector: +# - type: cloud +# subtypes: +# - service: network +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_watcher_flow_log#retention_policy +# good_examples: checks/cloud/azure/network/retention_policy_set.tf.go +# bad_examples: checks/cloud/azure/network/retention_policy_set.tf.go +package builtin.azure.network.azure0049 + +import rego.v1 + +flowlogs := input.azure.network.networkwatcherflowlogs + +deny contains res if { + some flowlog in flowlogs + isManaged(flowlog) + + not flowlog.retentionpolicy.enabled.value + res := result.new( + "Flow log does not enable the log retention policy.", + object.get(flowlog, ["retentionpolicy", "enabled"], flowlog), + ) +} + +deny contains res if { + some flowlog in flowlogs + isManaged(flowlog) + + flowlog.retentionpolicy.enabled.value + flowlog.retentionpolicy.days.value < 90 + res := result.new( + "Flow log has a log retention policy of less than 90 days.", + object.get(flowlog, ["retentionpolicy", "days"], flowlog), + ) +} diff --git a/checks/cloud/azure/network/retention_policy_set_test.go b/checks/cloud/azure/network/retention_policy_set_test.go deleted file mode 100644 index 32b765ed..00000000 --- a/checks/cloud/azure/network/retention_policy_set_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package network - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckRetentionPolicySet(t *testing.T) { - tests := []struct { - name string - input network.Network - expected bool - }{ - { - name: "Network watcher flow log retention policy disabled", - input: network.Network{ - NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{ - { - Metadata: trivyTypes.NewTestMetadata(), - RetentionPolicy: network.RetentionPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - Enabled: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - Days: trivyTypes.Int(100, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: true, - }, - { - name: "Network watcher flow log retention policy enabled for 30 days", - input: network.Network{ - NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{ - { - Metadata: trivyTypes.NewTestMetadata(), - RetentionPolicy: network.RetentionPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Days: trivyTypes.Int(30, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: true, - }, - { - name: "Network watcher flow log retention policy enabled for 100 days", - input: network.Network{ - NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{ - { - Metadata: trivyTypes.NewTestMetadata(), - RetentionPolicy: network.RetentionPolicy{ - Metadata: trivyTypes.NewTestMetadata(), - Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Days: trivyTypes.Int(100, trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Network = test.input - results := CheckRetentionPolicySet.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckRetentionPolicySet.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/azure/network/retention_policy_set_test.rego b/checks/cloud/azure/network/retention_policy_set_test.rego new file mode 100644 index 00000000..f49646ea --- /dev/null +++ b/checks/cloud/azure/network/retention_policy_set_test.rego @@ -0,0 +1,33 @@ +package builtin.azure.network.azure0049_test + +import rego.v1 + +import data.builtin.azure.network.azure0049 as check +import data.lib.test + +test_deny_flow_log_retention_policy_disabled if { + inp := {"azure": {"network": {"networkwatcherflowlogs": [{"retentionpolicy": {"enabled": {"value": false}}}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_deny_flow_log_retention_policy_less_than_90 if { + inp := {"azure": {"network": {"networkwatcherflowlogs": [{"retentionpolicy": { + "enabled": {"value": true}, + "days": {"value": 89}, + }}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_flow_log_retention_policy_greater_than_90 if { + inp := {"azure": {"network": {"networkwatcherflowlogs": [{"retentionpolicy": { + "enabled": {"value": true}, + "days": {"value": 90}, + }}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/network/ssh_blocked_from_internet.go b/checks/cloud/azure/network/ssh_blocked_from_internet.go index 1f930009..377d9b29 100755 --- a/checks/cloud/azure/network/ssh_blocked_from_internet.go +++ b/checks/cloud/azure/network/ssh_blocked_from_internet.go @@ -29,7 +29,8 @@ SSH access should not be permitted from the internet (*, 0.0.0.0, /0, internet, Links: terraformSshBlockedFromInternetLinks, RemediationMarkdown: terraformSshBlockedFromInternetRemediationMarkdown, }, - Severity: severity.Critical, + Severity: severity.Critical, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, group := range s.Azure.Network.SecurityGroups { diff --git a/checks/cloud/azure/network/ssh_blocked_from_internet.rego b/checks/cloud/azure/network/ssh_blocked_from_internet.rego new file mode 100644 index 00000000..a7d6fb4f --- /dev/null +++ b/checks/cloud/azure/network/ssh_blocked_from_internet.rego @@ -0,0 +1,50 @@ +# METADATA +# title: SSH access should not be accessible from the Internet, should be blocked on port 22 +# description: | +# SSH access can be configured on either the network security group or in the network security group rule. +# SSH access should not be permitted from the internet (*, 0.0.0.0, /0, internet, any) +# scope: package +# schemas: +# - input: schema["cloud"] +# custom: +# id: AVD-AZU-0050 +# avd_id: AVD-AZU-0050 +# provider: azure +# service: network +# severity: CRITICAL +# short_code: ssh-blocked-from-internet +# recommended_action: Block port 22 access from the internet +# input: +# selector: +# - type: cloud +# subtypes: +# - service: network +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/network_security_group#security_rule +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule#source_port_ranges +# good_examples: checks/cloud/azure/network/ssh_blocked_from_internet.tf.go +# bad_examples: checks/cloud/azure/network/ssh_blocked_from_internet.tf.go +package builtin.azure.network.azure0050 + +import rego.v1 + +deny contains res if { + some group in input.azure.network.securitygroups + some rule in group.rules + rule.allow.value + not rule.outbound.value + lower(rule.protocol.value) != "icmp" + some ports in rule.destinationports + port_range_includes(ports.start, ports.end, 22) + some ip in rule.sourceaddresses + cidr.is_public(ip.value) + cidr.count_addresses(ip.value) > 1 + res := result.new( + "Security group rule allows ingress to SSH port from multiple public internet addresses.", + ip, + ) +} + +port_range_includes(from, to, port) if from <= port <= to diff --git a/checks/cloud/azure/network/ssh_blocked_from_internet_test.go b/checks/cloud/azure/network/ssh_blocked_from_internet_test.go deleted file mode 100644 index 65ee23ad..00000000 --- a/checks/cloud/azure/network/ssh_blocked_from_internet_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package network - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckSshBlockedFromInternet(t *testing.T) { - tests := []struct { - name string - input network.Network - expected bool - }{ - { - name: "Security group rule allowing SSH access from the public internet", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - DestinationPorts: []network.PortRange{ - { - Metadata: trivyTypes.NewTestMetadata(), - Start: trivyTypes.IntTest(22), - End: trivyTypes.IntTest(22), - }, - }, - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("*", trivyTypes.NewTestMetadata()), - }, - Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: true, - }, - { - name: "Security group rule allowing SSH only ICMP", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - DestinationPorts: []network.PortRange{ - { - Metadata: trivyTypes.NewTestMetadata(), - Start: trivyTypes.IntTest(22), - End: trivyTypes.IntTest(22), - }, - }, - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("*", trivyTypes.NewTestMetadata()), - }, - Protocol: trivyTypes.String("Icmp", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - { - name: "Security group rule allowing SSH access from a specific address", - input: network.Network{ - SecurityGroups: []network.SecurityGroup{ - { - Metadata: trivyTypes.NewTestMetadata(), - Rules: []network.SecurityGroupRule{ - { - Metadata: trivyTypes.NewTestMetadata(), - Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - DestinationPorts: []network.PortRange{ - { - Metadata: trivyTypes.NewTestMetadata(), - Start: trivyTypes.IntTest(22), - End: trivyTypes.IntTest(22), - }, - }, - SourceAddresses: []trivyTypes.StringValue{ - trivyTypes.String("82.102.23.23", trivyTypes.NewTestMetadata()), - }, - Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), - }, - }, - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Network = test.input - results := CheckSshBlockedFromInternet.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckSshBlockedFromInternet.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/azure/network/ssh_blocked_from_internet_test.rego b/checks/cloud/azure/network/ssh_blocked_from_internet_test.rego new file mode 100644 index 00000000..b5fa396c --- /dev/null +++ b/checks/cloud/azure/network/ssh_blocked_from_internet_test.rego @@ -0,0 +1,54 @@ +package builtin.azure.network.azure0050_test + +import rego.v1 + +import data.builtin.azure.network.azure0050 as check +import data.lib.test + +test_deny_inbound_rule_allows_rdp_access_from_internet if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "outbound": {"value": false}, + "allow": {"value": true}, + "protocol": {"value": "Tcp"}, + "sourceaddresses": [{"value": "*"}], + "destinationports": [{ + "start": 22, + "end": 22, + }], + }]}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_inbound_rule_allow_rdp_access_from_specific_address if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "outbound": {"value": false}, + "allow": {"value": true}, + "protocol": {"value": "Tcp"}, + "sourceaddresses": [{"value": "237.84.2.178"}], + "destinationports": [{ + "start": 22, + "end": 22, + }], + }]}]}}} + + res := check.deny with input as inp + res == set() +} + +test_allow_inbound_rule_allow_access_for_icmp if { + inp := {"azure": {"network": {"securitygroups": [{"rules": [{ + "outbound": {"value": false}, + "allow": {"value": true}, + "protocol": {"value": "Icmp"}, + "sourceaddresses": [{"value": "*"}], + "destinationports": [{ + "start": 22, + "end": 22, + }], + }]}]}}} + + res := check.deny with input as inp + res == set() +} diff --git a/checks/cloud/azure/securitycenter/alert_on_severe_notifications.go b/checks/cloud/azure/securitycenter/alert_on_severe_notifications.go index e914c147..84dbabca 100755 --- a/checks/cloud/azure/securitycenter/alert_on_severe_notifications.go +++ b/checks/cloud/azure/securitycenter/alert_on_severe_notifications.go @@ -28,7 +28,8 @@ Microsoft will notify the security contact directly in the event of a security i Links: terraformAlertOnSevereNotificationsLinks, RemediationMarkdown: terraformAlertOnSevereNotificationsRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, contact := range s.Azure.SecurityCenter.Contacts { diff --git a/checks/cloud/azure/securitycenter/alert_on_severe_notifications.rego b/checks/cloud/azure/securitycenter/alert_on_severe_notifications.rego new file mode 100644 index 00000000..75381e4c --- /dev/null +++ b/checks/cloud/azure/securitycenter/alert_on_severe_notifications.rego @@ -0,0 +1,44 @@ +# METADATA +# title: Send notification emails for high severity alerts +# description: | +# It is recommended that at least one valid contact is configured for the security center. +# +# Microsoft will notify the security contact directly in the event of a security incident using email and require alerting to be turned on. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://azure.microsoft.com/en-us/services/security-center/ +# custom: +# id: AVD-AZU-0044 +# avd_id: AVD-AZU-0044 +# provider: azure +# service: security-center +# severity: MEDIUM +# short_code: alert-on-severe-notifications +# recommended_action: Set alert notifications to be on +# input: +# selector: +# - type: cloud +# subtypes: +# - service: securitycenter +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/security_center_contact#alert_notifications +# good_examples: checks/cloud/azure/securitycenter/alert_on_severe_notifications.tf.go +# bad_examples: checks/cloud/azure/securitycenter/alert_on_severe_notifications.tf.go +package builtin.azure.securitycenter.azure0044 + +import rego.v1 + +deny contains res if { + some contact in input.azure.securitycenter.contacts + isManaged(contact) + + not contact.enablealertnotifications.value + res := result.new( + "Security contact has alert notifications disabled.", + object.get(contact, "enablealertnotifications", contact), + ) +} diff --git a/checks/cloud/azure/securitycenter/alert_on_severe_notifications_test.go b/checks/cloud/azure/securitycenter/alert_on_severe_notifications_test.go deleted file mode 100644 index b869c052..00000000 --- a/checks/cloud/azure/securitycenter/alert_on_severe_notifications_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package securitycenter - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckAlertOnSevereNotifications(t *testing.T) { - tests := []struct { - name string - input securitycenter.SecurityCenter - expected bool - }{ - { - name: "Security center alert nofifications disabled", - input: securitycenter.SecurityCenter{ - Contacts: []securitycenter.Contact{ - { - Metadata: trivyTypes.NewTestMetadata(), - EnableAlertNotifications: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "Security center alert nofifications enabled", - input: securitycenter.SecurityCenter{ - Contacts: []securitycenter.Contact{ - { - Metadata: trivyTypes.NewTestMetadata(), - EnableAlertNotifications: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.SecurityCenter = test.input - results := CheckAlertOnSevereNotifications.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckAlertOnSevereNotifications.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/azure/securitycenter/alert_on_severe_notifications_test.rego b/checks/cloud/azure/securitycenter/alert_on_severe_notifications_test.rego new file mode 100644 index 00000000..2a301705 --- /dev/null +++ b/checks/cloud/azure/securitycenter/alert_on_severe_notifications_test.rego @@ -0,0 +1,18 @@ +package builtin.azure.securitycenter.azure0044_test + +import rego.v1 + +import data.builtin.azure.securitycenter.azure0044 as check +import data.lib.test + +test_deny_security_center_alert_notifications_disabled if { + res := check.deny with input as build_input(false) + count(res) == 1 +} + +test_allow_security_center_alert_notifications_enabled if { + res := check.deny with input as build_input(true) + count(res) == 0 +} + +build_input(enabled) := {"azure": {"securitycenter": {"contacts": [{"enablealertnotifications": {"value": enabled}}]}}} diff --git a/checks/cloud/azure/securitycenter/enable_standard_subscription.go b/checks/cloud/azure/securitycenter/enable_standard_subscription.go index 8167e3e0..147f5940 100755 --- a/checks/cloud/azure/securitycenter/enable_standard_subscription.go +++ b/checks/cloud/azure/securitycenter/enable_standard_subscription.go @@ -30,7 +30,8 @@ var CheckEnableStandardSubscription = rules.Register( Links: terraformEnableStandardSubscriptionLinks, RemediationMarkdown: terraformEnableStandardSubscriptionRemediationMarkdown, }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, sub := range s.Azure.SecurityCenter.Subscriptions { diff --git a/checks/cloud/azure/securitycenter/enable_standard_subscription.rego b/checks/cloud/azure/securitycenter/enable_standard_subscription.rego new file mode 100644 index 00000000..61300edf --- /dev/null +++ b/checks/cloud/azure/securitycenter/enable_standard_subscription.rego @@ -0,0 +1,46 @@ +# METADATA +# title: Enable the standard security center subscription tier +# description: | +# To benefit from Azure Defender you should use the Standard subscription tier. +# +# Enabling Azure Defender extends the capabilities of the free mode to workloads running in private and other public clouds, providing unified security management and threat protection across your hybrid cloud workloads. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/security-center/security-center-pricing +# custom: +# id: AVD-AZU-0045 +# avd_id: AVD-AZU-0045 +# provider: azure +# service: security-center +# severity: LOW +# short_code: enable-standard-subscription +# recommended_action: Enable standard subscription tier to benefit from Azure Defender +# input: +# selector: +# - type: cloud +# subtypes: +# - service: securitycenter +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/security_center_subscription_pricing#tier +# good_examples: checks/cloud/azure/securitycenter/enable_standard_subscription.tf.go +# bad_examples: checks/cloud/azure/securitycenter/enable_standard_subscription.tf.go +package builtin.azure.securitycenter.azure0045 + +import rego.v1 + +free_tier := "Free" + +deny contains res if { + some sub in input.azure.securitycenter.subscriptions + isManaged(sub) + + sub.tier.value == free_tier + res := result.new( + "Security center subscription uses the free tier.", + sub.tier, + ) +} diff --git a/checks/cloud/azure/securitycenter/enable_standard_subscription_test.go b/checks/cloud/azure/securitycenter/enable_standard_subscription_test.go deleted file mode 100644 index 95a68c74..00000000 --- a/checks/cloud/azure/securitycenter/enable_standard_subscription_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package securitycenter - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckEnableStandardSubscription(t *testing.T) { - tests := []struct { - name string - input securitycenter.SecurityCenter - expected bool - }{ - { - name: "Security center set with free subscription", - input: securitycenter.SecurityCenter{ - Subscriptions: []securitycenter.SubscriptionPricing{ - { - Metadata: trivyTypes.NewTestMetadata(), - Tier: trivyTypes.String(securitycenter.TierFree, trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "Security center set with standard subscription", - input: securitycenter.SecurityCenter{ - Subscriptions: []securitycenter.SubscriptionPricing{ - { - Metadata: trivyTypes.NewTestMetadata(), - Tier: trivyTypes.String(securitycenter.TierStandard, trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.SecurityCenter = test.input - results := CheckEnableStandardSubscription.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckEnableStandardSubscription.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/azure/securitycenter/enable_standard_subscription_test.rego b/checks/cloud/azure/securitycenter/enable_standard_subscription_test.rego new file mode 100644 index 00000000..14574f96 --- /dev/null +++ b/checks/cloud/azure/securitycenter/enable_standard_subscription_test.rego @@ -0,0 +1,20 @@ +package builtin.azure.securitycenter.azure0045_test + +import rego.v1 + +import data.builtin.azure.securitycenter.azure0045 as check +import data.lib.test + +test_deny_subscription_use_free_tier if { + inp := {"azure": {"securitycenter": {"subscriptions": [{"tier": {"value": check.free_tier}}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_subscription_use_standard_tier if { + inp := {"azure": {"securitycenter": {"subscriptions": [{"tier": {"value": "Standard"}}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/securitycenter/set_required_contact_details.go b/checks/cloud/azure/securitycenter/set_required_contact_details.go index 3ec21ee5..a605a3cd 100755 --- a/checks/cloud/azure/securitycenter/set_required_contact_details.go +++ b/checks/cloud/azure/securitycenter/set_required_contact_details.go @@ -28,7 +28,8 @@ Microsoft will notify the security contact directly in the event of a security i Links: terraformSetRequiredContactDetailsLinks, RemediationMarkdown: terraformSetRequiredContactDetailsRemediationMarkdown, }, - Severity: severity.Low, + Severity: severity.Low, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, contact := range s.Azure.SecurityCenter.Contacts { diff --git a/checks/cloud/azure/securitycenter/set_required_contact_details.rego b/checks/cloud/azure/securitycenter/set_required_contact_details.rego new file mode 100644 index 00000000..e54e593c --- /dev/null +++ b/checks/cloud/azure/securitycenter/set_required_contact_details.rego @@ -0,0 +1,46 @@ +# METADATA +# title: The required contact details should be set for security center +# description: | +# It is recommended that at least one valid contact is configured for the security center. +# +# Microsoft will notify the security contact directly in the event of a security incident and will look to use a telephone number in cases where a prompt response is required. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://azure.microsoft.com/en-us/services/security-center/ +# custom: +# id: AVD-AZU-0046 +# avd_id: AVD-AZU-0046 +# provider: azure +# service: security-center +# severity: LOW +# short_code: set-required-contact-details +# recommended_action: Set a telephone number for security center contact +# input: +# selector: +# - type: cloud +# subtypes: +# - service: securitycenter +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/security_center_contact#phone +# good_examples: checks/cloud/azure/securitycenter/set_required_contact_details.tf.go +# bad_examples: checks/cloud/azure/securitycenter/set_required_contact_details.tf.go +package builtin.azure.securitycenter.azure0046 + +import rego.v1 + +deny contains res if { + some contact in input.azure.securitycenter.contacts + isManaged(contact) + + not contact_has_phone(contact) + res := result.new( + "Security contact does not have a phone number listed.", + object.get(contact, "phone", contact), + ) +} + +contact_has_phone(contact) := contact.phone.value != "" diff --git a/checks/cloud/azure/securitycenter/set_required_contact_details_test.go b/checks/cloud/azure/securitycenter/set_required_contact_details_test.go deleted file mode 100644 index 6b5f15ac..00000000 --- a/checks/cloud/azure/securitycenter/set_required_contact_details_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package securitycenter - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckSetRequiredContactDetails(t *testing.T) { - tests := []struct { - name string - input securitycenter.SecurityCenter - expected bool - }{ - { - name: "Contact's phone number missing", - input: securitycenter.SecurityCenter{ - Contacts: []securitycenter.Contact{ - { - Metadata: trivyTypes.NewTestMetadata(), - Phone: trivyTypes.String("", trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "Contact's phone number provided", - input: securitycenter.SecurityCenter{ - Contacts: []securitycenter.Contact{ - { - Metadata: trivyTypes.NewTestMetadata(), - Phone: trivyTypes.String("+1-555-555-5555", trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.SecurityCenter = test.input - results := CheckSetRequiredContactDetails.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckSetRequiredContactDetails.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/azure/securitycenter/set_required_contact_details_test.rego b/checks/cloud/azure/securitycenter/set_required_contact_details_test.rego new file mode 100644 index 00000000..ed966866 --- /dev/null +++ b/checks/cloud/azure/securitycenter/set_required_contact_details_test.rego @@ -0,0 +1,20 @@ +package builtin.azure.securitycenter.azure0046_test + +import rego.v1 + +import data.builtin.azure.securitycenter.azure0046 as check +import data.lib.test + +test_deny_contact_without_phone if { + inp := {"azure": {"securitycenter": {"contacts": [{"phone": {"value": ""}}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_contact_with_phone if { + inp := {"azure": {"securitycenter": {"contacts": [{"phone": {"value": "555-555-5555"}}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/synapse/virtual_network_enabled.go b/checks/cloud/azure/synapse/virtual_network_enabled.go index 54dbf741..40e917cf 100755 --- a/checks/cloud/azure/synapse/virtual_network_enabled.go +++ b/checks/cloud/azure/synapse/virtual_network_enabled.go @@ -31,7 +31,8 @@ Managed private endpoints are private endpoints created in a Managed Virtual Net Links: terraformVirtualNetworkEnabledLinks, RemediationMarkdown: terraformVirtualNetworkEnabledRemediationMarkdown, }, - Severity: severity.Medium, + Severity: severity.Medium, + Deprecated: true, }, func(s *state.State) (results scan.Results) { for _, workspace := range s.Azure.Synapse.Workspaces { diff --git a/checks/cloud/azure/synapse/virtual_network_enabled.rego b/checks/cloud/azure/synapse/virtual_network_enabled.rego new file mode 100644 index 00000000..c5d2ce29 --- /dev/null +++ b/checks/cloud/azure/synapse/virtual_network_enabled.rego @@ -0,0 +1,47 @@ +# METADATA +# title: Synapse Workspace should have managed virtual network enabled, the default is disabled. +# description: | +# Synapse Workspace does not have managed virtual network enabled by default. +# +# When you create your Azure Synapse workspace, you can choose to associate it to a Microsoft Azure Virtual Network. The Virtual Network associated with your workspace is managed by Azure Synapse. This Virtual Network is called a Managed workspace Virtual Network. +# +# Managed private endpoints are private endpoints created in a Managed Virtual Network associated with your Azure Synapse workspace. Managed private endpoints establish a private link to Azure resources. You can only use private links in a workspace that has a Managed workspace Virtual Network. +# scope: package +# schemas: +# - input: schema["cloud"] +# related_resources: +# - https://docs.microsoft.com/en-us/azure/synapse-analytics/security/synapse-workspace-managed-private-endpoints +# - https://docs.microsoft.com/en-us/azure/synapse-analytics/security/synapse-workspace-managed-vnet +# custom: +# id: AVD-AZU-0034 +# avd_id: AVD-AZU-0034 +# provider: azure +# service: synapse +# severity: MEDIUM +# short_code: virtual-network-enabled +# recommended_action: Set manage virtual network to enabled +# input: +# selector: +# - type: cloud +# subtypes: +# - service: synapse +# provider: azure +# terraform: +# links: +# - https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/synapse_workspace#managed_virtual_network_enabled +# good_examples: checks/cloud/azure/synapse/virtual_network_enabled.tf.go +# bad_examples: checks/cloud/azure/synapse/virtual_network_enabled.tf.go +package builtin.azure.synapse.azure0034 + +import rego.v1 + +deny contains res if { + some workspace in input.azure.synapse.workspaces + isManaged(workspace) + + not workspace.enablemanagedvirtualnetwork.value + res := result.new( + "Workspace does not have a managed virtual network enabled.", + object.get(workspace, "enablemanagedvirtualnetwork", workspace), + ) +} diff --git a/checks/cloud/azure/synapse/virtual_network_enabled_test.go b/checks/cloud/azure/synapse/virtual_network_enabled_test.go deleted file mode 100644 index 3dd6daf2..00000000 --- a/checks/cloud/azure/synapse/virtual_network_enabled_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package synapse - -import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - - "github.com/aquasecurity/trivy/pkg/iac/providers/azure/synapse" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" -) - -func TestCheckVirtualNetworkEnabled(t *testing.T) { - tests := []struct { - name string - input synapse.Synapse - expected bool - }{ - { - name: "Synapse workspace managed VN disabled", - input: synapse.Synapse{ - Workspaces: []synapse.Workspace{ - { - Metadata: trivyTypes.NewTestMetadata(), - EnableManagedVirtualNetwork: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: true, - }, - { - name: "Synapse workspace managed VN enabled", - input: synapse.Synapse{ - Workspaces: []synapse.Workspace{ - { - Metadata: trivyTypes.NewTestMetadata(), - EnableManagedVirtualNetwork: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), - }, - }, - }, - expected: false, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Synapse = test.input - results := CheckVirtualNetworkEnabled.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckVirtualNetworkEnabled.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/azure/synapse/virtual_network_enabled_test.rego b/checks/cloud/azure/synapse/virtual_network_enabled_test.rego new file mode 100644 index 00000000..fd4b2b67 --- /dev/null +++ b/checks/cloud/azure/synapse/virtual_network_enabled_test.rego @@ -0,0 +1,20 @@ +package builtin.azure.synapse.azure0034_test + +import rego.v1 + +import data.builtin.azure.synapse.azure0034 as check +import data.lib.test + +test_deny_managed_virtual_network_disabled if { + inp := {"azure": {"synapse": {"workspaces": [{"enablemanagedvirtualnetwork": {"value": false}}]}}} + + res := check.deny with input as inp + count(res) == 1 +} + +test_allow_managed_virtual_network_enabled if { + inp := {"azure": {"synapse": {"workspaces": [{"enablemanagedvirtualnetwork": {"value": true}}]}}} + + res := check.deny with input as inp + count(res) == 0 +} diff --git a/checks/cloud/azure/monitor/capture_all_regions_test.go b/test/rego/azure_monitor_test.go similarity index 66% rename from checks/cloud/azure/monitor/capture_all_regions_test.go rename to test/rego/azure_monitor_test.go index 5a419da7..f650a49b 100644 --- a/checks/cloud/azure/monitor/capture_all_regions_test.go +++ b/test/rego/azure_monitor_test.go @@ -1,27 +1,99 @@ -package monitor +package test import ( - "testing" - - trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" - - "github.com/aquasecurity/trivy/pkg/iac/state" - + "github.com/aquasecurity/trivy/pkg/iac/providers/azure" "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" - "github.com/aquasecurity/trivy/pkg/iac/scan" - - "github.com/stretchr/testify/assert" + "github.com/aquasecurity/trivy/pkg/iac/state" + trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" ) -func TestCheckCaptureAllRegions(t *testing.T) { - tests := []struct { - name string - input monitor.Monitor - expected bool - }{ +var azureMonitorTestCases = testCases{ + "AVD-AZU-0031": { + { + name: "Log retention policy disabled", + input: state.State{Azure: azure.Azure{Monitor: monitor.Monitor{ + LogProfiles: []monitor.LogProfile{ + { + Metadata: trivyTypes.NewTestMetadata(), + RetentionPolicy: monitor.RetentionPolicy{ + Metadata: trivyTypes.NewTestMetadata(), + Enabled: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + Days: trivyTypes.Int(365, trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Log retention policy enabled for 90 days", + input: state.State{Azure: azure.Azure{Monitor: monitor.Monitor{ + LogProfiles: []monitor.LogProfile{ + { + Metadata: trivyTypes.NewTestMetadata(), + RetentionPolicy: monitor.RetentionPolicy{ + Metadata: trivyTypes.NewTestMetadata(), + Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Days: trivyTypes.Int(90, trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Log retention policy enabled for 365 days", + input: state.State{Azure: azure.Azure{Monitor: monitor.Monitor{ + LogProfiles: []monitor.LogProfile{ + { + Metadata: trivyTypes.NewTestMetadata(), + RetentionPolicy: monitor.RetentionPolicy{ + Metadata: trivyTypes.NewTestMetadata(), + Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Days: trivyTypes.Int(365, trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0033": { + { + name: "Log profile captures only write activities", + input: state.State{Azure: azure.Azure{Monitor: monitor.Monitor{ + LogProfiles: []monitor.LogProfile{ + { + Metadata: trivyTypes.NewTestMetadata(), + Categories: []trivyTypes.StringValue{ + trivyTypes.String("Write", trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Log profile captures action, write, delete activities", + input: state.State{Azure: azure.Azure{Monitor: monitor.Monitor{ + LogProfiles: []monitor.LogProfile{ + { + Metadata: trivyTypes.NewTestMetadata(), + Categories: []trivyTypes.StringValue{ + trivyTypes.String("Action", trivyTypes.NewTestMetadata()), + trivyTypes.String("Write", trivyTypes.NewTestMetadata()), + trivyTypes.String("Delete", trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0032": { { name: "Log profile captures only eastern US region", - input: monitor.Monitor{ + input: state.State{Azure: azure.Azure{Monitor: monitor.Monitor{ LogProfiles: []monitor.LogProfile{ { Metadata: trivyTypes.NewTestMetadata(), @@ -30,12 +102,12 @@ func TestCheckCaptureAllRegions(t *testing.T) { }, }, }, - }, + }}}, expected: true, }, { name: "Log profile captures all regions", - input: monitor.Monitor{ + input: state.State{Azure: azure.Azure{Monitor: monitor.Monitor{ LogProfiles: []monitor.LogProfile{ { Metadata: trivyTypes.NewTestMetadata(), @@ -111,26 +183,8 @@ func TestCheckCaptureAllRegions(t *testing.T) { }, }, }, - }, + }}}, expected: false, }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - var testState state.State - testState.Azure.Monitor = test.input - results := CheckCaptureAllRegions.Evaluate(&testState) - var found bool - for _, result := range results { - if result.Status() == scan.StatusFailed && result.Rule().LongID() == CheckCaptureAllRegions.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/test/rego/azure_network_test.go b/test/rego/azure_network_test.go new file mode 100644 index 00000000..3318f9f8 --- /dev/null +++ b/test/rego/azure_network_test.go @@ -0,0 +1,331 @@ +package test + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + "github.com/aquasecurity/trivy/pkg/iac/state" + trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +var azureNetworkTestCases = testCases{ + "AVD-AZU-0048": { + { + name: "Security group inbound rule allowing RDP access from the Internet", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("*", trivyTypes.NewTestMetadata()), + }, + SourcePorts: nil, + DestinationAddresses: nil, + DestinationPorts: []network.PortRange{ + { + Metadata: trivyTypes.NewTestMetadata(), + Start: trivyTypes.IntTest(3310), + End: trivyTypes.IntTest(3390), + }, + }, + Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Security group inbound rule allowing RDP access from a specific address", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + DestinationPorts: []network.PortRange{ + { + Metadata: trivyTypes.NewTestMetadata(), + Start: trivyTypes.IntTest(3310), + End: trivyTypes.IntTest(3390), + }, + }, + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("4.53.160.75", trivyTypes.NewTestMetadata()), + }, + Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }}}, + expected: false, + }, + { + name: "Security group inbound rule allowing only ICMP", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("*", trivyTypes.NewTestMetadata()), + }, + SourcePorts: nil, + DestinationAddresses: nil, + DestinationPorts: []network.PortRange{ + { + Metadata: trivyTypes.NewTestMetadata(), + Start: trivyTypes.IntTest(3310), + End: trivyTypes.IntTest(3390), + }, + }, + Protocol: trivyTypes.String("Icmp", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0051": { + { + name: "Security group outbound rule with wildcard destination address", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + DestinationAddresses: []trivyTypes.StringValue{ + trivyTypes.String("*", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Security group outbound rule with private destination address", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + DestinationAddresses: []trivyTypes.StringValue{ + trivyTypes.String("10.0.0.0/16", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0047": { + { + name: "Security group inbound rule with wildcard source address", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("*", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Security group inbound rule with private source address", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("10.0.0.0/16", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0049": { + { + name: "Network watcher flow log retention policy disabled", + input: state.State{Azure: azure.Azure{Network: network.Network{ + NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{ + { + Metadata: trivyTypes.NewTestMetadata(), + RetentionPolicy: network.RetentionPolicy{ + Metadata: trivyTypes.NewTestMetadata(), + Enabled: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + Days: trivyTypes.Int(100, trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Network watcher flow log retention policy enabled for 30 days", + input: state.State{Azure: azure.Azure{Network: network.Network{ + NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{ + { + Metadata: trivyTypes.NewTestMetadata(), + RetentionPolicy: network.RetentionPolicy{ + Metadata: trivyTypes.NewTestMetadata(), + Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Days: trivyTypes.Int(30, trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Network watcher flow log retention policy enabled for 100 days", + input: state.State{Azure: azure.Azure{Network: network.Network{ + NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{ + { + Metadata: trivyTypes.NewTestMetadata(), + RetentionPolicy: network.RetentionPolicy{ + Metadata: trivyTypes.NewTestMetadata(), + Enabled: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Days: trivyTypes.Int(100, trivyTypes.NewTestMetadata()), + }, + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0050": { + { + name: "Security group rule allowing SSH access from the public internet", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + DestinationPorts: []network.PortRange{ + { + Metadata: trivyTypes.NewTestMetadata(), + Start: trivyTypes.IntTest(22), + End: trivyTypes.IntTest(22), + }, + }, + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("*", trivyTypes.NewTestMetadata()), + }, + Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }}}, + expected: true, + }, + { + name: "Security group rule allowing SSH only ICMP", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + DestinationPorts: []network.PortRange{ + { + Metadata: trivyTypes.NewTestMetadata(), + Start: trivyTypes.IntTest(22), + End: trivyTypes.IntTest(22), + }, + }, + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("*", trivyTypes.NewTestMetadata()), + }, + Protocol: trivyTypes.String("Icmp", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }}}, + expected: false, + }, + { + name: "Security group rule allowing SSH access from a specific address", + input: state.State{Azure: azure.Azure{Network: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Metadata: trivyTypes.NewTestMetadata(), + Rules: []network.SecurityGroupRule{ + { + Metadata: trivyTypes.NewTestMetadata(), + Allow: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + Outbound: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + DestinationPorts: []network.PortRange{ + { + Metadata: trivyTypes.NewTestMetadata(), + Start: trivyTypes.IntTest(22), + End: trivyTypes.IntTest(22), + }, + }, + SourceAddresses: []trivyTypes.StringValue{ + trivyTypes.String("82.102.23.23", trivyTypes.NewTestMetadata()), + }, + Protocol: trivyTypes.String("Tcp", trivyTypes.NewTestMetadata()), + }, + }, + }, + }, + }}}, + expected: false, + }, + }, +} diff --git a/test/rego/azure_securitycenter_test.go b/test/rego/azure_securitycenter_test.go new file mode 100644 index 00000000..277fb22a --- /dev/null +++ b/test/rego/azure_securitycenter_test.go @@ -0,0 +1,89 @@ +package test + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" + "github.com/aquasecurity/trivy/pkg/iac/state" + trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +var azureSecurityCenterTestCases = testCases{ + "AVD-AZU-0044": { + { + name: "Security center alert nofifications disabled", + input: state.State{Azure: azure.Azure{SecurityCenter: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{ + { + Metadata: trivyTypes.NewTestMetadata(), + EnableAlertNotifications: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: true, + }, + { + name: "Security center alert nofifications enabled", + input: state.State{Azure: azure.Azure{SecurityCenter: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{ + { + Metadata: trivyTypes.NewTestMetadata(), + EnableAlertNotifications: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0045": { + { + name: "Security center set with free subscription", + input: state.State{Azure: azure.Azure{SecurityCenter: securitycenter.SecurityCenter{ + Subscriptions: []securitycenter.SubscriptionPricing{ + { + Metadata: trivyTypes.NewTestMetadata(), + Tier: trivyTypes.String(securitycenter.TierFree, trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: true, + }, + { + name: "Security center set with standard subscription", + input: state.State{Azure: azure.Azure{SecurityCenter: securitycenter.SecurityCenter{ + Subscriptions: []securitycenter.SubscriptionPricing{ + { + Metadata: trivyTypes.NewTestMetadata(), + Tier: trivyTypes.String(securitycenter.TierStandard, trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: false, + }, + }, + "AVD-AZU-0046": { + { + name: "Contact's phone number missing", + input: state.State{Azure: azure.Azure{SecurityCenter: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{ + { + Metadata: trivyTypes.NewTestMetadata(), + Phone: trivyTypes.String("", trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: true, + }, + { + name: "Contact's phone number provided", + input: state.State{Azure: azure.Azure{SecurityCenter: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{ + { + Metadata: trivyTypes.NewTestMetadata(), + Phone: trivyTypes.String("+1-555-555-5555", trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: false, + }, + }, +} diff --git a/test/rego/azure_synapse_test.go b/test/rego/azure_synapse_test.go new file mode 100644 index 00000000..9e50650b --- /dev/null +++ b/test/rego/azure_synapse_test.go @@ -0,0 +1,37 @@ +package test + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/synapse" + "github.com/aquasecurity/trivy/pkg/iac/state" + trivyTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +var azureSynapseTestCases = testCases{ + "AVD-AZU-0034": { + { + name: "Synapse workspace managed VN disabled", + input: state.State{Azure: azure.Azure{Synapse: synapse.Synapse{ + Workspaces: []synapse.Workspace{ + { + Metadata: trivyTypes.NewTestMetadata(), + EnableManagedVirtualNetwork: trivyTypes.Bool(false, trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: true, + }, + { + name: "Synapse workspace managed VN enabled", + input: state.State{Azure: azure.Azure{Synapse: synapse.Synapse{ + Workspaces: []synapse.Workspace{ + { + Metadata: trivyTypes.NewTestMetadata(), + EnableManagedVirtualNetwork: trivyTypes.Bool(true, trivyTypes.NewTestMetadata()), + }, + }, + }}}, + expected: false, + }, + }, +} diff --git a/test/rego/rego_checks_test.go b/test/rego/rego_checks_test.go index 3b5f9d34..1869b311 100644 --- a/test/rego/rego_checks_test.go +++ b/test/rego/rego_checks_test.go @@ -54,7 +54,11 @@ func TestRegoChecks(t *testing.T) { awsDocumentDBTestCases, awsDynamodbTestCases, awsS3TestCases, - + + azureMonitorTestCases, + azureNetworkTestCases, + azureSynapseTestCases, + azureSecurityCenterTestCases, azureDataFactoryTestCases, azureDataLakeTestCases, azureKeyVaultTestCases,