diff --git a/.expeditor/buildkite/verify.sh b/.expeditor/buildkite/verify.sh
index 3e547cba3..f2a3a3092 100755
--- a/.expeditor/buildkite/verify.sh
+++ b/.expeditor/buildkite/verify.sh
@@ -9,6 +9,7 @@ bundle --version
echo "--- bundle install"
bundle install --jobs=7 --retry=3 --without tools maintenance deploy
+bundle update
echo "+++ bundle exec rake lint"
bundle exec rake lint
diff --git a/.gitignore b/.gitignore
index 1e519d616..0b8158019 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,7 @@ inspec.lock
Gemfile.lock
.bundle
vendor
+.idea
+.env
+dev/
+.ruby-version
diff --git a/.rubocop.yml b/.rubocop.yml
index 8cc2c7452..e1b32a219 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -2,6 +2,8 @@
AllCops:
Exclude:
- bin/**/*
+ - vendor/**/*
+ - dev/**/*
AlignParameters:
Enabled: true
BlockDelimiters:
@@ -32,17 +34,17 @@ Layout/EmptyLineAfterGuardClause:
Layout/SpaceAroundOperators:
Enabled: false
MethodLength:
- Max: 40
+ Max: 60
Metrics/AbcSize:
- Max: 33
+ Max: 75
Metrics/BlockLength:
Max: 50
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
- Max: 10
+ Max: 25
Metrics/PerceivedComplexity:
- Max: 11
+ Max: 25
Naming/FileName:
Enabled: false
Naming/PredicateName:
@@ -104,3 +106,5 @@ Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: comma
Style/UnlessElse:
Enabled: false
+Style/ClassVars:
+ Enabled: false
diff --git a/.ruby-version b/.ruby-version
deleted file mode 100644
index 097a15a2a..000000000
--- a/.ruby-version
+++ /dev/null
@@ -1 +0,0 @@
-2.6.2
diff --git a/Gemfile b/Gemfile
index a140bb6a3..5a3a19189 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,17 +2,17 @@
source 'https://rubygems.org'
-gem 'faraday', '~> 0.15.0'
-gem 'faraday_middleware', '~> 0.12.2'
-gem 'inspec-bin', '>= 4.18.0'
-gem 'rake', '~> 12.3', '>= 12.3.1'
+gem 'faraday'
+gem 'faraday_middleware'
+gem 'inspec-bin'
+gem 'rake'
group :development do
- gem 'pry', '~> 0.11.3'
+ gem 'pry'
gem 'pry-byebug'
end
-group :developmen, :test do
+group :development, :test do
gem 'minitest', '~> 5.11.0'
gem 'rubocop', '~> 0.71.0'
end
diff --git a/README.md b/README.md
index 6ea84b326..3edc29b40 100644
--- a/README.md
+++ b/README.md
@@ -71,47 +71,163 @@ supports:
(For available inspec-azure versions, see this list of [inspec-azure versions](https://github.com/inspec/inspec-azure/releases).)
+## Resource Documentation
+
+The following is a list of generic resources and static resources.
+The static resources derived from the generic resources prepended with `azure_` are fully backward compatible with their `azurerm_` counterparts.
+
+- [azure_generic_resource](docs/resources/azure_generic_resource.md)
+- [azure_generic_resources](docs/resources/azure_generic_resources.md)
+- [azure_graph_generic_resource](docs/resources/azure_graph_generic_resource.md)
+- [azure_graph_generic_resources](docs/resources/azure_graph_generic_resources.md)
+- [azure_graph_user](docs/resources/azure_graph_user.md)
+- [azure_graph_users](docs/resources/azure_graph_users.md)
+- [azure_key_vault](docs/resources/azure_key_vault.md)
+- [azure_key_vaults](docs/resources/azure_key_vaults.md)
+- [azure_mysql_server](docs/resources/azure_mysql_server.md)
+- [azure_mysql_servers](docs/resources/azure_mysql_servers.md)
+- [azure_network_security_group](docs/resources/azure_network_security_group.md)
+- [azure_network_security_groups](docs/resources/azure_network_security_groups.md)
+- [azure_subnet](docs/resources/azure_subnet.md)
+- [azure_subnets](docs/resources/azure_subnets.md)
+- [azure_virtual_machine](docs/resources/azure_virtual_machine.md)
+- [azure_virtual_machines](docs/resources/azure_virtual_machines.md)
+- [azure_virtual_network](docs/resources/azure_virtual_network.md)
+- [azure_virtual_networks](docs/resources/azure_virtual_networks.md)
+
+With the generic resources:
+
+- Azure cloud resources that this resource pack does not include a static InSpec resource for can be tested.
+- Azure resources from different resource providers and resource groups can be tested at the same time.
+- Server side filtering can be used for more efficient tests.
+
+For more details and different use cases, please refer to the specific resource pages.
+
## Examples
-Verify properties of an Azure VM
+### Interrogate All Resources that Have `project_A` in Their Names within Your Subscription Regardless of Their Type and Resource Group
+
+```ruby
+azure_generic_resources(substring_of_name: 'project_A').ids.each do |id|
+ describe azure_generic_resource(resource_id: id) do
+ its('location') { should eq 'eastus' }
+ end
+end
+```
+### Interrogate All Resources that Have a Tag Defined with the Name `project_A` Regardless of its Value
+
```ruby
-control 'azurerm_virtual_machine' do
- describe azurerm_virtual_machine(resource_group: 'MyResourceGroup', name: 'prod-web-01') do
- it { should exist }
- it { should have_monitoring_agent_installed }
- it { should_not have_endpoint_protection_installed([]) }
- it { should have_only_approved_extensions(['MicrosoftMonitoringAgent']) }
- its('type') { should eq 'Microsoft.Compute/virtualMachines' }
- its('installed_extensions_types') { should include('MicrosoftMonitoringAgent') }
- its('installed_extensions_names') { should include('LogAnalytics') }
+azure_generic_resources(tag_name: 'project_A').ids.each do |id|
+ describe azure_generic_resource(resource_id: id) do
+ its('location') { should eq 'eastus' }
end
end
+```
+
+### Verify Properties of an Azure Virtual Machine
+
+```ruby
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'prod-web-01') do
+ it { should exist }
+ it { should have_monitoring_agent_installed }
+ it { should_not have_endpoint_protection_installed([]) }
+ it { should have_only_approved_extensions(['MicrosoftMonitoringAgent']) }
+ its('type') { should eq 'Microsoft.Compute/virtualMachines' }
+ its('installed_extensions_types') { should include('MicrosoftMonitoringAgent') }
+ its('installed_extensions_names') { should include('LogAnalytics') }
+end
```
-Verify properties of a security group
+### Verify Properties of a Network Security Group
```ruby
-control 'azure_network_security_group' do
- describe azure_network_security_group(resource_group: 'ProductionResourceGroup', name: 'ProdServers') do
- it { should exist }
- its('type') { should eq 'Microsoft.Network/networkSecurityGroups' }
- its('security_rules') { should_not be_empty }
- its('default_security_rules') { should_not be_empty }
- it { should_not allow_rdp_from_internet }
- it { should_not allow_ssh_from_internet }
- end
+describe azure_network_security_group(resource_group: 'ProductionResourceGroup', name: 'ProdServers') do
+ it { should exist }
+ its('type') { should eq 'Microsoft.Network/networkSecurityGroups' }
+ its('security_rules') { should_not be_empty }
+ its('default_security_rules') { should_not be_empty }
+ it { should_not allow_rdp_from_internet }
+ it { should_not allow_ssh_from_internet }
+ it { should allow(source_ip_range: '0.0.0.0', destination_port: '22', direction: 'inbound') }
+ it { should allow_in(service_tag: 'Internet', port: %w{1433-1434 1521 4300-4350 5000-6000}) }
end
```
-## Resource Documentation
+## Parameters Applicable To All Resources
+
+The generic resources and their derivations support following parameters unless stated otherwise in their specific resource page.
+
+### `api_version`
+
+As an Azure resource provider enables new features, it releases a new version of the REST API. They are generally in the format of `2020-01-01`.
+InSpec Azure resources can be forced to use a specific version of the API to eliminate the behavioural changes between the tests using different API versions.
+The latest version will be used unless a specific version is provided.
+
+```ruby
+describe azure_virtual_machine(resource_group: 'my_group', name: 'my_VM', api_version: '2020-01-01') do
+ its('api_version_used_for_query_state') { should eq 'user_provided' }
+ its('api_version_used_for_query') { should eq '2020-01-01' }
+end
+
+# `default` api version can be used if it is supported by the resource provider.
+describe azure_generic_resource(resource_provider: 'Microsoft.Compute/virtualMachines', name: 'my_VM', api_version: 'default') do
+ its('api_version_used_for_query_state') { should eq 'default' }
+end
+
+# `latest` version will be used if it is not provided
+describe azure_virtual_networks do
+ its('api_version_used_for_query_state') { should eq 'latest' }
+end
+
+# `latest` version will be used if the provided is invalid
+describe azure_network_security_groups(resource_group: 'my_group', api_version: 'invalid_api_version') do
+ its('api_version_used_for_query_state') { should eq 'latest' }
+end
+```
+
+### `endpoint`
+
+Microsoft Azure cloud services are available through a global and three national network of datacenter as described [here](https://docs.microsoft.com/en-us/graph/deployments).
+The preferred data center can be defined via `endpoint` parameter.
+Azure Global Cloud will be used if not provided.
+
+- `azure_cloud` (default)
+- `azure_china_cloud`
+- `azure_us_government_L4`
+- `azure_us_government_L5`
+- `azure_german_cloud`
+
+```ruby
+describe azure_virtual_machines(endpoint: 'azure_german_cloud') do
+ it { should exist }
+end
+```
+
+It can be defined as an environment variable or a resource parameter (has priority).
+
+The predefined environment variables for each cloud deployments can be found [here](libraries/backend/helpers.rb#L64).
+
+### http_client parameters
+
+The behavior of the http client can be defined with the following parameters:
+
+- `azure_retry_limit`: Maximum number of retries (default - `2`, Integer).
+- `azure_retry_backoff`: Pause in seconds between retries (default - `0`, Integer).
+- `azure_retry_backoff_factor`: The amount to multiply each successive retry's interval amount by (default - `1`, Integer).
+
+They can be defined as environment variables or resource parameters (has priority).
+
+
-The following resources are available in the InSpec Azure Resource Pack
+> WARNING The following resources do not support **api_version**, **endpoint** and **http_client** parameters and they will be deprecated in the InSpec Azure version **2**.
- [azurerm_ad_user](docs/resources/azurerm_ad_user.md)
- [azurerm_ad_users](docs/resources/azurerm_ad_users.md)
- [azurerm_aks_cluster](docs/resources/azurerm_aks_cluster.md)
- [azurerm_aks_clusters](docs/resources/azurerm_aks_clusters.md)
+- [azurerm_api_management](docs/resources/azurerm_api_management.md)
+- [azurerm_api_managements](docs/resources/azurerm_api_managements.md)
- [azurerm_application_gateway](docs/resources/azurerm_application_gateway.md)
- [azurerm_application_gateways](docs/resources/azurerm_application_gateways.md)
- [azurerm_cosmosdb_database_account](docs/resources/azurerm_cosmosdb_database_account.md)
@@ -181,9 +297,38 @@ See [Connectors](docs/reference/connectors.md) for more information on the diffe
## Development
-If you'd like to contribute to this project please see [Contributing Rules](CONTRIBUTING.md). The following instructions will help you get your development environment setup to run integration tests.
+If you'd like to contribute to this project please see [Contributing Rules](CONTRIBUTING.md).
-### Getting Started
+### Developing a Static Resources
+
+The easiest way to start is checking the existing static resources. They have detailed information on how to leverage the backend class within their comments.
+
+The common parameters are:
+- `resource_provider`: Such as `Microsoft.Compute/virtualMachines`. It has to be hardcoded in the code by the resource author.
+- `display_name`: A generic one will be created unless defined.
+- `required_parameters`: Define mandatory parameters. The `resource_group` and resource `name` in the singular resources are default mandatory in the base class.
+- `allowed_parameters`: Define optional parameters. The `resource_group` is default optional, but this can be made mandatory in the static resource.
+
+### Singular Resources
+
+- In most cases `resource_group` and resource `name` should be required from the users and a single API call would be enough for creating methods on the resource.
+See [azure_virtual_machine](libraries/azure_virtual_machine.rb) for a standard singular resource and how to create static methods from resource properties.
+- If it is beneficial to accept the resource name with a more specific keyword, such as `server_name`, see [azure_mysql_server](libraries/azure_mysql_server.rb).
+- If a resource exists in another resource, such as a subnet on a virtual network, see [azure_subnet](libraries/azure_subnet.rb).
+- If it is necessary to make an additional API call within a static method, the `get_resource` should be used. See [azure_key_vaults](libraries/azure_key_vault.rb).
+
+### Plural Resources
+
+- A standard plural resource does not require a parameter, except optional `resource_group`. See [azure_mysql_servers](libraries/azure_mysql_servers.rb).
+- All plural resources use [FilterTable](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md) to be able to provide filtering within returned resources. The filter criteria must be defined `table_schema` Hash variable.
+- If the properties of the resource are to be manipulated before populating the FilterTable, a `populate_table` method has to be defined. See [azure_virtual_machines](libraries/azure_virtual_machines.rb).
+- If the resources exist in another resource, such as subnets of a virtual network, a `resource_path` has to be created.
+For that, the identifiers of the parent resource, `resource_group` and virtual network name `vnet`, must be required from the users.
+See [azure_subnets](libraries/azure_subnets.rb).
+
+The following instructions will help you get your development environment setup to run integration tests.
+
+### Setting the Environment Variables
Copy `.envrc-example` to `.envrc` and fill in the fields with the values from your account.
@@ -224,8 +369,7 @@ This environment may be used to run your profile against or to run integration t
### Remote State
-Remote state has been removed. The first time you run Terraform after having
-remote state removed you will be presented with a message like:
+Remote state has been removed. The first time you run Terraform after having remote state removed you will be presented with a message like:
```
Do you want to migrate all workspaces to "local"?
@@ -253,89 +397,54 @@ Creating a new environment:
rake azure:login
rake tf:apply
```
-
Creating a new environment with a Network Watcher:
-
```
rake azure:login
rake network_watcher tf:apply
```
-
You may only have a single Network Watcher per a subscription. Use this carefully if you are working with other team members.
-
Updating a running environment (e.g. when you change the .tf file):
-
```
rake azure:login
rake tf:apply
```
-
Checking if your state has diverged from your plan:
-
```
rake azure:login
rake tf:plan
```
-
Destroying your environment:
```
rake azure:login
rake tf:destroy
```
-
-### Running integration tests
-
-To start up an environment and run all tests:
+To run Rubocop and Syntax check for Ruby and InSpec:
```
-bundle
-rake azure:login
-rake azure
+rake test:lint
```
-
-### Development
-
-To run all tests:
+To run unit tests:
```
-bundle
-rake
+rake test:unit
```
-
To run integration tests:
```
-bundle
rake test:integration
```
-
-To run integration tests including a Network Watcher:
-```
-bundle
-rake network_watcher test:integration
-```
-
-To run a control called `azurerm_virtual_machine`:
+To run all tests:
```
-bundle
-rake test:integration[azurerm_virtual_machine]
+rake
```
-
-You may run multiple controls:
+To run integration tests including a Network Watcher:
```
-bundle
-rake test:integration[azure_resource_group,azurerm_virtual_machine]
+rake network_watcher test:integration
```
-
### Optional Components
-By default, rake tasks will only use core components. Optional components have
-associated integrations that will be skipped unless you enable these. We have
-the following optional pieces that may be managed with Terraform.
+By default, rake tasks will only use core components. Optional components have associated integrations that will be skipped unless you enable these. We have the following optional pieces that may be managed with Terraform.
#### Network Watcher
-Network Watcher may be enabled to run integration tests related to the Network
-Watcher. We recommend leaving this disabled unless you are specifically working
-on related resources. You may only have one Network Watcher enabled per an
-Azure subscription at a time. To enable Network Watcher:
+Network Watcher may be enabled to run integration tests related to the Network Watcher. We recommend leaving this disabled unless you are specifically working on related resources. You may only have one Network Watcher enabled per an Azure subscription at a time. To enable Network Watcher:
```
rake options[network_watcher]
@@ -345,13 +454,10 @@ rake tf:apply
#### Graph API
-Graph API support may be enabled to test with `azure_ad` related resources.
-Graph requires granting "Active Directory Read" to the Service Principal. If
-your account does not have access leave this disabled.
-
-Please refer to the [Microsoft
-Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications#updating-an-application)
-for information on how to grant these permissions to your application.
+Graph API support may be enabled to test with `azure_graph` related resources.
+Each resource requires specific privileges granted to your service principal.
+Please refer to the [Microsoft Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications#updating-an-application) for information on how to grant these permissions to your application.
+If your account does not have access, leave this disabled.
Note: An Azure Administrator must grant your application these permissions.
@@ -364,10 +470,7 @@ rake tf:apply
#### Managed Service Identity
Managed Service Identity (MSI) is another way to connect to the Azure APIs.
-This option starts an additonal virtual machine with MSI enabled and a public
-ip address. You will need to put a hole in your firewall to connect to the
-virtual machine. You will also need to grant the `contributor` role to this
-identity for your subscription.
+This option starts an additional virtual machine with MSI enabled and a public ip address. You will need to put a hole in your firewall to connect to the virtual machine. You will also need to grant the `contributor` role to this identity for your subscription.
```
rake options[msi]
@@ -386,6 +489,7 @@ rake tf:apply
```
To disable optional components run `rake options[]` including only the optional components you wish to enable. Any omitted component will be disabled.
+
```
rake options[] # disable all optional components
rake options[option_1] # enables option_1 disabling all other optional components
diff --git a/Rakefile b/Rakefile
index 10617d413..5cf931b1c 100644
--- a/Rakefile
+++ b/Rakefile
@@ -52,8 +52,8 @@ end
namespace :syntax do
desc 'InSpec syntax check'
task :inspec do
- puts '-> Checking Inspec Control Syntax'
- stdout, status = Open3.capture2("bundle exec inspec vendor #{INTEGRATION_DIR} --overwrite &&
+ puts '-> Checking InSpec Control Syntax'
+ stdout, status = Open3.capture2("bundle exec inspec vendor #{INTEGRATION_DIR} --overwrite --chef-license accept-silent &&
bundle exec inspec check #{INTEGRATION_DIR} &&
bundle exec inspec check . && rm -rf #{INTEGRATION_DIR}/vendor")
puts stdout
@@ -68,7 +68,7 @@ namespace :syntax do
desc 'Ruby syntax check'
task :ruby do
puts '-> Checking Ruby Syntax'
- files = %w{Gemfile Rakefile} + Dir['./**/*.rb']
+ files = %w{Gemfile Rakefile} + Dir['./lib*/**/*.rb'] + Dir['./test/**/*.rb']
files.each do |file|
sh('ruby', '-c', file) do |ok, res|
@@ -100,12 +100,27 @@ namespace :azure do
end
end
+# Minitest
+Rake::TestTask.new(:unit) do |t|
+ t.libs << 'test/unit'
+ t.libs << 'libraries'
+ t.verbose = true
+ t.warning = false
+ t.test_files = FileList['test/unit/**/*_test.rb']
+end
+
namespace :test do
- Rake::TestTask.new(:unit) do |t|
- t.libs << 'test/unit'
- t.libs << 'libraries'
- t.test_files = FileList['test/unit/**/*_test.rb']
+ task :unit do
+ ENV['AZURE_SUBSCRIPTION_ID']='placeHolder' if !ENV['AZURE_SUBSCRIPTION_ID']
+ ENV['AZURE_CLIENT_ID']='placeHolder' if !ENV['AZURE_CLIENT_ID']
+ ENV['AZURE_TENANT_ID']='placeHolder' if !ENV['AZURE_TENANT_ID']
+ ENV['AZURE_CLIENT_SECRET']='placeHolder' if !ENV['AZURE_CLIENT_SECRET']
+ Rake::Task['unit'].execute
+ ENV['AZURE_SUBSCRIPTION_ID']=nil if ENV['AZURE_SUBSCRIPTION_ID']=='placeHolder'
+ ENV['AZURE_CLIENT_ID']=nil if ENV['AZURE_CLIENT_ID']=='placeHolder'
+ ENV['AZURE_TENANT_ID']=nil if ENV['AZURE_TENANT_ID']=='placeHolder'
+ ENV['AZURE_CLIENT_SECRET']=nil if ENV['AZURE_CLIENT_SECRET']=='placeHolder'
end
task :integration, [:controls] => ['attributes:write', :setup_env] do |_t, args|
@@ -113,7 +128,8 @@ namespace :test do
--input-file terraform/#{ENV['ATTRIBUTES_FILE']}
--reporter cli
--no-distinct-exit
- -t azure://#{ENV['AZURE_SUBSCRIPTION_ID']} )
+ -t azure://#{ENV['AZURE_SUBSCRIPTION_ID']}
+ --chef-license accept-silent )
if args[:controls]
sh(*cmd, '--controls', args[:controls], *args.extras)
diff --git a/docs/resources/azure_generic_resource.md b/docs/resources/azure_generic_resource.md
new file mode 100644
index 000000000..59b8915a5
--- /dev/null
+++ b/docs/resources/azure_generic_resource.md
@@ -0,0 +1,128 @@
+---
+title: About the azure_generic_resource Resource
+platform: azure
+---
+
+# azure_generic_resource
+
+Use the `azure_generic_resource` Inspec audit resource to test any valid Azure resource available through Azure Resource Manager.
+
+## Availability
+
+### Installation
+
+This resource is available in the `inspec-azure` [resource pack](/inspec/glossary/#resource-pack).
+For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal).
+
+## Syntax
+```ruby
+describe azure_generic_resource(resource_group: 'MyResourceGroup', name: 'MyResource') do
+ its('property') { should eq 'value' }
+end
+```
+
+where
+
+* Resource parameters are used to query Azure Resource Manager endpoint for the resource to be tested.
+* `property` - This generic resource dynamically creates the properties on the fly based on the type of resource that has been targeted.
+* `value` is the expected output from the chosen property.
+
+## Parameters
+
+The following parameters can be passed for targeting a specific Azure resource.
+
+| Name | Description |
+|-------------------|----------------------------------------------------------------------------------------------------------|
+| resource_group | Azure resource group that the targeted resource has been created in. `MyResourceGroup` |
+| name | Name of the Azure resource to test. `MyVM` |
+| resource_provider | Azure resource provider of the resource to be tested. `Microsoft.Compute/virtualMachines` |
+| resource_id | Unique id of Azure resource to be tested. `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName}` |
+| tag_name* | Tag name defined on the Azure resource. `name` |
+| tag_value | Tag value of the tag defined with the `tag_name`. `external_linux` |
+| api_version | API version to use when interrogating the resource. If not set or the provided api version is not supported by the resource provider then the latest version for the resource provider will be used. `2017-10-9`, `latest`, `default` |
+
+* When resources are filtered by a tag name and value, the tags for each resource are not returned in the results.
+
+Either one of the parameter sets can be provided for a valid query:
+- `resource_id`
+- `resource_group` and `name`
+- `name`
+- `resource_group`, `resource_provider` and `name`
+- `tag_name` and `tag_value`
+
+Different parameter combinations can be tried. If it is not supported either the InSpec resource or the Azure Rest API will raise an error.
+
+If the Azure Resource Manager endpoint returns multiple resources for a given query, this singular generic resource will fail. In that case, the [plural generic resource](azure_generic_resources.md) should be used.
+
+## Properties
+
+The properties that can be tested are dependent on the Azure Resource that is tested. One way to see what properties can be tested is checking their API pages. For example for virtual machines, see [here](https://docs.microsoft.com/en-us/rest/api/compute/virtualmachines/get).
+Also the [Azure Resources Portal](https://resources.azure.com) can be used to select the resource you are interested in and see what can be tested.
+
+The following properties are applicable to almost all resources.
+
+| Property | Description |
+|------------|-------------|
+| id | The unique resource identifier.|
+| name | The name of the resource. |
+| type | The resource type. |
+| location | The location of the resource. |
+| tags | The tag `key:value pairs` if defined on the resource. |
+| properties | The resource properties. |
+
+## Examples
+
+### Test Properties of a Virtual Machine and the Endpoint API Version
+```ruby
+describe azure_generic_resource(resource_group: 'my_vms', name: 'my_linux_vm') do
+ its('properties.storageProfile.osDisk.osType') { should cmp 'Linux' }
+ its('properties.storageProfile.osDisk.createOption') { should cmp 'FromImage' }
+ its('properties.storageProfile.osDisk.name') { should cmp 'linux-external-osdisk' }
+ its('properties.storageProfile.osDisk.caching') { should cmp 'ReadWrite' }
+
+ its('api_version_used_for_query_state') { should eq 'latest' }
+end
+```
+
+
+### Test the API Version Used for the Query
+```ruby
+describe azure_generic_resource(resource_id: '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Compute/virtualMachines/{vmName}', api_version: '2017-01-01') do
+ its('api_version_used_for_query_state') { should eq 'user_provided' }
+ its('api_version_used_for_query') { should eq '2017-01-01' }
+end
+```
+
+
+### Test the Tags if Include Specific Values
+```ruby
+describe azure_generic_resource(resource_group: 'my_vms', name: 'my_linux_vm') do
+ its('tags') { should include(name: 'MyVM') }
+ # The tag key name can be tested in String or Symbol.
+ its('tags') { should include(:name) } # regardless of the value
+ its('tags') { should include('name') } # regardless of the value
+end
+```
+
+For more examples, please see the [integration tests](/test/integration/verify/controls/azure_generic_resource.rb).
+
+## 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/).
+
+### exist
+```ruby
+# Should not exist if there is no resource with a given name
+describe azure_generic_resource(name: 'fake_name') do
+ it { should_not exist }
+end
+```
+```ruby
+# Should exist if there is one resource with a given name
+describe azure_generic_resource(name: 'a_very_unique_name_within_subscription') 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.
\ No newline at end of file
diff --git a/docs/resources/azure_generic_resources.md b/docs/resources/azure_generic_resources.md
new file mode 100644
index 000000000..5c71b060e
--- /dev/null
+++ b/docs/resources/azure_generic_resources.md
@@ -0,0 +1,138 @@
+---
+title: About the azure_generic_resources Resource
+platform: azure
+---
+
+# azure_generic_resources
+
+Use the `azure_generic_resources` Inspec audit resource to test any valid Azure resources.
+
+## Availability
+
+### Installation
+
+This resource is available in the `inspec-azure` [resource pack](/inspec/glossary/#resource-pack).
+For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal).
+
+## Syntax
+
+This resource will interrogate all resource in your subscription available through Azure Resource Manager when initiated without a parameter.
+
+```ruby
+describe azure_generic_resources do
+ it { should exist }
+end
+```
+
+## Parameters
+
+The following parameters can be passed for targeting Azure resources.
+All of them are optional.
+
+| Name | Description | Example |
+|--------------------------------|---------------------------------------------------------------------------------------------------------------------------|-------------------------------------|
+| resource_group | Azure resource group that the targeted resources have been created in. | `MyResourceGroup` |
+| substring_of_resource_group | Substring of an Azure resource group name that the targeted resources have been created in. | `My` |
+| name | Name of the Azure resources to test. | `MyVM` |
+| substring_of_name | Substring of a name of the Azure resources to test. | `My ` |
+| resource_provider | Azure resource provider of the resources to be tested. | `Microsoft.Compute/virtualMachines` |
+| tag_name* | Tag name defined on the Azure resources. | `name` |
+| tag_value | Tag value of the tag defined with the `tag_name`. | `external_linux` |
+
+* When resources are filtered by a tag name and value, the tags for each resource are not returned in the results.
+
+Either one of the parameter sets can be provided for a valid query:
+- `resource_group`
+- `substring_of_resource_group`
+- `name`
+- `substring_of_name`
+- `substring_of_resource_group` and `substring_of_name`
+- `resource_provider`
+- `resource_group` and `resource_provider`
+- `substring_of_resource_group` and `resource_provider`
+- `tag_name`
+- `tag_name` and `tag_value`
+
+Different parameter combinations can be tried. If it is not supported either the InSpec resource or the Azure Rest API will raise an error.
+
+It is advised to use these parameter sets to narrow down the targeted resources at the server side, Azure Rest API, for a more computing resource efficient test.
+
+## Properties
+
+| Property | Description | Filter Criteria* |
+|-----------|-------------|-----------------|
+| ids | A list of the unique resource ids. | `id`|
+| names | A list of the resource names that are unique within a resource group.| `name`|
+| tags | A list of `tag:value` pairs defined on the resources. | `tags`|
+| types | A list of resource types. | `type`|
+| locations | A list of locations where resources are created in. | `location`|
+| created_times | A list of created times of the resources. | `created_time`|
+| changed_times | A list of changed times of the resources. | `changed_time`|
+| provisioning_states | A list of provisioning states of the resources. | `provisioning_state`|
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+### Test All Virtual Machines in Your Subscription
+```ruby
+describe azure_generic_resources(resource_provider: 'Microsoft.Compute/virtualMachines') do
+ it { should exist }
+ its('count') { should eq 43 }
+end
+```
+### Test All Resources Regardless of Their Type and Resource Group with a Common String in Their Names (Server Side Filtering)
+```ruby
+azure_generic_resources(substring_of_name: 'project_a').ids.each do |id|
+ describe azure_generic_resource(resource_id: id) do
+ it { should exist }
+ its('location') { should eq 'eastus' }
+ end
+end
+```
+### Test All Resources Regardless of Their Type and Resource Group with a Common Tag `name:value` Pair (Server Side Filtering)
+```ruby
+azure_generic_resources(tag_name: 'demo', tag_value: 'shutdown_at_10_pm').ids.each do |id|
+ describe azure_generic_resource(resource_id: id) do
+ it { should exist }
+ its('location') { should eq 'eastus' }
+ end
+end
+```
+### Filters the Results to Only Include Those that Match the Given Location (Client Side Filtering)
+```ruby
+describe azure_generic_resources.where(location: 'eastus') do
+ it { should exist }
+end
+```
+### Filters the Results to Only Include Those that Created within Last 24 Hours (Client Side Filtering)
+```ruby
+describe azure_generic_resources.where{ created_time > Time.now - 86400 } do
+ it { should exist }
+end
+```
+Please see [here](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md) for more information on how to leverage FilterTable capabilities on plural resources.
+
+For more examples, please see the [integration tests](/test/integration/verify/controls/azure_generic_resources.rb).
+
+## 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/).
+
+### exist
+```ruby
+# Should not exist if there is no resource with a given resource group
+describe azure_generic_resources(resource_group: 'fake_group') do
+ it { should_not exist }
+end
+```
+```ruby
+# Should exist if there is at least one resource
+describe azure_generic_resources(resource_group: 'MyResourceGroup') 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/azure_graph_generic_resource.md b/docs/resources/azure_graph_generic_resource.md
new file mode 100644
index 000000000..67dc50d0e
--- /dev/null
+++ b/docs/resources/azure_graph_generic_resource.md
@@ -0,0 +1,84 @@
+---
+title: About the azure_graph_generic_resource Resource
+platform: azure
+---
+
+# azure_graph_generic_resource
+
+Use the `azure_graph_generic_resource` Inspec audit resource to test any valid Azure resource available through Microsoft Azure Graph API.
+
+## Azure REST API version, endpoint and http client parameters
+
+This resource interacts with api versions supported by the Azure Graph API.
+The `api_version` can be defined as a resource parameter.
+If not provided, the latest stable version will be used.
+
+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](/inspec/glossary/#resource-pack).
+For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal).
+
+## Syntax
+
+```ruby
+describe azure_graph_generic_resource(resource: 'resource', id: 'GUID', select: %w(attributes to be tested)) do
+ its('property') { should eq 'value' }
+end
+```
+
+where
+
+- Resource parameters are used to query Azure Graph API endpoint for the resource to be tested.
+- `property` - This generic resource dynamically creates the properties on the fly based on the property names provided with the `select` parameter.
+- `value` is the expected output from the chosen property.
+
+## Parameters
+
+The following parameters can be passed for targeting a specific Azure resource.
+
+| Name | Description | Example |
+|-------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------|
+| resource | Azure resource type that the targeted resource belongs to. | `users` |
+| id | Globally unique ID of the targeted resource. | `jdoe@contoso.com` |
+| select | The list of query parameters defining which attributes that the resource will expose. If not provided then the predefined attributes will be returned from the API. | `['givenName', 'surname', 'department']` |
+| api_version | API version of the GRAPH API to use when interrogating the resource. If not set then the predefined stable version will be used. | `v1.0`, `beta` |
+
+## Properties
+
+The properties that can be tested are entirely dependent on the Azure Resource that is tested and the query parameters provided with the `select` parameter.
+
+## Examples
+
+### Test Properties of a User Account
+```ruby
+describe azure_graph_generic_resource(resource: 'users', id: 'jdoe@contoso.com', select: %w{ surname givenName }) do
+ its('surname') { should cmp 'Doe' }
+ its('givenName') { should cmp 'John' }
+end
+```
+For more examples, please see the [integration tests](../../test/integration/verify/controls/azure_graph_generic_resource.rb).
+
+## 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/).
+
+### exist
+```ruby
+# Should not exist if there is no resource with a given name
+describe azure_graph_generic_resource(resource: 'users', id: 'fake_id') do
+ it { should_not exist }
+end
+# Should exist if there is one resource with a given name
+describe azure_graph_generic_resource(resource: 'users', id: 'valid_id') do
+ it { should exist }
+end
+```
+## Azure Permissions
+
+Graph resources require specific privileges granted to your service principal.
+Please refer to the [Microsoft Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications#updating-an-application) for information on how to grant these permissions to your application.
\ No newline at end of file
diff --git a/docs/resources/azure_graph_generic_resources.md b/docs/resources/azure_graph_generic_resources.md
new file mode 100644
index 000000000..9f35cf571
--- /dev/null
+++ b/docs/resources/azure_graph_generic_resources.md
@@ -0,0 +1,132 @@
+---
+title: About the azure_graph_generic_resources Resource
+platform: azure
+---
+
+# azure_graph_generic_resources
+
+Use the `azure_graph_generic_resources` Inspec audit resource to test any valid Azure resource available through Microsoft Azure Graph API.
+
+## Azure REST API version, endpoint and http client parameters
+
+This resource interacts with api versions supported by the Azure Graph API.
+The `api_version` can be defined as a resource parameter.
+If not provided, the latest stable version will be used.
+
+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](/inspec/glossary/#resource-pack).
+For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal).
+
+## Syntax
+
+```ruby
+describe azure_graph_generic_resources(resource: 'resource', filter: {starts_with_property_name: 'A'}, select: %w(properties to be tested)) do
+ its('property') { should eq 'value' }
+end
+```
+
+where
+
+- Resource parameters are used to query Azure Graph API endpoint for the resource to be tested.
+- `property` - This generic resource dynamically creates the properties on the fly based on the type of resource that has been targeted and the parameters provided with the `select` parameter.
+- `value` is the expected output from the chosen property.
+
+## Parameters
+
+The following parameters can be passed for targeting specific Azure resources.
+
+| Name | Description | Example |
+|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------|
+| resource | Azure resource type that the targeted resource belongs to. This is the only **MANDATORY** parameter. | `users` |
+| filter | A hash containing the filtering options and their values. The `starts_with_` operator can be used for fuzzy string matching. Parameter names are in snakecase. | `{ starts_with_given_name: 'J', starts_with_department: 'Core', country: 'United Kingdom', given_name: John}` |
+| filter_free_text | [OData](https://www.odata.org/getting-started/basic-tutorial/) query string in double quotes, `"`. Property names are in camelcase, refer to [here](https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter) for more information. | `"startswith(displayName,'J') and surname eq 'Doe'"` |
+| select | A list of the query parameters defining which attributes that the resource will expose and to be tested. Property names are in camelcase. If not provided then the predefined attributes will be returned from the API. | `['givenName', 'surname', 'department']` |
+| api_version | API version of the Azure Graph API to use when interrogating the resource. If not set then the predefined stable version will be used. | `v1.0`, `beta` |
+
+It is advised to use `filter` or `filter_free_text` to narrow down the targeted resources at the server side, Azure Graph API, for a more efficient test.
+
+## Properties
+
+Attributes will be created dynamically by pluralizing the name of the properties of the resources and converting them to `snake_case` form.
+
+E.g., if the query parameters are `select: %w{ country department givenName }`, then the parameters will be:
+
+- `ids` (default)
+- `countries`
+- `departments`
+- `given_names`
+
+## Filter Criteria
+
+Returned resources can be filtered by their parameters provided with the `select` option or the default values returned from the API unless the `select` is used.
+
+E.g., if the query parameters are `select: %w{ country department givenName }`, then the filter criteria will be:
+
+- `id` (default)
+- `country`
+- `department`
+- `givenName`
+
+## Examples
+
+### Test a Selection of User Accounts
+```ruby
+# Using filter parameter
+describe azure_graph_generic_resources(resource: 'users', filter: { starts_with_given_name: 'J', starts_with_department: 'customer', country: 'United Kingdom' }, select: %w{ country userPrincipalName}) do
+ it { should exist }
+ its('countries'.uniq) { should eq ['United Kingdom'] }
+end
+
+# Using filter_free_text parameter
+describe azure_graph_generic_resources(resource: 'users', filter_free_text: "startswith(givenName,'J') and startswith(department,'customer') and country eq 'United States'", select: %w{ country userPrincipalName}) do
+ it { should exist }
+ its('countries'.uniq) { should eq ['United States'] }
+end
+```
+
+## Filter the Results to Only Include Those that Match the Given Country
+```ruby
+ describe azure_graph_generic_resources(resource: 'users', select: %w{ country }).where(country: 'United Kingdom') do
+ it { should exist }
+ end
+```
+Please note that instead of client side filtering with `where`, it is much more efficient to use server side filtering at Azure Graph API with `filter` or `filter_free_text` at resource creation as described previously.
+
+## Test `given_names` Parameter
+```ruby
+azure_graph_generic_resources(resource: 'users', filter: { starts_with_given_name: 'J' }, select: %w{ givenName }).given_names.each do |name|
+ describe name do
+ it { should start_with('J') }
+ end
+end
+```
+
+Please see [here](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md) for more information on how to leverage FilterTable capabilities on plural resources.
+
+## 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/).
+
+### exist
+```ruby
+# Should not exist if there is no resource with a given name
+describe azure_graph_generic_resources(resource: 'users', filter: { given_name: 'fake_name'}, select: %w{ givenName }) do
+ it { should_not exist }
+end
+
+# Should exist if there is at least one resource with a given name
+describe azure_graph_generic_resources(resource: 'users', filter: { given_name: 'valid_name'}, select: %w{ givenName }) do
+ it { should exist }
+end
+```
+
+## Azure Permissions
+
+Graph resources require specific privileges granted to your service principal.
+Please refer to the [Microsoft Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications#updating-an-application) for information on how to grant these permissions to your application.
\ No newline at end of file
diff --git a/docs/resources/azure_graph_user.md b/docs/resources/azure_graph_user.md
new file mode 100644
index 000000000..34c5fc2b2
--- /dev/null
+++ b/docs/resources/azure_graph_user.md
@@ -0,0 +1,97 @@
+---
+title: About the azure_graph_user Resource
+platform: azure
+---
+
+# azure_graph_user
+
+Use the `azure_graph_user` InSpec audit resource to test properties of an Azure Active Directory user within a Tenant.
+
+## 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 stable version will be used.
+For more information, refer to [`azure_graph_generic_resource`](azure_graph_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](/inspec/glossary/#resource-pack).
+For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal).
+
+## Syntax
+```ruby
+describe azure_graph_user(user_principal_name: 'jdoe@contoso.com') do
+ it { should exist }
+end
+```
+## Parameters
+
+Either one of the following parameters is mandatory.
+
+| Name | Description | Example |
+|--------------------|-------------|---------|
+| user_principal_name | The user principal name. | `jdoe@contoso.com` |
+| id | Globally unique identifier. | `abcd-1234-efabc-5678` |
+| user_id | Globally unique identifier. (For backward compatibility.) | `abcd-1234-efabc-5678` |
+
+## Properties
+
+| Property | Description |
+|-------------------------------|-------------|
+| id | The user's globally unique ID. |
+| account_enabled | Whether the account is enabled. |
+| city | The user's city. |
+| country | The user's country. |
+| department | The user's department. |
+| display_name | The display name of the user. |
+| facsimile_telephone_number | The user's facsimile (fax) number. |
+| given_name | The given name for the user. |
+| job_title | The user's job title. |
+| mail | The primary email address of the user. |
+| mail_nickname | The mail alias for the user. |
+| mobile | The user's mobile (cell) phone number. |
+| password_policies | The password policies for the user. |
+| password_profile | The password profile for the user. |
+| postal_code | The user's postal (ZIP) code. |
+| state | The user's state. |
+| street_address | The user's street address. |
+| surname | The user's surname (family name or last name). |
+| telephone_number | The user's telephone number. |
+| usage_location | A two letter country code (ISO standard 3166). Examples include: `US`, `JP`, and `GB`. |
+| user_principal_name | The principal name of the user. |
+| user_type | A string value that can be used to classify user types in your directory, such as `Member` or `Guest`. |
+
+## Examples
+
+### Test If an Active Directory User Account is Referenced with a Valid ID
+```ruby
+describe azure_graph_user(id: 'someValidId')
+ it { should exist }
+end
+```
+### Test If an Active Directory User Account is Referenced with an Invalid ID
+```ruby
+describe azure_graph_user(id: 'someInvalidId')
+ it { should_not exist }
+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
+describe azure_graph_user(user_id: 'someUserId') do
+ it { should exist }
+end
+```
+## Azure Permissions
+
+Graph resources require specific privileges granted to your service principal.
+Please refer to the [Microsoft Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications#updating-an-application) for information on how to grant these permissions to your application.
\ No newline at end of file
diff --git a/docs/resources/azure_graph_users.md b/docs/resources/azure_graph_users.md
new file mode 100644
index 000000000..4f1821ca5
--- /dev/null
+++ b/docs/resources/azure_graph_users.md
@@ -0,0 +1,97 @@
+---
+title: About the azure_graph_users Resource
+platform: azure
+---
+
+# azure_graph_users
+Use the `azure_graph_users` InSpec audit resource to test properties of some or all Azure Active Directory users within a Tenant.
+
+## 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 stable version will be used.
+For more information, refer to [`azure_graph_generic_resources`](azure_graph_generic_resources.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](/inspec/glossary/#resource-pack).
+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_graph_users` resource block returns all Azure Active Directory user accounts contained within the configured Tenant and then tests that group of users.
+```ruby
+describe azure_graph_users do
+ #...
+end
+```
+## Parameters
+
+The following parameters can be passed for targeting specific users.
+
+| Name | Description | Example |
+|-------------------|-------------------------------------------------------------|-------------------------------------|
+| filter | A hash containing the filtering options and their values. The `starts_with_` operator can be used for fuzzy string matching. Parameter names are in snakecase. | `{ starts_with_given_name: 'J', starts_with_department: 'Core', country: 'United Kingdom', given_name: John}` |
+| filter_free_text | [OData](https://www.odata.org/getting-started/basic-tutorial/) query string in double quotes, `"`. Property names are in camelcase, refer to [here](https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter) for more information. | `"startswith(displayName,'J') and surname eq 'Doe'"` |
+
+It is advised to use these parameters to narrow down the targeted resources at the server side, Azure Graph API, for a more efficient test.
+
+## Properties
+
+| Property | Description | Filter Criteria* |
+|-----------------------|-------------|-----------------|
+| ids | The unique identifiers of users. | `id` |
+| object_ids | The unique identifiers of users. This is for backward compatibility, use `ids` instead. | `id` |
+| display_names | The display names of users. | `displayName` |
+| given_names | The given names of users. | `givenName` |
+| job_titles | The job titles of users. | `jobTitle` |
+| mails | The email addresses of users. | `mail` |
+| user_types | The user types of users, e.g.; `Member`, `Guest`. | `userType` |
+| user_principal_names | The user principal names of users, e.g.; `jdoe@contoso.com`. | `userPrincipalName` |
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+The following examples show how to use this InSpec audit resource.
+
+### Check Users with Some Filtering Parameters Applied at Server Side (Using `filter`)
+```ruby
+describe azure_graph_users(filter: {given_name: 'John', starts_with_department: 'Customer'}) do
+ it { should exist }
+end
+```
+### Check Users with Some Filtering Parameters Applied at Server Side (Using `filter_free_text`)
+```ruby
+describe azure_graph_users(filter_free_text: "startswith(givenName,'J') and startswith(department,'customer') and country eq 'United States'") do
+ it { should exist }
+end
+```
+### Ensure There are No Guest Accounts Active (Client Side Filtering)
+```ruby
+describe azure_graph_users.guest_accounts do
+ it { should_not exist }
+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_graph_users do
+ it { should exist }
+end
+```
+## Azure Permissions
+
+Graph resources require specific privileges granted to your service principal.
+Please refer to the [Microsoft Documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications#updating-an-application) for information on how to grant these permissions to your application.
diff --git a/docs/resources/azure_key_vault.md b/docs/resources/azure_key_vault.md
new file mode 100644
index 000000000..4939b2402
--- /dev/null
+++ b/docs/resources/azure_key_vault.md
@@ -0,0 +1,93 @@
+---
+title: About the azure_key_vault Resource
+platform: azure
+---
+
+# azure_key_vault
+
+Use the `azure_key_vault` InSpec audit resource to test properties related to a key vault.
+
+## 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](/inspec/glossary/#resource-pack).
+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 `resource_group` and `name` must be given as a parameter.
+```ruby
+describe azure_key_vault(resource_group: 'inspec-resource-group', name: 'vault-101') do
+ it { should exist }
+ its('name') { should cmp 'vault-101' }
+end
+```
+## Parameters
+
+| Name | Description |
+|--------------------------------|----------------------------------------------------------------------------------|
+| resource_group | Azure resource group that the targeted resource resides in. `MyResourceGroup` |
+| name | Name of the Azure resource to test. `MyVault` |
+| vault_name | Name of the Azure resource to test (for backward compatibility). `MyVault` |
+| resource_id | The unique resource ID. `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.KeyVault/vaults/{vaultName}` |
+
+Either one of the parameter sets can be provided for a valid query:
+- `resource_id`
+- `resource_group` and `name`
+- `resource_group` and `vault_name`
+
+## Properties
+
+| Property | Description |
+|---------------------------------------|-------------|
+| diagnostic_settings | The active diagnostic settings list for the key vault. |
+
+For parameters applicable to all resources, such as `type`, `name`, `id`, `location`, `properties`, refer to [`azure_generic_resource`](azure_generic_resource.md#parameters).
+
+Also, refer to [Azure documentation](https://docs.microsoft.com/en-us/rest/api/keyvault/vaults/get#vault) for other properties available.
+Any attribute in the response may be accessed with the key names separated by dots (`.`).
+
+## Examples
+
+### Test Key Vault's SKU Family
+```ruby
+describe azure_key_vault(resource_group: 'MyResourceGroup', name: 'MyVaultName') do
+ its('properties.sku.family') { should eq 'A' }
+end
+```
+### Test If Key Vault is Enabled for Disk Encryption
+```ruby
+describe azure_key_vault(resource_group: 'MyResourceGroup', name: 'MyVaultName') do
+ its('properties.enabledForDiskEncryption') { should be_true }
+end
+```
+## Matchers
+
+This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [Universal Matchers page](/inspec/matchers/).
+
+### exists
+```ruby
+# If a key vault is found it will exist
+describe azure_key_vault(resource_group: 'MyResourceGroup', name: 'MyVaultName') do
+ it { should exist }
+end
+
+# Key vaults that aren't found will not exist
+describe azure_key_vault(resource_group: 'MyResourceGroup', name: 'DoesNotExist') 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_key_vaults.md b/docs/resources/azure_key_vaults.md
new file mode 100644
index 000000000..76bfd6b6e
--- /dev/null
+++ b/docs/resources/azure_key_vaults.md
@@ -0,0 +1,98 @@
+---
+title: About the azure_key_vaults Resource
+platform: azure
+---
+
+# azure_key_vaults
+
+Use the `azure_key_vaults` InSpec audit resource to test properties related to key vaults for a resource group or the entire subscription.
+
+## 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](/inspec/glossary/#resource-pack).
+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_key_vaults` resource block returns all Azure key vaults, either within a Resource Group (if provided), or within an entire Subscription.
+```ruby
+describe azure_key_vaults do
+ #...
+end
+```
+or
+```ruby
+describe azure_key_vaults(resource_group: 'my-rg') do
+ #...
+end
+```
+## Parameters
+
+- `resource_group` (Optional)
+
+## Properties
+
+|Property | Description | Filter Criteria* |
+|---------------|--------------------------------------------------------------------------------------|-----------------|
+| ids | A list of the unique resource ids. | `id` |
+| names | A list of all the key vault names. | `name` |
+| tags | A list of `tag:value` pairs defined on the resources. | `tags` |
+| types | A list of types of all the key vaults. | `type` |
+| locations | A list of locations for all the key vaults. | `location` |
+| properties | A list of properties for all the key vaults. | `properties` |
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+### Loop through Key Vaults by Their Ids
+```ruby
+azure_key_vaults.ids.each do |id|
+ describe azure_key_vault(resource_id: id) do
+ it { should exist }
+ end
+end
+```
+### Test that There are Key Vaults that Includes a Certain String in their Names (Client Side Filtering)
+```ruby
+describe azure_key_vaults.where { name.include?('deployment') } do
+ it { should exist }
+end
+```
+### Test that There are Key Vaults that Includes a Certain String in their Names (Server Side Filtering via Generic Resource - Recommended)
+```ruby
+describe azure_generic_resources(resource_provider: 'Microsoft.KeyVault/vaults', substring_of_name: 'deployment') do
+ it { should exist }
+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
+# Should not exist if no key vaults are in the resource group
+describe azure_key_vaults(resource_group: 'MyResourceGroup') do
+ it { should_not exist }
+end
+
+# Should exist if the filter returns at least one key vault
+describe azure_key_vaults(resource_group: 'MyResourceGroup') 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/azure_mysql_server.md b/docs/resources/azure_mysql_server.md
new file mode 100644
index 000000000..9d4053153
--- /dev/null
+++ b/docs/resources/azure_mysql_server.md
@@ -0,0 +1,101 @@
+---
+title: About the azure_mysql_server Resource
+platform: azure
+---
+
+# azure_mysql_server
+
+Use the `azurerm_mysql_server` InSpec audit resource to test properties and configuration of an Azure MySQL Server.
+
+## 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](/inspec/glossary/#resource-pack).
+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 `resource_group` and `name` must be given as a parameter.
+```ruby
+describe azurerm_mysql_server(resource_group: 'inspec-resource-group-9', name: 'example_server') do
+ it { should exist }
+end
+```
+## Parameters
+
+| Name | Description |
+|--------------------------------|-----------------------------------------------------------------------------------|
+| resource_group | Azure resource group that the targeted resource resides in. `MyResourceGroup` |
+| name | Name of the MySql server to test. `MyServer` |
+| server_name | Name of the MySql server to test. `MyServer`. This is for backward compatibility, use `name` instead. |
+| resource_id | The unique resource ID. `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.DBforMySQL/servers/{serverName}` |
+
+Either one of the parameter sets can be provided for a valid query:
+- `resource_id`
+- `resource_group` and `name`
+- `resource_group` and `server_name`
+
+## Properties
+
+| Property | Description |
+|-------------------|-------------|
+| firewall_rules | A list of all firewall rules in the targeted server. |
+| sku | The SKU (pricing tier) of the server. |
+
+For parameters applicable to all resources, such as `type`, `name`, `id`, `properties`, refer to [`azure_generic_resource`](azure_generic_resource.md#parameters).
+
+Also, refer to [Azure documentation](https://docs.microsoft.com/en-us/rest/api/mysql/servers/get#server) for other properties available.
+Any attribute in the response may be accessed with the key names separated by dots (`.`).
+
+## Examples
+
+### Test If a MySQL Server is Referenced with a Valid Resource Group and Server Name
+```ruby
+describe azurerm_sql_server(resource_group: 'my-rg', name: 'sql-server-1') do
+ it { should exist }
+end
+```
+### Test If a MySQL Server is Referenced with an Invalid Resource Group or Server Name
+```ruby
+describe azurerm_sql_server(resource_group: 'invalid-rg', name: 'i-dont-exist') do
+ it { should_not exist }
+end
+```
+### Test If a MySQL Server Has Firewall Rules Set
+```ruby
+describe azurerm_sql_server(resource_group: 'my-rg', name: 'my-server') do
+ its('firewall_rules') { should_not be_empty }
+end
+```
+### Test a MySQL Server's Fully Qualified Domain Name, Location and Public Network Access Status
+```ruby
+describe azurerm_sql_server(resource_id: '/subscriptions/.../my-server') do
+ its('properties.fullyQualifiedDomainName') { should eq 'my-server.mysql.database.azure.com' }
+ its('properties.publicNetworkAccess') { should cmp 'Enabled' }
+ its('location') { should cmp 'westeurope' }
+end
+```
+## Matchers
+
+This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [Universal Matchers page](/inspec/matchers/).
+
+### exists
+```ruby
+describe azurerm_mysql_server(resource_group: 'my-rg', server_name: 'server-name-1') 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/azure_mysql_servers.md b/docs/resources/azure_mysql_servers.md
new file mode 100644
index 000000000..7f8e4ca8c
--- /dev/null
+++ b/docs/resources/azure_mysql_servers.md
@@ -0,0 +1,98 @@
+---
+title: About the azure_mysql_servers Resource
+platform: azure
+---
+
+# azure_mysql_servers
+
+Use the `azure_mysql_servers` InSpec audit resource to test properties and configuration of multiple Azure MySQL Servers.
+## 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](/inspec/glossary/#resource-pack).
+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_mysql_servers` resource block returns all Azure MySQL Servers, either within a Resource Group (if provided), or within an entire Subscription.
+```ruby
+describe azure_mysql_servers do
+ #...
+end
+```
+or
+```ruby
+describe azure_mysql_servers(resource_group: 'my-rg') do
+ #...
+end
+```
+## Parameters
+
+- `resource_group` (Optional)
+
+## Properties
+
+|Property | Description | Filter Criteria* |
+|---------------|--------------------------------------------------------------------------------------|-----------------|
+| ids | A list of the unique resource ids. | `id` |
+| locations | A list of locations for all the virtual networks. | `location` |
+| names | A list of all the virtual network names. | `name` |
+| tags | A list of `tag:value` pairs defined on the resources. | `tags` |
+| skus | A list of the SKUs (pricing tiers) of the server. | `sku` |
+| properties | A list of properties for all the key vaults. | `properties` |
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+### Check MySQL Servers are present
+```ruby
+describe azure_mysql_servers do
+ it { should exist }
+ its('names') { should include 'my-server-name' }
+end
+```
+### Filters the Results to Include Only Those Servers which Include the Given Name (Client Side Filtering)
+```ruby
+describe azure_mysql_servers.where{ name.include?('production') } do
+ it { should exist }
+end
+```
+## Filters the Results to Include Only Those Servers which Reside in a Given Location (Client Side Filtering)
+```ruby
+describe azure_mysql_servers.where{ location.eql?('westeurope') } do
+ it { should exist }
+end
+```
+## Filters the Results to Include Only Those Servers which Reside in a Given Location and Include the Given Name (Server Side Filtering - Recommended)
+```ruby
+describe azure_generic_resources(resource_provider: 'Microsoft.DBforMySQL/servers', substring_of_name: 'production', location: 'westeurope') do
+ it {should exist}
+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_mysql_servers 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/azure_network_security_group.md b/docs/resources/azure_network_security_group.md
new file mode 100644
index 000000000..b46f444a5
--- /dev/null
+++ b/docs/resources/azure_network_security_group.md
@@ -0,0 +1,146 @@
+---
+title: About the azure_network_security_group Resource
+platform: azure
+---
+
+# azure_network_security_group
+
+Use the `azure_network_security_group` InSpec audit resource to test properties of an Azure Network Security Group.
+
+## 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](/inspec/glossary/#resource-pack).
+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_network_security_group` resource block identifies a Network Security Group by name and Resource Group
+```ruby
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ #...
+end
+```
+
+## Parameters
+
+| Name | Description |
+|--------------------------------|----------------------------------------------------------------------------------|
+| resource_group | Azure resource group that the targeted resource resides in.`MyResourceGroup` |
+| name | Name of the Azure resource to test. `MyNSG` |
+| resource_id | The unique resource ID. `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/networkSecurityGroups/{nsgName}` |
+
+Either one of the parameter sets can be provided for a valid query:
+- `resource_id`
+- `resource_group` and `name`
+
+## Properties
+
+| Property | Description |
+|----------------------------|-------------------------------|
+| security_rules | The set of security rules. |
+| default_security_rules | The set of default security rules.|
+| allow_ssh_from_internet* | A boolean value determined by analysing the security rules and default security rules for unrestricted SSH access. `it { should_not allow_ssh_from_internet }` |
+| allow_rdp_from_internet* | A boolean value determined by analysing the security rules and default security rules for unrestricted RDP access. `it { should_not allow_rdp_from_internet }` |
+| allow_port_from_internet* | A boolean value determined by analysing the security rules and default security rules for unrestricted access to a specified port. `it { should_not allow_port_from_internet('443') }` |
+| allow?** | Indicates if a provided criteria is complaint with the security rules including the default ones. `it { should allow(source_ip_range: '10.0.0.0/24'), direction: 'inbound' }` |
+| allowed?** | Alias for `allow?`. `it { should be_allowed(source_ip_range: '10.0.0.0/24'), direction: 'inbound' }` |
+| allow_in?** | Indicates if a provided criteria is complaint with the **inbound** security rules including the default ones. `it { should_not allow_in(service_tag: 'Internet') }` |
+| allowed_in?** | Alias for `allow_in?`. `it { should_not be_allowed_in(service_tag: 'Internet') }` |
+| allow_out?** | Indicates if a provided criteria is complaint with the **outbound** security rules including the default ones. `it { should_not allow_out(service_tag: 'Internet') }` |
+| allowed_out?** | Alias for `allow_out?`. `it { should_not be_allowed_out(service_tag: 'Internet') }` |
+
+* These properties do not take the priorities of security rules into account.
+For example, if there are two security rules and one of them is allowing SSH from internet while the other one is prohibiting, `allow_ssh_from_internet` will pass without comparing the priority of the conflicting security rules.
+Therefore, it is recommended to use `allow`, `allow_in` or `allow_out` properties with which the priorities are taken into consideration.
+
+** These properties do not compare criteria defined by explicit ip ranges with the security rules defined by [Azure service tags](https://docs.microsoft.com/en-us/azure/virtual-network/service-tags-overview) and vice versa.
+For example, providing that a network security group has a single security rule allowing all traffics from internet by using `Internet` service tag in the source will fail the `allow_in(ip_range: '64.233.160.0')` test due to incompatible source definitions.
+This is because InSpec Azure resource pack has no control over which ip ranges are defined in Azure service tags.
+Therefore, tests using these methods should be written explicitly for service tags and ip ranges.
+For more information about network security groups and security rules refer to [here](https://docs.microsoft.com/en-us/azure/virtual-network/security-overview).
+`*ip_range` used in these methods support IPv4 and IPv6. The ip range criteriaom should be written in CIDR notation.
+
+For parameters applicable to all resources, such as `type`, `name`, `id`, `location`, `properties`, refer to [`azure_generic_resource`](azure_generic_resource.md#parameters).
+
+Also, refer to [Azure documentation](https://docs.microsoft.com/en-us/rest/api/virtualnetwork/networksecuritygroups/get#networksecuritygroup) for other properties available.
+Any property in the response may be accessed with the key names separated by dots (`.`).
+
+## Examples
+
+### Test that a Resource Group Has the Specified Network Security Group
+```ruby
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ it { should exist }
+end
+```
+### Test that a Network Security Group Allows SSH from the Internet
+```ruby
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ it { should allow_ssh_from_internet }
+end
+```
+### Test that a Network Security Group Allows Inbound Traffics from a Certain Ip Range in Any Port and Any Protocol
+```ruby
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ it { should allow(source_ip_range: '10.0.0.0/24', direction: 'inbound') }
+ it { should allow_in(ip_range: '10.0.0.0/24') } # same test with the specific inbound rule check
+end
+```
+### Test that a Network Security Group Allows Inbound Traffics from Internet Service Tag in Port `80` and `TCP` Protocol
+```ruby
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ it { should allow(source_service_tag: 'Internet', destination_port: '22', protocol: 'TCP', direction: 'inbound') }
+ it { should allow_in(service_tag: 'Internet', port: '22', protocol: 'TCP') } # same test with the specific inbound rule check
+end
+```
+### Test that a Network Security Group Allows Inbound Traffics from Virtual Network Service Tag in a Range of Ports and Any Protocol
+```ruby
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ it { should allow(source_service_tag: 'VirtualNetwork', destination_port: %w{22 8080 56-78}, direction: 'inbound') }
+ it { should allow_in(service_tag: 'VirtualNetwork', port: %w{22 8080 56-78}) } # same test with the specific inbound rule check
+end
+```
+### Test that a Network Security Group Allows Outbound Traffics to a Certain Ip Range in any Port and Any Protocol
+```ruby
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ it { should allow(destination_ip_range: '10.0.0.0/24', direction: 'outbound') }
+ it { should allow_out(ip_range: '10.0.0.0/24') } # same test with the specific outbound rule check
+end
+```
+Please note that `allow` requires `direction` parameter is set to either `inbound` or `outbound` and prefix the `ip_range`, `service_tag` and `port` with either `source_` or `destination_` identifiers.
+
+## 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 resource returns a result. Use `should_not` if you expect zero matches.
+```ruby
+# If we expect 'GroupName' to always e``xrubyst
+describe azure_network_security_group(resource_group: 'example', name: 'GroupName') do
+ it { should exist }
+end
+
+# If we expect 'EmptyGroupName' to never e``xrubyst
+describe azure_network_security_group(resource_group: 'example', name: 'EmptyGroupName') 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_network_security_groups.md b/docs/resources/azure_network_security_groups.md
new file mode 100644
index 000000000..221ab7c20
--- /dev/null
+++ b/docs/resources/azure_network_security_groups.md
@@ -0,0 +1,99 @@
+---
+title: About the azure_network_security_groups Resource
+platform: azure
+---
+
+# azure_network_security_groups
+
+Use the `azure_network_security_groups` InSpec audit resource to enumerate Network Security Groups.
+
+## 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](/inspec/glossary/#resource-pack).
+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_network_security_groups` resource block returns all Azure network security groups, either within a Resource Group (if provided), or within an entire Subscription.
+```ruby
+describe azure_network_security_groups do
+ #...
+end
+```
+or
+```ruby
+describe azure_network_security_groups(resource_group: 'my-rg') do
+ #...
+end
+```
+## Parameters
+
+- `resource_group` (Optional)
+
+## Properties
+
+|Property | Description | Filter Criteria* |
+|---------------|--------------------------------------------------------------------------------------|-----------------|
+| ids | A list of the unique resource ids. | `id` |
+| locations | A list of locations for all the network security groups. | `location` |
+| names | A list of all the network security group names. | `name` |
+| tags | A list of `tag:value` pairs defined on the resources. | `tags` |
+| etags | A list of etags defined on the resources. | `etag` |
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+### Test that an Example Resource Group Has the Named Network Security Group
+
+```ruby
+describe azure_network_security_groups(resource_group: 'ExampleGroup') do
+ its('names') { should include('ExampleNetworkSecurityGroup') }
+end
+```
+
+### Filters the Network Security Groups at Azure API to Only Those that Match the Given Name via Generic Resource (Recommended)
+```ruby
+# Fuzzy string matching
+describe azure_generic_resources(resource_provider: 'Microsoft.Network/networkSecurityGroups', substring_of_name: 'project_A') do
+ it { should exist }
+end
+
+# Exact name matching
+describe azure_generic_resources(resource_provider: 'Microsoft.Network/networkSecurityGroups', name: 'project_A') do
+ it { should exist }
+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 resource returns a result. Use `should_not` if you expect zero matches.
+```ruby
+# If we expect 'ExampleGroup' Resource Group to have Network Security Groups
+describe azure_network_security_groups(resource_group: 'ExampleGroup') do
+ it { should exist }
+end
+
+# If we expect 'EmptyExampleGroup' Resource Group to not have Network Security Groups
+describe azure_network_security_groups(resource_group: 'EmptyExampleGroup') 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_subnet.md b/docs/resources/azure_subnet.md
new file mode 100644
index 000000000..3d011fd21
--- /dev/null
+++ b/docs/resources/azure_subnet.md
@@ -0,0 +1,91 @@
+---
+title: About the azure_subnet Resource
+platform: azure
+---
+
+# azure_subnet
+
+Use the `azure_subnet` InSpec audit resource to test properties related to a subnet for a given virtual network.
+
+## 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](/inspec/glossary/#resource-pack).
+For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal).
+
+## Syntax
+```ruby
+describe azure_subnet(resource_group: 'MyResourceGroup', vnet: 'MyVnetName', name: 'MySubnetName') do
+ #...
+end
+```
+## Parameters
+
+| Name | Description |
+|--------------------------------|----------------------------------------------------------------------------------|
+| resource_group | Azure resource group that the targeted resource resides in. `MyResourceGroup` |
+| vnet | Name of the Azure virtual network that the subnet is created in. `MyVNetName` |
+| name | Name of the Azure subnet to test. `MySubnetName` |
+| resource_id | The unique resource ID. `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnName}/subnets/{subnetName}` |
+
+Either one of the parameter sets can be provided for a valid query:
+- `resource_id`
+- `resource_group`, `vnet` and `name`
+
+## Parameters
+
+| Property | Description |
+|----------|-------------|
+| address_prefix | The address prefix for the subnet. `its('address_prefix') { should eq "x.x.x.x/x" }` |
+| nsg | The network security group attached to the subnet. `its('nsg') { should eq 'MyNetworkSecurityGroupName' }` |
+
+For parameters applicable to all resources, such as `type`, `name`, `id`, `location`, `properties`, refer to [`azure_generic_resource`](azure_generic_resource.md#parameters).
+
+Also, refer to [Azure documentation](https://docs.microsoft.com/en-us/rest/api/virtualnetwork/subnets/get#subnet) for other properties available.
+Any property in the response may be accessed with the key names separated by dots (`.`).
+
+## Examples
+
+### Ensure that the Subnets Address Prefix is Configured As Expected
+```ruby
+describe azure_subnet(resource_group: 'MyResourceGroup', vnet: 'MyVnetName', name: 'MySubnetName') do
+ its('address_prefix') { should eq '192.168.0.0/24' }
+end
+```
+### Ensure that the Subnet is Attached to the Right Network Security Group
+```ruby
+describe azure_subnet(resource_group: 'MyResourceGroup', vnet: 'MyVnetName', name: 'MySubnetName') do
+ its('nsg') { should eq 'NetworkSecurityGroupName'}
+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 a subnet is found it will exist
+describe azure_subnet(resource_group: 'MyResourceGroup', vnet: 'MyVnetName', name: 'MySubnetName') do
+ it { should exist }
+end
+
+# subnets that aren't found will not exist
+describe azure_subnet(resource_group: 'MyResourceGroup', vnet: 'MyVnetName', name: 'DoesNotExist') 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_subnets.md b/docs/resources/azure_subnets.md
new file mode 100644
index 000000000..3740d176a
--- /dev/null
+++ b/docs/resources/azure_subnets.md
@@ -0,0 +1,80 @@
+---
+title: About the azure_subnets Resource
+platform: azure
+---
+
+# azure_subnets
+
+Use the `azure_subnets` InSpec audit resource to test properties related to subnets of a virtual network.
+
+## 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](/inspec/glossary/#resource-pack).
+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 `resource_group` and `vnet` must be given as a parameter.
+```ruby
+describe azure_subnets(resource_group: 'MyResourceGroup', vnet: 'MyVnetName') do
+ #...
+end
+```
+## Parameters
+
+|Name | Description |
+|-------------------|--------------------|
+| resource_group | Azure resource group that the targeted resource resides in. `MyResourceGroup` |
+| vnet | The virtual network that the subnet that you wish to test is a part of. |
+
+## Properties
+
+|Property | Description | Filter Criteria* |
+|---------------|--------------------------------------------------------------------------------------|-----------------|
+| ids | A list of the unique resource ids. | `id` |
+| names | A list of all the virtual network names. | `name` |
+| etags | A list of etags defined on the resources. | `etag` |
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+### Exists if Any Subnets Exist for a Given Virtual Network in the Resource Group
+```ruby
+describe azure_subnets(resource_group: 'MyResourceGroup', vnet: 'MyVnetName') do
+ it { should exist }
+end
+```
+### Filters the Results to Only Those that Match the Given Name
+```ruby
+describe azure_subnets(resource_group: 'MyResourceGroup', vnet: 'MyVnetName')
+ .where(name: 'MySubnet') do
+ it { should exist }
+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
+# Should not exist if no subnets are in the virtual network
+describe azure_subnets(resource_group: 'MyResourceGroup', vnet: 'MyVnetName') 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_virtual_machine.md b/docs/resources/azure_virtual_machine.md
new file mode 100644
index 000000000..36e8bade2
--- /dev/null
+++ b/docs/resources/azure_virtual_machine.md
@@ -0,0 +1,122 @@
+---
+title: About the azure_virtual_machine Resource
+platform: azure
+---
+
+# azure_virtual_machine
+
+Use the `azure_virtual_machine` InSpec audit resource to test properties related to a virtual machine.
+
+## 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](/inspec/glossary/#resource-pack).
+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 `resource_group` and virtual machine `name` must be given as parameters.
+```ruby
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
+ #...
+end
+```
+## Parameters
+
+| Name | Description |
+|--------------------------------|----------------------------------------------------------------------------------|
+| resource_group | Azure resource group that the targeted resource resides in. `MyResourceGroup` |
+| name | Name of the Azure resource to test. `MyVM` |
+| resource_id | The unique resource ID. `/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` and `name`
+
+## Properties
+
+| Property | Description |
+|---------------------------------------|-------------|
+| admin_username | The admin user name. |
+| resources | The virtual machine child extension resources. |
+| zones | The virtual machine's availability zones. `its('zones') should include('zone1', 'zone2')` |
+| installed_extensions_types | List of all installed extensions' types for the virtual machine. `its('installed_extensions_types') { should include('ExtensionType') }` |
+| installed_extensions_names | List of all installed extensions' names for the virtual machine. `its('installed_extensions_names') { should include('ExtensionName') }` |
+| has_monitoring_agent_installed? | Indicates whether a monitoring agent is installed. |
+| has_endpoint_protection_installed? | Indicates whether a list of endpoint protection extension types are installed. `it { should have_endpoint_protection_installed(%w{ep_type_1 ep_type_2}) }` |
+| has_only_approved_extensions? | Indicates whether only provided extension types are installed. `it { should have_only_approved_extensions(%w{extension_type_1 extension_type_2}) }` |
+| os_disk_name | The virtual machine's operating system disk name. `its('os_disk_name') { should cmp 'OsDiskName' }` |
+| data_disk_names | The virtual machine's data disk names. `its('data_disk_names') { should include('DataDisk1') }` |
+
+For parameters applicable to all resources, such as `type`, `name`, `id`, `location`, `properties`, refer to [`azure_generic_resource`](azure_generic_resource.md#parameters).
+
+Also, refer to [Azure documentation](https://docs.microsoft.com/en-us/rest/api/compute/virtualmachines/get#virtualmachine) for other properties available.
+Any attribute in the response may be accessed with the key names separated by dots (`.`).
+
+
+## Examples
+
+### Ensure that the Virtual Machine has the Expected Data Disks
+```ruby
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
+ its('data_disk_names') { should include('DataDisk1') }
+end
+```
+### Ensure that the Virtual Machine has the Expected Monitoring Agent Installed
+```ruby
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
+ it { should have_monitoring_agent_installed }
+end
+```
+## Matchers
+
+This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [Universal Matchers page](/inspec/matchers/).
+
+### exists
+```ruby
+# If a virtual machine is found it will exist
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
+ it { should exist }
+end
+
+# virtual machines that aren't found will not exist
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'DoesNotExist') do
+ it { should_not exist }
+end
+```
+### have_only_approved_extensions
+```ruby
+# Check if a virtual machine has only approved extensions. If an extension
+# is used that's not in the list then the check will fail.
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
+ it { should have_only_approved_extensions(['ApprovedExtension', 'OtherApprovedExtensions']) }
+end
+```
+### have_monitoring_agent_installed
+```ruby
+# Will be true if the MicrosoftMonitoringAgent is installed (Windows only)
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
+ it { should have_monitoring_agent_installed }
+end
+```
+### have_endpoint_protection_installed
+```ruby
+# Will be true if any of the given extensions are installed.
+describe azure_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
+ it { should have_endpoint_protection_installed(['Extension1', 'Extension2']) }
+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_virtual_machines.md b/docs/resources/azure_virtual_machines.md
new file mode 100644
index 000000000..156ac9ad4
--- /dev/null
+++ b/docs/resources/azure_virtual_machines.md
@@ -0,0 +1,116 @@
+---
+title: About the azure_virtual_machines Resource
+platform: azure
+---
+
+# azure_virtual_machines
+
+Use the `azure_virtual_machines` InSpec audit resource to test properties related to virtual machines for a resource group or the entire subscription.
+
+## 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](/inspec/glossary/#resource-pack).
+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_virtual_machines` resource block returns all Azure virtual machines, either within a Resource Group (if provided), or within an entire Subscription.
+```ruby
+describe azure_virtual_machines do
+ #...
+end
+```
+or
+```ruby
+describe azure_virtual_machines(resource_group: 'my-rg') do
+ #...
+end
+```
+## Parameters
+
+- `resource_group` (Optional)
+
+## Properties
+
+|Property | Description | Filter Criteria* |
+|---------------|--------------------------------------------------------------------------------------|-----------------|
+| ids | A list of the unique resource ids. | `id` |
+| os_disks | A list of OS disk names for all the virtual machines. | `os_disk` |
+| data_disks | A list of data disks for all the virtual machines. | `data_disks` |
+| vm_names | A list of all the virtual machine names. | `name` |
+| platforms | A list of virtual machine operation system platforms. Supported values are `windows` and `linux`.| `platform`|
+| tags | A list of `tag:value` pairs defined on the resources. | `tags` |
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+### Test If Any Virtual Machines Exist in the Resource Group
+```ruby
+describe azure_virtual_machines(resource_group: 'MyResourceGroup') do
+ it { should exist }
+end
+```
+### Filters Based on Platform
+```ruby
+describe azure_virtual_machines(resource_group: 'MyResourceGroup').where(platform: 'windows') do
+ it { should exist }
+end
+```
+### Loop through Virtual Machines by Their Ids
+```ruby
+azure_virtual_machines.ids.each do |id|
+ describe azure_virtual_machine(resource_id: id) do
+ it { should exist }
+ end
+end
+```
+### Test If There are Windows Virtual Machines
+```ruby
+describe azure_virtual_machines(resource_group: 'MyResourceGroup').where(platform: 'windows') do
+ it { should exist }
+end
+```
+### Test that There are Virtual Machines that Includes a Certain String in their Names (Client Side Filtering)
+```ruby
+describe azure_virtual_machines(resource_group: 'MyResourceGroup').where { name.include?('WindowsVm') } do
+ it { should exist }
+end
+```
+### Test that There are Virtual Machine that Includes a Certain String in their Names (Server Side Filtering via Generic Resource - Recommended)
+```ruby
+describe azure_generic_resources(resource_group: 'MyResourceGroup', resource_provider: 'Microsoft.Compute/virtualMachine', substring_of_name: 'WindowsVm') do
+ it { should exist }
+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
+# Should not exist if no virtual machines are in the resource group
+describe azure_virtual_machines(resource_group: 'MyResourceGroup') do
+ it { should_not exist }
+end
+
+# Should exist if the filter returns a single virtual machine
+describe azure_virtual_machines(resource_group: 'MyResourceGroup').where(platform: 'windows') 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/azure_virtual_network.md b/docs/resources/azure_virtual_network.md
new file mode 100644
index 000000000..ed2eb0629
--- /dev/null
+++ b/docs/resources/azure_virtual_network.md
@@ -0,0 +1,102 @@
+---
+title: About the azure_virtual_network Resource
+platform: azure
+---
+
+# azure_virtual_network
+
+Use the `azure_virtual_network` InSpec audit resource to test properties related to a virtual network.
+
+## 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](/inspec/glossary/#resource-pack).
+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 `resource_group` and virtual network `name` must be given as parameters.
+```ruby
+describe azure_virtual_network(resource_group: 'MyResourceGroup', name: 'MyVnetName') do
+ #...
+end
+```
+## Parameters
+
+| Name | Description |
+|--------------------------------|----------------------------------------------------------------------------------|
+| resource_group | Azure resource group that the targeted resource resides in. `MyResourceGroup` |
+| name | Name of the virtual network to test. `MyVNetwork` |
+| resource_id | The unique resource ID. `/subscriptions/{subscriptionId}/resourceGroups/{resourceGroup}/providers/Microsoft.Network/virtualNetworks/{vnName}` |
+
+Either one of the parameter sets can be provided for a valid query:
+- `resource_id`
+- `resource_group` and `name`
+
+## Properties
+
+| Property | Description |
+|----------|-------------|
+| subnets | The list of subnet names that are attached to this virtual network. `its('subnets') { should eq ["MySubnetName"] }` |
+| address_space | The list of address spaces used by the virtual network. `its('address_space') { should eq ["x.x.x.x/x"] }` |
+| dns_servers | The list of DNS servers configured for the virtual network. The virtual network returns these IP addresses when virtual machines makes a DHCP request. `its('dns_servers') { should eq ["x.x.x.x", "x.x.x.x"] }` |
+| vnet_peerings | A mapping of names and the virtual network ids of the virtual network peerings. `its('vnet_peerings') { should eq "MyVnetPeeringConnection"=>"PeeringConnectionID"}` |
+| enable_ddos\_protection | Boolean value showing if Azure DDoS standard protection is enabled on the virtual network. `its('enable_ddos_protection') { should eq true }` |
+| enable_vm_protection | Boolean value showing if the virtual network has VM protection enabled. `its('enable_vm_protection') { should eq false }` |
+
+For parameters applicable to all resources, such as `type`, `name`, `id`, `location`, `properties`, refer to [`azure_generic_resource`](azure_generic_resource.md#parameters).
+
+Also, refer to [Azure documentation](https://docs.microsoft.com/en-us/rest/api/virtualnetwork/virtualnetworks/get#virtualnetwork) for other properties available.
+Any property in the response may be accessed with the key names separated by dots (`.`).
+
+## Examples
+
+### Ensure that the Virtual Network Exists in the East US Region
+```ruby
+describe azure_virtual_network(resource_group: 'resource_group', name: 'MyVnetName') do
+ it { should exist }
+ its('location') { should eq 'eastus' }
+end
+```
+### Ensure that the Virtual Network's DNS Servers are Configured as Expected
+```ruby
+describe azure_virtual_network(resource_group: 'resource_group', name: 'MyVnetName') do
+ its('dns_servers') { should eq ["192.168.0.6"] }
+end
+```
+### Ensure that the Virtual Network's Address Space is Configured as Expected
+```ruby
+describe azure_virtual_network(resource_group: 'resource_group', name: 'MyVnetName') do
+ its('address_space') { should eq ["192.168.0.0/24"] }
+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 a virtual network is found it will exist
+describe azure_virtual_network(resource_group: 'MyResourceGroup', name: 'MyVnetName') do
+ it { should exist }
+end
+
+# virtual networks that aren't found will not exist
+describe azure_virtual_network(resource_group: 'MyResourceGroup', name: 'DoesNotExist') 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_virtual_networks.md b/docs/resources/azure_virtual_networks.md
new file mode 100644
index 000000000..d12bae35d
--- /dev/null
+++ b/docs/resources/azure_virtual_networks.md
@@ -0,0 +1,108 @@
+---
+title: About the azure_virtual_networks Resource
+platform: azure
+---
+
+# azure_virtual_networks
+
+Use the `azure_virtual_networks` InSpec audit resource to test properties related to virtual networks within your subscription.
+
+## 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](/inspec/glossary/#resource-pack).
+For an example `inspec.yml` file and how to set up your Azure credentials, refer to resource pack [README](../../README.md#Service-Principal).
+
+### Version
+
+This resource first became available in X.Y.Z of the InSpec Azure resource pack.
+
+## Syntax
+
+An `azure_virtual_networks` resource block returns all Azure virtual networks, either within a Resource Group (if provided), or within an entire Subscription.
+```ruby
+describe azure_virtual_networks do
+ #...
+end
+```
+or
+```ruby
+describe azure_virtual_networks(resource_group: 'my-rg') do
+ #...
+end
+```
+## Parameters
+
+- `resource_group` (Optional)
+
+## Properties
+
+|Property | Description | Filter Criteria* |
+|---------------|--------------------------------------------------------------------------------------|-----------------|
+| ids | A list of the unique resource ids. | `id` |
+| locations | A list of locations for all the virtual networks. | `location` |
+| names | A list of all the virtual network names. | `name` |
+| tags | A list of `tag:value` pairs defined on the resources. | `tags` |
+| etags | A list of etags defined on the resources. | `etag` |
+
+* For information on how to use filter criteria on plural resources refer to [FilterTable usage](https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md#a-where-method-you-can-call-with-hash-params-with-loose-matching).
+
+## Examples
+
+### Exists If Any Virtual Networks Exist in the Resource Group
+```ruby
+describe azure_virtual_networks(resource_group: 'MyResourceGroup') do
+ it { should exist }
+end
+```
+### Filters the Results to Only Those that Match the Given Name (Client Side)
+```ruby
+# Insist that MyVnetName exists
+describe azure_virtual_networks(resource_group: 'MyResourceGroup').where(name: 'MyVnetName') do
+ it { should exist }
+end
+```
+```ruby
+# Insist that you have at least one virtual network that starts with 'prefix'
+describe azure_virtual_networks(resource_group: 'MyResourceGroup').where { name.include?('project_A') } do
+ it { should exist }
+end
+```
+### Filters the Networks at Azure API to Only Those that Match the Given Name via Generic Resource (Recommended)
+```ruby
+# Fuzzy string matching
+describe azure_generic_resources(resource_group: 'MyResourceGroup', resource_provider: 'Microsoft.Network/virtualNetworks', substring_of_name: 'project_A') do
+ it { should exist }
+end
+```
+```ruby
+# Exact name matching
+describe azure_generic_resources(resource_group: 'MyResourceGroup', resource_provider: 'Microsoft.Network/virtualNetworks', name: 'MyVnetName') do
+ it { should exist }
+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
+# Should not exist if no virtual networks are in the resource group
+describe azure_virtual_networks(resource_group: 'MyResourceGroup') 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/azurerm_ad_user.md b/docs/resources/azurerm_ad_user.md
index e26cf9561..7ed6d0a0e 100644
--- a/docs/resources/azurerm_ad_user.md
+++ b/docs/resources/azurerm_ad_user.md
@@ -3,6 +3,8 @@ title: About the azurerm_ad_user Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_graph_user`](azure_graph_user.md) InSpec audit resource.
+
# azurerm\_ad\_user
Use the `azurerm_ad_user` InSpec audit resource to test properties of
diff --git a/docs/resources/azurerm_ad_users.md b/docs/resources/azurerm_ad_users.md
index 382c08750..dd2134b49 100644
--- a/docs/resources/azurerm_ad_users.md
+++ b/docs/resources/azurerm_ad_users.md
@@ -3,6 +3,8 @@ title: About the azurerm_ad_users Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_graph_users`](azure_graph_users.md) InSpec audit resource.
+
# azurerm\_ad\_users
Use the `azurerm_ad_users` InSpec audit resource to test properties of
diff --git a/docs/resources/azurerm_key_vault.md b/docs/resources/azurerm_key_vault.md
index 84e2f72a8..5656e11cf 100644
--- a/docs/resources/azurerm_key_vault.md
+++ b/docs/resources/azurerm_key_vault.md
@@ -3,6 +3,8 @@ title: About the azurerm_key_vault Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_key_vault`](azure_key_vault.md) InSpec audit resource.
+
# azurerm\_key\_vault
Use the `azurerm_key_vault` InSpec audit resource to test properties and configuration of
diff --git a/docs/resources/azurerm_key_vaults.md b/docs/resources/azurerm_key_vaults.md
index 28e093065..7911d0e44 100644
--- a/docs/resources/azurerm_key_vaults.md
+++ b/docs/resources/azurerm_key_vaults.md
@@ -3,6 +3,8 @@ title: About the `azurerm_key_vaults` Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_key_vaults`](azure_key_vaults.md) InSpec audit resource.
+
# azurerm\_key\_vaults
Use the `azurerm_key_vaults` InSpec audit resource to test properties and configuration of Azure Key Vaults.
diff --git a/docs/resources/azurerm_mysql_server.md b/docs/resources/azurerm_mysql_server.md
index 987eac1a3..10eb9b367 100644
--- a/docs/resources/azurerm_mysql_server.md
+++ b/docs/resources/azurerm_mysql_server.md
@@ -3,6 +3,7 @@ title: About the azurerm_mysql_server Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_mysql_server`](azure_mysql_server.md) InSpec audit resource.
# azurerm\_mysql\_server
diff --git a/docs/resources/azurerm_mysql_servers.md b/docs/resources/azurerm_mysql_servers.md
index bcb2caf06..9b4551f64 100644
--- a/docs/resources/azurerm_mysql_servers.md
+++ b/docs/resources/azurerm_mysql_servers.md
@@ -3,6 +3,8 @@ title: About the azurerm_mysql_servers Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_mysql_servers`](azure_mysql_servers.md) InSpec audit resource.
+
# azurerm\_mysql\_servers
Use the `azurerm_mysql_servers` InSpec audit resource to test properties and configuration of multiple Azure MySQL Servers.
diff --git a/docs/resources/azurerm_network_security_group.md b/docs/resources/azurerm_network_security_group.md
index 36be74391..74ccb262b 100644
--- a/docs/resources/azurerm_network_security_group.md
+++ b/docs/resources/azurerm_network_security_group.md
@@ -3,6 +3,8 @@ title: About the azurerm_network_security_group Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_network_security_group`](azure_network_security_group.md) InSpec audit resource.
+
# azurerm\_network\_security\_group
Use the `azurerm_network_security_group` InSpec audit resource to test properties of an
diff --git a/docs/resources/azurerm_network_security_groups.md b/docs/resources/azurerm_network_security_groups.md
index 4c2e4e2eb..a01136ea1 100644
--- a/docs/resources/azurerm_network_security_groups.md
+++ b/docs/resources/azurerm_network_security_groups.md
@@ -3,6 +3,8 @@ title: About the azurerm_network_security_groups Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_network_security_groups`](azure_network_security_groups.md) InSpec audit resource.
+
# azurerm\_network\_security\_groups
Use the `azurerm_network_security_groups` InSpec audit resource to enumerate Network
diff --git a/docs/resources/azurerm_resource_group.md b/docs/resources/azurerm_resource_groups.md
similarity index 100%
rename from docs/resources/azurerm_resource_group.md
rename to docs/resources/azurerm_resource_groups.md
diff --git a/docs/resources/azurerm_subnet.md b/docs/resources/azurerm_subnet.md
index 9bfd124b0..949ddd1e4 100644
--- a/docs/resources/azurerm_subnet.md
+++ b/docs/resources/azurerm_subnet.md
@@ -3,6 +3,8 @@ title: About the azurerm_subnet Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_subnet`](azure_subnet.md) InSpec audit resource.
+
# azurerm\_subnet
Use the `azurerm_subnet` InSpec audit resource to test properties related to a
diff --git a/docs/resources/azurerm_subnets.md b/docs/resources/azurerm_subnets.md
index 070585eff..02d234534 100644
--- a/docs/resources/azurerm_subnets.md
+++ b/docs/resources/azurerm_subnets.md
@@ -3,6 +3,8 @@ title: About the azurerm_subnets Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_subnets`](azure_subnets.md) InSpec audit resource.
+
# azurerm\_subnets
Use the `azurerm\_subnets` InSpec audit resource to test properties related to
diff --git a/docs/resources/azurerm_virtual_machine.md b/docs/resources/azurerm_virtual_machine.md
index 935c8c37e..29425dc0a 100644
--- a/docs/resources/azurerm_virtual_machine.md
+++ b/docs/resources/azurerm_virtual_machine.md
@@ -3,6 +3,8 @@ title: About the azurerm_virtual_machine Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_virtual_machine`](azure_virtual_machine.md) InSpec audit resource.
+
# azurerm\_virtual\_machine
Use the `azurerm_virtual_machine` InSpec audit resource to test properties related to a
@@ -55,8 +57,7 @@ The `resource_group` and virtual machine `name` must be given as
### Ensure that the virtual machine has the expected data disks
describe azurerm_virtual_machine(resource_group: 'MyResourceGroup', name: 'MyVmName') do
- its('data_disks') { should include 'DataDisk1' }
- its('data_disks') { should include 'DataDisk2' }
+ its('data_disk_names') { should include 'DataDisk1' }
end
### Ensure that the virtual machine has the expected monitoring agent installed
diff --git a/docs/resources/azurerm_virtual_machines.md b/docs/resources/azurerm_virtual_machines.md
index 3189fcd17..25c48c23e 100644
--- a/docs/resources/azurerm_virtual_machines.md
+++ b/docs/resources/azurerm_virtual_machines.md
@@ -3,6 +3,8 @@ title: About the azurerm_virtual_machines Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_virtual_machines`](azure_virtual_machines.md) InSpec audit resource.
+
# azurerm\_virtual\_machines
Use the `azurerm_virtual_machines` InSpec audit resource to test properties related to
diff --git a/docs/resources/azurerm_virtual_network.md b/docs/resources/azurerm_virtual_network.md
index 0ef0ff0b8..4102e295c 100644
--- a/docs/resources/azurerm_virtual_network.md
+++ b/docs/resources/azurerm_virtual_network.md
@@ -3,6 +3,8 @@ title: About the azurerm_virtual_network Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_virtual_network`](azure_virtual_network.md) InSpec audit resource.
+
# azurerm\_virtual\_network
Use the `azurerm_virtual_network` InSpec audit resource to test properties related to a
diff --git a/docs/resources/azurerm_virtual_networks.md b/docs/resources/azurerm_virtual_networks.md
index 3ded822d2..9c55c97de 100644
--- a/docs/resources/azurerm_virtual_networks.md
+++ b/docs/resources/azurerm_virtual_networks.md
@@ -3,6 +3,8 @@ title: About the azurerm_virtual_networks Resource
platform: azure
---
+> WARNING This resource will be deprecated in InSpec Azure Resource Pack version **2**. Please start using fully backward compatible [`azure_virtual_networks`](azure_virtual_networks.md) InSpec audit resource.
+
# azurerm\_virtual\_networks
Use the `azurerm_virtual_networks` InSpec audit resource to test properties related to
diff --git a/libraries/azure_backend.rb b/libraries/azure_backend.rb
new file mode 100644
index 000000000..b3ed4e5cb
--- /dev/null
+++ b/libraries/azure_backend.rb
@@ -0,0 +1,713 @@
+require 'backend/azure_require'
+
+ENV_HASH = ENV.map { |k, v| [k.downcase, v] }.to_h
+
+# Base class for Azure resources.
+#
+# Provides:
+# - Connection to Azure Rest API.
+# - Parameter validation.
+# - Short description of resources. This includes resource_id.
+# - Detailed (long) description of resources upon providing a resource_id.
+# - Latest or default api version for a resource provider endpoint.
+# - Rescuing invalid api version error by using the suggested api version in the error message.
+# - Creating resource methods dynamically.
+#
+class AzureResourceBase < Inspec.resource(1)
+ def initialize(opts = {})
+ raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash)
+ @opts = opts
+
+ # Populate client_args to specify AzureConnection
+ #
+ # The valid client args (all of them are optional):
+ # - endpoint: [String] azure_cloud (default), azure_china_cloud, azure_us_government, azure_german_cloud
+ # - azure_retry_limit: [Integer] Maximum number of retries (default - 2)
+ # - azure_retry_backoff: [Integer] Pause in seconds between retries (default - 0)
+ # - azure_retry_backoff_factor: [Integer] The amount to multiply each successive retry's interval amount
+ # by in order to provide back-off (default - 1)
+ @client_args = {}
+ # If not provided, the endpoint will be the Global Cloud portal.
+ # https://azure.microsoft.com/en-gb/global-infrastructure/
+ @client_args[:endpoint] = @opts[:endpoint] || ENV_HASH['endpoint'] || 'azure_cloud'
+ unless AzureEnvironments::ENDPOINTS.key?(@client_args[:endpoint])
+ raise ArgumentError, "Invalid endpoint: `#{@client_args[:endpoint]}`."\
+ " Expected one of the following options: #{AzureEnvironments::ENDPOINTS.keys}."
+ end
+ # Replace endpoint value with the content of AzureEnvironment instance.
+ # @type [Hash]
+ endpoint = AzureEnvironments.get_endpoint(@client_args[:endpoint])
+ @client_args[:endpoint] = endpoint
+ # Set HTTP client retry parameters, defining the timeout exception behavior, if provided.
+ @client_args[:azure_retry_limit] = @opts[:azure_retry_limit] || ENV_HASH['azure_retry_limit']
+ @client_args[:azure_retry_backoff] = @opts[:azure_retry_backoff] || ENV_HASH['azure_retry_backoff']
+ @client_args[:azure_retry_backoff_factor] = @opts[:azure_retry_backoff_factor] || ENV_HASH['azure_retry_backoff_factor']
+
+ # Fail resource if the http client is not properly set up.
+ begin
+ @azure = AzureConnection.new(@client_args)
+ rescue HTTPClientError::MissingCredentials => e
+ message = "HTTP Client Error.\n#{e.message}"
+ resource_fail(message)
+ raise HTTPClientError, message
+ rescue StandardError => e
+ message = "Resource is failed due to #{e}"
+ resource_fail(message)
+ raise StandardError, message
+ end
+ end
+
+ private
+
+ # A Graph API HTTP request is in the form of:
+ # {HTTP method} https://graph.microsoft.com/{version}/{resource}?{query-parameters}
+ # For reading resource data, HTTP GET method is required.
+ # https://docs.microsoft.com/en-us/graph/use-the-api#http-methods
+ #
+ # OData system query options can be used to select or filter resources from the endpoint.
+ # https://docs.microsoft.com/en-us/graph/use-the-api#query-parameters
+ # api_version will be defaulted to the endpoint settings if not provided.
+ # AzureEnvironments::ENDPOINTS
+ #
+ # @example
+ # - Query a single user
+ # GET https://graph.microsoft.com/v1.0/users/{id | userPrincipalName}
+ # id is the object id: 875603e5-1f77-4aab-367c-7a66be11a774
+ # userPrincipalName: jdoe@mycompany.com
+ # - Query multiple users
+ # GET https://graph.microsoft.com/v1.0/users/
+ # result can be tailored by passing parameters as `?$select=objectId,displayName,givenName`
+ #
+ def resource_from_graph_api(opts)
+ Helpers.validate_parameters(resource_name: @__resource_name__, allow: %i(api_version query_parameters),
+ required: %i(resource), opts: opts)
+ api_version = opts[:api_version] || @azure.graph_api_endpoint_api_version
+ if api_version.size > 10 || api_version.include?('/')
+ raise ArgumentError, 'api version can not be longer than 10 characters and contain `/`.'
+ end
+ resource_trimmed = opts[:resource].delete_suffix('/').delete_prefix('/')
+ endpoint_url = @azure.graph_api_endpoint_url
+ url = [endpoint_url, api_version, resource_trimmed].join('/')
+ query_parameters = opts[:query_parameters].nil? ? {} : opts[:query_parameters]
+ @azure.rest_get_call(url, query_parameters)
+ end
+
+ # Talk to resource manager endpoint to get the short description of a resource.
+ # This will include the resource_id.
+ #
+ # This operation is not pageable.
+ #
+ # @return [Array] The short description of resources.
+ #
+ # Example result will look like:
+ # :id=>"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Compute/virtualMachines/{resource_name}",
+ # :name=>"{resource_name}",
+ # :type=>"Microsoft.Compute/virtualMachines",
+ # :location=>"westeurope",
+ # :createdTime=>"2018-04-30T08:13:08.5601634Z",
+ # :changedTime=>"2018-04-30T09:23:10.8753511Z",
+ # :provisioningState=>"Succeeded"
+ # @note
+ # - Reference: https://docs.microsoft.com/en-us/rest/api/resources/resources/list
+ # - $filter=resourceType eq 'Microsoft.Network/virtualNetworks'
+ # - $filter=tagName eq 'tag1' and tagValue eq 'Value1'
+ # - If filtered by tags, short description won't have the tags property.
+ # - GET https://management.azure.com/subscriptions/{subscription_id}/resources?api-version=2019-10-01&
+ # $filter=resourceGroup eq '{resource_group}' and name eq '{resource_name}'
+ def resource_short(opts)
+ raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash)
+ url = "#{@azure.resource_manager_endpoint_url}subscriptions/#{@azure.credentials[:subscription_id]}/resources"
+ api_version = @azure.resource_manager_endpoint_api_version
+ params = {
+ '$filter' => Helpers.odata_query(opts),
+ '$expand' => Helpers.odata_query(%w{createdTime changedTime provisioningState}),
+ 'api-version' => api_version,
+ }
+ short_description, suggested_api_version = rescue_wrong_api_call(url, params)
+ # If suggested_api_version is not nil, then the resource manager api version should be updated.
+ unless suggested_api_version.nil?
+ @resource_manager_endpoint_api = suggested_api_version
+ Inspec::Log.warn "Resource manager endpoint api version should be updated with #{suggested_api_version} in"\
+ ' `libraries/backend/helpers.rb`'
+ end
+ short_description[:value] || []
+ end
+
+ # @return [Array] [ HTTP_response_body, api_version_suggested ]
+ # @param url [String] The url without any parameters or headers.
+ # @param params [Hash] The query parameters without the api version.
+ #
+ # @note
+ # - HTTP_response_body will be in JSON with symbolized keys.
+ #
+ # @example Result
+ # [{:value => 'blah'}, '2020-01-01']
+ def rescue_wrong_api_call(url, params = {})
+ begin
+ response = @azure.rest_get_call(url, params)
+ rescue UnsuccessfulAPIQuery::UnexpectedHTTPResponse::InvalidApiVersionParameter => e
+ api_version_suggested = e.suggested_api_version(params['api-version'])
+ Inspec::Log.warn "Incompatible api version: #{params['api-version']}\n"\
+ "Trying with the latest api version suggested by the Azure Rest API: #{api_version_suggested}."
+ if api_version_suggested.nil?
+ Inspec::Log.warn 'Failed to acquire suggested api version from the Azure Rest API.'
+ else
+ response = @azure.rest_get_call(url, params.merge!({ 'api-version' => api_version_suggested }))
+ end
+ end
+ [response, api_version_suggested]
+ end
+
+ # Construct resource_id from the provided parameters.
+ #
+ # @return [String] The resource_id of an Azure cloud resource.
+ # "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/
+ # Microsoft.Compute/virtualMachines/{resource_name}"
+ #
+ # @note: resource_provider should include parent resource path if there is, e.g.:
+ # Microsoft.Compute/virtualMachines/extensions
+ # @see https://docs.microsoft.com/en-us/rest/api/resources/resources/get
+ #
+ def construct_resource_id
+ required_arguments = %i(resource_group name resource_provider)
+ raise ArgumentError, "Following parameters should be provided to construct a resource_id: #{required_arguments}" \
+ unless required_arguments.all? { |resource_provider| @opts.keys.include?(resource_provider) }
+ id_in_list = [
+ "/subscriptions/#{@azure.credentials[:subscription_id]}",
+ 'resourceGroups', @opts[:resource_group],
+ 'providers', @opts[:resource_provider], @opts[:resource_path],
+ @opts[:name]
+ ].compact
+ id_in_list.join('/').gsub('//', '/')
+ end
+
+ # Get the detailed information of an Azure cloud resource by its resource_id from resource manager endpoint.
+ # or
+ # Get the detailed information of all resources of a subscription by its resource_provider and/or resource_group e.g.:
+ # https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachines?
+ # api-version=2019-12-01
+ # or
+ # Get the detailed information of all resources from a parent resource endpoint, e.g.:
+ # https://docs.microsoft.com/en-us/rest/api/sql/databases/listbyserver
+ # https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Sql/servers/{serverName}/databases?api-version=2017-10-01-preview
+ #
+ # This operation is pageable, the caller method should check if there is a nextLink property in the response body.
+ #
+ # @return [Hash]
+ # - The detailed information of a resource if resource_id is provided.
+ # - For plural resources:
+ # - The details of the resources will be in an array. {:value => [{resource_1}, {resource_2}, .. {resource_n}], }
+ #
+ # @param opts [Hash]
+ # - For singular resources:
+ # - resource_uri: (This is equal to the resource_id in most cases.)
+ # - The ID of the resource, "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/
+ # Microsoft.Compute/virtualMachines/{resource_name}"
+ # - The path of a child resource, "/subscriptions/.../{parentResourceName}/{subDomain}{childResourceName}"
+ # @see: azure_mysql_server.firewall_rules, azure_key_vault.diagnostic_settings
+ # - For plural resource:
+ # - resource_provider: 'Microsoft.Compute/virtualMachines'
+ # - resource_path:
+ # E.g: If the required resource is in '/providers/Microsoft.DBforMySQL/servers/{serverName}/databases'
+ # resource_provider => 'Microsoft.DBforMySQL/servers'
+ # resource_path => '/{serverName}/databases'
+ # - For singular and plural:
+ # api_version => `2020-06-01`, `latest` (default), `default`.
+ #
+ # Resource group will be added appropriately unless `resource_uri` is provided.
+ # example:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/providers/
+ # Microsoft.Compute/virtualMachines?api-version=2019-12-01
+ # @example:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Compute/virtualMachines?api-version=2019-12-01
+ #
+ # Following properties will be added additional to the resource information returned from the Azure Rest API:
+ # - api_version_used_for_query [String] The specific api version used for the query.
+ # - api_version_used_for_query_state [String] The state of the api version used for the query,
+ # `user_provided`, `latest`, `default`.
+ #
+ def get_resource(opts = {})
+ Helpers.validate_parameters(resource_name: @__resource_name__,
+ require_any_of: %i(resource_uri resource_provider resource_path resource_group),
+ allow: %i(api_version),
+ opts: opts)
+ api_version = opts[:api_version] || 'latest'
+ argument_error_message = 'Parameters error. For singular resources `resource_uri`; '\
+ 'for plural resources `resource_provider` and/or `resource_path` should be provided.'
+ if opts.key?(:resource_uri) && opts.keys.any? { |k| %i(resource_provider resource_path).include?(k) }
+ raise ArgumentError, argument_error_message
+ end
+ if opts.key?(:resource_uri)
+ uri_subdomain = opts[:resource_uri]
+ else
+ uri_subdomain = ["/subscriptions/#{@azure.credentials[:subscription_id]}/providers",
+ opts[:resource_provider],
+ opts[:resource_path]].compact.join('/').gsub('//', '/')
+ end
+ _resource_group, provider, r_type = Helpers.res_group_provider_type_from_uri(uri_subdomain)
+ # Add resource_group if provided.
+ unless opts[:resource_group].nil?
+ uri_subdomain = uri_subdomain.sub('/providers/', "/resourceGroups/#{opts[:resource_group]}/providers/")
+ end
+ # Some resource names can contain spaces. Decode them before parsing with URI.
+ url = URI.join(@azure.resource_manager_endpoint_url, uri_subdomain.gsub(' ', '%20'))
+ api_version = api_version.downcase
+ if %w{latest default}.include?(api_version)
+ # api_version is not a specific version yet: latest or default.
+ api_version_info = get_api_version(provider, r_type, api_version)
+ # Something was wrong at get_api_version, and we will try to get a valid api_version via rescue_wrong_api_call
+ # by providing an invalid api_version intentionally.
+ api_version_info[:api_version] = 'failed_attempt' if api_version_info[:api_version].nil?
+ else
+ api_version_info = { api_version: api_version, api_version_status: 'user_provided' }
+ end
+ long_description, suggested_api_version = rescue_wrong_api_call(url, { 'api-version' => api_version_info[:api_version] })
+ if long_description.is_a?(Hash)
+ long_description[:api_version_used_for_query] = suggested_api_version || api_version_info[:api_version]
+ long_description[:api_version_used_for_query_state] = suggested_api_version.nil? ? api_version_info[:api_version_status] : 'latest'
+ else
+ raise StandardError, "Expected a Hash object for querying #{opts}, but received #{long_description.class}."
+ end
+ long_description
+ end
+
+ # Get the latest or default api version of a resource provider's endpoint from the resource manager endpoint.
+ # @return [Hash] {api_version: '2020-06-01', api_version_status: 'default' or 'latest'}
+ # @param provider [String] The resource provider, e.g., Microsoft.Compute
+ # @param resource_type [String] The resource type, e.g., virtualMachines
+ # @param api_version_status [String] `latest` (default), `default`.
+ #
+ # Not all providers' endpoints define a default api version.
+ # In that case, the latest version will be returned regardless of the requested api state.
+ #
+ # @see https://docs.microsoft.com/en-us/rest/api/resources/providers/get
+ #
+ def get_api_version(provider, resource_type, api_version_status = 'latest')
+ unless %w{latest default}.include?(api_version_status)
+ raise ArgumentError, "The api version status should be either `latest` or `default`, given: #{api_version_status}."
+ end
+ response = { api_version: nil, api_version_status: nil }
+ resource_type_env = resource_type.gsub('/', '_')
+ in_cache = ENV["#{provider}__#{resource_type_env}__#{api_version_status}"]
+ unless in_cache.nil?
+ if in_cache == 'use_latest'
+ in_cache = ENV["#{provider}__#{resource_type}__latest"]
+ api_version_status = 'latest'
+ end
+ response[:api_version] = in_cache
+ response[:api_version_status] = api_version_status
+ end
+ return response unless response[:api_version].nil?
+
+ # If the resource manager api version is updated earlier, use that.
+ api_version_mgm = @resource_manager_endpoint_api || @azure.resource_manager_endpoint_api_version
+ url = "#{@azure.resource_manager_endpoint_url}subscriptions/#{@azure.credentials[:subscription_id]}/providers/#{provider}"
+ provider_details, suggested_api_version = rescue_wrong_api_call(url, { 'api-version' => api_version_mgm })
+ # If suggested_api_version is not nil, then the resource manager api version should be updated.
+ unless suggested_api_version.nil?
+ @resource_manager_endpoint_api = suggested_api_version
+ Inspec::Log.warn "Resource manager endpoint api version should be updated with #{suggested_api_version} in `libraries/backend/helpers.rb`"
+ end
+ resource_type_details = provider_details[:resourceTypes].select { |rt| rt[:resourceType] == resource_type }&.first
+ # For some resource types the api version might be available with their parent resource.
+ if resource_type_details.nil? && resource_type.include?('/')
+ parent_resource_type = resource_type.split('/').first
+ resource_type_details = provider_details[:resourceTypes].select { |rt| rt[:resourceType] == parent_resource_type }&.first
+ end
+ if resource_type_details.nil? || !resource_type_details.is_a?(Hash)
+ Inspec::Log.warn "Couldn't get the #{api_version_status} API version for `#{provider}/#{resource_type}`. " \
+ 'Please make sure that the provider/resourceType are in the correct format, e.g. `Microsoft.Compute/virtualMachines`.'
+ else
+ api_versions = resource_type_details[:apiVersions]
+ api_versions_stable = api_versions.reject { |a| a.include?('preview') }
+ api_versions_preview = api_versions.select { |a| a.include?('preview') }
+ # If the latest stable version is older than 2 years then use preview versions.
+ latest_api_version = Helpers.normalize_api_list(2, api_versions_stable, api_versions_preview).first
+ ENV["#{provider}__#{resource_type_env}__latest"] = latest_api_version
+ ENV["#{provider}__#{resource_type_env}__default"] = \
+ resource_type_details[:defaultApiVersion].nil? ? 'use_latest' : resource_type_details[:defaultApiVersion]
+ if api_version_status == 'default'
+ if resource_type_details[:defaultApiVersion].nil?
+ # This will be used to inform caller function about the actual status of the returned api version.
+ api_version_status = 'latest'
+ returned_api_version = latest_api_version
+ else
+ returned_api_version = resource_type_details[:defaultApiVersion]
+ end
+ else
+ returned_api_version = latest_api_version
+ end
+ response[:api_version] = returned_api_version
+ response[:api_version_status] = api_version_status
+ end
+ response
+ end
+
+ # Validate the short description list returned from the resource manager endpoint against a query.
+ #
+ # @return [TrueClass, FalseClass] Validation status of the resource description list.
+ #
+ # @param resource_list [Array] The list of short descriptions of resources.
+ # @param filter [Hash] The parameters used for the query.
+ # @param singular [TrueClass, FalseClass] Define whether or not the expected result is for a singular resource (default - true).
+ def validate_short_desc(resource_list, filter, singular = true)
+ message = "#{@__resource_name__}: #{@display_name}. Unable to get the resource short description with the provided data: #{filter}"
+ if resource_list.nil?
+ resource_fail(message)
+ false
+ elsif resource_list.empty?
+ empty_response_warn(message)
+ false
+ elsif singular && resource_list.size > 1
+ resource_fail
+ false
+ else
+ true
+ end
+ end
+
+ # Get the paginated result.
+ # The next_link url won't be validated since it is provided by the Azure Rest API.
+ # @see https://docs.microsoft.com/en-us/rest/api/azure/#async-operations-throttling-and-paging
+ #
+ # @param next_link [String] The nextLink url provided by the Azure Rest API.
+ # @return [Hash] The HTTP response body in JSON/Hash format with symbolized keys.
+ #
+ def get_next_link(next_link)
+ @azure.rest_get_call(next_link)
+ end
+
+ # Enforce specific resource type constraint in static resources, e.g: 'azure_virtual_machine'.
+ #
+ # @return [String] The resource type, 'Microsoft.Compute/virtualMachines'.
+ # @param resource_provider [String] The resource type, 'Microsoft.Compute/virtualMachines'.
+ # @param opts [Hash] Parameters to check if the ':resource_provider' key exists.
+ # If the resource type exists in the opts, this will raise an ArgumentError.
+ # The resource_provider parameter should not be provided by the user in static resources.
+ # Otherwise, static resource would not behave as expected if a different resource type is provided.
+ #
+ def specific_resource_constraint(resource_provider, opts)
+ if opts.is_a?(Hash)
+ if opts.key?(:resource_provider)
+ raise ArgumentError, "#{@__resource_name__}: The `resource_provider` parameter is not allowed."\
+ " `#{resource_provider}` is predefined for this resource."
+ end
+ else
+ raise ArgumentError, "#{@__resource_name__}: Parameters must be provided in an Hash object."
+ end
+ resource_provider
+ end
+
+ # Intercept failed resource queries.
+ #
+ # Inform if it is an api issue and present the suggested version by the Azure Rest API.
+ #
+ # This should be used to ensure to fail the resources properly if they can not be created.
+ def catch_failed_resource_queries
+ yield
+ # Inform user if it is an API incompatibility issue and recommend how to solve it.
+ rescue UnsuccessfulAPIQuery::UnexpectedHTTPResponse::InvalidApiVersionParameter => e
+ api_version_suggested_list = e.suggested_api_version
+ message = "Incompatible api version is provided.\n"\
+ "The list of api versions suggested by the Azure REST API is #{api_version_suggested_list}.\n #{e.message}"\
+ 'Note that if this list includes the invalid api version and it should be removed before using the list.'
+ resource_fail(message)
+ rescue UnsuccessfulAPIQuery::ResourceNotFound => e
+ empty_response_warn(e.message)
+ rescue UnsuccessfulAPIQuery::UnexpectedHTTPResponse => e
+ message = "Unable to get information from the REST API for #{@__resource_name__}: #{@display_name}.\n#{e.message}"
+ resource_fail(message)
+ rescue StandardError => e
+ message = "Resource is failed due to #{e}"
+ resource_fail(message)
+ end
+
+ # Track the status of the resource at InSpec Azure resource pack level.
+ #
+ # @return [TrueClass, FalseClass] Whether the resource is failed or not.
+ def failed_resource?
+ @failed_resource ||= false
+ end
+
+ # Ensure required parameters have been set to perform backend operations.
+ #
+ # Some resources may require several parameters to be set, in which case use `required`.
+ # Some resources may require at least 1 of n parameters to be set, in which case use `require_any_of`.
+ # If a parameter is entirely optional, use `allow`.
+ #
+ # @see https://github.com/inspec/inspec-aws/blob/master/libraries/aws_backend.rb#L209
+ def validate_parameters(allow: [], required: nil, require_any_of: nil)
+ opts = @opts
+ allow += %i(azure_retry_limit azure_retry_backoff azure_retry_backoff_factor
+ endpoint api_version required_parameters allowed_parameters display_name)
+ Helpers.validate_parameters(resource_name: @__resource_name__,
+ allow: allow, required: required,
+ require_any_of: require_any_of, opts: opts)
+ if opts.key?(:resource_id) && \
+ opts.keys.any? { |key| %i(resource_group resource_provider name tag_name tag_value).include?(key) }
+ raise ArgumentError, 'If `resource_id` is provided, the following parameters should not be provided.'\
+ ' ["resource_group", "resource_provider", "name", "tag_name", "tag_value"]'
+ end
+ true
+ end
+
+ # Fail resource for various reasons, such as:
+ # - Multiple resources for the provided criteria in singular resources.
+ # - HTTP issues.
+ # This will update the @failed_resource variable.
+ # The status of the resource can be checked with the 'resource_failed?' method when needed.
+ # @param message [String] The reason of the failure (default - Multiple resources returned for the provided criteria.).
+ # @return [String] The reason of the failure.
+ def resource_fail(message = nil)
+ message ||= "#{@__resource_name__}: #{@display_name}. "\
+ 'Multiple Azure resources were returned for the provided criteria. '\
+ 'If you wish to test multiple entities, please use the plural resource. '\
+ 'Otherwise, please provide more specific criteria to lookup the resource.'
+ # Fail resource in resource pack. `exists?` method will return `false`.
+ @failed_resource = true
+ # Fail resource in InSpec core. Tests in InSpec profile will return the message.
+ fail_resource(message)
+ message
+ end
+
+ # This method should be used when Azure API returns an empty response, e.g. '[]'.
+ # This will update the @failed_resource variable.
+ # The status of the resource can be checked with the 'resource_failed?' method when needed.
+ # @return [String] The reason of the failure.
+ def empty_response_warn(message = nil)
+ message ||= "#{@__resource_name__}: #{@display_name} not found."
+ # Fail resource in resource pack. `exists?` method will return `false`.
+ @failed_resource = true
+ # Do not fail in InSpec core. The test `it { should_not exist }` will pass.
+ Inspec::Log.warn message
+ message
+ end
+
+ # Prevent undefined method error by returning nil.
+ # This will prevent breaking a test when queried a non-existing method.
+ # @return [NilClass]
+ # @see https://github.com/inspec/inspec-azure/blob/master/libraries/support/azure/response.rb
+ def method_missing(method_name, *args, &block)
+ if respond_to?(method_name)
+ super
+ else
+ NullResponse.new
+ end
+ end
+
+ # This is a RuboCop requirement.
+ def respond_to_missing?(*several_variants)
+ super
+ end
+
+ # Create the methods for the resource object from the detailed information.
+ # This will allow using dot notation to access nested properties of a resource.
+ #
+ # @example
+ # {:properties => {:osProfile => {:adminUsername}}}
+ # can be accessed via
+ # 'properties.osProfile.adminUsername'
+ def create_resource_methods(object)
+ dm = AzureResourceDynamicMethods.new
+ dm.create_methods(self, object)
+ end
+end
+
+# ================================
+#
+# This code is taken from here:
+# https://github.com/inspec/inspec-azure/blob/0.6.2/libraries/azure_backend.rb#L320
+# It is unchanged, except modifying the missing method behavior.
+#
+# ================================
+#
+# Class to create methods on the calling object at run time.
+# Each of the Azure Resources have different attributes and properties, and they all need
+# to be testable. To do this no methods are hardcoded, each on is created based on the
+# information returned from Azure.
+#
+# The class is a helper class essentially as it creates the methods on the calling class
+# rather than itself. This means that there is less duplication of code and it can be
+# reused easily.
+#
+# @author Russell Seymour
+# @since 0.2.0
+class AzureResourceDynamicMethods
+ # Given the calling object and its data, create the methods on the object according
+ # to the data that has been retrieved. Various types of data can be returned so the method
+ # checks the type to ensure that the necessary methods are configured correctly
+ #
+ # @param object AzureResourceProbe|AzureResource The object on which the methods should be created.
+ # @param data variant The data from which the methods should be created
+ def create_methods(object, data)
+ if data.is_a?(Hash)
+ data.each do |key, value|
+ create_method(object, key, value)
+ end
+ else
+ raise ArgumentError, "Unsupported data type: #{data.class}. Expected Hash."
+ end
+ end
+
+ private
+
+ # Method that is responsible for creating the method on the calling object. This is
+ # because some nesting maybe required. For example of the value is a Hash then it will
+ # need to have an AzureResourceProbe create for each key, whereas if it is a simple
+ # string then the value just needs to be returned
+ #
+ # @private
+ #
+ # @param object [AzureResourceProbe, AzureResource] Object on which the methods need to be created
+ # @param name [string] The name of the method
+ # @param value [variant] The value that needs to be returned by the method
+ def create_method(object, name, value)
+ # Create the necessary method based on the var that has been passed
+ # Test the value for its type so that the method can be setup correctly
+ case value.class.to_s
+ when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum'
+ object.define_singleton_method name do
+ value
+ end
+ when 'Hash'
+ value.count.zero? ? return_value = value : return_value = AzureResourceProbe.new(value)
+ object.define_singleton_method name do
+ return_value
+ end
+ when 'Array'
+ # Some things are just string or integer arrays
+ # Check this by seeing if the first element is a string / integer / boolean or
+ # a hashtable
+ # This may not be the best method, but short of testing all elements in the array, this is
+ # the quickest test
+ case value[0].class.to_s
+ when 'String', 'Integer', 'TrueClass', 'FalseClass', 'Fixnum'
+ probes = value
+ else
+ probes = []
+ value.each do |value_item|
+ probes << AzureResourceProbe.new(value_item)
+ end
+ end
+ object.define_singleton_method name do
+ probes
+ end
+ end
+ end
+end
+
+# Class object that is created for each element that is returned by Azure.
+# This is what is interrogated by Inspec. If they are nested hashes, then this results
+# in nested AzureResourceProbe objects.
+#
+# For example, if the following was seen in an Azure Resource
+# properties -> storageProfile -> imageReference
+# Would result in the following nested classes
+# AzureResource -> AzureResourceProbe -> AzureResourceProbe
+#
+# The methods for each of the classes are dynamically defined at run time and will
+# match the items that are retrieved from Azure. See the 'test/integration/verify/controls' for examples
+#
+# This class will not be called externally
+#
+# @author Russell Seymour
+# @since 0.2.0
+# @attr_reader string name Name of the Azure resource
+# @attr_reader string type Type of the Azure Resource
+# @attr_reader string location Location in Azure of the resource
+class AzureResourceProbe
+ attr_reader :name, :type, :location, :item, :count
+
+ # Initialize method for the class. Accepts an item, be it a scalar value, hash or Azure object
+ # It will then create the necessary dynamic methods so that they can be called in the tests
+ # This is accomplished by call the AzureResourceDynamicMethods
+ #
+ # @return AzureResourceProbe
+ def initialize(item)
+ dm = AzureResourceDynamicMethods.new
+ dm.create_methods(self, item)
+
+ # Set the item as a property on the class
+ # This is so that it is possible to interrogate what has been added to the class and isolate them from
+ # the standard methods that a Ruby class has.
+ # This used for checking Tags on a resource for example
+ # It also allows direct access if so required
+ @item = item
+
+ # Set how many items have been set
+ @count = item.length
+ end
+
+ # Allows resources to respond to the include test
+ # This means that things like tags can be checked for and then their value tested
+ #
+ # @author Russell Seymour
+ #
+ # @param [String, Hash] opt Name (or Name=>Value) of the item to look for in the @item property
+ def include?(opt)
+ unless opt.is_a?(Symbol) || opt.is_a?(Hash) || opt.is_a?(String)
+ raise ArgumentError, 'Key or Key:Value pair should be provided.'
+ end
+ if opt.is_a?(Hash)
+ raise ArgumentError, 'Only one item can be provided' if opt.keys.size > 1
+ return @item[opt.keys.first] == opt.values.first
+ end
+ @item.key?(opt.to_sym)
+ end
+
+ # Prevent undefined method error by returning nil.
+ # This will prevent breaking a test when queried a non-existing method.
+ # @return [NilClass]
+ # @see https://github.com/inspec/inspec-azure/blob/master/libraries/support/azure/response.rb
+ def method_missing(method_name, *args, &block)
+ if respond_to?(method_name)
+ super
+ else
+ NullResponse.new
+ end
+ end
+
+ # This is a RuboCop requirement.
+ def respond_to_missing?(*several_variants)
+ super
+ end
+end
+
+# Ensure to return nil recursively.
+# @see https://github.com/inspec/inspec-azure/blob/master/libraries/support/azure/response.rb
+#
+class NullResponse
+ def nil?
+ true
+ end
+ alias empty? nil?
+
+ def ==(other)
+ other.nil?
+ end
+ alias === ==
+ alias <=> ==
+
+ def key?(_key)
+ false
+ end
+
+ def method_missing(method_name, *args, &block)
+ if respond_to?(method_name)
+ super
+ else
+ NullResponse.new
+ end
+ end
+
+ # This is a RuboCop requirement.
+ def respond_to_missing?(*several_variants)
+ super
+ end
+
+ def to_s
+ 'Do not exist.'
+ end
+end
diff --git a/libraries/azure_generic_resource.rb b/libraries/azure_generic_resource.rb
new file mode 100644
index 000000000..fb15a47f2
--- /dev/null
+++ b/libraries/azure_generic_resource.rb
@@ -0,0 +1,134 @@
+require 'azure_backend'
+
+class AzureGenericResource < AzureResourceBase
+ name 'azure_generic_resource'
+ desc 'Inspec Resource to interrogate any resource type available through Azure Resource Manager'
+ example <<-EXAMPLE
+ describe azure_generic_resource(resource_group: 'example', name: 'my_resource') do
+ its('name') { should eq 'my_resource' }
+ end
+ EXAMPLE
+
+ def initialize(opts = {}, static_resource = false)
+ super(opts)
+
+ if static_resource && !@opts.key?(:resource_id)
+ if @opts[:resource_identifiers]
+ raise ArgumentError, '`:resource_identifiers` have to be provided within a list.' \
+ unless @opts[:resource_identifiers].is_a?(Array)
+ # The `name` parameter should have been required in the static resource.
+ # Since it is a mandatory field, it is better to make sure that it is in the required list before validations.
+ @opts[:resource_identifiers] << :name unless @opts[:resource_identifiers].include?(:name)
+ provided = Helpers.validate_params_only_one_of(@__resource_name__, @opts[:resource_identifiers], @opts)
+ # Remove resource identifiers other than `:name`.
+ unless provided == :name
+ @opts[:name] = @opts[provided]
+ @opts.delete(provided)
+ end
+ end
+ required_params = %i(resource_group name)
+ required_params += @opts[:required_parameters] if @opts.key?(:required_parameters)
+ validate_parameters(required: required_params, allow: %i(resource_path resource_identifiers resource_provider))
+ elsif static_resource && @opts.key?(:resource_id)
+ # Ensure that the provided resource id is for the correct resource provider.
+ raise ArgumentError, "Resource provider must be #{@opts[:resource_provider]}." \
+ unless @opts[:resource_id].include?(@opts[:resource_provider])
+ @opts.delete(:resource_provider)
+ validate_parameters(required: %i(resource_id), allow: %i(resource_path resource_identifiers resource_provider))
+ else
+ # Either one of the following sets can be provided for a valid short description query (to get the resource_id).
+ # resource_group + name
+ # name
+ # tag_name + tag_value
+ # resource_group + resource_provider + name
+ # resource_id: no other parameters (within above mentioned) should exist
+ #
+ # If there are static resource specific validations they can be passed here:
+ # required parameters via `opts[:required_parameters]`
+ validate_parameters(require_any_of: %i(resource_group name tag_name tag_value resource_id resource_provider))
+ end
+ @display_name = @opts.slice(:resource_group, :resource_provider, :name, :tag_name, :tag_value, :resource_id)
+ .values.join(' ')
+
+ # Use the latest api_version unless provided.
+ api_version = @opts[:api_version] || 'latest'
+
+ # Get/create or acquire the resource_id.
+ # The resource_id is a MUST to get the detailed resource information.
+ #
+ # Use the provided resource_id
+ if @opts[:resource_id]
+ @resource_id = @opts[:resource_id]
+
+ # Construct the resource_id from parameters if they are sufficient
+ elsif %i(resource_group resource_provider name).all? { |param| @opts.keys.include?(param) }
+ @resource_id = construct_resource_id
+
+ # Query the resource management endpoint to get the resource_id with the provided parameters.
+ else
+ filter = @opts.slice(:resource_group, :name, :resource_provider, :tag_name, :tag_value, :location)
+ catch_failed_resource_queries do
+ # This filter will be used to query the Rest API.
+ # At this point the resource_provider should be identical to resource_type which is an allowed query parameter.
+ filter[:resource_type] = filter[:resource_provider] unless filter[:resource_provider].nil?
+ filter.delete(:resource_provider)
+ @resources = resource_short(filter)
+ end
+ # If an exception is raised above then the resource is failed.
+ # This check should be done every time after using catch_failed_resource_queries
+ #
+ return if failed_resource?
+
+ # Validate short description whether:
+ # There is a resource description? (0: it should_not exist, nil: fail resource)
+ # There are multiple resource description? (fail resource for singular resource)
+ #
+ validated = validate_short_desc(@resources, filter, true)
+ # If resource description is not in expected format, resource will be failed here.
+ return unless validated
+
+ # For a singular resource there must be one and only resource description with a resource_id.
+ @resource_id = @resources.first[:id]
+ end
+
+ # This is the last check on resource_id before talking to resource manager endpoint to get the detailed information.
+ Helpers.validate_resource_uri(@resource_id)
+ catch_failed_resource_queries do
+ params = { resource_uri: @resource_id, api_version: api_version }
+ @resource_long_desc = get_resource(params)
+ end
+ # If an exception is raised above then the resource is failed.
+ # This check should be done every time after using catch_failed_resource_queries
+ return if failed_resource?
+
+ # resource_long_desc should be a Hash object
+ # &
+ # All resources must have a name:
+ # https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/naming-and-tagging
+ unless @resource_long_desc.is_a?(Hash) && @resource_long_desc.key?(:name)
+ resource_fail("Unable to get the detailed information for the resource_id: #{@resource_id}")
+ end
+
+ # Create resource methods with the properties of the resource.
+ create_resource_methods(@resource_long_desc)
+ end
+
+ def exists?
+ !failed_resource?
+ end
+
+ def to_s(class_name = nil)
+ api_info = "- api_version: #{api_version_used_for_query} #{api_version_used_for_query_state}" if defined?(api_version_used_for_query)
+ if class_name.nil?
+ "#{AzureGenericResource.name.split('_').map(&:capitalize).join(' ')} #{api_info}: #{@display_name}"
+ else
+ "#{class_name.name.split('_').map(&:capitalize).join(' ')} #{api_info}: #{@display_name}"
+ end
+ end
+
+ def resource_group
+ return unless exists?
+ res_group, _provider, _res_type = Helpers.res_group_provider_type_from_uri(id)
+ res_group
+ end
+end
diff --git a/libraries/azure_generic_resources.rb b/libraries/azure_generic_resources.rb
new file mode 100644
index 000000000..272aa79b1
--- /dev/null
+++ b/libraries/azure_generic_resources.rb
@@ -0,0 +1,177 @@
+require 'azure_backend'
+
+class AzureGenericResources < AzureResourceBase
+ name 'azure_generic_resources'
+ desc 'Inspec Resource to interrogate any resource type in bulk available through Azure resource manager.'
+ example <<-EXAMPLE
+ describe azure_static_resources(resource_group: 'my_group') do
+ it { should exist }
+ end
+ EXAMPLE
+
+ attr_reader :table
+
+ def initialize(opts = {}, static_resource = false)
+ # A HTTP client will be created in the backend.
+ super(opts)
+
+ @display_name = @opts.slice(:resource_group, :resource_path, :name, :resource_provider, :tag_name, :tag_value).values.join(' ')
+ if static_resource
+ raise ArgumentError, 'Warning for the resource author: `resource_provider` must be defined.' \
+ unless opts.key?(:resource_provider)
+ @table = []
+ @resources = {}
+ opts[:api_version] = 'latest' unless opts.key?(:api_version)
+ # These are the parameters created in the static resource code, NOT provided by the user.
+ allowed_params = %i(resource_path resource_group resource_provider)
+ # User provided parameters will be passed here for validation with:
+ # opts[:required_parameters]
+ # opts[:allowed_parameters]
+ allowed_params += opts[:allowed_parameters] unless opts[:allowed_parameters].nil?
+ parameters_to_validate = {
+ required: opts[:required_parameters],
+ allow: allowed_params,
+ }.each_with_object({}) { |(k, v), acc| acc[k] = v unless v.nil? }
+ validate_parameters(**parameters_to_validate)
+ @display_name = @opts[:display_name] unless @opts[:display_name].nil?
+ return
+ end
+
+ # Either one of the following sets can be provided for a valid short description query.
+ # resource_group
+ # name
+ # substring_of_name, substring_of_resource_group
+ # tag_name + tag_value
+ # resource_group + resource_provider
+ raise ArgumentError, "#{@__resource_name__}: The `api_version` parameter is not allowed." if opts.key?(:api_version)
+ validate_parameters(allow: %i(name
+ substring_of_name
+ resource_group
+ substring_of_resource_group
+ resource_provider
+ tag_name
+ tag_value
+ location))
+ filter = @opts.slice(:resource_group, :name, :resource_provider, :tag_name, :tag_value, :location,
+ :substring_of_name, :substring_of_resource_group)
+ catch_failed_resource_queries do
+ # This filter will be used to query the Rest API.
+ # At this point the resource_provider should be identical to resource_type which is the allowed query parameter.
+ filter[:resource_type] = filter[:resource_provider] unless filter[:resource_provider].nil?
+ filter.delete(:resource_provider)
+ @resources = resource_short(filter)
+ end
+ # If an exception is raised above then the resource is failed.
+ # This check should be done every time after using catch_failed_resource_queries
+ return if failed_resource?
+ validated = validate_short_desc(@resources, filter, false)
+ # When @resources is an empty list the `validated` will be `false`.
+ # However, an empty FilterTable should still be created to be able to response `should_not exist` test.
+ return unless validated || @resources.empty?
+ @table = @resources.empty? ? [] : @resources
+
+ # @table = fetch_data
+ table_schema = [
+ { column: :ids, field: :id },
+ { column: :names, field: :name },
+ { column: :tags, field: :tags },
+ { column: :types, field: :type },
+ { column: :locations, field: :location },
+ { column: :created_times, field: :createdTime },
+ { column: :changed_times, field: :changedTime },
+ { column: :provisioning_states, field: :provisioningState },
+ ]
+ AzureGenericResources.populate_filter_table(:table, table_schema)
+ end
+
+ def to_s(class_name = nil)
+ if defined?(api_version_used_for_query)
+ api_info = "- api_version: #{api_version_used_for_query} #{api_version_used_for_query_state}" unless api_version_used_for_query.nil?
+ end
+ if class_name.nil?
+ "#{AzureGenericResources.name.split('_').map(&:capitalize).join(' ')} #{@display_name}"
+ else
+ "#{class_name.name.split('_').map(&:capitalize).join(' ')} #{api_info} #{@display_name}"
+ end
+ end
+
+ def api_version_used_for_query
+ @api_response[:api_version_used_for_query] if @api_response
+ end
+
+ def api_version_used_for_query_state
+ @api_response[:api_version_used_for_query_state] if @api_response
+ end
+
+ # Populate the FilterTable.
+ # FilterTable is a class bound object so is this method.
+ # @param raw_data [Symbol] Method name of the table with raw data.
+ # @param table_scheme [Array] [{column: :blahs, field: :blah}, {..}]
+ def self.populate_filter_table(raw_data, table_scheme)
+ filter_table = FilterTable.create
+ # puts "Table scheme in pop fil met #{table_scheme}"
+ # puts "Raw data: #{raw_data}"
+ table_scheme.each do |col_field|
+ filter_table.register_column(col_field[:column], field: col_field[:field])
+ end
+ filter_table.install_filter_methods_on_resource(self, raw_data)
+ end
+
+ private
+
+ # Call this in the static resources.
+ # Get plural resource details and populate @table to be used in FilterTable.
+ # Paginate API responses if necessary.
+ # @param resource_path [String, nil] A part of the URL that will be used to query resources.
+ # If the endpoint is
+ # `https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/
+ # virtualMachines?api-version=2019-12-01`
+ # resource_path should be: nil
+ #
+ # If the endpoint is
+ # `https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.DBforMySQL/servers/{serverName}/databases?api-version=2017-12-01`
+ # resource_path should be: `{serverName}/databases`
+ #
+ # `resource_group` will be added if provided at resource initialization.
+ #
+ def get_resources(resource_path = nil)
+ # Get details of resources and populate the FilterTable via @table.
+ # @see https://docs.microsoft.com/en-us/rest/api/compute/virtualmachines/listall
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.Compute/virtualMachines?
+ # api-version=2019-12-01
+ query_params = @opts.slice(:api_version, :resource_group, :resource_provider)
+ query_params[:resource_path] = resource_path unless resource_path.nil?
+ catch_failed_resource_queries do
+ @api_response = get_resource(query_params)
+ end
+ # If the resource is failed at this point, the `should_not exist` test should pass.
+ # Empty value will ensure that.
+ @api_response = { value: [] } if @api_response.nil?
+ @resources = @api_response[:value]
+ next_link = @api_response[:nextLink]
+ # Populate the @table to be used filling the FilterTable
+ if respond_to?(:populate_table, true)
+ populate_table
+ else
+ @table = @resources
+ end
+ return nil if next_link.nil?
+ # If there are more than 1000 resources than a nextLink will be returned for paging.
+ # @see https://docs.microsoft.com/en-us/rest/api/azure/#async-operations-throttling-and-paging
+ loop do
+ api_response = get_next_link(next_link)
+ return if failed_resource?
+ @resources = api_response[:value]
+ # Add new items to the @table.
+ if method_defined?(:populate_table)
+ populate_table
+ else
+ @table += @resources
+ end
+ next_link = api_response[:nextLink]
+ break if next_link.nil?
+ end
+ nil
+ end
+end
diff --git a/libraries/azure_graph_generic_resource.rb b/libraries/azure_graph_generic_resource.rb
new file mode 100644
index 000000000..563fb13f1
--- /dev/null
+++ b/libraries/azure_graph_generic_resource.rb
@@ -0,0 +1,79 @@
+require 'azure_backend'
+
+class AzureGraphGenericResource < AzureResourceBase
+ name 'azure_graph_generic_resource'
+ desc 'Inspec Resource to interrogate any resource type available through Azure Graph API'
+ example <<-EXAMPLE
+ describe azure_graph_generic_resource(resource_provider: 'users', name: 'jdoe@contoso.com') do
+ its('display_name') { should eq 'John Doe' }
+ end
+ EXAMPLE
+
+ def initialize(opts = {}, static_resource = false)
+ super(opts)
+
+ # A Graph API HTTP request is in the form of:
+ # {HTTP method} https://graph.microsoft.com/{version}/{resource}?{query-parameters}
+ #
+ # The dynamic part that has to be created in this resource:
+ # {version}/{resource}?{query-parameters}
+ #
+ # User supplied parameters:
+ # - api_version => Optional {version}. Default is defined in libraries/backend/helpers.rb as Graph API version.
+ # - resource => Mandatory {resource}. E.g., users, messages.
+ # - id => Mandatory. The unique identifier of an individual resource.
+ # - select => Optional. Query parameters defining which attributes that the resource will expose.
+ #
+ # If the queried entity does not exist, this resource will pass `it { should_not exist }` test.
+ #
+ if static_resource
+ raise ArgumentError, '`:resource_identifiers` have to be provided within a list' unless @opts[:resource_identifiers]
+ provided = Helpers.validate_params_only_one_of(@__resource_name__, @opts[:resource_identifiers], @opts)
+ # We should remove resource identifiers other than `:id`.
+ unless provided == :id
+ @opts[:id] = @opts[provided]
+ @opts.delete(provided)
+ end
+ end
+
+ validate_parameters(
+ required: %i(resource id),
+ allow: %i(select resource_identifiers),
+ )
+ @display_name = @opts.slice(:resource, :id).values.join(' ')
+
+ query = {}
+ query[:resource] = [@opts[:resource], @opts[:id]].join('/')
+ query[:api_version] = @opts[:api_version] unless @opts[:api_version].nil?
+
+ query_parameters = {}
+ if @opts[:select]
+ query_parameters['$select'] = Helpers.odata_query(@opts[:select])
+ end
+ query[:query_parameters] = query_parameters unless query_parameters.empty?
+
+ catch_failed_resource_queries do
+ @resource = resource_from_graph_api(query)
+ end
+
+ # If an exception is raised above then the resource is failed.
+ # This check should be done every time after using catch_failed_resource_queries
+ #
+ return if failed_resource?
+ create_resource_methods(@resource)
+ end
+
+ def exists?
+ !failed_resource?
+ end
+
+ def to_s(class_name = nil)
+ api_version = @opts[:api_version] || @azure.graph_api_endpoint_api_version
+ api_info = "- api_version: #{api_version} "
+ if class_name.nil?
+ "#{AzureGraphGenericResource.name.split('_').map(&:capitalize).join(' ')} #{api_info}: #{@display_name}"
+ else
+ "#{class_name.name.split('_').map(&:capitalize).join(' ')} #{api_info}: #{@display_name}"
+ end
+ end
+end
diff --git a/libraries/azure_graph_generic_resources.rb b/libraries/azure_graph_generic_resources.rb
new file mode 100644
index 000000000..006a9a884
--- /dev/null
+++ b/libraries/azure_graph_generic_resources.rb
@@ -0,0 +1,129 @@
+require 'azure_backend'
+
+class AzureGraphGenericResources < AzureResourceBase
+ name 'azure_graph_generic_resources'
+ desc 'Inspec plural resource to interrogate any resource type available through Azure Graph API'
+ example <<-EXAMPLE
+ describe azure_graph_generic_resources(resource_provider: 'users', filter: {given_name: 'John'}) do
+ it { should exist }
+ end
+ EXAMPLE
+
+ attr_reader :table
+
+ def initialize(opts = {}, static_resource = false)
+ super(opts)
+
+ # A Graph API HTTP request is in the form of:
+ # {HTTP method} https://graph.microsoft.com/{version}/{resource}?{query-parameters}
+ #
+ # The dynamic part that has to be created in this resource:
+ # {version}/{resource}?{query-parameters}
+ #
+ # User supplied parameters:
+ # - api_version => Optional {version}. Default is defined in libraries/backend/helpers.rb as Graph API version.
+ # - resource => Mandatory {resource}. E.g., users, messages.
+ # - filter => Optional. Query parameters to filter the interrogated resource.
+ # E.g.: filter: {given_name: 'John', substring_of_user_principal_name: 'chef'}.
+ # - filter_free_text => Optional. Parameters to filter resource in OData format.
+ # E.g.: filter_free_text: 'givenName eq "John" and substringof("chef", userPrincipalName)'
+ # @see: https://www.odata.org/getting-started/basic-tutorial/
+ # @note: Either `filter` or `filter_free_text` can be provided at the same time.
+ # - select => Optional. Query parameters defining which attributes that the resource will expose.
+ # E.g.: select: ['id', 'displayName', 'givenName']
+ #
+ # If the queried entity does not exist, this resource will pass `it { should_not exist }` test.
+ #
+ validate_parameters(
+ required: %i(resource),
+ allow: %i(select filter filter_free_text),
+ )
+ @display_name = @opts.slice(:resource, :filter, :filter_free_text).values.join(' ')
+
+ query = {}
+ query[:resource] = @opts[:resource]
+ query[:api_version] = @opts[:api_version] unless @opts[:api_version].nil?
+
+ query_parameters = {}
+ # Ensure that `id` of the resource is returned from the API.
+ query_parameters['$select'] = 'id,'
+ if @opts[:select]
+ # Remove `id` if it is duplicated in user supplied 'select' parameters.
+ @opts[:select].delete('id')
+ query_parameters['$select'] += Helpers.odata_query(@opts[:select])
+ end
+ if %i(filter filter_free_text).all? { |a| @opts.keys.include?(a) }
+ raise ArgumentError, 'Either `:filter` or `:filter_free_text` should be provided.'
+ end
+ if @opts[:filter]
+ query_parameters['$filter'] = Helpers.odata_query(@opts[:filter])
+ end
+
+ # This will allow passing:
+ # $filter=startswith(displayName,'J') or startswith(givenName,'J') or startswith(surname,'M') or
+ # startswith(mail,'J') or startswith(userPrincipalName,'J')
+ # @see
+ # https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
+ if @opts[:filter_free_text]
+ query_parameters['$filter'] = @opts[:filter_free_text]
+ end
+ query[:query_parameters] = query_parameters unless query_parameters.empty?
+
+ catch_failed_resource_queries do
+ @resource = resource_from_graph_api(query)
+ end
+
+ # If an exception is raised above then the resource is failed.
+ # This check should be done every time after using catch_failed_resource_queries
+ #
+ return if failed_resource?
+
+ @resources = @resource[:value]
+ next_link = @resource[:"@odata.nextLink"]
+ unless next_link.nil?
+ loop do
+ api_response = @azure.rest_get_call(next_link)
+ @resources += api_response[:value]
+ return if failed_resource?
+ next_link = api_response[:"@odata.nextLink"]
+ break if next_link.nil?
+ end
+ end
+ @table = @resources
+ if @table == []
+ @table_schema = {}
+ else
+ # Create FilterTable layout dynamically.
+ # Column names will be in snake_case and the pluralized form of the `select` parameters.
+ @table_schema = @table.first.keys.each_with_object([{ column: :ids, field: :id }]) do |t_key, table_schema|
+ unless t_key == :id
+ table_schema << { column: t_key.to_s.pluralize.snakecase.to_sym, field: t_key }
+ end
+ end
+ end
+ return if static_resource
+ AzureGraphGenericResources.populate_filter_table(:table, @table_schema)
+ end
+
+ def to_s(class_name = nil)
+ api_version = @opts[:api_version] || @azure.graph_api_endpoint_api_version
+ api_info = "- api_version: #{api_version} "
+ if class_name.nil?
+ "#{AzureGraphGenericResources.name.split('_').map(&:capitalize).join(' ')} #{api_info}: #{@display_name}"
+ else
+ "#{class_name.name.split('_').map(&:capitalize).join(' ')} #{api_info}: #{@display_name}"
+ end
+ end
+
+ # Populate the FilterTable.
+ # FilterTable is a class bound object so is this method.
+ # @param raw_data [Symbol] Method name of the table with raw data.
+ # @param table_scheme [Array] [{column: :blahs, field: :blah}, {..}]
+ def self.populate_filter_table(raw_data, table_scheme)
+ filter_table = FilterTable.create
+ table_scheme.each do |col_field|
+ filter_table.register_column(col_field[:column], field: col_field[:field])
+ end
+ filter_table.install_filter_methods_on_resource(self, raw_data)
+ end
+end
diff --git a/libraries/azure_graph_user.rb b/libraries/azure_graph_user.rb
new file mode 100644
index 000000000..c1a777144
--- /dev/null
+++ b/libraries/azure_graph_user.rb
@@ -0,0 +1,107 @@
+require 'azure_graph_generic_resource'
+
+class AzureGraphUser < AzureGraphGenericResource
+ name 'azure_graph_user'
+ desc 'Verifies settings for an Azure Active Directory User'
+ example <<-EXAMPLE
+ describe azure_graph_user(user_id: 'userId') do
+ it { should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ # Options should be Hash type. Otherwise Ruby error will be raised.
+ raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash)
+
+ # @see
+ # https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#examples
+ # Azure Graph API HTTP request is in the form of:
+ # GET https://graph.microsoft.com/v1.0/users/{id | userPrincipalName}
+ #
+ # The dynamic part that has to be created in this resource:
+ # v1.0/users/{id | userPrincipalName}?$select='propertiesToFilterInCamelCase'
+ # (v1.0 is the API version)
+ #
+ # User supplied parameters:
+ # - api_version => Optional {version}. Default is defined in libraries/backend/helpers.rb as Graph API version.
+ # - id => Mandatory. The unique identifier( {id | userPrincipalName} ) of an individual resource.
+ #
+ # Hard coded parameter(s):
+ # - resource => users
+ # - select => Query parameters defining which attributes that the resource will expose.
+ #
+ # If the queried entity does not exist, this resource will pass `it { should_not exist }` test.
+
+ # The unique resource identifiers must be defined here.
+ # If more than one are provided at the same time, argument error will be raised.
+ opts[:resource_identifiers] = %i(user_principal_name user_id id)
+
+ # Define the resource.
+ opts[:resource] = 'users'
+
+ # Properties to expose.
+ opts[:select] = 'objectId,accountEnabled,city,country,department,displayName,givenName,jobTitle,mail,'\
+ 'mailNickname,mobilePhone,passwordPolicies,passwordProfile,postalCode,state,streetAddress,surname,'\
+ 'businessPhones,usageLocation,userPrincipalName,userType,faxNumber,id'.split(',')
+
+ # At this point there is enough data to make the query.
+ # super must be called with `static_resource => true` switch.
+ super(opts, true)
+ end
+
+ def exists?
+ !failed_resource?
+ end
+
+ def to_s
+ super(AzureGraphUser)
+ end
+
+ # Methods for backward compatibility starts here >>>>
+ legacy_methods = %w{account_enabled given_name mail_nickname
+ password_policies password_profile postal_code street_address
+ usage_location user_principal_name user_type}
+ legacy_methods.each do |method_name|
+ define_method method_name.to_sym do
+ method(method_name.camelcase(:lower).to_sym).call
+ end
+ end
+
+ def display_name
+ displayName
+ end
+
+ define_method :facsimileTelephoneNumber do
+ faxNumber
+ end
+
+ define_method :mobile do
+ mobilePhone
+ end
+
+ define_method :telephoneNumber do
+ businessPhones.first
+ end
+
+ def guest?
+ userType == 'Guest'
+ end
+ # Methods for backward compatibility ends here <<<<
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermAdUser < AzureGraphUser
+ name 'azurerm_ad_user'
+ desc 'Verifies settings for an Azure Active Directory User'
+ example <<-EXAMPLE
+ describe azurerm_ad_user(user_id: 'userId') do
+ it { should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureGraphUser.name)
+ super
+ end
+end
diff --git a/libraries/azure_graph_users.rb b/libraries/azure_graph_users.rb
new file mode 100644
index 000000000..2fdf8dd7f
--- /dev/null
+++ b/libraries/azure_graph_users.rb
@@ -0,0 +1,85 @@
+require 'azure_graph_generic_resources'
+
+class AzureGraphUsers < AzureGraphGenericResources
+ name 'azure_graph_users'
+ desc 'Verifies settings for an Azure Active Directory User'
+ example <<-EXAMPLE
+ describe azure_graph_users do
+ it { should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ # Options should be Hash type. Otherwise Ruby error will be raised.
+ raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash)
+
+ # @see
+ # https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http#examples
+ # Azure Graph API HTTP request is in the form of:
+ # GET https://graph.microsoft.com/v1.0/users
+ #
+ # The dynamic part that has to be created in this resource:
+ # v1.0/users?$select='propertiesToFilterInCamelCase'&$filter='givenName eq "John"'
+ # (v1.0 is the API version)
+ #
+ # User supplied parameters:
+ # - api_version => Optional {version}. Default is defined in libraries/backend/helpers.rb as Graph API version.
+ # - filter => Optional. Query parameters to filter the interrogated resource.
+ # E.g.: filter: {given_name: 'John', substring_of_user_principal_name: 'chef'}.
+ # - filter_free_text => Optional. Parameters to filter resource in OData format.
+ # E.g.: filter_free_text: 'givenName eq "John" and substringof("chef", userPrincipalName)'
+ # @see: https://www.odata.org/getting-started/basic-tutorial/
+ # @note: Either `filter` or `filter_free_text` can be provided at the same time.
+ #
+ # Hard coded parameter(s):
+ # - resource => users
+ # - select => Query parameters defining which attributes that the resource will expose.
+ # E.g.: select: ['id', 'displayName', 'givenName']
+ #
+
+ # Define the resource.
+ opts[:resource] = 'users'
+
+ # Properties to expose.
+ opts[:select] = 'id,displayName,givenName,jobTitle,mail,userType,userPrincipalName'.split(',')
+
+ # At this point there is enough data to make the query.
+ # super must be called with `static_resource => true` switch.
+ super(opts, true)
+
+ # FilterTable is populated at the very end due to being an expensive operation.
+ AzureGraphUsers.populate_filter_table(:table, @table_schema)
+ end
+
+ def guest_accounts
+ @guest_accounts ||= where(userType: 'Guest').mails
+ end
+
+ # For backward compatibility.
+ # In the legacy resource `objectId` was the GUID.
+ # New Graph API provides the GUID with the `id` attribute.
+ def object_ids
+ ids
+ end
+
+ def to_s
+ super(AzureGraphUsers)
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermAdUsers < AzureGraphUsers
+ name 'azurerm_ad_users'
+ desc 'Verifies settings for an Azure Active Directory User'
+ example <<-EXAMPLE
+ describe azurerm_ad_users do
+ it { should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureGraphUsers.name)
+ super
+ end
+end
diff --git a/libraries/azure_key_vault.rb b/libraries/azure_key_vault.rb
new file mode 100644
index 000000000..cfe413933
--- /dev/null
+++ b/libraries/azure_key_vault.rb
@@ -0,0 +1,104 @@
+require 'azure_generic_resource'
+
+class AzureKeyVault < AzureGenericResource
+ name 'azure_key_vault'
+ desc 'Verifies settings and configuration for an Azure Key Vault'
+ example <<-EXAMPLE
+ describe azure_key_vault(resource_group: 'rg-1', vault_name: 'vault-1') do
+ it { should exist }
+ its('name') { should eq('vault-1') }
+ 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)
+
+ # Azure REST API endpoint URL format for the resource:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.KeyVault/vaults/{vaultName}?api-version=2019-09-01
+ #
+ # The dynamic part that has to be created in this resource:
+ # Microsoft.KeyVault/vaults/{vaultName}?api-version=2019-09-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Required parameter unless `resource_id` is provided. {resourceGroupName}
+ # - name => Required parameter unless `resource_id` is provided. Name of the resource to be tested.
+ # - resource_id => Optional parameter. If exists, other resource related parameters must not be provided.
+ # In the following format:
+ # /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.KeyVault/vaults/{vaultName}
+ # - api_version => Optional parameter. The latest version will be used unless provided.
+ #
+ # **`resource_group`, (resource) `name` and `resource_id` will be validated in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.KeyVault/vaults
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.KeyVault/vaults', opts)
+ # Key vault name can be accepted with a different keyword, `vault_name`. `name` is default accepted.
+ opts[:resource_identifiers] = %i(vault_name)
+
+ # At this point there is enough data to construct the resource id.
+ super(opts, true)
+ end
+
+ def to_s
+ super(AzureKeyVault)
+ end
+
+ # Resource specific methods can be created.
+ # `return unless exists?` is necessary to prevent any unforeseen Ruby error.
+ # Following methods are created to provide the same functionality with the current resource pack >>>>
+ # @see https://github.com/inspec/inspec-azure
+
+ # Diagnostic settings can be acquired from:
+ # GET https://management.azure.com/{resourceUri}/
+ # providers/microsoft.insights/diagnosticSettings?api-version=2017-05-01-preview
+ # resource uri is the same as (resource) `id` of the key vault.
+ # @see: https://docs.microsoft.com/en-us/rest/api/monitor/diagnosticsettings/list
+ #
+ # `#get_resource` method will be used to get the diagnostic settings from the Rest API.
+ # api_version => the api_version for the microsoft.insights/diagnosticSettings
+ # resource_uri => id + '/providers/microsoft.insights/diagnosticSettings'
+ #
+ def diagnostic_settings
+ return unless exists?
+ if @diagnostic_settings.nil?
+ resource_uri = id + '/providers/microsoft.insights/diagnosticSettings'
+ api_query_diagnostic_settings = {
+ resource_uri: resource_uri,
+ # api_version is fixed due to this operation is not supported by other versions.
+ api_version: '2017-05-01-preview',
+ }
+ # The `:value` will return the diagnostic settings.
+ @diagnostic_settings = get_resource(api_query_diagnostic_settings)[:value]
+ end
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermKeyVault < AzureKeyVault
+ name 'azurerm_key_vault'
+ desc 'Verifies settings and configuration for an Azure Key Vault'
+ example <<-EXAMPLE
+ describe azurerm_key_vault(resource_group: 'rg-1', vault_name: 'vault-1') do
+ it { should exist }
+ its('name') { should eq('vault-1') }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureKeyVault.name)
+ super
+ end
+end
diff --git a/libraries/azure_key_vaults.rb b/libraries/azure_key_vaults.rb
new file mode 100644
index 000000000..56a7b31f9
--- /dev/null
+++ b/libraries/azure_key_vaults.rb
@@ -0,0 +1,101 @@
+require 'azure_generic_resources'
+
+class AzureKeyVaults < AzureGenericResources
+ name 'azure_key_vaults'
+ desc 'Verifies settings for a collection of Azure Key Vaults'
+ example <<-EXAMPLE
+ describe azurerm_key_vaults(resource_group: 'rg-1') do
+ it { should exist }
+ its('names') { should include 'vault-1'}
+ 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)
+
+ # Azure REST API endpoint URL format listing the all resources for a given subscription:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/providers/
+ # Microsoft.KeyVault/vaults?api-version=2019-09-01
+ #
+ # or in a resource group only
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.KeyVault/vaults?api-version=2019-09-01
+ #
+ # The dynamic part that has to be created for this resource:
+ # Microsoft.KeyVault/vaults?api-version=2019-09-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Optional parameter.
+ # - api_version => Optional parameter. The latest version will be used unless provided.
+ #
+ # **`resource_group` will be used in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined here.
+ # - resource_provider => Microsoft.KeyVault/vaults
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.KeyVault/vaults', opts)
+
+ # static_resource parameter must be true for setting the scene 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`.
+ # @see https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md
+ table_schema = [
+ { column: :ids, field: :id },
+ { column: :names, field: :name },
+ { column: :locations, field: :location },
+ { column: :types, field: :type },
+ { column: :tags, field: :tags },
+ { column: :properties, field: :properties },
+ ]
+
+ # Talk to Azure Rest API and gather resources data in @resources.
+ # Paginate if necessary.
+ # Use the `populate_table` method (if defined) for filling the @table with the desired resource attributes.
+ get_resources
+
+ # Check if the resource is failed.
+ return if failed_resource?
+
+ # 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(AzureKeyVaults)
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermKeyVaults < AzureKeyVaults
+ name 'azurerm_key_vaults'
+ desc 'Verifies settings for a collection of Azure Key Vaults'
+ example <<-EXAMPLE
+ describe azurerm_key_vaults(resource_group: 'rg-1') do
+ it { should exist }
+ its('names') { should include 'vault-1'}
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureKeyVaults.name)
+ super
+ end
+end
diff --git a/libraries/azure_mysql_server.rb b/libraries/azure_mysql_server.rb
new file mode 100644
index 000000000..9c368c7c6
--- /dev/null
+++ b/libraries/azure_mysql_server.rb
@@ -0,0 +1,92 @@
+require 'azure_generic_resource'
+
+class AzureMysqlServer < AzureGenericResource
+ name 'azure_mysql_server'
+ desc 'Verifies settings for an Azure My SQL Server'
+ example <<-EXAMPLE
+ describe azurerm_mysql_server(resource_group: 'example', server_name: 'vm-name') do
+ it { should have_monitoring_agent_installed }
+ 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)
+
+ # Azure REST API endpoint URL format for the resource:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.DBforMySQL/servers/{serverName}?api-version=2017-12-01
+ #
+ # The dynamic part that has to be created in this resource:
+ # Microsoft.DBforMySQL/servers/{serverName}?api-version=2017-12-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Required parameter unless `resource_id` is provided. {resourceGroupName}
+ # - name => Required parameter unless `resource_id` is provided. Name of the resource to be tested.
+ # - resource_id => Optional parameter. If exists, other resource related parameters must not be provided.
+ # In the following format:
+ # /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.DBforMySQL/servers/{serverName}
+ # - api_version => Optional parameter. The latest version will be used unless provided.
+ #
+ # **`resource_group`, (resource) `name` and `resource_id` will be validated in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.Network/virtualNetworks
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.DBforMySQL/servers', opts)
+ opts[:resource_identifiers] = %i(server_name)
+
+ # At this point there is enough data to construct the resource id.
+ super(opts, true)
+ end
+
+ def to_s
+ super(AzureMysqlServer)
+ end
+
+ # Resource specific methods can be created.
+ # `return unless exists?` is necessary to prevent any unforeseen Ruby error.
+ # Following methods are created to provide the same functionality with the current resource pack >>>>
+ # @see https://github.com/inspec/inspec-azure
+
+ # Azure Rest API endpoint for MySql firewall rules
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.DBforMySQL/servers/{serverName}/firewallRules?api-version=2017-12-01
+ #
+ # #getresource method
+ #
+ def firewall_rules
+ return unless exists?
+ resource_uri = id + '/firewallRules'
+ api_query_for_firewall = {
+ resource_uri: resource_uri,
+ }
+ @firewall_rules ||= get_resource(api_query_for_firewall)[:value]
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermMysqlServer < AzureMysqlServer
+ name 'azurerm_mysql_server'
+ desc 'Verifies settings for an Azure My SQL Server'
+ example <<-EXAMPLE
+ describe azurerm_mysql_server(resource_group: 'example', server_name: 'vm-name') do
+ it { should have_monitoring_agent_installed }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureMysqlServer.name)
+ super
+ end
+end
diff --git a/libraries/azure_mysql_servers.rb b/libraries/azure_mysql_servers.rb
new file mode 100644
index 000000000..3edd4ff18
--- /dev/null
+++ b/libraries/azure_mysql_servers.rb
@@ -0,0 +1,100 @@
+require 'azure_generic_resources'
+
+class AzureMysqlServers < AzureGenericResources
+ name 'azure_mysql_servers'
+ desc 'Verifies settings for a collection of Azure MySQL Servers'
+ example <<-EXAMPLE
+ describe azurerm_mysql_servers do
+ its('names') { should include 'my-sql-server' }
+ 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)
+
+ # Azure REST API endpoint URL format listing the all resources for a given subscription:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/providers/
+ # Microsoft.DBforMySQL/servers?api-version=2017-12-01
+ #
+ # or in a resource group only
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.DBforMySQL/servers?api-version=2017-12-01
+ #
+ # The dynamic part that has to be created for this resource:
+ # Microsoft.DBforMySQL/servers?api-version=2017-12-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Optional parameter.
+ # - api_version => Optional parameter. The latest version will be used unless provided.
+ #
+ # **`resource_group` will be used in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.DBforMySQL/servers
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.DBforMySQL/servers', opts)
+
+ # static_resource parameter must be true for setting the scene 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`.
+ # @see https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md
+ table_schema = [
+ { column: :names, field: :name },
+ { column: :skus, field: :sku },
+ { column: :ids, field: :id },
+ { column: :tags, field: :tags },
+ { column: :types, field: :type },
+ { column: :locations, field: :location },
+ { column: :properties, field: :properties },
+ ]
+
+ # Talk to Azure Rest API and gather resources data in @resources.
+ # Paginate if necessary.
+ # Use the `populate_table` method (if defined) for filling the @table with the desired resource attributes.
+ get_resources
+
+ # Check if the resource is failed.
+ return if failed_resource?
+
+ # 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(AzureMysqlServers)
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermMysqlServers < AzureMysqlServers
+ name 'azurerm_mysql_servers'
+ desc 'Verifies settings for a collection of Azure MySQL Servers'
+ example <<-EXAMPLE
+ describe azurerm_mysql_servers do
+ its('names') { should include 'my-sql-server' }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureMysqlServers.name)
+ super
+ end
+end
diff --git a/libraries/azure_network_security_group.rb b/libraries/azure_network_security_group.rb
new file mode 100644
index 000000000..e1aef40d1
--- /dev/null
+++ b/libraries/azure_network_security_group.rb
@@ -0,0 +1,228 @@
+require 'azure_generic_resource'
+require 'backend/azure_security_rules_helpers'
+require 'rspec/expectations'
+
+class AzureNetworkSecurityGroup < AzureGenericResource
+ name 'azure_network_security_group'
+ desc 'Verifies settings for Network Security Groups'
+ example <<-EXAMPLE
+ describe azure_network_security_group(resource_group: 'example', name: 'name') do
+ its(name) { should eq 'name'}
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ # Options should be Hash type. Otherwise Ruby error will be raised.
+ raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash)
+
+ # Azure REST API endpoint URL format for the resource:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Network/networkSecurityGroups/{networkSecurityGroupName}?api-version=2020-05-01
+ #
+ # The dynamic part that has to be created in this resource:
+ # Microsoft.Network/networkSecurityGroups/{networkSecurityGroupName}?api-version=2020-05-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Required parameter unless `resource_id` is provided. {resourceGroupName}
+ # - name => Required parameter unless `resource_id` is provided. Name of the resource to be tested.
+ # - resource_id => Optional parameter. If exists, other resource related parameters must not be provided.
+ # In the following format:
+ # /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Network/networkSecurityGroups/{networkSecurityGroupName}
+ # - api_version => Optional parameter. The latest version will be used unless provided.
+ #
+ # **`resource_group`, (resource) `name` and `resource_id` will be validated in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined here.
+ # - resource_provider => Microsoft.Network/virtualNetworks
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.Network/networkSecurityGroups', opts)
+
+ # At this point there is enough data to construct the resource id.
+ super(opts, true)
+ end
+
+ def to_s
+ super(AzureNetworkSecurityGroup)
+ end
+
+ # Resource specific methods can be created.
+ # `return unless exists?` is necessary to prevent any unforeseen Ruby error.
+
+ def inbound_rules
+ @inbound_rules ||= normalized_security_rules.one_direction_rules('inbound')
+ end
+
+ def outbound_rules
+ @outbound_rules ||= normalized_security_rules.one_direction_rules('outbound')
+ end
+
+ def allow_rules
+ @allow_rules ||= normalized_security_rules.access_type_rules('allow')
+ end
+
+ def deny_rules
+ @deny_rules ||= normalized_security_rules.access_type_rules('deny')
+ end
+
+ # @example
+ # it { should allow(source_ip_range: '10.0.0.0/24', direction: 'inbound') }
+ # it { should allow(destination_ip_range: '10.0.0.1', direction: 'outbound' destination_port: '22') }
+ # it { should_not allow(source_service_tag: 'Internet', direction: 'inbound' destination_port: ['22', '100-150']) }
+ # it { should allow(destination_service_tag: 'VirtualNetwork', direction: 'outbound', protocol: 'TCP') }
+ # it { should allow(source_ip_range: '0:0:0:0:0:ffff:a05:0', direction: 'inbound') }
+ def allow?(criteria = {})
+ Helpers.validate_params_required(@__resource_name__, %i(direction), criteria)
+ criteria[:access] = 'allow'
+ rules = criteria[:direction] == 'inbound' ? inbound_rules : outbound_rules
+ normalized_security_rules.go_compare(rules, criteria)
+ end
+ RSpec::Matchers.alias_matcher :allow, :be_allow
+ alias allowed? allow?
+
+ # @example
+ # it { should allow_in(service_tag: 'VirtualNetwork') }
+ # it { should_not allow_in(service_tag: 'Internet') }
+ # it { should_not allow_in(ip_range: '10.0.0.0/24', port: '22') }
+ # it { should allow_in(ip_range: '10.0.0.5', port: %w{22 8080 56-78}, protocol: 'TCP' ) }
+ def allow_in?(criteria)
+ criteria[:source_ip_range] = criteria[:ip_range] if criteria.key?(:ip_range)
+ criteria[:source_service_tag] = criteria[:service_tag] if criteria.key?(:service_tag)
+ criteria[:destination_port] = criteria[:port] if criteria.key?(:port)
+ %i(ip_range port service_tag).each { |k| criteria.delete(k) }
+ criteria[:direction] = 'inbound'
+ allow?(criteria)
+ end
+ RSpec::Matchers.alias_matcher :allow_in, :be_allow_in
+ alias allowed_in? allow_in?
+
+ # @example
+ # See AzureNetworkSecurityGroup#allow_in?
+ def allow_out?(criteria)
+ criteria[:destination_ip_range] = criteria[:ip_range] if criteria.key?(:ip_range)
+ criteria[:destination_service_tag] = criteria[:service_tag] if criteria.key?(:service_tag)
+ criteria[:destination_port] = criteria[:port] if criteria.key?(port)
+ %i(ip_range port service_tag).each { |k| criteria.delete(k) }
+ criteria[:direction] = 'outbound'
+ allow?(criteria)
+ end
+ RSpec::Matchers.alias_matcher :allow_out, :be_allow_out
+ alias allowed_out? allow_out?
+
+ def normalized_security_rules
+ @normalized_security_rules ||= ConsolidateSecurityRules.new(default_security_rules + security_rules)
+ end
+
+ # Following methods are created to provide the same functionality with the current resource pack >>>>
+ # @see https://github.com/inspec/inspec-azure
+ # Code for backward compatibility starts here >>>>>>
+
+ def security_rules
+ return unless exists?
+ @security_rules ||= properties.securityRules
+ end
+
+ def default_security_rules
+ return unless exists?
+ @default_security_rules ||= properties.defaultSecurityRules
+ end
+
+ def allow_ssh_from_internet?
+ return unless exists?
+ allow_port_from_internet?('22')
+ end
+ RSpec::Matchers.alias_matcher :allow_ssh_from_internet, :be_allow_ssh_from_internet
+
+ def allow_rdp_from_internet?
+ return unless exists?
+ allow_port_from_internet?('3389')
+ end
+ RSpec::Matchers.alias_matcher :allow_rdp_from_internet, :be_allow_rdp_from_internet
+
+ SPECIFIC_CRITERIA = %i(specific_port access_allow direction_inbound source_open).freeze
+ def allow_port_from_internet?(specific_port)
+ return unless exists?
+ @specific_port = specific_port
+ matches_criteria?(SPECIFIC_CRITERIA, security_rules_properties)
+ end
+ RSpec::Matchers.alias_matcher :allow_port_from_internet, :be_allow_port_from_internet
+
+ private
+
+ def security_rules_properties
+ security_rules.collect(&:properties) + default_security_rules.collect(&:properties)
+ end
+
+ def matches_criteria?(criteria, properties)
+ properties.any? { |property| criteria.all? { |method| send(:"#{method}?", property) } }
+ end
+
+ def specific_port?(properties)
+ matches_port?(destination_port_ranges(properties), @specific_port)
+ end
+
+ def destination_port_ranges(properties)
+ properties_hash = properties.to_h
+ return Array(properties.destinationPortRange) unless properties_hash.include?(:destinationPortRanges)
+
+ return properties.destinationPortRanges unless properties_hash.include?(:destinationPortRange)
+
+ properties.destinationPortRanges + Array(properties.destinationPortRange)
+ end
+
+ def matches_port?(ports, match_port)
+ return true if ports.detect { |p| p =~ /^(#{match_port}|\*)$/ }
+
+ ports.select { |port| port.include?('-') }
+ .collect { |range| range.split('-') }
+ .any? { |range| (range.first.to_i..range.last.to_i).cover?(match_port.to_i) }
+ end
+
+ def tcp?(properties)
+ properties.protocol.match?(/TCP|\*/)
+ end
+
+ def access_allow?(properties)
+ properties.access == 'Allow'
+ end
+
+ def source_open?(properties)
+ properties_hash = properties.to_h
+ if properties_hash.include?(:sourceAddressPrefix)
+ return properties.sourceAddressPrefix =~ %r{\*|0\.0\.0\.0|\/0|\/0|Internet|any}
+ end
+ if properties_hash.include?(:sourceAddressPrefixes)
+ return properties.sourceAddressPrefixes.include?('0.0.0.0')
+ end
+ end
+
+ def direction_inbound?(properties)
+ properties.direction == 'Inbound'
+ end
+ # Code for backward compatibility ends here <<<<<<<<
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermNetworkSecurityGroup < AzureNetworkSecurityGroup
+ name 'azurerm_network_security_group'
+ desc 'Verifies settings for Network Security Groups'
+ example <<-EXAMPLE
+ describe azurerm_network_security_group(resource_group: 'example', name: 'name') do
+ its(name) { should eq 'name'}
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureNetworkSecurityGroup.name)
+ super
+ end
+end
diff --git a/libraries/azure_network_security_groups.rb b/libraries/azure_network_security_groups.rb
new file mode 100644
index 000000000..2bf31eefb
--- /dev/null
+++ b/libraries/azure_network_security_groups.rb
@@ -0,0 +1,98 @@
+require 'azure_generic_resources'
+
+class AzureNetworkSecurityGroups < AzureGenericResources
+ name 'azure_network_security_groups'
+ desc 'Verifies settings for Network Security Groups'
+ example <<-EXAMPLE
+ azure_network_security_groups(resource_group: 'example') 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)
+
+ # Azure REST API endpoint URL format listing the all resources for a given subscription:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/providers/
+ # Microsoft.Network/networkSecurityGroups?api-version=2020-05-01
+ #
+ # or in a resource group only
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Network/networkSecurityGroups?api-version=2020-05-01
+ #
+ # The dynamic part that has to be created for this resource:
+ # Microsoft.Network/networkSecurityGroups?api-version=2020-05-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Optional parameter.
+ # - api_version => Optional parameter. The latest version will be used unless provided.
+ #
+ # **`resource_group` will be used in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.Network/networkSecurityGroups
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.Network/networkSecurityGroups', opts)
+
+ # static_resource parameter must be true for setting the scene 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`.
+ # @see https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md
+ table_schema = [
+ { column: :names, field: :name },
+ { column: :etags, field: :etag },
+ { column: :tags, field: :tags },
+ { column: :ids, field: :id },
+ { column: :locations, field: :location },
+ ]
+
+ # Talk to Azure Rest API and gather resources data in @resources.
+ # Paginate if necessary.
+ # Use the `populate_table` method (if defined) for filling the @table with the desired resource attributes.
+ get_resources
+
+ # Check if the resource is failed.
+ return if failed_resource?
+
+ # 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(AzureNetworkSecurityGroups)
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermNetworkSecurityGroups < AzureNetworkSecurityGroups
+ name 'azurerm_network_security_groups'
+ desc 'Verifies settings for Network Security Groups'
+ example <<-EXAMPLE
+ azurerm_network_security_groups(resource_group: 'example') do
+ it{ should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureNetworkSecurityGroups.name)
+ super
+ end
+end
diff --git a/libraries/azure_subnet.rb b/libraries/azure_subnet.rb
new file mode 100644
index 000000000..5cddc2d45
--- /dev/null
+++ b/libraries/azure_subnet.rb
@@ -0,0 +1,95 @@
+require 'azure_generic_resource'
+
+class AzureSubnet < AzureGenericResource
+ name 'azure_subnet'
+ desc 'Verifies settings for an Azure Virtual Network Subnet'
+ example <<-EXAMPLE
+ describe azure_subnet(resource_group: 'example',vnet: 'virtual-network-name' name: 'subnet-name') do
+ it { should exist }
+ its('name') { should eq 'subnet-name' }
+ 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)
+
+ # Azure REST API endpoint URL format for the resource:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Network/virtualNetworks/{virtualNetworkName}/subnets/{subnetName}?api-version=2020-05-01
+ #
+ # The dynamic part that will be created in this resource:
+ # Microsoft.Network/virtualNetworks/{virtualNetworkName}/subnets/{subnetName}?api-version=2020-05-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Required parameter unless `resource_id` is provided. {resourceGroupName}
+ # - vnet => Required parameter unless `resource_id` is provided. Virtual network name. {virtualNetworkName}
+ # It has to be defined as a required parameter.
+ # opts[:required_parameters] = %i(vnet)
+ # - name => Required parameter unless `resource_id` is provided. Subnet name. {subnetName}
+ # - resource_id => Optional parameter. If exists, other resource related parameters must not be provided.
+ # In the following format:
+ # /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Network/virtualNetworks/{virtualNetworkName}/subnets/{subnetName}
+ # - api_version => Optional parameter. The latest version will be used unless provided. api-version
+ #
+ # **`resource_group`, (resource) `name` and `resource_id` will be validated in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.Network/virtualNetworks
+ # resource_path => {virtualNetworkName}/subnets
+ #
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:required_parameters] = %i(vnet)
+ opts[:resource_path] = [opts[:vnet], 'subnets'].join('/')
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.Network/virtualNetworks', opts)
+ # At this point there is enough data to construct the resource id.
+ super(opts, true)
+ end
+
+ def to_s
+ super(AzureSubnet)
+ end
+
+ # Resource specific methods can be created.
+ # `return unless exists?` is necessary to prevent any unforeseen Ruby error.
+ # Following methods are created to provide the same functionality with the current resource pack >>>>
+ # @see https://github.com/inspec/inspec-azure
+
+ def address_prefix
+ return unless exists?
+ properties.addressPrefix
+ end
+
+ def nsg
+ return unless exists?
+ return nil unless properties.respond_to?(:networkSecurityGroup)
+ properties.networkSecurityGroup.id.split('/').last
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermSubnet < AzureSubnet
+ name 'azurerm_subnet'
+ desc 'Verifies settings for an Azure Virtual Network Subnet'
+ example <<-EXAMPLE
+ describe azurerm_subnet(resource_group: 'example',vnet: 'virtual-network-name' name: 'subnet-name') do
+ it { should exist }
+ its('name') { should eq 'subnet-name' }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureSubnet.name)
+ super
+ end
+end
diff --git a/libraries/azure_subnets.rb b/libraries/azure_subnets.rb
new file mode 100644
index 000000000..1160d22eb
--- /dev/null
+++ b/libraries/azure_subnets.rb
@@ -0,0 +1,106 @@
+require 'azure_generic_resources'
+
+class AzureSubnets < AzureGenericResources
+ name 'azure_subnets'
+ desc 'Verifies settings for Azure Virtual Network Subnets'
+ example <<-EXAMPLE
+ azure_subnets(resource_group: 'example', vnet: 'virtual-network-name') 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)
+
+ # Azure REST API endpoint URL format listing all subnets in a virtual network:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Network/virtualNetworks/{virtualNetworkName}/subnets?api-version=2020-05-01
+ #
+ # The dynamic part that has to be created for this resource:
+ # Microsoft.Network/virtualNetworks/{virtualNetworkName}/subnets?api-version=2020-05-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Required parameter unless `resource_id` is provided. {resourceGroupName}
+ # - vnet => Required parameter unless `resource_id` is provided. Virtual network name. {virtualNetworkName}
+ # It has to be defined as a required parameter.
+ # opts[:required_parameters] = %i(vnet)
+ # - api_version => Optional parameter. The latest version will be used unless provided. api-version
+ #
+ # **`resource_group` will be added into the URL appropriately in the backend.
+ # We don't have to do anything here except making it mandatory (required) parameter.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.Network/virtualNetworks
+ # resource_path => {virtualNetworkName}/subnets
+ #
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+
+ opts[:required_parameters] = %i(resource_group vnet)
+ # Unless provided here, a generic display name will be created in the backend.
+ opts[:display_name] = "Subnets for #{opts[:vnet]} Virtual Network"
+
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.Network/virtualNetworks', opts)
+
+ # static_resource parameter must be true for setting the scene 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`.
+ # @see https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md
+ table_schema = [
+ { column: :names, field: :name },
+ { column: :etags, field: :etag },
+ { column: :ids, field: :id },
+ ]
+
+ # Construct and provide the `resource_path`.
+ resource_path = "#{@opts[:vnet]}/subnets"
+ # All of the following tasks will be done via `get_resource` method:
+ # - Talk to Azure Rest API and gather resources data in @resources.
+ # - Paginate if necessary.
+ # - Use the `populate_table` method for filling the @table with the desired resource attributes according to the
+ # table_schema layout.
+ get_resources(resource_path)
+
+ # Check if the resource is failed.
+ return if failed_resource?
+
+ # 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(AzureSubnets)
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermSubnets < AzureSubnets
+ name 'azurerm_subnets'
+ desc 'Verifies settings for Azure Virtual Network Subnets'
+ example <<-EXAMPLE
+ azurerm_subnets(resource_group: 'example', vnet: 'virtual-network-name') do
+ it{ should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureSubnets.name)
+ super
+ end
+end
diff --git a/libraries/azure_virtual_machine.rb b/libraries/azure_virtual_machine.rb
new file mode 100644
index 000000000..15469aa47
--- /dev/null
+++ b/libraries/azure_virtual_machine.rb
@@ -0,0 +1,117 @@
+require 'azure_generic_resource'
+
+class AzureVirtualMachine < AzureGenericResource
+ name 'azure_virtual_machine'
+ desc 'Verifies settings for an Azure Virtual Machine'
+ example <<-EXAMPLE
+ describe azure_virtual_machine(resource_group: 'example', name: 'vm-name') do
+ it { should have_monitoring_agent_installed }
+ 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)
+
+ # Azure REST API endpoint URL format for the resource:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Compute/virtualMachines/{vmName}?api-version=2019-12-01
+ #
+ # The dynamic part that has to be created in this resource:
+ # Microsoft.Compute/virtualMachines/{vmName}?api-version=2019-12-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # User supplied parameters:
+ # - resource_group => Required parameter unless `resource_id` is provided. {resourceGroupName}
+ # - name => Required parameter unless `resource_id` is provided. Virtual machine name. {vmName}
+ # - resource_id => Optional parameter. If exists, `resource_group` and `name` must not be provided.
+ # In the following format:
+ # /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Compute/virtualMachines/{vmName}
+ # - api_version => Optional parameter. The latest version will be used unless provided. api-version
+ #
+ # **`resource_group` and (resource) `name` or `resource_id` will be validated in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined here.
+ # - resource_provider => Microsoft.Network/virtualNetworks
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.Compute/virtualMachines', opts)
+
+ # static_resource parameter of the super method must be `true` for setting the scene in the backend.
+ super(opts, true)
+ end
+
+ def to_s
+ super(AzureVirtualMachine)
+ end
+
+ # Resource specific methods can be created.
+ # `return unless exists?` is necessary to prevent any unforeseen Ruby error.
+ # Following methods are created to provide the same functionality with the current resource pack >>>>
+ # @see https://github.com/inspec/inspec-azure
+
+ def admin_username
+ properties.osProfile.adminUsername if exists?
+ end
+
+ # Following methods are created to provide the same functionality with the current resource pack >>>>
+ # @see https://github.com/inspec/inspec-azure
+ def os_disk_name
+ properties.storageProfile.osDisk.name if exists?
+ end
+
+ def data_disk_names
+ properties.storageProfile.dataDisks.map(&:name) if exists?
+ end
+
+ def installed_extensions_types
+ return unless exists?
+ return [] if resources.nil?
+ @installed_extensions_types ||= resources.map { |resource| resource.properties.type }
+ end
+
+ def has_only_approved_extensions?(approved_extensions)
+ (installed_extensions_types - approved_extensions).empty?
+ end
+
+ def has_endpoint_protection_installed?(endpoint_protection_extensions)
+ installed_extensions_types.any? { |extension| endpoint_protection_extensions.include?(extension) }
+ end
+
+ def installed_extensions_names
+ return unless exists?
+ return [] if resources.nil?
+ @installed_extensions_names ||= resources.map { |resource| resource&.name }
+ end
+
+ def has_monitoring_agent_installed?
+ return unless exists?
+ return false if resources.nil?
+ resources&.select do |res|
+ res&.properties&.type == 'MicrosoftMonitoringAgent' && res&.properties&.provisioning_state == 'Succeeded'
+ end
+ resources.size == 1
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermVirtualMachine < AzureVirtualMachine
+ name 'azurerm_virtual_machine'
+ desc 'Verifies settings for an Azure Virtual Machine'
+ example <<-EXAMPLE
+ describe azurerm_virtual_machine(resource_group: 'example', name: 'vm-name') do
+ it { should have_monitoring_agent_installed }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureVirtualMachine.name)
+ super
+ end
+end
diff --git a/libraries/azure_virtual_machines.rb b/libraries/azure_virtual_machines.rb
new file mode 100644
index 000000000..1eb968fbb
--- /dev/null
+++ b/libraries/azure_virtual_machines.rb
@@ -0,0 +1,132 @@
+require 'azure_generic_resources'
+
+class AzureVirtualMachines < AzureGenericResources
+ name 'azure_virtual_machines'
+ desc 'Verifies settings for Azure Virtual Machines'
+ example <<-EXAMPLE
+ azure_virtual_machines(resource_group: 'example') 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)
+
+ # Azure REST API endpoint URL format listing the all resources for a given subscription:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/providers/
+ # Microsoft.Compute/virtualMachines?api-version=2019-12-01
+ #
+ # or in a resource group only
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Compute/virtualMachines?api-version=2019-12-01
+ #
+ # The dynamic part that has to be created for this resource:
+ # Microsoft.Compute/virtualMachines?api-version=2019-12-01
+ #
+ # Parameters acquired from environment variables:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ #
+ # For parameters applicable to all resources, see project's README.
+ #
+ # User supplied parameters:
+ # - resource_group => Optional parameter.
+ # - api_version => Optional parameter. The latest version will be used unless provided.
+ #
+ # **`resource_group` will be used in the backend appropriately.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.Compute/virtualMachines
+ # The `specific_resource_constraint` method will validate the user input
+ # not to accept a different `resource_provider`.
+ #
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.Compute/virtualMachines', opts)
+
+ # static_resource parameter must be true for setting the scene in the backend.
+ super(opts, true)
+
+ # Check if the resource is failed.
+ # It is recommended to check that after every usage of superclass methods or API calls.
+ return if failed_resource?
+
+ # Define the column and field names for FilterTable.
+ # - column: It is defined as an instance method, callable on the resource, and present `field` values in a list.
+ # - field: It has to be identical with the `key` names in @table items that will be presented in the FilterTable.
+ # @see https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md
+ table_schema = [
+ { column: :os_disks, field: :os_disk },
+ { column: :data_disks, field: :data_disks },
+ { column: :vm_names, field: :name },
+ { column: :platforms, field: :platform },
+ { column: :ids, field: :id },
+ { column: :tags, field: :tags },
+ ]
+
+ # Before calling the `get_resources` method, a private `populate_table` method has to be defined.
+ #
+ # Talk to Azure Rest API and gather resources data in @resources.
+ # Paginate if necessary.
+ # Use the `populate_table` method for filling the @table with the desired resource attributes according to the
+ # `table_schema` layout.
+ get_resources
+
+ # 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(AzureVirtualMachines)
+ end
+
+ private
+
+ # Populate the @table with the resource attributes.
+ # @table has been declared in the super class as an empty array.
+ # Each item in the @table
+ # - should be a Hash object
+ # - should have the exact key names defined in the @table_schema as `field`.
+ def populate_table
+ # If @resources empty than @table should stay as an empty array as declared in superclass.
+ # This will ensure constructing resource and passing `should_not exist` test.
+ return if @resources.empty?
+ @resources.each do |resource|
+ os_profile = resource[:properties][:osProfile]
+ platform = \
+ if os_profile.key?(:windowsConfiguration)
+ 'windows'
+ elsif os_profile.key?(:linuxConfiguration)
+ 'linux'
+ else
+ 'unknown'
+ end
+ @table << {
+ id: resource[:id],
+ os_disk: resource[:properties][:storageProfile][:osDisk][:name],
+ data_disks: resource[:properties][:storageProfile][:dataDisks].map { |dd| dd[:name] unless dd.nil? },
+ name: resource[:name],
+ platform: platform,
+ tags: resource[:tags],
+ }
+ end
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermVirtualMachines < AzureVirtualMachines
+ name 'azurerm_virtual_machines'
+ desc 'Verifies settings for Azure Virtual Machines'
+ example <<-EXAMPLE
+ azurerm_virtual_machines(resource_group: 'example') do
+ it{ should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureVirtualMachines.name)
+ super
+ end
+end
diff --git a/libraries/azure_virtual_network.rb b/libraries/azure_virtual_network.rb
new file mode 100644
index 000000000..fc7acfde7
--- /dev/null
+++ b/libraries/azure_virtual_network.rb
@@ -0,0 +1,112 @@
+require 'azure_generic_resource'
+
+class AzureVirtualNetwork < AzureGenericResource
+ name 'azure_virtual_network'
+ desc 'Verifies settings for an Azure Virtual Network'
+ example <<-EXAMPLE
+ describe azure_virtual_network(resource_group: 'example', name: 'vnet-name') do
+ it { should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ # Options should be Hash type. Otherwise Ruby error will be raised.
+ raise ArgumentError, 'Parameters must be provided in an Hash object.' unless opts.is_a?(Hash)
+
+ # Azure REST API endpoint URL format for the resource:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers
+ # /Microsoft.Network/virtualNetworks/{virtualNetworkName}?api-version=2020-05-01
+ #
+ # The dynamic part that has to be created in this resource:
+ # Microsoft.Network/virtualNetworks/{virtualNetworkName}?api-version=2020-05-01
+ #
+ # User supplied parameters:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ # - {resourceGroupName} => Required parameter. It must be provided by the user. (`resource_group`)
+ # - {virtualNetworkName} => Required parameter. It must be provided by the user. (`name`)
+ # - api-version => Optional parameter. The latest version will be used by the backend if not provided. (`api_version`)
+ #
+ # **`resource_group` and (resource) `name` will be used appropriately in the backend.
+ # We don't have to do anything here except making them mandatory (required) parameters.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.Network/virtualNetworks
+ #
+ # Either the `resource_id` itself or the necessary parameters should be provided to the backend by calling `super(opts)`.
+ resource_provider = 'Microsoft.Network/virtualNetworks'
+ # if opts[:resource_id].nil?
+ # # The resource_id will be created in the backend with the provided parameters.
+ # #
+ # opts[:required_parameters] = %i(resource_group name)
+ #
+ # opts[:resource_provider] = specific_resource_constraint(resource_provider, opts)
+ # else
+ # raise ArgumentError, "Resource provider must be #{resource_provider}." \
+ # unless opts[:resource_id].include?(resource_provider)
+ # end
+
+ opts[:resource_provider] = specific_resource_constraint(resource_provider, opts)
+ # At this point there is enough data to construct the resource id.
+ super(opts, true)
+ end
+
+ def to_s
+ super(AzureVirtualNetwork)
+ end
+
+ # Resource specific methods can be created.
+ # `return unless exists?` is necessary to prevent any unforeseen Ruby error.
+ # Following methods are created to provide the same functionality with the current resource pack >>>>
+ # @see https://github.com/inspec/inspec-azure
+
+ def address_space
+ return unless exists?
+ properties.addressSpace.addressPrefixes
+ end
+
+ def dns_servers
+ return unless exists?
+ properties.dhcpOptions.dnsServers
+ end
+
+ def vnet_peerings
+ return unless exists?
+ name_id = ->(peer) { [peer.name, peer.properties.remoteVirtualNetwork.id] }
+ any_nils = ->(pair) { pair.any?(&:nil?) }
+
+ properties.virtualNetworkPeerings.collect(&name_id).reject(&any_nils).to_h
+ end
+
+ def enable_ddos_protection
+ return unless exists?
+ properties.enableDdosProtection
+ end
+
+ def enable_vm_protection
+ return unless exists?
+ properties.enableVmProtection
+ end
+
+ def subnets
+ return unless exists?
+ subs = properties.subnets || []
+ subs.collect(&:name)
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermVirtualNetwork < AzureVirtualNetwork
+ name 'azurerm_virtual_network'
+ desc 'Verifies settings for an Azure Virtual Network'
+ example <<-EXAMPLE
+ describe azurerm_virtual_network(resource_group: 'example', name: 'vnet-name') do
+ it { should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureVirtualNetwork.name)
+ super
+ end
+end
diff --git a/libraries/azure_virtual_networks.rb b/libraries/azure_virtual_networks.rb
new file mode 100644
index 000000000..dac9e9cf3
--- /dev/null
+++ b/libraries/azure_virtual_networks.rb
@@ -0,0 +1,100 @@
+require 'azure_generic_resources'
+
+class AzureVirtualNetworks < AzureGenericResources
+ name 'azure_virtual_networks'
+ desc 'Verifies settings for Azure Virtual Networks'
+ example <<-EXAMPLE
+ azure_virtual_networks(resource_group: 'example') 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)
+
+ # Azure REST API endpoint URL format listing the all resources for a given subscription:
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/providers/
+ # Microsoft.Network/virtualNetworks?api-version=2020-05-01
+ #
+ # or in a resource group only
+ # GET https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/
+ # Microsoft.Network/virtualNetworks?api-version=2020-05-01
+ #
+ # The dynamic part that has to be created for this resource:
+ # Microsoft.Network/virtualNetworks?api-version=2020-05-01
+ #
+ # User supplied parameters:
+ # - {subscriptionId} => Required parameter. It will be acquired by the backend from environment variables.
+ # - {resourceGroupName} => Optional parameter. It should be provided by the user. (`resource_group`)
+ # - api-version => Optional parameter. The latest version will be used by the backend if not provided. (`api_version`)
+ #
+ # **`resource_group` will be added into the URL appropriately in the backend.
+ # We don't have to do anything here.
+ #
+ # Following resource parameters have to be defined/created here.
+ # resource_provider => Microsoft.Network/virtualNetworks
+
+ opts[:resource_provider] = specific_resource_constraint('Microsoft.Network/virtualNetworks', opts)
+
+ # Establish a connection with Azure REST API.
+ # Initiate instance variables: @table and @resources.
+ # - @table => It will be used for populating FilterTable in the `AzureGenericResources.populate_filter_table` class method.
+ # - @resources => It will be used for populating the @table in the `populate_table` instance method.
+ #
+ # static_resource parameter must be true for setting the scene 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.
+ # - column: It is defined as an instance method, callable on the resource, and present `field` values in a list.
+ # - field: It has to be identical with the `key` names in @table items that will be presented in the FilterTable.
+ # @see https://github.com/inspec/inspec/blob/master/docs/dev/filtertable-usage.md
+ table_schema = [
+ { column: :names, field: :name },
+ { column: :etags, field: :etag },
+ { column: :ids, field: :id },
+ { column: :tags, field: :tags },
+ { column: :locations, field: :location },
+ ]
+
+ # Before calling the `get_resources` method, a private `populate_table` method has to be defined for this static resource.
+ # Talk to Azure Rest API and gather resources data in @resources.
+ # Paginate if necessary.
+ # Use the `populate_table` method for filling the @table with the desired resource attributes according to the
+ # table_schema layout.
+ get_resources
+
+ # Check if the resource is failed.
+ return if failed_resource?
+
+ # 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(AzureVirtualNetworks)
+ end
+end
+
+# Provide the same functionality under the old resource name.
+# This is for backward compatibility.
+class AzurermVirtualNetworks < AzureVirtualNetworks
+ name 'azurerm_virtual_networks'
+ desc 'Verifies settings for Azure Virtual Networks'
+ example <<-EXAMPLE
+ azurerm_virtual_networks(resource_group: 'example') do
+ it{ should exist }
+ end
+ EXAMPLE
+
+ def initialize(opts = {})
+ Inspec::Log.warn Helpers.resource_deprecation_message(@__resource_name__, AzureVirtualNetworks.name)
+ super
+ end
+end
diff --git a/libraries/azurerm_ad_user.rb b/libraries/azurerm_ad_user.rb
deleted file mode 100644
index 81caaf05e..000000000
--- a/libraries/azurerm_ad_user.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermAdUser < AzurermSingularResource
- name 'azurerm_ad_user'
- desc 'Verifies settings for an Azure Active Directory User'
- example <<-EXAMPLE
- describe azurerm_ad_user(user_id: 'userId') do
- it { should exist }
- end
- EXAMPLE
-
- ATTRS = %i(
- objectId
- accountEnabled
- city
- country
- department
- displayName
- facsimileTelephoneNumber
- givenName
- jobTitle
- mail
- mailNickname
- mobile
- passwordPolicies
- passwordProfile
- postalCode
- state
- streetAddress
- surname
- telephoneNumber
- usageLocation
- userPrincipalName
- userType
- ).freeze
-
- attr_reader(*ATTRS)
-
- def initialize(user_id: nil)
- user = graph.user(user_id)
- return if has_error?(user)
-
- assign_fields(ATTRS, user)
-
- @exists = true
- end
-
- def guest?
- @userType == 'Guest'
- end
-
- def to_s
- "Azure Active Directory Username: '#{displayName}' with objectId '#{objectId}'"
- end
-end
diff --git a/libraries/azurerm_ad_users.rb b/libraries/azurerm_ad_users.rb
deleted file mode 100644
index 51302fa2e..000000000
--- a/libraries/azurerm_ad_users.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-require 'json'
-
-class AzurermAdUsers < AzurermPluralResource
- name 'azurerm_ad_users'
- desc 'Verifies settings for a collection of Azure Active Directory Users'
- example <<-EXAMPLE
- describe azurerm_ad_users do
- it { should exist }
- end
- EXAMPLE
-
- attr_reader :table
-
- FilterTable.create
- .register_column(:object_ids, field: :objectId)
- .register_column(:display_names, field: :displayName)
- .register_column(:mails, field: :mail)
- .register_column(:user_types, field: :userType)
- .install_filter_methods_on_resource(self, :table)
-
- def initialize(params = {})
- @params = params
- resp = graph.users(@params)
- return if has_error?(resp)
-
- @table = resp
- end
-
- include Azure::Deprecations::StringsInWhereClause
-
- def guest_accounts
- @guest_accounts ||= where(userType: 'Guest').mails
- end
-
- def to_s
- if @params[:filter]
- "Azure Active Directory Users with filter( #{@params[:filter]} )"
- else
- 'Azure Active Directory Users'
- end
- end
-end
diff --git a/libraries/azurerm_key_vault.rb b/libraries/azurerm_key_vault.rb
deleted file mode 100644
index 3e85d1b2b..000000000
--- a/libraries/azurerm_key_vault.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermKeyVault < AzurermSingularResource
- name 'azurerm_key_vault'
- desc 'Verifies settings and configuration for an Azure Key Vault'
- example <<-EXAMPLE
- describe azurerm_key_vault(resource_group: 'rg-1', vault_name: 'vault-1') do
- it { should exist }
- its('name') { should eq('vault-1') }
- end
- EXAMPLE
-
- ATTRS = %i(
- id
- name
- location
- type
- tags
- properties
- ).freeze
-
- attr_reader(*ATTRS)
-
- def initialize(resource_group: nil, vault_name: nil)
- key_vault = management.key_vault(resource_group, vault_name)
- return if has_error?(key_vault)
-
- assign_fields(ATTRS, key_vault)
-
- @exists = true
- end
-
- def diagnostic_settings
- @diagnostic_settings ||= management.key_vault_diagnostic_settings(id)
- end
-
- def to_s
- "Azure Key Vault: '#{name}'"
- end
-end
diff --git a/libraries/azurerm_key_vaults.rb b/libraries/azurerm_key_vaults.rb
deleted file mode 100644
index 4ea4e111a..000000000
--- a/libraries/azurerm_key_vaults.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-require 'json'
-
-class AzurermKeyVaults < AzurermPluralResource
- name 'azurerm_key_vaults'
- desc 'Verifies settings for a collection of Azure Key Vaults'
- example <<-EXAMPLE
- describe azurerm_key_vaults(resource_group: 'rg-1') do
- it { should exist }
- its('names') { should include 'vault-1'}
- end
- EXAMPLE
-
- attr_reader :table
-
- FilterTable.create
- .register_column(:ids, field: :id)
- .register_column(:names, field: :name)
- .register_column(:locations, field: :location)
- .register_column(:types, field: :type)
- .register_column(:tags, field: :tag)
- .register_column(:properties, field: :properties)
- .install_filter_methods_on_resource(self, :table)
-
- def initialize(resource_group: nil)
- vaults = management.key_vaults(resource_group)
- return if has_error?(vaults)
-
- @table = vaults
- end
-
- def to_s
- 'Azure Key Vaults'
- end
-end
diff --git a/libraries/azurerm_mysql_server.rb b/libraries/azurerm_mysql_server.rb
deleted file mode 100644
index 5d362c53e..000000000
--- a/libraries/azurerm_mysql_server.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermMySqlServer < AzurermSingularResource
- name 'azurerm_mysql_server'
- desc 'Verifies settings for an Azure My SQL Server'
- example <<-EXAMPLE
- describe azure_mysql_server(resource_group: 'rg-1', server_name: 'my-server-name') do
- it { should exist }
- end
- EXAMPLE
-
- ATTRS = %i(
- id
- name
- sku
- location
- type
- tags
- properties
- ).freeze
-
- attr_reader(*ATTRS)
-
- def initialize(resource_group: nil, server_name: nil)
- mysql_server = management.mysql_server(resource_group, server_name)
- return if has_error?(mysql_server)
-
- assign_fields(ATTRS, mysql_server)
-
- @resource_group = resource_group
- @server_name = server_name
- @exists = true
- end
-
- def firewall_rules
- @firewall_rules ||= management.mysql_server_firewall_rules(@resource_group, @server_name)
- end
-
- def to_s
- "Azure MySQL Server: '#{name}'"
- end
-end
diff --git a/libraries/azurerm_mysql_servers.rb b/libraries/azurerm_mysql_servers.rb
deleted file mode 100644
index 4685f3c9b..000000000
--- a/libraries/azurerm_mysql_servers.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-require 'json'
-
-class AzurermMySqlServers < AzurermPluralResource
- name 'azurerm_mysql_servers'
- desc 'Verifies settings for a collection of Azure MySQL Servers'
- example <<-EXAMPLE
- describe azurerm_mysql_servers do
- its('names') { should include 'my-sql-server' }
- end
- EXAMPLE
-
- attr_reader :table
-
- FilterTable.create
- .register_column(:ids, field: :id)
- .register_column(:names, field: :name)
- .register_column(:skus, field: :sku)
- .register_column(:locations, field: :location)
- .register_column(:properties, field: :properties)
- .register_column(:tags, field: :tags)
- .register_column(:types, field: :type)
- .install_filter_methods_on_resource(self, :table)
-
- def initialize(resource_group: nil)
- servers = management.mysql_servers(resource_group)
- return if has_error?(servers)
-
- @table = servers
- end
-
- def to_s
- 'Azure MySQL Servers'
- end
-end
diff --git a/libraries/azurerm_network_security_group.rb b/libraries/azurerm_network_security_group.rb
deleted file mode 100644
index 25b35a72e..000000000
--- a/libraries/azurerm_network_security_group.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermNetworkSecurityGroup < AzurermSingularResource
- name 'azurerm_network_security_group'
- desc 'Verifies settings for Network Security Groups'
- example <<-EXAMPLE
- describe azurerm_network_security_group(resource_group: 'example', name: 'name') do
- its(name) { should eq 'name'}
- end
- EXAMPLE
-
- ATTRS = %i(
- name
- id
- etag
- type
- location
- tags
- properties
- ).freeze
-
- attr_reader(*ATTRS)
-
- def initialize(resource_group: nil, name: nil)
- resp = management.network_security_group(resource_group, name)
- return if has_error?(resp)
-
- assign_fields(ATTRS, resp)
-
- @exists = true
- end
-
- def to_s
- "'#{name}' Network Security Group"
- end
-
- def security_rules
- @security_rules ||= @properties['securityRules']
- end
-
- def default_security_rules
- @default_security_rules ||= @properties['defaultSecurityRules']
- end
-
- def allow_ssh_from_internet?
- allow_port_from_internet?('22')
- end
- RSpec::Matchers.alias_matcher :allow_ssh_from_internet, :be_allow_ssh_from_internet
-
- def allow_rdp_from_internet?
- allow_port_from_internet?('3389')
- end
- RSpec::Matchers.alias_matcher :allow_rdp_from_internet, :be_allow_rdp_from_internet
-
- SPECIFIC_CRITERIA = %i(specific_port access_allow direction_inbound source_open).freeze
- def allow_port_from_internet?(specific_port)
- @specific_port = specific_port
- matches_criteria?(SPECIFIC_CRITERIA, security_rules_properties)
- end
- RSpec::Matchers.alias_matcher :allow_port_from_internet, :be_allow_port_from_internet
-
- private
-
- def security_rules_properties
- security_rules.collect { |rule| rule['properties'] }
- end
-
- def matches_criteria?(criteria, properties)
- properties.any? { |property| criteria.all? { |method| send(:"#{method}?", property) } }
- end
-
- def specific_port?(properties)
- matches_port?(destination_port_ranges(properties), @specific_port)
- end
-
- def destination_port_ranges(properties)
- properties_hash = properties.to_h
- return Array(properties['destinationPortRange']) if !properties_hash.include?(:destinationPortRanges)
-
- return properties['destinationPortRanges'] if !properties_hash.include?(:destinationPortRange)
-
- properties['destinationPortRanges'] + Array(properties['destinationPortRange'])
- end
-
- def matches_port?(ports, match_port)
- return true if ports.detect { |p| p =~ /^(#{match_port}|\*)$/ }
-
- ports.select { |port| port.include?('-') }
- .collect { |range| range.split('-') }
- .any? { |range| (range.first.to_i..range.last.to_i).cover?(match_port.to_i) }
- end
-
- def tcp?(properties)
- properties['protocol'].match?(/TCP|\*/)
- end
-
- def access_allow?(properties)
- properties['access'] == 'Allow'
- end
-
- def source_open?(properties)
- properties_hash = properties.to_h
- if properties_hash.include?(:sourceAddressPrefix)
- return properties['sourceAddressPrefix'] =~ %r{\*|0\.0\.0\.0|\/0|\/0|Internet|any}
- end
- if properties_hash.include?(:sourceAddressPrefixes)
- return properties['sourceAddressPrefixes'].include?('0.0.0.0')
-
- end
- end
-
- def direction_inbound?(properties)
- properties['direction'] == 'Inbound'
- end
-end
diff --git a/libraries/azurerm_network_security_groups.rb b/libraries/azurerm_network_security_groups.rb
deleted file mode 100644
index 8e5417d8b..000000000
--- a/libraries/azurerm_network_security_groups.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermNetworkSecurityGroups < AzurermPluralResource
- name 'azurerm_network_security_groups'
- desc 'Verifies settings for Network Security Groups'
- example <<-EXAMPLE
- azurerm_network_security_groups(resource_group: 'example') do
- it{ should exist }
- end
- EXAMPLE
-
- attr_reader :table
-
- FilterTable.create
- .register_column(:names, field: 'name')
- .install_filter_methods_on_resource(self, :table)
-
- def initialize(resource_group: nil)
- resp = management.network_security_groups(resource_group)
- return if has_error?(resp)
-
- @table = resp
- end
-
- include Azure::Deprecations::StringsInWhereClause
-
- def to_s
- 'Network Security Groups'
- end
-end
diff --git a/libraries/azurerm_subnet.rb b/libraries/azurerm_subnet.rb
deleted file mode 100644
index b507d40a2..000000000
--- a/libraries/azurerm_subnet.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermSubnet < AzurermSingularResource
- name 'azurerm_subnet'
- desc 'Verifies settings for an Azure Virtual Network Subnet'
- example <<-EXAMPLE
- describe azurerm_subnet(resource_group: 'example',vnet: 'virtual-network-name' name: 'subnet-name') do
- it { should exist }
- its('name') { should eq 'subnet-name' }
- end
- EXAMPLE
-
- ATTRS = %i(
- id
- name
- type
- properties
- ).freeze
-
- attr_reader(*ATTRS)
-
- def initialize(resource_group: nil, vnet: nil, name: nil)
- resp = management.subnet(resource_group, vnet, name)
- return if has_error?(resp)
-
- assign_fields(ATTRS, resp)
- @exists = true
- end
-
- def address_prefix
- properties['addressPrefix']
- end
-
- def nsg
- return nil unless properties.respond_to?(:networkSecurityGroup)
- @nsg ||= id_to_h(properties.networkSecurityGroup.id)[:network_security_groups]
- end
-
- def to_s
- "'#{name}' subnet"
- end
-end
diff --git a/libraries/azurerm_subnets.rb b/libraries/azurerm_subnets.rb
deleted file mode 100644
index 5e8465d90..000000000
--- a/libraries/azurerm_subnets.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermSubnets < AzurermPluralResource
- name 'azurerm_subnets'
- desc 'Verifies settings for Azure Virtual Network Subnets'
- example <<-EXAMPLE
- azurerm_subnets(resource_group: 'example', vnet: 'virtual-network-name') do
- it{ should exist }
- end
- EXAMPLE
-
- FilterTable.create
- .register_column(:names, field: 'name')
- .install_filter_methods_on_resource(self, :table)
-
- attr_reader :table
-
- def initialize(resource_group: nil, vnet: nil)
- resp = management.subnets(resource_group, vnet)
- return if has_error?(resp)
- @vnet = vnet
- @table = resp
- end
-
- include Azure::Deprecations::StringsInWhereClause
-
- def to_s
- "Azure Subnets for virtual network: '#{@vnet}'"
- end
-end
diff --git a/libraries/azurerm_virtual_machine.rb b/libraries/azurerm_virtual_machine.rb
deleted file mode 100644
index 53164571f..000000000
--- a/libraries/azurerm_virtual_machine.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermVirtualMachine < AzurermSingularResource
- name 'azurerm_virtual_machine'
- desc 'Verifies settings for an Azure Virtual Machine'
- example <<-EXAMPLE
- describe azurerm_virtual_machine(resource_group: 'example', name: 'vm-name') do
- it { should have_monitoring_agent_installed }
- end
- EXAMPLE
-
- ATTRS = %i(
- id
- name
- location
- properties
- resources
- tags
- type
- zones
- ).freeze
-
- attr_reader(*ATTRS)
-
- def initialize(resource_group: nil, name: nil)
- resp = management.virtual_machine(resource_group, name)
- return if has_error?(resp)
-
- assign_fields(ATTRS, resp)
-
- @exists = true
- end
-
- def to_s
- "'#{name}' Virtual Machine"
- end
-
- def installed_extensions_types
- @installed_extensions_types ||= Array(resources).map do |extension|
- extension.properties.type
- end
- end
-
- def has_only_approved_extensions?(approved_extensions)
- (installed_extensions_types - approved_extensions).empty?
- end
-
- def has_endpoint_protection_installed?(endpoint_protection_extensions)
- installed_extensions_types.any? { |extension| endpoint_protection_extensions.include?(extension) }
- end
-
- def installed_extensions_names
- @installed_extensions_names ||= Array(resources).map do |extension|
- extension['name']
- end
- end
-
- def has_monitoring_agent_installed?
- return false unless properties.osProfile.key?(:windowsConfiguration)
-
- Array(resources).any? do |extension|
- status = extension.properties.provisioningState
- type = extension.properties.type
-
- type == 'MicrosoftMonitoringAgent' && status == 'Succeeded'
- end
- end
-
- def os_disk_name
- properties.storageProfile.osDisk.name
- end
-
- def data_disk_names
- Array(properties.storageProfile.dataDisks).map(&:name)
- end
-end
diff --git a/libraries/azurerm_virtual_machines.rb b/libraries/azurerm_virtual_machines.rb
deleted file mode 100644
index d1664b658..000000000
--- a/libraries/azurerm_virtual_machines.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermVirtualMachines < AzurermPluralResource
- name 'azurerm_virtual_machines'
- desc 'Verifies settings for Azure Virtual Machines'
- example <<-EXAMPLE
- azurerm_virtual_machines(resource_group: 'example') do
- it{ should exist }
- end
- EXAMPLE
-
- FilterTable.create
- .register_column(:os_disks, field: 'os_disk')
- .register_column(:data_disks, field: 'data_disks')
- .register_column(:vm_names, field: 'name')
- .install_filter_methods_on_resource(self, :table)
-
- attr_reader :table
-
- def initialize(resource_group: nil)
- resp = management.virtual_machines(resource_group)
- return if has_error?(resp)
-
- @table = resp.collect(&with_platform)
- .collect(&with_os_disk)
- .collect(&with_data_disks)
- end
-
- include Azure::Deprecations::StringsInWhereClause
-
- def to_s
- 'Azure Virtual Machines'
- end
-
- def with_platform
- lambda do |vm|
- os_profile = vm.properties.osProfile
-
- platform = \
- if os_profile.key?(:windowsConfiguration)
- 'windows'
- elsif os_profile.key?(:linuxConfiguration)
- 'linux'
- else
- 'unknown'
- end
-
- Azure::Response.create(vm.members << :platform, vm.values << platform)
- end
- end
-
- def with_os_disk
- lambda do |vm|
- os_disk = vm.properties.storageProfile.osDisk
-
- disk_name = os_disk.key?(:name) ? os_disk.name : ''
-
- Azure::Response.create(vm.members << :os_disk, vm.values << disk_name)
- end
- end
-
- def with_data_disks
- lambda do |vm|
- disks = Array(vm.properties.storageProfile.dataDisks)
-
- Azure::Response.create(vm.members << :data_disks, vm.values << disks.collect(&:name))
- end
- end
-end
diff --git a/libraries/azurerm_virtual_network.rb b/libraries/azurerm_virtual_network.rb
deleted file mode 100644
index 1a3588779..000000000
--- a/libraries/azurerm_virtual_network.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermVirtualNetwork < AzurermSingularResource
- name 'azurerm_virtual_network'
- desc 'Verifies settings for an Azure Virtual Network'
- example <<-EXAMPLE
- describe azurerm_virtual_network(resource_group: 'example', name: 'vnet-name') do
- it { should have_monitoring_agent_installed }
- end
- EXAMPLE
-
- ATTRS = %i(
- id
- name
- location
- type
- tags
- properties
- ).freeze
-
- attr_reader(*ATTRS)
-
- def initialize(resource_group: nil, name: nil)
- resp = management.virtual_network(resource_group, name)
- return if has_error?(resp)
-
- assign_fields(ATTRS, resp)
-
- @subs = Array(resp.properties.subnets) || []
-
- @exists = true
- end
-
- def address_space
- properties.addressSpace.addressPrefixes
- end
-
- def dns_servers
- properties.dhcpOptions.dnsServers
- end
-
- def vnet_peerings
- name_id = ->(peer) { [peer.name, peer.properties.remoteVirtualNetwork.id] }
- any_nils = ->(pair) { pair.any?(&:nil?) }
-
- properties.virtualNetworkPeerings.collect(&name_id).reject(&any_nils).to_h
- end
-
- def enable_ddos_protection
- properties.enableDdosProtection
- end
-
- def enable_vm_protection
- properties.enableVmProtection
- end
-
- def subnets
- @subs.collect(&:name)
- end
-
- def to_s
- "'#{name}' Virtual Network"
- end
-end
diff --git a/libraries/azurerm_virtual_networks.rb b/libraries/azurerm_virtual_networks.rb
deleted file mode 100644
index 35887abe7..000000000
--- a/libraries/azurerm_virtual_networks.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_resource'
-
-class AzurermVirtualNetworkss < AzurermPluralResource
- name 'azurerm_virtual_networks'
- desc 'Verifies settings for Azure Virtual Networks'
- example <<-EXAMPLE
- azurerm_virtual_networks(resource_group: 'example') do
- it{ should exist }
- end
- EXAMPLE
-
- FilterTable.create
- .register_column(:names, field: :name)
- .install_filter_methods_on_resource(self, :table)
-
- attr_reader :table
-
- def initialize(resource_group: nil)
- resp = management.virtual_networks(resource_group)
- return if has_error?(resp)
-
- @table = resp
- end
-
- include Azure::Deprecations::StringsInWhereClause
-
- def to_s
- 'Azure Virtual Networks'
- end
-end
diff --git a/libraries/backend/azure_connection.rb b/libraries/backend/azure_connection.rb
new file mode 100644
index 000000000..319d06a1f
--- /dev/null
+++ b/libraries/backend/azure_connection.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+require 'backend/helpers'
+
+# Client class to manage the Azure REST API connection.
+#
+# An instance of this class will:
+# - create a HTTP client.
+# - read environment variables and gather necessary information for authentication.
+# - authenticate with Azure Rest API and gets an access token.
+# - make the access token available to use.
+class AzureConnection
+ @@token_data = HashRecursive.recursive
+ # This will be included in headers for statistical purposes.
+ INSPEC_USER_AGENT = 'pid-18d63047-6cdf-4f34-beed-62f01fc73fc2'
+
+ # @return [String] the resource management endpoint url
+ attr_reader :resource_manager_endpoint_url
+
+ # @return [String] the resource management endpoint api version, e.g. 2020-01-01
+ attr_reader :resource_manager_endpoint_api_version
+
+ # @return [String] the graph api endpoint url
+ attr_reader :graph_api_endpoint_url
+
+ # @return [String] the graph api endpoint api version, e.g. v1.0
+ attr_reader :graph_api_endpoint_api_version
+
+ # @return [Hash] tenant_id, client_id, client_secret, subscription_id
+ attr_reader :credentials
+
+ # Creates a HTTP client.
+ def initialize(client_args)
+ # Validate parameter's type.
+ raise ArgumentError, 'Parameters must be provided in an Hash object.' unless client_args.is_a?(Hash)
+
+ # The valid client args:
+ # - endpoint: [String]
+ # azure_cloud, azure_china_cloud, azure_us_government_L4, azure_us_government_L5, azure_german_cloud
+ # - azure_retry_limit: [Integer] Maximum number of retries (default - 2)
+ # - azure_retry_backoff: [Integer] Pause in seconds between retries (default - 0)
+ # - azure_retry_backoff_factor: [Integer] The amount to multiply each successive retry's interval amount by
+ # in order to provide back-off (default - 1)
+ @client_args = client_args
+
+ raise StandardError, 'Endpoint has to be provided to establish a connection with Azure REST API.' \
+ unless @client_args.key?(:endpoint)
+ @resource_manager_endpoint_url = @client_args[:endpoint].resource_manager_endpoint_url
+ @resource_manager_endpoint_api_version = @client_args[:endpoint].resource_manager_endpoint_api_version
+ @graph_api_endpoint_url = @client_args[:endpoint].graph_api_endpoint_url
+ @graph_api_endpoint_api_version = @client_args[:endpoint].graph_api_endpoint_api_version
+
+ @credentials = {
+ tenant_id: ENV['AZURE_TENANT_ID'],
+ client_id: ENV['AZURE_CLIENT_ID'],
+ client_secret: ENV['AZURE_CLIENT_SECRET'],
+ subscription_id: ENV['AZURE_SUBSCRIPTION_ID'],
+ }
+ # Validate the presence of credentials.
+ unless @credentials.values.compact.delete_if(&:empty?).size == 4
+ raise HTTPClientError::MissingCredentials, 'The following must be set in the Environment:'\
+ " #{@credentials.keys}.\n"\
+ "Provided: #{@credentials}"
+ end
+
+ @connection ||= Faraday.new do |conn|
+ # Implement user provided HTTP client params for handling TimeOut exceptions.
+ # https://www.rubydoc.info/gems/faraday/Faraday/Request/Retry
+ conn.request(:retry, max: @client_args[:azure_retry_limit],
+ interval: @client_args[:azure_retry_backoff],
+ backoff_factor: @client_args[:azure_retry_backoff_factor])
+ # Convert response to a JSON object and symbolize names.
+ conn.response :json, content_type: /\bjson$/, parser_options: { symbolize_names: true }
+ conn.adapter Faraday.default_adapter
+ end
+ end
+
+ # Make a HTTP GET request to Azure Rest API.
+ #
+ # Azure Rest API requires access token for every query.
+ # If a token data exist for a resource it will be used, if not, new one will be created by #authenticate.
+ #
+ # @return [Hash] The HTTP response body as a JSON object. Properties can be accessed via symbol key names.
+ # @param url [String] The url without any parameters or headers.
+ # @param params [Hash] The query parameters without the api version.
+ def rest_get_call(url, params = {})
+ # Update access token if expired.
+ uri = URI(url)
+ resource = "#{uri.scheme}://#{uri.host}"
+ authenticate(resource) if @@token_data[resource.to_sym].empty?
+ authenticate(resource) if Time.now > @@token_data[resource.to_sym][:token_expires_on]
+ # Create the necessary headers.
+ headers = {}
+ headers['User-Agent'] = INSPEC_USER_AGENT
+ headers['Authorization'] = "#{@@token_data[resource.to_sym][:token_type]} #{@@token_data[resource.to_sym][:token]}"
+ headers['Content-Type'] = 'application/json'
+ # For graph api, api_version is embedded into the url
+ resp = @connection.get(uri) do |req|
+ req.params = req.params.merge(params) unless params.empty?
+ req.headers = headers
+ end
+ if resp.status == 200
+ resp.body
+ else
+ fail_api_query(resp)
+ end
+ end
+
+ # Get the access token for Azure Rest API.
+ #
+ # @return [nil]
+ #
+ # Following class variables will be created:
+ # @@token_data[:resource][:token] access_token for Azure Rest API queries
+ # @@token_data[:resource][:token_expires_on] [TimeClass]
+ # @@token_data[:resource][:token_type] token_type, e.g.: Bearer
+ #
+ # https://docs.microsoft.com/en-us/rest/api/azure/
+ #
+ def authenticate(resource)
+ # Build up the url that is required to authenticate with Azure REST API
+ auth_url = "#{@client_args[:endpoint].active_directory_endpoint_url}#{@credentials[:tenant_id]}/oauth2/token"
+ body = {
+ grant_type: 'client_credentials',
+ client_id: @credentials[:client_id],
+ client_secret: @credentials[:client_secret],
+ resource: resource,
+ }
+ headers = {
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ 'Accept' => 'application/json',
+ }
+ resp = @connection.post(auth_url) do |req|
+ req.body = URI.encode_www_form(body)
+ req.headers = headers
+ end
+ if resp.status == 200
+ response_body = resp.body
+ @@token_data[resource.to_sym][:token] = response_body[:access_token]
+ @@token_data[resource.to_sym][:token_expires_on] = Time.at(Integer(response_body[:expires_on]))
+ @@token_data[resource.to_sym][:token_type] = response_body[:token_type]
+ else
+ fail_api_query(resp)
+ end
+ end
+
+ # Raise custom exceptions for failed Azure Rest API calls.
+ #
+ # @raise [UnsuccessfulAPIQuery]
+ #
+ def fail_api_query(resp, message = nil)
+ message ||= "Unsuccessful HTTP request to Azure REST API.\n"
+ message += "HTTP #{resp.status}.\n"
+ body = resp.body
+ unless body.empty?
+ error = body[:error]
+ if error&.is_a?(Hash)
+ code = error[:code]
+ error_message = error[:message]
+ message += "#{code} #{error_message}"
+ end
+ message += resp.body.to_s if code.nil?
+ end
+ resource_not_found_codes = %w{Request_ResourceNotFound ResourceGroupNotFound ResourceNotFound NotFound}
+ wrong_api_keyword = 'The supported api-versions are'
+ invalid_api_codes = %w{InvalidApiVersionParameter NoRegisteredProviderFound InvalidResourceType}
+ if code
+ if invalid_api_codes.include?(code) && error_message&.include?(wrong_api_keyword)
+ raise UnsuccessfulAPIQuery::UnexpectedHTTPResponse::InvalidApiVersionParameter, error_message
+ elsif resource_not_found_codes.include?(code)
+ raise UnsuccessfulAPIQuery::ResourceNotFound, error_message
+ end
+ end
+ raise UnsuccessfulAPIQuery::UnexpectedHTTPResponse, message
+ end
+end
diff --git a/libraries/backend/azure_environment.rb b/libraries/backend/azure_environment.rb
new file mode 100644
index 000000000..8c892d211
--- /dev/null
+++ b/libraries/backend/azure_environment.rb
@@ -0,0 +1,162 @@
+# encoding: utf-8
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See License.txt in the project root for license information.
+
+module MicrosoftRestAzure
+ module AzureEnvironments
+ #
+ # An instance of this class describes an environment in Azure
+ #
+ class AzureEnvironment
+ # @return [String] the Environment name
+ attr_reader :name
+
+ # @return [String] the management portal URL
+ attr_reader :portal_url
+
+ # @return [String] the publish settings file URL
+ attr_reader :publishing_profile_url
+
+ # @return [String] the management service endpoint
+ attr_reader :management_endpoint_url
+
+ # @return [String] the resource management endpoint
+ attr_reader :resource_manager_endpoint_url
+
+ # @return [String] the sql server management endpoint for mobile commands
+ attr_reader :sql_management_endpoint_url
+
+ # @return [String] the dns suffix for sql servers
+ attr_reader :sql_server_hostname_suffix
+
+ # @return [String] the template gallery endpoint
+ attr_reader :gallery_endpoint_url
+
+ # @return [String] the Active Directory login endpoint
+ attr_reader :active_directory_endpoint_url
+
+ # @return [String] the resource ID to obtain AD tokens for
+ attr_reader :active_directory_resource_id
+
+ # @return [String] the Active Directory resource ID
+ attr_reader :active_directory_graph_resource_id
+
+ # @return [String] the Active Directory resource ID
+ attr_reader :active_directory_graph_api_version
+
+ # @return [String] the endpoint suffix for storage accounts
+ attr_reader :storage_endpoint_suffix
+
+ # @return [String] the KeyVault service dns suffix
+ attr_reader :key_vault_dns_suffix
+
+ # @return [String] the data lake store filesystem service dns suffix
+ attr_reader :datalake_store_filesystem_endpoint_suffix
+
+ # @return [String] the data lake analytics job and catalog service dns suffix
+ attr_reader :datalake_analytics_catalog_and_job_endpoint_suffix
+
+ # @return [Boolean] determines whether the authentication endpoint should be validated with Azure AD. Default value is true.
+ attr_reader :validate_authority
+
+ def initialize(options)
+ required_properties = [:name, :portal_url, :management_endpoint_url, :resource_manager_endpoint_url, :active_directory_endpoint_url, :active_directory_resource_id]
+
+ required_supplied_properties = required_properties & options.keys
+
+ if required_supplied_properties.nil? || required_supplied_properties.empty? || (required_supplied_properties & required_properties) != required_properties
+ raise ArgumentError, "#{required_properties} are the required properties but provided properties are #{options}"
+ end
+
+ required_supplied_properties.each do |prop|
+ if options[prop].nil? || !options[prop].is_a?(String) || options[prop].empty?
+ raise ArgumentError, "Value of the '#{prop}' property must be of type String and non empty."
+ end
+ end
+
+ # Setting default to true
+ @validate_authority = true
+
+ options.each do |k, v|
+ instance_variable_set("@#{k}", v) unless v.nil?
+ end
+ end
+ end
+
+ AzureCloud = AzureEnvironments::AzureEnvironment.new({
+ name: 'AzureCloud',
+ portal_url: 'https://portal.azure.com',
+ publishing_profile_url: 'http://go.microsoft.com/fwlink/?LinkId=254432',
+ management_endpoint_url: 'https://management.core.windows.net',
+ resource_manager_endpoint_url: 'https://management.azure.com/',
+ sql_management_endpoint_url: 'https://management.core.windows.net:8443/',
+ sql_server_hostname_suffix: '.database.windows.net',
+ gallery_endpoint_url: 'https://gallery.azure.com/',
+ active_directory_endpoint_url: 'https://login.microsoftonline.com/',
+ active_directory_resource_id: 'https://management.core.windows.net/',
+ active_directory_graph_resource_id: 'https://graph.windows.net/',
+ active_directory_graph_api_version: '2013-04-05',
+ storage_endpoint_suffix: '.core.windows.net',
+ key_vault_dns_suffix: '.vault.azure.net',
+ datalake_store_filesystem_endpoint_suffix: 'azuredatalakestore.net',
+ datalake_analytics_catalog_and_job_endpoint_suffix: 'azuredatalakeanalytics.net',
+ })
+ AzureChinaCloud = AzureEnvironments::AzureEnvironment.new({
+ name: 'AzureChinaCloud',
+ portal_url: 'https://portal.azure.cn',
+ publishing_profile_url: 'http://go.microsoft.com/fwlink/?LinkID=301774',
+ management_endpoint_url: 'https://management.core.chinacloudapi.cn',
+ resource_manager_endpoint_url: 'https://management.chinacloudapi.cn',
+ sql_management_endpoint_url: 'https://management.core.chinacloudapi.cn:8443/',
+ sql_server_hostname_suffix: '.database.chinacloudapi.cn',
+ gallery_endpoint_url: 'https://gallery.chinacloudapi.cn/',
+ active_directory_endpoint_url: 'https://login.chinacloudapi.cn/',
+ active_directory_resource_id: 'https://management.core.chinacloudapi.cn/',
+ active_directory_graph_resource_id: 'https://graph.chinacloudapi.cn/',
+ active_directory_graph_api_version: '2013-04-05',
+ storage_endpoint_suffix: '.core.chinacloudapi.cn',
+ key_vault_dns_suffix: '.vault.azure.cn',
+ # TODO: add dns suffixes for the china cloud for datalake store and datalake analytics once they are defined.
+ datalake_store_filesystem_endpoint_suffix: 'N/A',
+ datalake_analytics_catalog_and_job_endpoint_suffix: 'N/A',
+ })
+ AzureUSGovernment = AzureEnvironments::AzureEnvironment.new({
+ name: 'AzureUSGovernment',
+ portal_url: 'https://portal.azure.us',
+ publishing_profile_url: 'https://manage.windowsazure.us/publishsettings/index',
+ management_endpoint_url: 'https://management.core.usgovcloudapi.net',
+ resource_manager_endpoint_url: 'https://management.usgovcloudapi.net',
+ sql_management_endpoint_url: 'https://management.core.usgovcloudapi.net:8443/',
+ sql_server_hostname_suffix: '.database.usgovcloudapi.net',
+ gallery_endpoint_url: 'https://gallery.usgovcloudapi.net/',
+ active_directory_endpoint_url: 'https://login.microsoftonline.us/',
+ active_directory_resource_id: 'https://management.core.usgovcloudapi.net/',
+ active_directory_graph_resource_id: 'https://graph.windows.net/',
+ active_directory_graph_api_version: '2013-04-05',
+ storage_endpoint_suffix: '.core.usgovcloudapi.net',
+ key_vault_dns_suffix: '.vault.usgovcloudapi.net',
+ # TODO: add dns suffixes for the US government for datalake store and datalake analytics once they are defined.
+ datalake_store_filesystem_endpoint_suffix: 'N/A',
+ datalake_analytics_catalog_and_job_endpoint_suffix: 'N/A',
+ })
+ AzureGermanCloud = AzureEnvironments::AzureEnvironment.new({
+ name: 'AzureGermanCloud',
+ portal_url: 'http://portal.microsoftazure.de/',
+ publishing_profile_url: 'https://manage.microsoftazure.de/publishsettings/index',
+ management_endpoint_url: 'https://management.core.cloudapi.de',
+ resource_manager_endpoint_url: 'https://management.microsoftazure.de',
+ sql_management_endpoint_url: 'https://management.core.cloudapi.de:8443/',
+ sql_server_hostname_suffix: '.database.cloudapi.de',
+ gallery_endpoint_url: 'https://gallery.cloudapi.de/',
+ active_directory_endpoint_url: 'https://login.microsoftonline.de/',
+ active_directory_resource_id: 'https://management.core.cloudapi.de/',
+ active_directory_graph_resource_id: 'https://graph.cloudapi.de/',
+ active_directory_graph_api_version: '2013-04-05',
+ storage_endpoint_suffix: '.core.cloudapi.de',
+ key_vault_dns_suffix: '.vault.microsoftazure.de',
+ # TODO: add dns suffixes for the US government for datalake store and datalake analytics once they are defined.
+ datalake_store_filesystem_endpoint_suffix: 'N/A',
+ datalake_analytics_catalog_and_job_endpoint_suffix: 'N/A',
+ })
+ end
+end
diff --git a/libraries/backend/azure_require.rb b/libraries/backend/azure_require.rb
new file mode 100644
index 000000000..bd7e33089
--- /dev/null
+++ b/libraries/backend/azure_require.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+require 'backend/azure_connection'
+require 'backend/azure_environment'
+require 'backend/azure_security_rules_helpers'
+require 'backend/helpers'
diff --git a/libraries/backend/azure_security_rules_helpers.rb b/libraries/backend/azure_security_rules_helpers.rb
new file mode 100644
index 000000000..5c7f11d64
--- /dev/null
+++ b/libraries/backend/azure_security_rules_helpers.rb
@@ -0,0 +1,298 @@
+require 'backend/helpers'
+
+# Normalise Azure security rules to make them comparable with criteria or other security rules.
+#
+class NormalizeSecurityRule
+ CIDR_IPV4_REG = %r{^([0-9]{1,3}\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$}i.freeze
+ CIDR_IPV6_REG = %r{^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$}i.freeze
+ PORT_RANGE = ('0'..'65535').freeze
+
+ attr_reader :name, :etag, :id, :access, :direction, :protocol, :priority
+
+ def initialize(rule)
+ allowed_types = %w{Microsoft.Network/networkSecurityGroups/defaultSecurityRules \
+ Microsoft.Network/networkSecurityGroups/securityRules}
+ unless allowed_types.include?(rule.type)
+ raise ArgumentError, "Security rule should be either one of #{allowed_types}. Provided: `#{rule.type}`."
+ end
+ @properties = rule.properties
+ @name = rule.name.downcase
+ @id = rule.id
+ @etag = rule.etag
+ # allow or deny
+ @access = rule.properties.access.downcase
+ # inbound or outbound
+ @direction = @properties.direction.downcase
+ @description = @properties.description.downcase
+ @protocol = @properties.protocol.upcase
+ @priority = @properties.priority
+ end
+
+ def destination_address_prefixes
+ if @properties.destinationAddressPrefixes.empty? || @properties.destinationAddressPrefixes.nil?
+ Array(@properties.destinationAddressPrefix)
+ else
+ @properties.destinationAddressPrefixes
+ end
+ end
+
+ def source_address_prefixes
+ if @properties.sourceAddressPrefixes.empty? || @properties.sourceAddressPrefixes.nil?
+ Array(@properties.sourceAddressPrefix)
+ else
+ @properties.sourceAddressPrefixes
+ end
+ end
+
+ def destination_port_ranges_raw
+ if @properties.destinationPortRanges.empty? || @properties.destinationPortRanges.nil?
+ Array(@properties.destinationPortRange)
+ else
+ @properties.destinationPortRanges
+ end
+ end
+
+ def source_port_ranges_raw
+ if @properties.sourcePortRanges.empty? || @properties.sourcePortRanges.nil?
+ Array(@properties.sourcePortRange)
+ else
+ @properties.sourcePortRanges
+ end
+ end
+
+ def destination_addresses_as_ip
+ if @destination_ip_addresses.nil?
+ ip_and_tag = extract_ip_addresses(destination_address_prefixes)
+ @destination_ip_addresses = ip_and_tag[:ip_addresses]
+ @destination_service_tags ||= ip_and_tag[:service_tags]
+ end
+ @destination_ip_addresses
+ end
+
+ def source_addresses_as_ip
+ if @source_ip_addresses.nil?
+ ip_and_tag = extract_ip_addresses(source_address_prefixes)
+ @source_ip_addresses = ip_and_tag[:ip_addresses]
+ @source_service_tags ||= ip_and_tag[:service_tags]
+ end
+ @source_ip_addresses
+ end
+
+ def destination_addresses_as_service_tag
+ if @destination_service_tags.nil?
+ ip_and_tag = extract_ip_addresses(destination_address_prefixes)
+ @destination_ip_addresses ||= ip_and_tag[:ip_addresses]
+ @destination_service_tags = ip_and_tag[:service_tags]
+ end
+ @destination_service_tags
+ end
+
+ def source_addresses_as_service_tag
+ if @source_service_tags.nil?
+ ip_and_tag = extract_ip_addresses(source_address_prefixes)
+ @source_ip_addresses ||= ip_and_tag[:ip_addresses]
+ @source_service_tags = ip_and_tag[:service_tags]
+ end
+ @source_service_tags
+ end
+
+ def destination_ports
+ extract_ports(destination_port_ranges_raw)
+ end
+
+ def source_ports
+ extract_ports(source_port_ranges_raw)
+ end
+
+ # return one of the following: true, false, nil
+ # nil: The criteria is not within the scope (Ip range) of the rule.
+ # This can be used by the caller method to make a decision, such as moving to another rule.
+ # @param criteria [Hash] Please see comments for the details.
+ # required: access (allow/deny), direction (inbound/outbound)
+ # require_any: destination/source_ip_range destination/source_service_tag
+ # allow: destination/source_port, protocol
+ #
+ # @note:
+ # service_tag test is explicit. If the provided service tag does not exist, it will fail.
+ # E.g: Even though the security rule allows requests from all IP addresses (0.0.0.0),
+ # testing `Internet` source_service_tag will fail.
+ # This is because this resource pack has no control over what IP ranges are included in the Azure service tags.
+ #
+ def compliant?(criteria)
+ allowed = %i(source_port destination_port protocol)
+ required = %i(access direction)
+ require_any = %i(destination_ip_range source_ip_range destination_service_tag source_service_tag)
+ Helpers.validate_parameters(allow: allowed, required: required, require_any_of: require_any, opts: criteria)
+
+ # This will be updated by the relevant checks.
+ compliant = false
+
+ # From this point onwards:
+ # Every check will result either:
+ # true: Continue to the next check.
+ # false: Finish test, this criteria does not comply with the rule.
+ # nil: Finish test, this criteria is not in the scope of the rule.
+ unless criteria[:source_ip_range].nil?
+ within_range = ip_range_check(source_addresses_as_ip, criteria[:source_ip_range])
+ return nil unless within_range
+ compliant = access == criteria[:access]
+ return compliant unless compliant
+ end
+
+ unless criteria[:destination_ip_range].nil?
+ within_range = ip_range_check(destination_addresses_as_ip, criteria[:destination_ip_range])
+ return nil unless within_range
+ compliant = access == criteria[:access]
+ return compliant unless compliant
+ end
+
+ unless criteria[:source_service_tag].nil?
+ within_range = source_addresses_as_service_tag.include?(criteria[:source_service_tag])
+ return nil unless within_range
+ compliant = access == criteria[:access]
+ return compliant unless compliant
+ end
+
+ unless criteria[:destination_service_tag].nil?
+ within_range = destination_addresses_as_service_tag.include?(criteria[:destination_service_tag])
+ return nil unless within_range
+ compliant = access == criteria[:access]
+ return compliant unless compliant
+ end
+
+ # `Any`, `all` will fail.
+ # If this is the case, `protocol` parameter should not be provided at all.
+ unless criteria[:protocol].nil?
+ return nil unless criteria[:protocol].upcase == protocol
+ compliant = access == criteria[:access]
+ compliant = true if protocol == '*' && access == 'allow'
+ compliant = false if protocol == '*' && access == 'deny'
+ return compliant unless compliant
+ end
+
+ # Individual protocol number and/or a range can be provided.
+ # ['8080', '50-60']
+ # '22'
+ unless criteria[:source_port].nil?
+ ports = extract_ports(Array(criteria[:source_port]))
+ return nil unless ports.all? { |p| source_ports.include?(p) }
+ compliant = access == criteria[:access]
+ return compliant unless compliant
+ end
+
+ unless criteria[:destination_port].nil?
+ ports = extract_ports(Array(criteria[:destination_port]))
+ return nil unless ports.all? { |p| destination_ports.include?(p) }
+ compliant = access == criteria[:access]
+ end
+
+ compliant
+ end
+
+ private
+
+ # @return [Boolean]
+ # @param base_ip_ranges [Array] The list of IPAddr objects.
+ # @param criteria_ip_range [String] The IP range or address in CIDR format.
+ #
+ # criteria ip range will be checked whether it is within one of the base ip ranges.
+ #
+ def ip_range_check(base_ip_ranges, criteria_ip_range)
+ unless criteria_ip_range.match?(CIDR_IPV4_REG) || criteria_ip_range.match?(CIDR_IPV6_REG)
+ raise ArgumentError, 'IP range/address must be in CIDR format, e.g: `192.168.0.1/24, 2001:1234::/64`.'
+ end
+ criteria_ip_range_cidr = IPAddr.new(criteria_ip_range)
+ within_range = []
+ base_ip_ranges.each do |b|
+ within_range << b.include?(criteria_ip_range_cidr)
+ end
+ within_range.any? { |check| check == true }
+ end
+
+ # @return [Array] The list of ports as String type.
+ # The range of valid port numbers if '*' provided.
+ # @param sources [Array, Class] The list of ports.
+ #
+ def extract_ports(sources)
+ return PORT_RANGE if sources.any? { |s| s == '*' }
+ sources.each_with_object([]) do |s, ports|
+ if s.include?('-')
+ from, to = s.split('-')
+ ports << [*from..to]
+ else
+ ports << s
+ end
+ end.flatten.sort
+ end
+
+ # @return [Hash] {ip_addresses: A list of IPAddr objects, service_tags: A list of service tags}
+ # @param sources [String] IP addresses in CIDR format or service tags, e.g: '10.0.0.0/24', 'VirtualNetwork'.
+ #
+ def extract_ip_addresses(sources)
+ service_tags = []
+ ip_addresses = sources.each_with_object([]) do |source, ip_adds|
+ if source == '*'
+ ip_adds << IPAddr.new('0.0.0.0/0')
+ elsif source.match?(CIDR_IPV4_REG) || source.match?(CIDR_IPV6_REG)
+ ip_adds << IPAddr.new(source)
+ else
+ service_tags << source
+ end
+ end
+ { ip_addresses: ip_addresses, service_tags: service_tags }
+ end
+end
+
+# Consolidated provided Azure security rules as normalised security rules.
+# Provide helper methods to be used in Azure resources.
+#
+class ConsolidateSecurityRules
+ attr_reader :normalized_security_rules
+
+ # Normalise and sort Azure security rules by their priority.
+ def initialize(rules)
+ @normalized_security_rules = rules.map { |rule| NormalizeSecurityRule.new(rule) }.sort_by(&:priority)
+ end
+
+ # Filter rules by their directions.
+ def one_direction_rules(direction)
+ unless %w{inbound outbound}.include?(direction)
+ raise ArgumentError, "Accepted parameters are `inbound` or `outbound`. Provided `#{direction}`."
+ end
+ @normalized_security_rules.select { |rule| rule.direction == direction }
+ end
+
+ # Filter rules by their access type.
+ def access_type_rules(access)
+ unless %w{allow deny}.include?(access)
+ raise ArgumentError, "Accepted parameters are `allow` or `deny`. Provided `#{access}`."
+ end
+ @normalized_security_rules.select { |rule| rule.access == access }
+ end
+
+ # @return [Boolean] Indicated whether the criteria is compliant to the provided set of security rules or not.
+ # true: The criteria is compliant.
+ # false: The criteria is not compliant or it does not fall into the scope of provided security rules.
+ #
+ # @note: The security rules will be sorted by their priority.
+ # Tests will be started from the highest priority rule.
+ # @param rules [Array] A list of NormalizeSecurityRule objects.
+ # @param criteria [Hash] See NormalizeSecurityRule#compliant?
+ def go_compare(rules, criteria)
+ unless rules.first.is_a?(NormalizeSecurityRule)
+ raise ArgumentError, "Security rules must be a `NormalizeSecurityRule` object, Provided #{rules.first.class}."
+ end
+ raise ArgumentError, "Criteria must be a `Hash` object. Provided #{criteria.class}" unless criteria.is_a?(Hash)
+ # Ensure that the rules are sorted by their priority.
+ rules = rules.sort_by(&:priority)
+ result = false
+ rules.each do |rule|
+ result = rule.compliant?(criteria)
+ break if [FalseClass, TrueClass].include?(result.class)
+ end
+ # If result is nil, that means the criteria does not fall in the scope of any security rules.
+ # Fail safe: If the criteria is not regulated by a rule, then it is not allowed.
+ return false if result.nil?
+ result
+ end
+end
diff --git a/libraries/backend/helpers.rb b/libraries/backend/helpers.rb
new file mode 100644
index 000000000..2c219716f
--- /dev/null
+++ b/libraries/backend/helpers.rb
@@ -0,0 +1,333 @@
+# frozen_string_literal: true
+require 'backend/azure_environment'
+
+# TODO: This file should be updated at every release.
+# Source:
+# https://github.com/Azure/azure-sdk-for-ruby/blob/master/runtime/ms_rest_azure/lib/ms_rest_azure/azure_environment.rb
+# Base module name should be changed to => module MicrosoftRestAzure
+
+# Azure REST API specific errors.
+#
+# If the API returns an invalid api_version error,
+# the suggested api_version can be acquired from the error message and used at consecutive calls.
+#
+# E.g.:
+# rescue UnsuccessfulAPIQuery::UnexpectedHTTPResponse::InvalidApiVersionParameter => e
+# api_version_suggested = e.get_suggested_api
+#
+class UnsuccessfulAPIQuery < StandardError
+ class ResourceNotFound < StandardError; end
+ class UnexpectedHTTPResponse < StandardError
+ class InvalidApiVersionParameter < StandardError
+ # Return a list if the wrong api is not provided.
+ def suggested_api_version(wrong_api_version = nil)
+ # Capture all the api versions within the error message.
+ # This will include the wrong one used in HTTP request.
+ # It has to be removed.
+ # Example for specific resource type api (for detailed description)
+ # "No registered resource provider found for location 'westeurope' and API version '2022-01-01' for type
+ # 'virtualMachines'. The supported api-versions are '2015-05-01-preview, 2015-06-15, 2016-03-30,
+ # 2016-04-30-preview, 2016-08-30, 2017-03-30, 2017-12-01, 2018-04-01, 2018-06-01, 2018-10-01, 2019-03-01,
+ # 2019-07-01, 2019-12-01, 2020-06-01'. The supported locations are 'eastus, eastus2, westus, centralus,
+ # northcentralus, southcentralus, northeurope, westeurope, eastasia, southeastasia, japaneast, japanwest,
+ # australiaeast, australiasoutheast, australiacentral, brazilsouth, southindia, centralindia, westindia,
+ # canadacentral, canadaeast, westus2, westcentralus, uksouth, ukwest, koreacentral, koreasouth, francecentral,
+ # southafricanorth, uaenorth, switzerlandnorth, germanywestcentral, norwayeast'."
+ #
+ # Example for resource manager api (for short description)
+ # "The api-version '2019-10-11' is invalid. The supported versions are '2020-01-01,2019-11-01,2019-10-01,
+ # 2019-09-01,2019-08-01,2019-07-01,2019-06-01,2019-05-10,2019-05-01,2019-03-01,2018-11-01,2018-09-01,
+ # 2018-08-01,2018-07-01,2018-06-01,2018-05-01,2018-02-01,2018-01-01,2017-12-01,2017-08-01,2017-06-01,
+ # 2017-05-10,2017-05-01,2017-03-01,2016-09-01,2016-07-01,2016-06-01,2016-02-01,2015-11-01,2015-01-01,
+ # 2014-04-01-preview,2014-04-01,2014-01-01,2013-03-01,2014-02-26,2014-04'."
+ #
+ # There are cases where the stable api_versions are too old and don't return JSON response.
+ # If the latest stable is too old (based on the age_criteria), then return the preview versions as well.
+ # This is a quick fix until TODO finding a more stable solution.
+ stable_api_versions = message.scan(/\d{4}-\d{2}-\d{2}[,']/).map(&:chop).sort.reverse
+ preview_api_versions = message.scan(/\d{4}-\d{2}-\d{2}-preview/).sort.reverse
+ if wrong_api_version
+ stable_api_versions.delete(wrong_api_version) if stable_api_versions.include?(wrong_api_version)
+ preview_api_versions.delete(wrong_api_version) if preview_api_versions.include?(wrong_api_version)
+ end
+ age_criteria = 2
+ n_a_l = Helpers.normalize_api_list(age_criteria, stable_api_versions, preview_api_versions)
+ n_a_l.first
+ end
+ end
+ end
+end
+
+class HTTPClientError < StandardError
+ class MissingCredentials < StandardError; end
+end
+
+# Create necessary Azure environment variables and provide access to them
+#
+# @example:
+# my_env = AzureEnvironments.get_endpoint('azure_cloud')
+# my_env.resource_manager_endpoint_url => 'https://management.azure.com/'
+#
+# For graph api endpoint urls:
+# https://docs.microsoft.com/en-us/graph/deployments
+# For graph api endpoint api versions:
+# https://docs.microsoft.com/en-us/azure/active-directory/develop/microsoft-graph-intro
+class AzureEnvironments
+ # Following data can be modified if necessary.
+ # TODO: Update API versions if there is a newer version available.
+ ENDPOINTS = {
+ 'azure_cloud' => {
+ resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureCloud.resource_manager_endpoint_url,
+ active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureCloud.active_directory_endpoint_url,
+ resource_manager_endpoint_api_version: '2020-01-01',
+ graph_api_endpoint_url: 'https://graph.microsoft.com',
+ graph_api_endpoint_api_version: 'v1.0',
+ },
+ # The latest version can be acquired from the error message if the current ones don't work.
+ 'azure_china_cloud' => {
+ resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureChinaCloud.resource_manager_endpoint_url,
+ active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureChinaCloud.active_directory_endpoint_url,
+ resource_manager_endpoint_api_version: '2020-01-01',
+ graph_api_endpoint_url: 'https://microsoftgraph.chinacloudapi.cn',
+ graph_api_endpoint_url_api_version: 'v1.0',
+ },
+ 'azure_us_government_L4' => {
+ resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.resource_manager_endpoint_url,
+ active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.active_directory_endpoint_url,
+ resource_manager_endpoint_api_version: '2020-01-01',
+ graph_api_endpoint_url: 'https://graph.microsoft.us',
+ graph_api_endpoint_url_api_version: 'v1.0',
+ },
+ 'azure_us_government_L5' => {
+ resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.resource_manager_endpoint_url,
+ active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureUSGovernment.active_directory_endpoint_url,
+ resource_manager_endpoint_api_version: '2020-01-01',
+ graph_api_endpoint_url: 'https://dod-graph.microsoft.us',
+ graph_api_endpoint_url_api_version: 'v1.0',
+ },
+ 'azure_german_cloud' => {
+ resource_manager_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureGermanCloud.resource_manager_endpoint_url,
+ active_directory_endpoint_url: MicrosoftRestAzure::AzureEnvironments::AzureGermanCloud.active_directory_endpoint_url,
+ resource_manager_endpoint_api_version: '2020-01-01',
+ graph_api_endpoint_url: 'https://graph.microsoft.de',
+ graph_api_endpoint_url_api_version: 'v1.0',
+ },
+ }.freeze
+
+ # @return [String] the resource management endpoint
+ # Used for getting short descriptions of resources including resource_id.
+ attr_reader :resource_manager_endpoint_url
+
+ # @return [String] the resource management endpoint latest api version
+ attr_reader :resource_manager_endpoint_api_version
+
+ # @return [String] the Active Directory login endpoint
+ # Used for authentication.
+ attr_reader :active_directory_endpoint_url
+
+ # @return [String] the graph api endpoint url
+ attr_reader :graph_api_endpoint_url
+
+ # @return [String] the graph api endpoint api version, e.g. v1.0
+ attr_reader :graph_api_endpoint_api_version
+
+ def initialize(options)
+ required_properties = %i(resource_manager_endpoint_url resource_manager_endpoint_api_version)
+
+ required_supplied_properties = required_properties & options.keys
+
+ if required_supplied_properties.nil? || required_supplied_properties.empty? || (required_supplied_properties & required_properties) != required_properties
+ raise ArgumentError, "#{required_properties} are the required properties but provided properties are #{options}"
+ end
+
+ required_supplied_properties.each do |prop|
+ if options[prop].nil? || !options[prop].is_a?(String) || options[prop].empty?
+ raise ArgumentError, "Value of the '#{prop}' property must be of type String and non empty."
+ end
+ end
+
+ options.each do |k, v|
+ instance_variable_set("@#{k}", v) unless v.nil?
+ end
+ end
+
+ # Provide access to the endpoint properties.
+ def self.get_endpoint(endpoint)
+ options = ENDPOINTS[endpoint]
+ new(options)
+ end
+end
+
+# Make Hash return {} when accessing undefined keys.
+class HashRecursive < Hash
+ def self.recursive
+ new { |hash, key| hash[key] = recursive }
+ end
+end
+
+module Helpers
+ # @see https://github.com/inspec/inspec-aws/blob/master/libraries/aws_backend.rb#L209
+ #
+ # @param opts [Hash] The parameters to be validated.
+ # @param resource_name [String] The name of the method/resource that the parameters are validated in.
+ # @param allow [Array] The list of optional parameters.
+ # @param required [Array] The list of required parameters.
+ # @param require_any_of [Array] The list of parameters that at least one of them are required.
+ def self.validate_parameters(resource_name: nil, allow: [], required: nil, require_any_of: nil, opts: {})
+ unless opts.is_a?(Hash)
+ raise ArgumentError, "Parameters must be provided with as a Hash object. Provided #{opts.class}"
+ end
+ if required
+ allow += Helpers.validate_params_required(resource_name, required, opts)
+ end
+ if require_any_of
+ allow += Helpers.validate_params_require_any_of(resource_name, require_any_of, opts)
+ end
+ Helpers.validate_params_allow(allow, opts)
+ true
+ end
+
+ # @return [String] Provided parameter within require only one of parameters.
+ # @param require_only_one_of [Array]
+ def self.validate_params_only_one_of(resource_name, require_only_one_of, opts)
+ # At least one of them has to exist.
+ Helpers.validate_params_require_any_of(resource_name, require_only_one_of, opts)
+ provided = require_only_one_of.select { |i| opts.key?(i) }
+ if provided.size > 1
+ raise ArgumentError, "Either one of #{require_only_one_of} is required. Provided: #{provided}."
+ end
+ # There should be only one parameter at this point.
+ provided.first
+ end
+
+ # @return [Array] Required parameters
+ # @param required [Array]
+ def self.validate_params_required(resource_name, required, opts)
+ raise ArgumentError, "#{resource_name}: `#{required}` must be provided" unless opts.is_a?(Hash) && required.all? { |req| opts.key?(req) && !opts[req].nil? && opts[req] != '' }
+ required
+ end
+
+ # @return [Array] Require any of parameters
+ # @param require_any_of [Array]
+ def self.validate_params_require_any_of(resource_name, require_any_of, opts)
+ raise ArgumentError, "#{resource_name}: One of `#{require_any_of}` must be provided." unless opts.is_a?(Hash) && require_any_of.any? { |req| opts.key?(req) && !opts[req].nil? && opts[req] != '' }
+ require_any_of
+ end
+
+ # @return [Array] Allowed parameters
+ # @param allow [Array]
+ def self.validate_params_allow(allow, opts)
+ raise ArgumentError, 'Arguments or values can not be longer than 256 characters.' if opts.any? { |k, v| k.size > 100 || v.to_s.size > 500 }
+ raise ArgumentError, 'Scalar arguments not supported' unless defined?(opts.keys)
+ raise ArgumentError, 'Unexpected arguments found' unless opts.keys.all? { |a| allow.include?(a) }
+ raise ArgumentError, 'Provided parameter should not be empty' unless opts.values.all? do |a|
+ return true if a.class == Integer
+ !a.empty?
+ end
+ end
+
+ # Convert provided data into Odata query format.
+ # @see
+ # https://www.odata.org/getting-started/basic-tutorial/
+ #
+ # This is a very simple approach, and tested with a very limited data.
+ # The result of the new operators should be tested thoroughly before using this method.
+ #
+ # Supported key words:
+ # - substring_of => substring_of_name: 'Mc'
+ # - starts_with => starts_with_name: 'J'
+ #
+ # @param data [Array, Hash] The data to be used in the query statement.
+ # @return [String] The query string in the Odata format.
+ #
+ def self.odata_query(data)
+ supported_types = [Hash, Array]
+ unless supported_types.include?(data.class)
+ raise ArgumentError, "Data should be #{supported_types}. Provided #{data.class}."
+ end
+ # This approach works for $filter.
+ if data.is_a?(Hash)
+ # TODO: implement 'ne' operator
+ query = data.each_with_object([]) do |(k, v), acc|
+ v = v.delete_suffix('/').delete_prefix('/')
+ if k.to_s.start_with?('substring_of_')
+ acc << "substringof('#{v}',#{k.to_s[13..-1].camelcase(:lower)})"
+ elsif k.to_s.start_with?('starts_with_')
+ acc << "startswith(#{k.to_s[12..-1].camelcase(:lower)},'#{v}')"
+ else
+ acc << "#{k.to_s.camelcase(:lower)} eq '#{v}'"
+ end
+ end.join(' and ')
+ end
+ # This works for `$select, $expand`.
+ if data.is_a?(Array)
+ query = data.join(',')
+ end
+ query
+ end
+
+ def self.validate_resource_uri(resource_uri)
+ resource_uri_format = '/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/'\
+ 'Microsoft.Compute/virtualMachines/{resource_name}'
+ raise ArgumentError, "Resource URI should be in the format of #{resource_uri_format}" \
+ unless resource_uri.start_with?('/subscriptions/') && resource_uri.include?('/providers/')
+ end
+
+ # Disassemble resource_id and extract the resource_group, provider and resource_provider.
+ #
+ # This is the one and only method where the `resource_provider` is defined differently from the rest.
+ # Example: the resource type for the virtual machines is `Microsoft.Compute/virtualMachines` in all other methods.
+ # However, it is divided into 2 entities here as `provider` and `resource_provider`.
+ # provider => `Microsoft.Compute`
+ # resource_provider => `virtualMachines`
+ #
+ # This can be used for acquiring the latest/default api version for a specific provider.
+ #
+ # @see https://docs.microsoft.com/en-us/rest/api/resources/resources/getbyid
+ #
+ # @return [Array] [resource_group, provider, resource_provider]
+ # @param resource_uri [String] The URI of the resource,
+ # /subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/
+ # Microsoft.Compute/virtualMachines/{resource_name}
+ def self.res_group_provider_type_from_uri(resource_uri)
+ Helpers.validate_resource_uri(resource_uri)
+ subscription_resource_group, provider_resource_type = resource_uri.split('/providers/')
+ resource_group = subscription_resource_group.split('/').last
+ interim_array = provider_resource_type.split('/')
+ provider = interim_array[0]
+ # interim array can be one of two
+ # 1- provider/resource_provider/resource/name
+ # 2- provider/parent_resource_type/parent_resource_name/resource_provider/resource_name
+ # For the second case, the desired resource_provider is provide/parent_resource_type/resource_provider
+ # E.g.
+ # if resource_id: "/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Compute/virtualMachines/{vm_name}/extensions/{extension_name}"
+ # provider => "Microsoft.Compute"
+ # resource_provider => "virtualMachines/extensions"
+ resource_type = [interim_array[1], interim_array[3]].compact.join('/')
+ [resource_group, provider, resource_type]
+ end
+
+ # Decide whether to include preview api-versions in the api_Version list.
+ # If the latest stable is too old (based on the age_criteria) and there is a newer preview,
+ # then return the preview versions as well.
+ def self.normalize_api_list(age_criteria, stable_versions, preview_versions)
+ return stable_versions if preview_versions.empty?
+ return preview_versions if stable_versions.empty?
+ r_stable_versions = stable_versions.sort.reverse
+ return_list = r_stable_versions
+ r_preview_versions = preview_versions.sort.reverse
+ latest_stable_year = r_stable_versions.first[0..3].to_i
+ latest_preview_year = r_preview_versions.first[0..3].to_i
+ if latest_stable_year < (Time.now.year - age_criteria) && latest_preview_year > latest_stable_year
+ return_list += r_preview_versions
+ end
+ return_list.sort.reverse
+ end
+
+ # Deprecation message for the old resources.
+ def self.resource_deprecation_message(old_resource_name, new_resource_class)
+ "DEPRECATION: `#{old_resource_name}` uses the new resource `#{new_resource_class}` under the hood. "\
+ "#{old_resource_name} will be deprecated soon and it is advised to switch to the fully backward compatible new resource. "\
+ 'Please see the documentation for the additional features available.'
+ end
+end
diff --git a/libraries/deprecated_resources.rb b/libraries/deprecated_resources.rb
deleted file mode 100644
index 55df93915..000000000
--- a/libraries/deprecated_resources.rb
+++ /dev/null
@@ -1,213 +0,0 @@
-# frozen_string_literal: true
-
-require 'azurerm_monitor_activity_log_alert'
-
-class AzureMonitorActivityLogAlert < AzurermMonitorActivityLogAlert
- name 'azure_monitor_activity_log_alert'
- desc '[DEPRECATED] Please use the azurerm_monitor_activity_log_alert resource'
- example <<-EXAMPLE
- describe azure_monitor_activity_log_alert(resource_group: 'example', name: 'AlertName') do
- it { should exist }
- its('operations') { should include 'Microsoft.Authorization/policyAssignments/write' }
- end
- EXAMPLE
-
- def initialize(resource_group: nil, name: nil)
- warn '[DEPRECATION] The `azure_monitor_activity_log_alert` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_monitor_activity_log_alert` resource instead.'
- super
- end
-end
-
-require 'azurerm_monitor_activity_log_alerts'
-
-class AzureMonitorActivityLogAlerts < AzurermMonitorActivityLogAlerts
- name 'azure_monitor_activity_log_alerts'
- desc '[DEPRECATED] Please use the azurerm_monitor_activity_log_alerts resource'
- example <<-EXAMPLE
- describe azure_monitor_activity_log_alerts do
- its('names') { should include('example-log-alert') }
- end
- EXAMPLE
-
- def initialize
- warn '[DEPRECATION] The `azure_monitor_activity_log_alerts` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_monitor_activity_log_alerts` resource instead.'
- super
- end
-end
-
-require 'azurerm_monitor_log_profile'
-
-class AzureMonitorLogProfile < AzurermMonitorLogProfile
- name 'azure_monitor_log_profile'
- desc '[DEPRECATED] Please uze azurerm_monitor_log_profile'
- example <<-EXAMPLE
- describe azure_monitor_log_profile(name: 'default') do
- it { should exist }
- its('retention_enabled') { should be true }
- its('retention_days') { should eq(365) }
- end
- EXAMPLE
-
- def initialize(options = { name: 'default' })
- warn '[DEPRECATION] The `azure_monitor_log_profile` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_monitor_log_profile` resource instead.'
- super
- end
-end
-
-require 'azurerm_monitor_log_profiles'
-
-class AzureMonitorLogProfiles < AzurermMonitorLogProfiles
- name 'azure_monitor_log_profiles'
- desc '[DEPRECATED] Please use azurerm_monitor_log_profiles'
- example <<-EXAMPLE
- describe azure_monitor_log_profiles do
- its('names') { should include('default') }
- end
- EXAMPLE
-
- def initialize
- warn '[DEPRECATION] The `azure_monitor_log_profiles` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_monitor_log_profiles` resource instead.'
- super
- end
-end
-
-require 'azurerm_network_security_group'
-
-class AzureNetworkSecurityGroup < AzurermNetworkSecurityGroup
- name 'azure_network_security_group'
- desc 'Verifies settings for Network Security Groups'
- example <<-EXAMPLE
- describe azure_network_security_group(resource_group: 'example', name: 'name') do
- its(name) { should eq 'name'}
- end
- EXAMPLE
-
- def initialize(resource_group: nil, name: nil)
- warn '[DEPRECATION] The `azure_network_security_group` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_network_security_group` resource instead.'
- super
- end
-end
-
-require 'azurerm_network_security_groups'
-
-class AzureNetworkSecurityGroups < AzurermNetworkSecurityGroups
- name 'azure_network_security_groups'
- desc '[DEPRECATED] Please use azurerm_network_security_groups'
- example <<-EXAMPLE
- azure_network_security_groups(resource_group: 'example') do
- it{ should exist }
- end
- EXAMPLE
-
- def initialize(resource_group: nil)
- warn '[DEPRECATION] The `azure_network_security_groups` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_network_security_groups` resource instead.'
- super
- end
-end
-
-require 'azurerm_network_watcher'
-
-class AzureNetworkWatcher < AzurermNetworkWatcher
- name 'azure_network_watcher'
- desc '[DEPRECATED] Please use azurerm_network_watcher'
- example <<-EXAMPLE
- describe azure_network_watcher(resource_group: 'example', name: 'name') do
- its(name) { should eq 'name'}
- end
- EXAMPLE
-
- def initialize(resource_group: nil, name: nil)
- warn '[DEPRECATION] The `azure_network_watcher` resource is deprecated ' \
- 'will be removed in version 2.0. Use the `azurerm_network_watcher` ' \
- 'resource instead.'
- super
- end
-end
-
-require 'azurerm_network_watchers'
-
-class AzureNetworkWatchers < AzurermNetworkWatchers
- name 'azure_network_watchers'
- desc '[DEPRECATED] Please use azurerm_network_watchers'
- example <<-EXAMPLE
- azure_network_watchers(resource_group: 'example') do
- it{ should exist }
- end
- EXAMPLE
-
- def initialize(resource_group:)
- warn '[DEPRECATION] The `azure_network_watchers` resource is deprecated ' \
- 'and will be removed in version 2.0. Use the ' \
- '`azurerm_network_watchers` resource instead.'
- super
- end
-end
-
-require 'azurerm_resource_groups'
-
-class AzureResourceGroups < AzurermResourceGroups
- name 'azure_resource_groups'
- desc '[DEPRECATED] Please use the azurerm_resource_groups resource'
- example <<-EXAMPLE
- describe azure_resource_groups do
- its('names') { should include('example-group') }
- end
- EXAMPLE
-
- def initialize
- warn '[DEPRECATION] The `azure_resource_groups` resource is deprecated ' \
- 'and will be removed in version 2.0. Use the ' \
- '`azurerm_resource_groups` resource instead.'
- super
- end
-end
-
-require 'azurerm_security_center_policies'
-
-class AzureSecurityCenterPolicies < AzurermSecurityCenterPolicies
- name 'azure_security_center_policies'
- desc '[DEPRECATED] Please use the azurerm_security_center_policies resource'
- example <<-EXAMPLE
- describe azure_security_center_policies do
- its('policy_names') { should include('default') }
- end
- EXAMPLE
-
- def initialize
- warn '[DEPRECATION] The `azure_security_center_policies` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_security_center_policies` resource instead.'
- super
- end
-end
-
-require 'azurerm_security_center_policy'
-
-class AzureSecurityCenterPolicy < AzurermSecurityCenterPolicy
- name 'azure_security_center_policy'
- desc '[DEPRECATED] Please use the azurerm_security_center_policy resource'
- example <<-EXAMPLE
- describe azure_security_center_policy(name: 'default') do
- its('log_collection') { should eq('On') }
- end
- EXAMPLE
-
- def initialize(*args)
- warn '[DEPRECATION] The `azure_security_center_policy` resource is ' \
- 'deprecated and will be removed in version 2.0. Use the ' \
- '`azurerm_security_center_policy` resource instead.'
- super
- end
-end
diff --git a/test/integration/verify/controls/azure_generic_resource.rb b/test/integration/verify/controls/azure_generic_resource.rb
new file mode 100644
index 000000000..2f3147323
--- /dev/null
+++ b/test/integration/verify/controls/azure_generic_resource.rb
@@ -0,0 +1,55 @@
+resource_group = input('resource_group', value: nil)
+win_name = input('windows_vm_name', value: nil)
+win_id = input('windows_vm_id', value: nil)
+win_location = input('windows_vm_location', value: nil)
+win_tags = input('windows_vm_tags', value: nil)
+
+control 'azure_generic_resource' do
+ describe azure_generic_resource(resource_group: resource_group, name: win_name) do
+ it { should exist }
+ its('id') { should cmp win_id }
+ its('name') { should eq win_name }
+ its('location') { should eq win_location }
+ its('tags') { should eq win_tags }
+ its('type') { should eq 'Microsoft.Compute/virtualMachines' }
+ its('zones') { should be_nil }
+ end
+
+ # If api_version is not provided, latest version should be used.
+ describe azure_generic_resource(resource_group: resource_group, name: win_name) do
+ its('api_version_used_for_query_state') { should eq 'latest' }
+ end
+
+ # If supported by the resource type, the default api_version can be asked to use in the query.
+ # If not supported, it will fall back to the latest api version.
+ describe azure_generic_resource(resource_group: resource_group, name: win_name, api_version: 'default') do
+ its('api_version_used_for_query_state') { should eq 'default' }
+ end
+
+ # Invalid api version issue should be handled and the latest version should be used.
+ describe azure_generic_resource(resource_group: resource_group, name: win_name, api_version: 'invalid_api') do
+ its('api_version_used_for_query_state') { should eq 'latest' }
+ end
+
+ # If valid api version is provided, this can be confirmed.
+ describe azure_generic_resource(resource_group: resource_group, name: win_name, api_version: '2020-06-01') do
+ its('api_version_used_for_query_state') { should eq 'user_provided' }
+ its('api_version_used_for_query') { should eq '2020-06-01' }
+ end
+
+ describe azure_generic_resource(resource_group: resource_group, name: 'fake') do
+ it { should_not exist }
+ end
+
+ describe azure_generic_resource(resource_group: 'does-not-exist', name: win_name) do
+ it { should_not exist }
+ end
+
+ describe azure_generic_resource(resource_group: resource_group, name: win_name) do
+ its('properties.osProfile.linuxConfiguration.ssh') { should be_nil }
+ end
+
+ describe azure_generic_resource(resource_id: win_id) do
+ its('name') { should eq win_name }
+ end
+end
diff --git a/test/integration/verify/controls/azure_generic_resources.rb b/test/integration/verify/controls/azure_generic_resources.rb
new file mode 100644
index 000000000..2c8edb3c0
--- /dev/null
+++ b/test/integration/verify/controls/azure_generic_resources.rb
@@ -0,0 +1,40 @@
+resource_group = input('resource_group', value: nil)
+vm_names = input('vm_names', value: [])
+win_location = input('windows_vm_location', value: nil)
+win_tags = input('windows_vm_tags', value: nil)
+
+control 'azure_generic_resources' do
+ describe azure_generic_resources do
+ it { should exist }
+ end
+
+ # Loop through resources via singular resource.
+ azure_generic_resources(resource_group: resource_group).ids.each do |id|
+ describe azure_generic_resource(resource_id: id) do
+ its('resource_group') { should cmp resource_group }
+ end
+ end
+
+ describe azure_generic_resources(resource_group: resource_group) do
+ its('names') { should include(vm_names.first) }
+ its('tags') { should include(win_tags) }
+ its('locations') { should include(win_location) }
+ its('types') { should include('Microsoft.Compute/virtualMachines') }
+ its('provisioning_states') { should include('Succeeded') }
+ end
+
+ describe azure_generic_resources(resource_group: 'fake-group') do
+ it { should_not exist }
+ end
+
+ # Test substring_of name and resource_group
+ describe azure_generic_resources(substring_of_resource_group: resource_group[0..-2]) do
+ it { should exist }
+ end
+
+ azure_generic_resources(substring_of_name: vm_names[-10..-1]).names.each do |name|
+ describe azure_generic_resource(resource_group: resource_group, name: name)
+ it { should exist }
+ its('name') { should cmp name }
+ end
+end
diff --git a/test/integration/verify/controls/azure_graph_generic_resource.rb b/test/integration/verify/controls/azure_graph_generic_resource.rb
new file mode 100644
index 000000000..07cd10c7e
--- /dev/null
+++ b/test/integration/verify/controls/azure_graph_generic_resource.rb
@@ -0,0 +1,9 @@
+control 'azure_graph_generic_resource' do
+ only_if { ENV['GRAPH'] }
+
+ user_id = azure_graph_users.user_principal_names.sample
+ describe azure_graph_generic_resource(resource: 'users', id: user_id) do
+ it { should exist }
+ its('userPrincipalName') { should eq user_id }
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_key_vaults.rb b/test/integration/verify/controls/azurerm_key_vaults.rb
index 2cce18ad8..4849e5099 100644
--- a/test/integration/verify/controls/azurerm_key_vaults.rb
+++ b/test/integration/verify/controls/azurerm_key_vaults.rb
@@ -8,3 +8,14 @@
its('names') { should include vault_name }
end
end
+
+control 'azure_key_vaults' do
+ impact 1.0
+ title 'Ensure that azure_key_vaults plural resource works without a parameter.'
+
+ azure_key_vaults.ids.each do |id|
+ describe azure_key_vault(resource_id: id) do
+ its('type') { should eq 'Microsoft.KeyVault/vaults' }
+ end
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_mysql_server.rb b/test/integration/verify/controls/azurerm_mysql_server.rb
index 610a70712..d0505c9d2 100644
--- a/test/integration/verify/controls/azurerm_mysql_server.rb
+++ b/test/integration/verify/controls/azurerm_mysql_server.rb
@@ -14,3 +14,16 @@
its('properties') { should have_attributes(version: '5.7') }
end
end
+
+control 'azure_mysql_server' do
+
+ impact 1.0
+ title 'Ensure resource_id is supported.'
+
+ resource_id = azure_mysql_server(resource_group: resource_group, server_name: mysql_server_name).id
+
+ describe azure_mysql_server(resource_id: resource_id) do
+ it { should exist }
+ its('name') { should eq mysql_server_name }
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_network_security_group.rb b/test/integration/verify/controls/azurerm_network_security_group.rb
index 487d9ed79..d1b871eb8 100644
--- a/test/integration/verify/controls/azurerm_network_security_group.rb
+++ b/test/integration/verify/controls/azurerm_network_security_group.rb
@@ -56,3 +56,16 @@
it { should_not exist }
end
end
+
+control 'azure_network_security_group' do
+
+ describe azure_network_security_group(resource_group: resource_group, name: nsg_insecure) do
+ it { should allow_in(ip_range: '0.0.0.0', port: '22') }
+ it { should allow(source_ip_range: '0.0.0.0', destination_port: '22', direction: 'inbound') }
+ it { should allow_in(service_tag: 'Internet', port: %w{1433-1434 1521 4300-4350 5000-6000}) }
+ it { should allow(source_service_tag: 'Internet', destination_port: %w{1433-1434 1521 4300-4350 5000-6000}, direction: 'inbound') }
+ it { should allow_in(service_tag: 'Internet', port: '3389') }
+ it { should allow(source_service_tag: 'Internet', destination_port: '3389', direction: 'inbound') }
+ end
+
+end
diff --git a/test/integration/verify/controls/azurerm_network_security_groups.rb b/test/integration/verify/controls/azurerm_network_security_groups.rb
index 654e16159..8876be9ee 100644
--- a/test/integration/verify/controls/azurerm_network_security_groups.rb
+++ b/test/integration/verify/controls/azurerm_network_security_groups.rb
@@ -6,3 +6,12 @@
its('names') { should be_an(Array) }
end
end
+
+control 'azure_network_security_groups' do
+ impact 1.0
+ title 'Ensure that the resource tests all network security groups in a subscription.'
+
+ describe azure_network_security_groups do
+ it { should exist }
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_subnet.rb b/test/integration/verify/controls/azurerm_subnet.rb
index 8b6ef57a4..db5d905ce 100644
--- a/test/integration/verify/controls/azurerm_subnet.rb
+++ b/test/integration/verify/controls/azurerm_subnet.rb
@@ -23,3 +23,12 @@
it { should_not exist }
end
end
+
+control 'azure_subnet' do
+ impact 1.0
+ title 'Ensure that azure_subnet supports `resource_id` as a parameter.'
+
+ describe azure_virtual_network(resource_id: id) do
+ its('name') { should cmp name }
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_virtual_machine.rb b/test/integration/verify/controls/azurerm_virtual_machine.rb
index 69a5e3d03..e678e5065 100644
--- a/test/integration/verify/controls/azurerm_virtual_machine.rb
+++ b/test/integration/verify/controls/azurerm_virtual_machine.rb
@@ -37,3 +37,12 @@
its('properties.osProfile.linuxConfiguration.ssh') { should be_nil }
end
end
+
+control 'azure_virtual_machine' do
+ impact 1.0
+ title 'Ensure azure_virtual_machine accepts resource_id and tests resource_group as a property.'
+ describe azure_virtual_machine(resource_id: win_id) do
+ its('name') { should eq win_name }
+ its('resource_group') { should eq resource_group }
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_virtual_machines.rb b/test/integration/verify/controls/azurerm_virtual_machines.rb
index 0c863109e..cc95d5a71 100644
--- a/test/integration/verify/controls/azurerm_virtual_machines.rb
+++ b/test/integration/verify/controls/azurerm_virtual_machines.rb
@@ -40,3 +40,19 @@
it { should_not exist }
end
end
+
+control 'azure_virtual_machines' do
+ impact 1.0
+ title 'Ensure azure_virtual_machines works without providing resource_group.'
+
+ describe azure_virtual_machines do
+ it { should exist }
+ end
+
+ # In depth test with singular resource
+ azure_virtual_machines.ids.each do |id|
+ describe azure_virtual_machine(resource_id: id) do
+ it { should exist }
+ end
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_virtual_network.rb b/test/integration/verify/controls/azurerm_virtual_network.rb
index fbf95fbae..e4bf9b320 100644
--- a/test/integration/verify/controls/azurerm_virtual_network.rb
+++ b/test/integration/verify/controls/azurerm_virtual_network.rb
@@ -34,3 +34,12 @@
it { should_not exist }
end
end
+
+control 'azure_virtual_network' do
+ impact 1.0
+ title 'Ensure that azure_virtual_network supports `resource_id` as a parameter.'
+
+ describe azure_virtual_network(resource_id: vnet_id) do
+ its('name') { should cmp vnet }
+ end
+end
diff --git a/test/integration/verify/controls/azurerm_virtual_networks.rb b/test/integration/verify/controls/azurerm_virtual_networks.rb
index 3054d6e6c..581fde817 100644
--- a/test/integration/verify/controls/azurerm_virtual_networks.rb
+++ b/test/integration/verify/controls/azurerm_virtual_networks.rb
@@ -18,3 +18,12 @@
it { should exist }
end
end
+
+control 'azure_virtual_networks' do
+ impact 1.0
+ title 'Ensure that the resource tests all virtual networks in a subscription.'
+
+ describe azure_virtual_networks do
+ it { should exist }
+ end
+end
diff --git a/test/unit/resources/azure_generic_resource_test.rb b/test/unit/resources/azure_generic_resource_test.rb
new file mode 100644
index 000000000..2be093f1b
--- /dev/null
+++ b/test/unit/resources/azure_generic_resource_test.rb
@@ -0,0 +1,21 @@
+require_relative 'helper'
+require 'azure_generic_resource'
+
+class AzureGenericResourceConstructorTest < Minitest::Test
+ # Generic resource requires a parameter.
+ def test_empty_params_not_ok
+ assert_raises(ArgumentError) { AzureGenericResource.new }
+ end
+
+ # If resource_id is provided, there shouldn't be any other resource related parameters.
+ # E.g.: resource_type, resource_group, etc.
+ # They all exist in resource_id:
+ # /subscriptions/{guid}/resourceGroups/{resource-group-name}/{resource-provider-namespace}/{resource-type}/{resource-name}
+ def test_only_resource_id_ok
+ assert_raises(ArgumentError) { AzureGenericResource.new(resource_id: 'some_id', resource_provider: 'some_type') }
+ end
+
+ def test_invalid_endpoint
+ assert_raises(ArgumentError) { AzureGenericResource.new(endpoint: 'fake_endpoint') }
+ end
+end
diff --git a/test/unit/resources/azure_generic_resources_test.rb b/test/unit/resources/azure_generic_resources_test.rb
new file mode 100644
index 000000000..62e6c5f12
--- /dev/null
+++ b/test/unit/resources/azure_generic_resources_test.rb
@@ -0,0 +1,13 @@
+require_relative 'helper'
+require 'azure_generic_resources'
+
+class AzureGenericResourcesConstructorTest < Minitest::Test
+ # resource_id is not allowed
+ def test_resource_id_not_ok
+ assert_raises(ArgumentError) { AzureGenericResources.new(resource_id: 'some_id') }
+ end
+
+ def test_api_version_not_ok
+ assert_raises(ArgumentError) { AzureGenericResources.new(api_version: '2020-01-01') }
+ end
+end
diff --git a/test/unit/resources/azure_graph_generic_resource_test.rb b/test/unit/resources/azure_graph_generic_resource_test.rb
new file mode 100644
index 000000000..04d1d69cd
--- /dev/null
+++ b/test/unit/resources/azure_graph_generic_resource_test.rb
@@ -0,0 +1,24 @@
+require_relative 'helper'
+require 'azure_graph_generic_resource'
+
+class AzureGraphGenericResourceConstructorTest < Minitest::Test
+ # Generic resource requires a parameter.
+ def test_empty_params_not_ok
+ assert_raises(ArgumentError) { AzureGraphGenericResource.new }
+ end
+
+ def test_not_allowed_parameter
+ assert_raises(ArgumentError) { AzureGraphGenericResource.new(resource: 'users', id: 'some_id', fake: 'rubbish') }
+ end
+
+ def test_filter_not_allowed
+ assert_raises(ArgumentError) { AzureGraphGenericResource.new(resource: 'users', id: 'some_id', filter: 'rubbish') }
+ end
+
+ def test_resource_identifier_is_a_list
+ assert_raises(ArgumentError) do
+ AzureGraphGenericResource.new(resource: 'users', id: 'some_id',
+ resource_identifier: 'rubbish')
+ end
+ end
+end
diff --git a/test/unit/resources/azure_graph_generic_resources_test.rb b/test/unit/resources/azure_graph_generic_resources_test.rb
new file mode 100644
index 000000000..893d7afe7
--- /dev/null
+++ b/test/unit/resources/azure_graph_generic_resources_test.rb
@@ -0,0 +1,30 @@
+require_relative 'helper'
+require 'azure_graph_generic_resources'
+
+class AzureGraphGenericResourcesConstructorTest < Minitest::Test
+ # Generic resource requires `resource` parameter at least.
+ def test_empty_params_not_ok
+ assert_raises(ArgumentError) { AzureGraphGenericResources.new }
+ end
+
+ def test_not_allowed_parameter
+ assert_raises(ArgumentError) { AzureGraphGenericResources.new(resource: 'users', fake: 'rubbish') }
+ end
+
+ def test_id_not_allowed
+ assert_raises(ArgumentError) { AzureGraphGenericResources.new(resource: 'users', id: 'some_id') }
+ end
+
+ def test_filter_filter_free_text_together_not_allowed
+ assert_raises(ArgumentError) do
+ AzureGraphGenericResources.new(resource: 'users',
+ filter: { name: 'some_id' }, filter_free_text: %w{some_filter})
+ end
+ end
+
+ def test_filter_is_hash
+ assert_raises(ArgumentError) do
+ AzureGraphGenericResources.new(resource: 'users', filter: 'some_filter')
+ end
+ end
+end
diff --git a/test/unit/resources/azure_key_vault_test.rb b/test/unit/resources/azure_key_vault_test.rb
new file mode 100644
index 000000000..344032bcf
--- /dev/null
+++ b/test/unit/resources/azure_key_vault_test.rb
@@ -0,0 +1,17 @@
+require_relative 'helper'
+require 'azure_key_vault'
+
+class AzureKeyVaultConstructorTest < Minitest::Test
+ def test_empty_param_not_ok
+ assert_raises(ArgumentError) { AzureKeyVault.new }
+ end
+
+ # resource_provider should not be allowed.
+ def test_resource_provider_not_ok
+ assert_raises(ArgumentError) { AzureKeyVault.new(resource_provider: 'some_type') }
+ end
+
+ def test_resource_group_should_exist
+ assert_raises(ArgumentError) { AzureKeyVault.new(name: 'my-name') }
+ end
+end
diff --git a/test/unit/resources/azure_key_vaults_test.rb b/test/unit/resources/azure_key_vaults_test.rb
new file mode 100644
index 000000000..c8dd78f34
--- /dev/null
+++ b/test/unit/resources/azure_key_vaults_test.rb
@@ -0,0 +1,24 @@
+require_relative 'helper'
+require 'azure_key_vaults'
+
+class AzureKeyVaultsConstructorTest < Minitest::Test
+ def test_resource_provider_not_ok
+ assert_raises(ArgumentError) { AzureKeyVaults.new(resource_provider: 'some_type') }
+ end
+
+ def tag_value_not_ok
+ assert_raises(ArgumentError) { AzureKeyVaults.new(tag_value: 'some_tag_value') }
+ end
+
+ def tag_name_not_ok
+ assert_raises(ArgumentError) { AzureKeyVaults.new(tag_name: 'some_tag_name') }
+ end
+
+ def test_resource_id_not_ok
+ assert_raises(ArgumentError) { AzureKeyVaults.new(resource_id: 'some_id') }
+ end
+
+ def test_name_not_ok
+ assert_raises(ArgumentError) { AzureKeyVaults.new(name: 'some_name') }
+ end
+end
diff --git a/test/unit/resources/azure_mysql_server_test.rb b/test/unit/resources/azure_mysql_server_test.rb
new file mode 100644
index 000000000..068f10fe0
--- /dev/null
+++ b/test/unit/resources/azure_mysql_server_test.rb
@@ -0,0 +1,17 @@
+require_relative 'helper'
+require 'azure_mysql_server'
+
+class AzureMysqlServerConstructorTest < Minitest::Test
+ def test_empty_param_not_ok
+ assert_raises(ArgumentError) { AzureMysqlServer.new }
+ end
+
+ # resource_provider should not be allowed.
+ def test_resource_provider_not_ok
+ assert_raises(ArgumentError) { AzureMysqlServer.new(resource_provider: 'some_type') }
+ end
+
+ def test_resource_group
+ assert_raises(ArgumentError) { AzureMysqlServer.new(name: 'my-name') }
+ end
+end
diff --git a/test/unit/resources/azure_mysql_servers_test.rb b/test/unit/resources/azure_mysql_servers_test.rb
new file mode 100644
index 000000000..a142429fc
--- /dev/null
+++ b/test/unit/resources/azure_mysql_servers_test.rb
@@ -0,0 +1,25 @@
+require_relative 'helper'
+require 'azure_mysql_servers'
+
+class AzureMysqlServersConstructorTest < Minitest::Test
+ # resource_type should not be allowed.
+ def test_resource_type_not_ok
+ assert_raises(ArgumentError) { AzureMysqlServers.new(resource_provider: 'some_type') }
+ end
+
+ def tag_value_not_ok
+ assert_raises(ArgumentError) { AzureMysqlServers.new(tag_value: 'some_tag_value') }
+ end
+
+ def tag_name_not_ok
+ assert_raises(ArgumentError) { AzureMysqlServers.new(tag_name: 'some_tag_name') }
+ end
+
+ def test_resource_id_not_ok
+ assert_raises(ArgumentError) { AzureMysqlServers.new(resource_id: 'some_id') }
+ end
+
+ def test_name_not_ok
+ assert_raises(ArgumentError) { AzureMysqlServers.new(name: 'some_name') }
+ end
+end
diff --git a/test/unit/resources/azure_network_security_group_test.rb b/test/unit/resources/azure_network_security_group_test.rb
new file mode 100644
index 000000000..b7d090650
--- /dev/null
+++ b/test/unit/resources/azure_network_security_group_test.rb
@@ -0,0 +1,17 @@
+require_relative 'helper'
+require 'azure_network_security_group'
+
+class AzureNetworkSecurityGroupConstructorTest < Minitest::Test
+ def test_empty_param_not_ok
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroup.new }
+ end
+
+ # resource_provider should not be allowed.
+ def test_resource_provider_not_ok
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroup.new(resource_provider: 'some_type') }
+ end
+
+ def test_resource_group_should_exist
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroup.new(name: 'my-name') }
+ end
+end
diff --git a/test/unit/resources/azure_network_security_groups_test.rb b/test/unit/resources/azure_network_security_groups_test.rb
new file mode 100644
index 000000000..7703a9dff
--- /dev/null
+++ b/test/unit/resources/azure_network_security_groups_test.rb
@@ -0,0 +1,25 @@
+require_relative 'helper'
+require 'azure_network_security_groups'
+
+class AzureNetworkSecurityGroupsConstructorTest < Minitest::Test
+ # resource_type should not be allowed.
+ def test_resource_type_not_ok
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroups.new(resource_provider: 'some_type') }
+ end
+
+ def tag_value_not_ok
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroups.new(tag_value: 'some_tag_value') }
+ end
+
+ def tag_name_not_ok
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroups.new(tag_name: 'some_tag_name') }
+ end
+
+ def test_resource_id_not_ok
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroups.new(resource_id: 'some_id') }
+ end
+
+ def test_name_not_ok
+ assert_raises(ArgumentError) { AzureNetworkSecurityGroups.new(name: 'some_name') }
+ end
+end
diff --git a/test/unit/resources/azure_subnet_test.rb b/test/unit/resources/azure_subnet_test.rb
new file mode 100644
index 000000000..6e5a29a33
--- /dev/null
+++ b/test/unit/resources/azure_subnet_test.rb
@@ -0,0 +1,22 @@
+require_relative 'helper'
+require 'azure_subnet'
+
+class AzureSubnetConstructorTest < Minitest::Test
+ def test_empty_param_not_ok
+ assert_raises(ArgumentError) { AzureSubnet.new }
+ end
+
+ # resource_type should not be allowed.
+ def test_resource_type_not_ok
+ assert_raises(ArgumentError) { AzureSubnet.new(resource_provider: 'some_type') }
+ end
+
+ # resource_group, vnet and name should be provided together
+ def test_missing_required_params_1
+ assert_raises(ArgumentError) { AzureSubnet.new(resource_group: 'some_r_g') }
+ end
+
+ def test_missing_required_params_2
+ assert_raises(ArgumentError) { AzureSubnet.new(resource_group: 'some_r_g', vnet: 'virtual_net_name') }
+ end
+end
diff --git a/test/unit/resources/azure_subnets_test.rb b/test/unit/resources/azure_subnets_test.rb
new file mode 100644
index 000000000..893e05819
--- /dev/null
+++ b/test/unit/resources/azure_subnets_test.rb
@@ -0,0 +1,25 @@
+require_relative 'helper'
+require 'azure_subnets'
+
+class AzureSubnetsConstructorTest < Minitest::Test
+ # resource_provider should not be allowed.
+ def test_resource_type_not_ok
+ assert_raises(ArgumentError) { AzureSubnets.new(resource_provider: 'some_type') }
+ end
+
+ def tag_value_not_ok
+ assert_raises(ArgumentError) { AzureSubnets.new(tag_value: 'some_tag_value') }
+ end
+
+ def tag_name_not_ok
+ assert_raises(ArgumentError) { AzureSubnets.new(tag_name: 'some_tag_name') }
+ end
+
+ def test_resource_id_not_ok
+ assert_raises(ArgumentError) { AzureSubnets.new(resource_id: 'some_id') }
+ end
+
+ def test_name_not_ok
+ assert_raises(ArgumentError) { AzureSubnets.new(name: 'some_name') }
+ end
+end
diff --git a/test/unit/resources/azure_virtual_machine_test.rb b/test/unit/resources/azure_virtual_machine_test.rb
new file mode 100644
index 000000000..a4c9b5aba
--- /dev/null
+++ b/test/unit/resources/azure_virtual_machine_test.rb
@@ -0,0 +1,17 @@
+require_relative 'helper'
+require 'azure_virtual_machine'
+
+class AzureVirtualMachineConstructorTest < Minitest::Test
+ def test_empty_param_not_ok
+ assert_raises(ArgumentError) { AzureVirtualMachine.new }
+ end
+
+ # resource_provider should not be allowed.
+ def test_resource_provider_not_ok
+ assert_raises(ArgumentError) { AzureVirtualMachine.new(resource_provider: 'some_type') }
+ end
+
+ def test_resource_group
+ assert_raises(ArgumentError) { AzureVirtualMachine.new(name: 'my-name') }
+ end
+end
diff --git a/test/unit/resources/azure_virtual_machines_test.rb b/test/unit/resources/azure_virtual_machines_test.rb
new file mode 100644
index 000000000..613708790
--- /dev/null
+++ b/test/unit/resources/azure_virtual_machines_test.rb
@@ -0,0 +1,25 @@
+require_relative 'helper'
+require 'azure_virtual_machines'
+
+class AzureVirtualMachinesConstructorTest < Minitest::Test
+ # resource_type should not be allowed.
+ def test_resource_type_not_ok
+ assert_raises(ArgumentError) { AzureVirtualMachines.new(resource_provider: 'some_type') }
+ end
+
+ def tag_value_not_ok
+ assert_raises(ArgumentError) { AzureVirtualMachines.new(tag_value: 'some_tag_value') }
+ end
+
+ def tag_name_not_ok
+ assert_raises(ArgumentError) { AzureVirtualMachines.new(tag_name: 'some_tag_name') }
+ end
+
+ def test_resource_id_not_ok
+ assert_raises(ArgumentError) { AzureVirtualMachines.new(resource_id: 'some_id') }
+ end
+
+ def test_name_not_ok
+ assert_raises(ArgumentError) { AzureVirtualMachines.new(name: 'some_name') }
+ end
+end
diff --git a/test/unit/resources/azure_virtual_network_test.rb b/test/unit/resources/azure_virtual_network_test.rb
new file mode 100644
index 000000000..63b021cca
--- /dev/null
+++ b/test/unit/resources/azure_virtual_network_test.rb
@@ -0,0 +1,17 @@
+require_relative 'helper'
+require 'azure_virtual_network'
+
+class AzureVirtualNetworkConstructorTest < Minitest::Test
+ def test_empty_param_not_ok
+ assert_raises(ArgumentError) { AzureVirtualNetwork.new }
+ end
+
+ # resource_provider should not be allowed.
+ def test_resource_provider_not_ok
+ assert_raises(ArgumentError) { AzureVirtualNetwork.new(resource_provider: 'some_type') }
+ end
+
+ def test_resource_group
+ assert_raises(ArgumentError) { AzureVirtualNetwork.new(name: 'my-name') }
+ end
+end
diff --git a/test/unit/resources/azure_virtual_networks_test.rb b/test/unit/resources/azure_virtual_networks_test.rb
new file mode 100644
index 000000000..0e8e224c8
--- /dev/null
+++ b/test/unit/resources/azure_virtual_networks_test.rb
@@ -0,0 +1,25 @@
+require_relative 'helper'
+require 'azure_virtual_networks'
+
+class AzureVirtualNetworksConstructorTest < Minitest::Test
+ # resource_type should not be allowed.
+ def test_resource_type_not_ok
+ assert_raises(ArgumentError) { AzureVirtualNetworks.new(resource_provider: 'some_type') }
+ end
+
+ def tag_value_not_ok
+ assert_raises(ArgumentError) { AzureVirtualNetworks.new(tag_value: 'some_tag_value') }
+ end
+
+ def tag_name_not_ok
+ assert_raises(ArgumentError) { AzureVirtualNetworks.new(tag_name: 'some_tag_name') }
+ end
+
+ def test_resource_id_not_ok
+ assert_raises(ArgumentError) { AzureVirtualNetworks.new(resource_id: 'some_id') }
+ end
+
+ def test_name_not_ok
+ assert_raises(ArgumentError) { AzureVirtualNetworks.new(name: 'some_name') }
+ end
+end
diff --git a/test/unit/resources/helper.rb b/test/unit/resources/helper.rb
new file mode 100644
index 000000000..6c4ddbd66
--- /dev/null
+++ b/test/unit/resources/helper.rb
@@ -0,0 +1,4 @@
+require 'minitest/autorun'
+require 'minitest/unit'
+require 'minitest/pride'
+require 'inspec/resource'