diff --git a/README.md b/README.md index e0c3e002b..5fcb2a15e 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ The static resources derived from the generic resources prepended with `azure_` - [azure_key_vaults](docs/resources/azure_key_vaults.md) - [azure_load_balancer](docs/resources/azure_load_balancer.md) - [azure_load_balancers](docs/resources/azure_load_balancers.md) +- [azure_lock](docs/resources/azure_lock.md) +- [azure_locks](docs/resources/azure_locks.md) - [azure_mariadb_server](docs/resources/azure_mariadb_server.md) - [azure_mariadb_servers](docs/resources/azure_mariadb_servers.md) - [azure_monitor_activity_log_alert](docs/resources/azure_monitor_activity_log_alert.md) diff --git a/docs/resources/azure_lock.md b/docs/resources/azure_lock.md new file mode 100644 index 000000000..577dd2989 --- /dev/null +++ b/docs/resources/azure_lock.md @@ -0,0 +1,93 @@ +--- +title: About the azure_lock Resource +platform: azure +--- + +# azure_lock + +Use the `azure_lock` InSpec audit resource to test properties and configuration of a management lock. + +## Azure REST API version, endpoint and http client parameters + +This resource interacts with api versions supported by the resource provider. +The `api_version` can be defined as a resource parameter. +If not provided, the latest version will be used. +For more information, refer to [`azure_generic_resource`](azure_generic_resource.md). + +Unless defined, `azure_cloud` global endpoint, and default values for the http client will be used. +For more information, refer to the resource pack [README](../../README.md). + +## Availability + +### Installation + +This resource is available in the [InSpec Azure resource pack](https://github.com/inspec/inspec-azure). +For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal). + +## Syntax + +The management lock resources do not follow the common `resouce_group` and `name` pattern for identification. +As a result of that, the `resource_id` must be given as a parameter to the `azure_lock` resource. +The [`azure_locks`](azure_locks.md) resource can be used for gathering the management lock resource ids to be tested within the desired level, such as, subscription, resource group or individual resource. +```ruby +describe azure_lock(resource_id: '/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{parentResourcePath}/{resourceType}/{resourceName}/providers/Microsoft.Authorization/locks/{lockName}') do + it { should exist } +end +``` +## Parameters + +| Name | Description | +|---------------------------------------|-----------------------------------------------------------------------------------| +| resource_id | The unique resource ID. `/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{parentResourcePath}/{resourceType}/{resourceName}/providers/Microsoft.Authorization/locks/{lockName}` | + +## Properties + +| Property | Description | +|---------------------------|-------------| +| properties.level | The level of the lock. Possible values are: `NotSpecified`, `CanNotDelete`, `ReadOnly`. For more see [here](https://docs.microsoft.com/en-us/rest/api/resources/managementlocks/getatresourcelevel#locklevel). | +| properties.notes | Notes about the lock. Maximum of 512 characters. | +| properties.owners | A list of the owners of the lock with [these](https://docs.microsoft.com/en-us/rest/api/resources/managementlocks/getatresourcelevel#managementlockowner) properties. | + +Please note that the properties can vary depending on the `api_version` used for the lookup. + +For properties applicable to all resources, such as `type`, `name`, `id`, `properties`, refer to [`azure_generic_resource`](azure_generic_resource.md#properties). + +Also, refer to [Azure documentation](https://docs.microsoft.com/en-us/rest/api/resources/managementlocks/getatresourcelevel#managementlockobject) for other properties available. +Any attribute in the response may be accessed with the key names separated by dots (`.`), eg. `properties.`. + +## Examples + +### Test If a `ReadOnly` Management Lock Exist in a Specific Resource Group +```ruby +azure_locks(resource_group: 'example-group').ids.each do |id| + describe azure_lock(resource_id: id) do + its('properties.level') { should_not cmp `ReadOnly` } + end +end +``` +### Test If Management Locks on a Specific Resource Contain a Certain String +```ruby +azure_locks(resouce_id: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName}').ids.each do |lock_id| + describe azure_lock(resource_id: lock_id) do + it('properties.notes') { should include 'contact jdoe@chef.io' } + end +end +``` +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [Universal Matchers page](https://www.inspec.io/docs/reference/matchers/). + +### exists +```ruby +# If we expect a resource to always exist +describe azure_lock(resource_id: '/subscriptions/..{lockName}') do + it { should exist } +end +# If we expect a resource to never exist +describe azure_lock(resource_id: '/subscriptions/..{lockName}') do + it { should_not exist } +end +``` +## Azure Permissions + +Your [Service Principal](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal) must be setup with a `contributor` role on the subscription you wish to test. diff --git a/docs/resources/azure_locks.md b/docs/resources/azure_locks.md new file mode 100644 index 000000000..232a007b3 --- /dev/null +++ b/docs/resources/azure_locks.md @@ -0,0 +1,114 @@ +--- +title: About the azure_locks Resource +platform: azure +--- + +# azure_locks + +Use the `azure_locks` InSpec audit resource to test properties and configuration of all management locks for an Azure resource or any level below it. + +## Azure REST API version, endpoint and http client parameters + +This resource interacts with api versions supported by the resource provider. +The `api_version` can be defined as a resource parameter. +If not provided, the latest version will be used. +For more information, refer to [`azure_generic_resource`](azure_generic_resource.md). + +Unless defined, `azure_cloud` global endpoint, and default values for the http client will be used. +For more information, refer to the resource pack [README](../../README.md). + +## Availability + +### Installation + +This resource is available in the [InSpec Azure resource pack](https://github.com/inspec/inspec-azure). +For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal). + +## Syntax + +An `azure_locks` resource block returns all management locks, either within a Resource Group (if provided), or within an entire Subscription. +```ruby +describe azure_locks do + it { should exist } +end +``` +or +```ruby +describe azure_locks(resource_group: 'my-rg') do + it { should exist } +end +``` +Also, at resource level test can be done providing the following identifiers: `resource_group`, `resource_name` and `resource_type` or the `resource_id`. +```ruby +describe azure_locks(resource_group: 'rg-1', resource_name: 'my-VM', resource_type: 'Microsoft.Compute/virtualMachines') do + it { should exist } +end +``` +or +```ruby +describe azure_locks(resource_id: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName}') do + it { should exist } +end +``` +## Parameters + +| Name | Description | +|--------------------------------|----------------------------------------------------------------------------------| +| resource_group | Azure resource group that the targeted resource resides in. `MyResourceGroup` | +| resource_name | Name of the Azure resource on which the management locks are being tested. `MyVM` | +| resource_type | Type of the Azure resource on which the management locks are being tested. `Microsoft.Compute/virtualMachines` | +| resource_id | The unique resource ID of the Azure resource on which the management locks are being tested. `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName}` | + +Either one of the parameter sets can be provided for a valid query: +- `resource_id` +- `resource_group`, `resource_name` and `resource_type` +- `resource_group` +- None for a subscription level test. + +## Properties + +|Property | Description | Filter Criteria* | +|---------------|--------------------------------------------------------------------------------------|-----------------| +| ids | A list of the unique resource ids of the management locks. | `id` | +| names | A list of names of all the management locks being interrogated. | `name` | +| properties | A list of properties for all the management locks being interrogated. | `properties` | + +* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/dev-docs/filtertable-usage.md). + +## Examples + +### Check If a Specific Management Lock is Present for a Resource +```ruby +describe azure_locks(resource_group: 'rg-1', resource_name: 'my-VM', resource_type: 'Microsoft.Compute/virtualMachines') do + its('names') { should include 'production_agents' } +end +``` +### Filters the Results to Include Only Those Management Locks which Include the Given Name +```ruby +describe azure_locks.where{ name.include?('production') } do + it { should exist } +end +``` +### Loop through All Virtual Machines to Test If They have Management Locks Defined on +```ruby +azure_virtual_machines.ids.each do |id| + describe azure_locks(resource_id: id) do + it { should exist } + end +end +``` +## Matchers + +This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [Universal Matchers page](https://www.inspec.io/docs/reference/matchers/). + +### exists + +The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches. +```ruby +describe azure_locks(resource_group: 'rg-1', resource_name: 'my-VM', resource_type: 'Microsoft.Compute/virtualMachines') do + it { should exist } +end +``` +## Azure Permissions + +Your [Service Principal](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal) must be setup with a `contributor` role on the subscription you wish to test. diff --git a/docs/resources/azurerm_locks.md b/docs/resources/azurerm_locks.md index d843bdade..989457c8a 100644 --- a/docs/resources/azurerm_locks.md +++ b/docs/resources/azurerm_locks.md @@ -3,6 +3,8 @@ title: About the azurerm_locks Resource platform: azure --- +> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_locks`](azure_locks.md) InSpec audit resource. + # azurerm\_locks Use the `azurerm_locks` InSpec audit resource to test properties of diff --git a/libraries/azure_backend.rb b/libraries/azure_backend.rb index 9bdd79904..58efcd530 100644 --- a/libraries/azure_backend.rb +++ b/libraries/azure_backend.rb @@ -236,7 +236,7 @@ def get_resource(opts = {}) allow: %i(api_version), opts: opts) api_version = opts[:api_version] || 'latest' - if opts[:resource_uri].include?('providers') + if opts[:resource_uri].scan('providers').size == 1 # If the resource provider is unknown then this method can't find the api_version. # The latest api_version will de acquired from the error message via #rescue_wrong_api_call method. _resource_group, provider, r_type = Helpers.res_group_provider_type_from_uri(opts[:resource_uri]) diff --git a/libraries/azure_lock.rb b/libraries/azure_lock.rb new file mode 100644 index 000000000..3e0a7a49d --- /dev/null +++ b/libraries/azure_lock.rb @@ -0,0 +1,29 @@ +require 'azure_generic_resource' + +class AzureLock < AzureGenericResource + name 'azure_lock' + desc 'Verifies settings for an Azure Lock' + example <<-EXAMPLE + describe azure_lock(resource_group: 'rg-1', name: 'my-lock-name') do + it { should exist } + end + EXAMPLE + + def initialize(opts = {}) + # Options should be Hash type. Otherwise Ruby will raise an error when we try to access the keys. + raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash) + raise ArgumentError, '`resource_id` must be provided.' if opts[:resource_id].nil? + unless opts.slice(:name, :resource_group).keys.empty? + raise ArgumentError, '`name` and `resource_group` parameters are not allowed.' + end + + opts[:resource_provider] = specific_resource_constraint('Microsoft.Authorization/locks', opts) + + # static_resource parameter must be true for setting the resource_provider in the backend. + super(opts, true) + end + + def to_s + super(AzureLock) + end +end diff --git a/libraries/azure_locks.rb b/libraries/azure_locks.rb new file mode 100644 index 000000000..fd1f4a6bc --- /dev/null +++ b/libraries/azure_locks.rb @@ -0,0 +1,84 @@ +require 'azure_generic_resources' + +class AzureLocks < AzureGenericResources + name 'azure_locks' + desc 'Verifies settings for an Azure Lock on a Resource' + example <<-EXAMPLE + describe azure_locks(resource_group: 'my-rg', resource_name: 'my-vm', resource_type: 'Microsoft.Compute/virtualMachines') do + it { should exist } + end + EXAMPLE + + attr_reader :table + + def initialize(opts = {}) + # Options should be Hash type. Otherwise Ruby will raise an error when we try to access the keys. + raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash) + + # Resource level parameter validation is done here due to `resource_type` is a special parameter in the backend. + if opts[:resource_id] + opts[:resource_name] = opts[:resource_id].split('/').last + opts[:resource_group], provider, r_type = Helpers.res_group_provider_type_from_uri(opts[:resource_id]) + opts[:type] = [provider, r_type].join('/') + # `resource_id` is not allowed for plural resources in the backend + opts.delete(:resource_id) + elsif opts[:resource_name] + required_params = [:resource_group, :resource_name, :resource_type] + missing_params = required_params - opts.keys + raise ArgumentError, "#{missing_params} must be provided." unless missing_params.empty? + # `resource_type` is a special parameter in the backend. + # Change the key name here to something else + opts[:type] = opts[:resource_type] + opts.delete(:resource_type) + end + + # This validation is done at this point due to the `resource_type` => `type` conversion has to happen before + opts[:resource_provider] = specific_resource_constraint('Microsoft.Authorization/locks', opts) + # This is for passing the validation in the backend. + opts[:allowed_parameters] = %i(resoure_id resource_group resource_name type) + + opts[:resource_uri] = "/providers/#{opts[:resource_provider]}" + opts[:resource_uri].prepend("/providers/#{opts[:type]}/#{opts[:resource_name]}") unless opts[:resource_name].nil? + opts[:resource_uri].prepend("/resourceGroups/#{opts[:resource_group]}") unless opts[:resource_group].nil? + opts[:add_subscription_id] = true + + # static_resource parameter must be true for setting the resource_provider in the backend. + super(opts, true) + + # Check if the resource is failed. + # It is recommended to check that after every usage of inherited methods or making API calls. + return if failed_resource? + + # Define the column and field names for FilterTable. + # In most cases, the `column` should be the pluralized form of the `field`. + table_schema = [ + { column: :names, field: :name }, + { column: :ids, field: :id }, + { column: :properties, field: :properties }, + ] + + # FilterTable is populated at the very end due to being an expensive operation. + AzureGenericResources.populate_filter_table(:table, table_schema) + end + + def to_s + super(AzureLocks) + end +end + +# Provide the same functionality under the old resource name. +# This is for backward compatibility. +class AzurermLocks < AzureLocks + name 'azurerm_locks' + desc 'Verifies settings for an Azure Lock on a Resource' + example <<-EXAMPLE + describe azurerm_locks(resource_group: 'my-rg', resource_name: 'my-vm', resource_type: 'Microsoft.Compute/virtualMachines') do + it { should exist } + end + EXAMPLE + + def initialize(opts = {}) + Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureLocks.name) + super + end +end diff --git a/libraries/azurerm_locks.rb b/libraries/azurerm_locks.rb deleted file mode 100644 index 65928bca8..000000000 --- a/libraries/azurerm_locks.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'azurerm_resource' - -class AzurermLocks < AzurermPluralResource - name 'azurerm_locks' - desc 'Verifies settings for an Azure Lock on a Resource' - example <<-EXAMPLE - describe azurerm_locks(resource_group: 'my-rg', resource_name: 'my-vm', resource_type: 'Microsoft.Compute/virtualMachines') do - it { should exist } - end - EXAMPLE - - attr_reader :table - - FilterTable.create - .register_column(:ids, field: :id) - .register_column(:names, field: :name) - .register_column(:properties, field: :properties) - .install_filter_methods_on_resource(self, :table) - - def initialize(resource_group: nil, resource_name: nil, resource_type: nil) - resp = management.locks(resource_group, resource_name, resource_type) - return if has_error?(resp) - - @table = resp - end - - def to_s - 'Azure Locks' - end -end diff --git a/test/integration/verify/controls/azurerm_locks.rb b/test/integration/verify/controls/azurerm_locks.rb index 91461e3b0..bf49f2533 100644 --- a/test/integration/verify/controls/azurerm_locks.rb +++ b/test/integration/verify/controls/azurerm_locks.rb @@ -8,3 +8,10 @@ it { should_not exist } end end + +control 'azure_locks' do + vm_id = azure_virtual_machine(resource_group: resource_group, name: resource_name).id + describe azure_locks(resource_id: vm_id) do + it { should_not exist } + end +end diff --git a/test/unit/resources/azure_lock_test.rb b/test/unit/resources/azure_lock_test.rb new file mode 100644 index 000000000..1f43f293e --- /dev/null +++ b/test/unit/resources/azure_lock_test.rb @@ -0,0 +1,17 @@ +require_relative 'helper' +require 'azure_lock' + +class AzureLockConstructorTest < Minitest::Test + def test_empty_param_not_ok + assert_raises(ArgumentError) { AzureLock.new } + end + + # resource_provider should not be allowed. + def test_resource_provider_not_ok + assert_raises(ArgumentError) { AzureLock.new(resource_provider: 'some_type') } + end + + def test_resource_group_name_not_ok + assert_raises(ArgumentError) { AzureLock.new(name: 'my-name', resource_group: 'test') } + end +end diff --git a/test/unit/resources/azure_locks_test.rb b/test/unit/resources/azure_locks_test.rb new file mode 100644 index 000000000..4aded7dd3 --- /dev/null +++ b/test/unit/resources/azure_locks_test.rb @@ -0,0 +1,21 @@ +require_relative 'helper' +require 'azure_locks' + +class AzureLocksConstructorTest < Minitest::Test + # resource_type should not be allowed. + def test_resource_type_not_ok + assert_raises(ArgumentError) { AzureLocks.new(resource_provider: 'some_type') } + end + + def tag_value_not_ok + assert_raises(ArgumentError) { AzureLocks.new(tag_value: 'some_tag_value') } + end + + def tag_name_not_ok + assert_raises(ArgumentError) { AzureLocks.new(tag_name: 'some_tag_name') } + end + + def test_name_not_ok + assert_raises(ArgumentError) { AzureLocks.new(name: 'some_name') } + end +end