mirror of
https://github.com/inspec/inspec
synced 2024-11-26 22:50:36 +00:00
4f341acfbc
Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
377 lines
14 KiB
Ruby
377 lines
14 KiB
Ruby
# Base class for Azure Resources. This allows the generic class to work
|
|
# as well as the specific target resources for Azure Resources
|
|
#
|
|
# @author Russell Seymour
|
|
module Inspec::Resources
|
|
class AzureResourceBase < Inspec.resource(1)
|
|
attr_reader :opts, :client, :azure
|
|
|
|
# Constructor that retreives the specified resource
|
|
#
|
|
# The opts hash should contain the following
|
|
# :group_name - name of the resource group in which to look for items
|
|
# :type - the type of Azure resource to look for
|
|
# :apiversion - API version to use when looking for a specific resource
|
|
# :name - name of the resource to find
|
|
#
|
|
# @author Russell Seymour
|
|
#
|
|
# @param [Hash] opts Hashtable of options as highlighted above
|
|
# rubocop:disable Metrics/AbcSize
|
|
def initialize(opts)
|
|
# declare the hashtable of counts
|
|
@counts = {}
|
|
@total = 0
|
|
@opts = opts
|
|
|
|
# Determine if the environment variables for the options have been set
|
|
option_var_names = {
|
|
group_name: 'AZURE_RESOURCE_GROUP_NAME',
|
|
name: 'AZURE_RESOURCE_NAME',
|
|
type: 'AZURE_RESOURCE_TYPE',
|
|
apiversion: 'AZURE_RESOURCE_API_VERSION',
|
|
}
|
|
option_var_names.each do |option_name, env_var_name|
|
|
opts[option_name] = ENV[env_var_name] unless ENV[env_var_name].nil?
|
|
end
|
|
|
|
@azure = inspec.backend
|
|
@client = azure.azure_client
|
|
@failed_resource = false
|
|
end
|
|
|
|
def failed_resource?
|
|
@failed_resource
|
|
end
|
|
|
|
def catch_azure_errors
|
|
yield
|
|
rescue MsRestAzure::AzureOperationError => e
|
|
# e.message is actually a massive stringified JSON, which might be useful in the future.
|
|
# You want error_message here.
|
|
fail_resource e.error_message
|
|
@failed_resource = true
|
|
nil
|
|
end
|
|
|
|
# Return information about the resource group
|
|
def resource_group
|
|
catch_azure_errors do
|
|
resource_group = client.resource_groups.get(opts[:group_name])
|
|
|
|
# create the methods for the resource group object
|
|
dm = AzureResourceDynamicMethods.new
|
|
dm.create_methods(self, resource_group)
|
|
end
|
|
end
|
|
|
|
def resources
|
|
resources = nil
|
|
catch_azure_errors do
|
|
resources = client.resources.list_by_resource_group(opts[:group_name])
|
|
end
|
|
return if failed_resource?
|
|
|
|
# filter the resources based on the type, and the name if they been specified
|
|
resources = filter_resources(resources, opts)
|
|
|
|
# if there is one resource then define methods on this class
|
|
if resources.count == 1
|
|
@total = 1
|
|
|
|
resource = nil
|
|
catch_azure_errors do
|
|
# get the apiversion for the resource, if one has not been specified
|
|
apiversion = azure.get_api_version(resources[0].type, opts)
|
|
|
|
# get the resource by id so it can be interrogated
|
|
resource = client.resources.get_by_id(resources[0].id, apiversion)
|
|
end
|
|
return if failed_resource?
|
|
|
|
dm = AzureResourceDynamicMethods.new
|
|
|
|
dm.create_methods(self, resource)
|
|
else
|
|
|
|
# As there are many resources, parse each one so that it can be
|
|
# interrogated by the FilterTable
|
|
# @probes = parse_resources(resources, azure)
|
|
@probes = resources.each.map do |item|
|
|
# update the total
|
|
@total += 1
|
|
|
|
# determine the counts for each type
|
|
namespace, type_name = item.type.split(/\./)
|
|
counts.key?(namespace) ? false : counts[namespace] = {}
|
|
counts[namespace].key?(type_name) ? counts[namespace][type_name] += 1 : counts[namespace][type_name] = 1
|
|
|
|
# get the detail about the resource
|
|
apiversion = azure.get_api_version(item.type, opts)
|
|
resource = client.resources.get_by_id(item.id, apiversion)
|
|
|
|
# parse the resource
|
|
parse_resource(resource)
|
|
end.compact
|
|
|
|
# Iterate around the counts and create the necessary classes
|
|
counts.each do |namespace, ns_counts|
|
|
define_singleton_method namespace do
|
|
AzureResourceTypeCounts.new(ns_counts)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Does the resource have any tags?
|
|
#
|
|
# If it is a Hashtable then it does not, because there was nothing to parse so there is not
|
|
# a nested object to work with
|
|
#
|
|
# @author Russell Seymour
|
|
def has_tags?
|
|
tags.is_a?(Hash) ? false : true
|
|
end
|
|
|
|
# Returns how many tags have been set on the resource
|
|
#
|
|
# @author Russell Seymour
|
|
def tag_count
|
|
tags.count
|
|
end
|
|
|
|
# It is necessary to be able to test the tags of a resource. It is possible to say of the
|
|
# resource has tags or not, and it is possible to check that the tags include a specific tag
|
|
# However the value is not accessible, this function creates methods for all the tags that
|
|
# are available.
|
|
#
|
|
# The format of the method name is '<TAG_NAME>_tag' and will return the value of that tag
|
|
#
|
|
# Disabling rubopcop check. If this is set as a normal if..then..end statement there is a
|
|
# violation stating it should use a guard. When using a guard it throws this error
|
|
#
|
|
# @author Russell Seymour
|
|
def create_tag_methods
|
|
# Iterate around the items of the tags and create the necessary access methods
|
|
tags.item.each do |name, value|
|
|
method_name = format('%s_tag', name)
|
|
define_singleton_method method_name do
|
|
value
|
|
end
|
|
end if defined?(tags.item)
|
|
end
|
|
|
|
private
|
|
|
|
# Filter the resources that are returned by the options that have been specified
|
|
#
|
|
def filter_resources(resources, opts)
|
|
if opts[:type] && opts[:name]
|
|
resources.select { |r| r.type == opts[:type] && r.name == opts[:name] }
|
|
elsif opts[:type]
|
|
resources.select { |r| r.type == opts[:type] }
|
|
elsif opts[:name]
|
|
resources.select { |r| r.name == opts[:name] }
|
|
else
|
|
resources
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# 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 craeted 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 AzureResourceProbe|AzureResource object The object on which the methods should be craeted
|
|
# @param variant data The data from which the methods should be created
|
|
def create_methods(object, data)
|
|
# Check the type of data as this affects the setup of the methods
|
|
# If it is an Azure Generic Resource then setup methods for each of
|
|
# the instance variables
|
|
case data.class.to_s
|
|
when /^Azure::Resources::Mgmt::.*::Models::GenericResource$/,
|
|
/^Azure::Resources::Mgmt::.*::Models::ResourceGroup$/
|
|
# iterate around the instance variables
|
|
data.instance_variables.each do |var|
|
|
create_method(object, var.to_s.delete('@'), data.instance_variable_get(var))
|
|
end
|
|
# When the data is a Hash object iterate around each of the key value pairs and
|
|
# craete a method for each one.
|
|
when 'Hash'
|
|
data.each do |key, value|
|
|
create_method(object, key, value)
|
|
end
|
|
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 AzureResourceProbe|AzureResource object Object on which the methods need to be created
|
|
# @param string name The name of the method
|
|
# @param variant value 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 /^Azure::Resources::Mgmt::.*::Models::ResourceGroupProperties$/
|
|
# This is a special case where the properties of the resource group is not a simple JSON model
|
|
# This is because the plugin is using the Azure SDK to get this information so it is an SDK object
|
|
# that has to be interrogated in a different way. This is the only object type that behaves like this
|
|
value.instance_variables.each do |var|
|
|
create_method(object, var.to_s.delete('@'), value.instance_variable_get(var))
|
|
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 methid, 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 to maintain a count of the Azure Resource types that are found
|
|
# when a less specific test is carried out. For example if all the resoures of a resource
|
|
# group are called for, there will be variaous types and number of those types.
|
|
#
|
|
# Each type is namespaced, so for example a virtual machine has the type 'Microsoft.Compute/virtualMachines'
|
|
# This is broken down into the 'Microsoft' class with the type 'Compute/virtualMachines'
|
|
# This has been done for two reasons:
|
|
# 1. Enable the dotted notation to work in the test
|
|
# 2. Allow third party resource types ot be catered for if they are ever enabled by Microsoft
|
|
#
|
|
# @author Russell Seymour
|
|
# @since 0.2.0
|
|
class AzureResourceTypeCounts
|
|
# Constructor to setup a new class for a specific Azure Resource type.
|
|
# It should be passed a hashtable with information such as:
|
|
# {
|
|
# "Compute/virtualMachines" => 2,
|
|
# "Network/networkInterfaces" => 3
|
|
# }
|
|
# This will result in two methods being created on the class:
|
|
# - Compute/virtualNetworks
|
|
# - Network/networkInterfaces
|
|
# Each of which will return the corresponding count value
|
|
#
|
|
# @param Hash counts Hash table of types and the count of each one
|
|
#
|
|
# @return AzureResourceTypeCounts
|
|
def initialize(counts)
|
|
counts.each do |type, count|
|
|
define_singleton_method type do
|
|
count
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Class object that is created for each element that is returned by Azure.
|
|
# This is what is interogated 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 nestec 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
|
|
#
|
|
# @param varaint The item from which the class will be initialized
|
|
#
|
|
# @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] key Name of the item to look for in the @item property
|
|
def include?(key)
|
|
@item.key?(key)
|
|
end
|
|
|
|
# Give a sting like `computer_name` return the camelCase version, e.g.
|
|
# computerName
|
|
#
|
|
# @param string data Data that needs to be converted from snake_case to camelCase
|
|
#
|
|
# @return string
|
|
def camel_case(data)
|
|
camel_case_data = data.split('_').inject([]) { |buffer, e| buffer.push(buffer.empty? ? e : e.capitalize) }.join
|
|
|
|
# Ensure that gb (as in gigabytes) is uppercased
|
|
camel_case_data.gsub(/[gb]/, &:upcase)
|
|
end
|
|
end
|