Catch cloud exceptions and document connection info (#2636)

Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Clinton Wolfe 2018-02-14 14:15:20 -05:00 committed by GitHub
parent 59fd0e8775
commit 4f341acfbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 287 additions and 59 deletions

View file

@ -249,6 +249,11 @@ inspec exec test.rb -t aws://
# or store your AWS credentials in your ~/.aws/credentials profiles file
inspec exec test.rb -t aws://us-east-2/my-profile
# run a profile targeting Azure using env vars
inspec exec test.rb -t azure://
# or store your Azure credentials in your ~/.azure/credentials profiles file
inspec exec test.rb -t azure://subscription_id
```
### detect

111
docs/platforms.md Normal file
View file

@ -0,0 +1,111 @@
# Using InSpec 2.0 with Platforms
New in InSpec 2.0, you may now use certain InSpec resources to audit properties of things that aren't individual machines - for example, an Amazon Web Services S3 bucket.
In the initial release of InSpec 2.0, support for selected AWS and Azure resources is included.
## AWS Platform Support
### Setting up AWS credentials for InSpec
InSpec uses the standard AWS authentication mechanisms. Typically, you will create an IAM user which will be used for auditing activities.
1. Create an IAM user in the AWS console, with your choice of username. Check the box marked "Programmatic Access."
2. On the Permissions screen, choose Direct Attach. Select the AWS-managed IAM Profile named "ReadOnlyAccess." If you wish to restrict the user further, you may do so; see individual InSpec resources to identify which permissions are required.
3. After the key is generated, record the Access Key ID and Secret Key.
#### Using Environment Variables to provide credentials
You may provide the credentials to InSpec by setting the following environment variables: `AWS_REGION`, `AWS_ACCESS_KEY_ID`, and `AWS_SECRET_KEY_ID`. You may also use `AWS_PROFILE`, or if you are using MFA, `AWS_SESSION_TOKEN`. See the [AWS Command Line Interface Docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html) for details.
Once you have your environment variables set, you can verify your credentials by running:
```bash
you$ inspec detect -t aws://
== Platform Details
Name: aws
Families: cloud, api
```
#### Using the InSpec target option to provide credentials
Look for a file in your home directory named `~/.aws/credentials`. If it does not exist, create it. Choose a name for your profile; here, we're using the name 'auditing'. Add your credentials as a new profile, in INI format:
```
[auditing]
aws_access_key_id = AKIA....
aws_secret_access_key = 1234....abcd
```
You may now run InSpec using the `--target` / `-t` option, using the format `-t aws://region/profile`. For example, to connect to the Ohio region using a profile named 'auditing', use `-t aws://us-east-2/auditing`.
To verify your credentials,
```bash
you$ inspec detect -t aws://
== Platform Details
Name: aws
Families: cloud, api
```
#### Verifying your credentials
To verify your credentials
## Azure Platform Support in InSpec 2.0
### Setting up Azure credentials for InSpec
To use InSpec Azure resources, you will need a Service Principal Name (SPN) to be created in the Azure subscription that is being audited.
This can be done on the command line or from the Azure Portal:
- Azure CLI: https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-authenticate-service-principal-cli
- PowerShell: https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-authenticate-service-principal
- Azure Portal: https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-create-service-principal-portal
The information from the SPN can be specified either in a file `~/.azure/credentials`, as environment variables, or using InSpec target URIs.
#### Using the Azure Credentials File
By default InSpec is configured to look at ~/.azure/credentials, and it should contain:
```
[<SUBSCRIPTION_ID>]
client_id = "<CLIENT_ID>"
client_secret = "<CLIENT_SECRET>"
tenant_id = "<TENANT_ID>"
```
With the credentials are in place you may now execute InSpec:
```bash
inspec exec my-inspec-profile -t azure://
```
#### Using Environment variables
You may also set the Azure credentials via environment variables:
- `AZURE_SUBSCRIPTION_ID`
- `AZURE_CLIENT_ID`
- `AZURE_CLIENT_SECRET`
- `AZURE_TENANT_ID`
For example:
```bash
AZURE_SUBSCRIPTION_ID="2fbdbb02-df2e-11e6-bf01-fe55135034f3" \
AZURE_CLIENT_ID="58dc4f6c-df2e-11e6-bf01-fe55135034f3" \
AZURE_CLIENT_SECRET="Jibr4iwwaaZwBb6W" \
AZURE_TENANT_ID="6ad89b58-df2e-11e6-bf01-fe55135034f3" inspec exec my-profile -t azure://
```
#### Using InSpec Target Syntax
If you have created a `~/.azure/credentials` file as above, you may also use the InSpec command line `--target` / `-t` option to select a subscription ID. For example:
```bash
inspec exec my-profile -t azure://2fbdbb02-df2e-11e6-bf01-fe55135034f3
```

View file

@ -3,7 +3,9 @@ module AwsResourceMixin
validate_params(resource_params).each do |param, value|
instance_variable_set(:"@#{param}", value)
end
fetch_from_api
catch_aws_errors do
fetch_from_api
end
end
# Default implementation of validate params accepts everything.
@ -45,4 +47,16 @@ module AwsResourceMixin
# TODO: remove with https://github.com/chef/inspec-aws/issues/216
inspec if respond_to?(:inspec)
end
# Intercept AWS exceptions
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
end

View file

@ -25,6 +25,21 @@ EOX
@iam_resource = conn ? conn.iam_resource : inspec_runner.backend.aws_resource(Aws::IAM::Resource, {})
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_resource_mixin.rb
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_singular_resource_mixin.rb
def inspec_runner
# When running under inspec-cli, we have an 'inspec' method that
# returns the runner. When running under unit tests, we don't
@ -37,19 +52,21 @@ EOX
def id
return @instance_id if defined?(@instance_id)
if @opts.is_a?(Hash)
first = @ec2_resource.instances(
{
filters: [{
name: 'tag:Name',
values: [@opts[:name]],
}],
},
).first
# catch case where the instance is not known
@instance_id = first.id unless first.nil?
else
@instance_id = @opts
catch_aws_errors do
if @opts.is_a?(Hash)
first = @ec2_resource.instances(
{
filters: [{
name: 'tag:Name',
values: [@opts[:name]],
}],
},
).first
# catch case where the instance is not known
@instance_id = first.id unless first.nil?
else
@instance_id = @opts
end
end
end
alias instance_id id
@ -61,7 +78,9 @@ EOX
# returns the instance state
def state
instance&.state&.name
catch_aws_errors do
instance&.state&.name
end
end
# helper methods for each state
@ -82,18 +101,24 @@ EOX
instance_type image_id vpc_id
}.each do |attribute|
define_method attribute do
instance.send(attribute) if instance
catch_aws_errors do
instance.send(attribute) if instance
end
end
end
def security_groups
@security_groups ||= instance.security_groups.map { |sg|
{ id: sg.group_id, name: sg.group_name }
}
catch_aws_errors do
@security_groups ||= instance.security_groups.map { |sg|
{ id: sg.group_id, name: sg.group_name }
}
end
end
def tags
@tags ||= instance.tags.map { |tag| { key: tag.key, value: tag.value } }
catch_aws_errors do
@tags ||= instance.tags.map { |tag| { key: tag.key, value: tag.value } }
end
end
def to_s
@ -101,23 +126,25 @@ EOX
end
def has_roles?
instance_profile = instance.iam_instance_profile
catch_aws_errors do
instance_profile = instance.iam_instance_profile
if instance_profile
roles = @iam_resource.instance_profile(
instance_profile.arn.gsub(%r{^.*\/}, ''),
).roles
else
roles = nil
if instance_profile
roles = @iam_resource.instance_profile(
instance_profile.arn.gsub(%r{^.*\/}, ''),
).roles
else
roles = nil
end
roles && !roles.empty?
end
roles && !roles.empty?
end
private
def instance
@instance ||= @ec2_resource.instance(id)
catch_aws_errors { @instance ||= @ec2_resource.instance(id) }
end
end

View file

@ -54,7 +54,9 @@ class AwsIamAccessKey < Inspec.resource(1)
return nil unless exists?
return @last_used_date if defined? @last_used_date
backend = BackendFactory.create(inspec_runner)
@last_used_date = backend.get_access_key_last_used({ access_key_id: access_key_id }).access_key_last_used.last_used_date
catch_aws_errors do
@last_used_date = backend.get_access_key_last_used({ access_key_id: access_key_id }).access_key_last_used.last_used_date
end
end
def fetch_from_api

View file

@ -16,12 +16,29 @@ EOX
# TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin
def initialize(conn = nil)
iam_resource = conn ? conn.iam_resource : inspec_runner.backend.aws_resource(Aws::IAM::Resource, {})
@policy = iam_resource.account_password_policy
catch_aws_errors do
iam_resource = conn ? conn.iam_resource : inspec_runner.backend.aws_resource(Aws::IAM::Resource, {})
@policy = iam_resource.account_password_policy
end
rescue Aws::IAM::Errors::NoSuchEntity
@policy = nil
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_resource_mixin.rb
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_singular_resource_mixin.rb
def inspec_runner
# When running under inspec-cli, we have an 'inspec' method that
# returns the runner. When running under unit tests, we don't

View file

@ -93,7 +93,10 @@ class AwsIamPolicy < Inspec.resource(1)
end
backend = AwsIamPolicy::BackendFactory.create(inspec_runner)
criteria = { policy_arn: arn }
resp = backend.list_entities_for_policy(criteria)
resp = nil
catch_aws_errors do
resp = backend.list_entities_for_policy(criteria)
end
@attached_groups = resp.policy_groups.map(&:group_name)
@attached_users = resp.policy_users.map(&:user_name)
@attached_roles = resp.policy_roles.map(&:role_name)

View file

@ -13,6 +13,21 @@ class AwsIamRootUser < Inspec.resource(1)
@client = conn ? conn.iam_client : inspec_runner.backend.aws_client(Aws::IAM::Client)
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_resource_mixin.rb
def catch_aws_errors
yield
rescue Aws::Errors::MissingCredentialsError
# The AWS error here is unhelpful:
# "unable to sign request without credentials set"
Inspec::Log.error "It appears that you have not set your AWS credentials. You may set them using environment variables, or using the 'aws://region/aws_credentials_profile' target. See https://www.inspec.io/docs/reference/platforms for details."
fail_resource('No AWS credentials available')
rescue Aws::Errors::ServiceError => e
fail_resource e.message
end
# TODO: DRY up, see https://github.com/chef/inspec/issues/2633
# Copied from resource_support/aws/aws_singular_resource_mixin.rb
def inspec_runner
# When running under inspec-cli, we have an 'inspec' method that
# returns the runner. When running under unit tests, we don't
@ -38,6 +53,8 @@ class AwsIamRootUser < Inspec.resource(1)
private
def summary_account
@summary_account ||= @client.get_account_summary.summary_map
catch_aws_errors do
@summary_account ||= @client.get_account_summary.summary_map
end
end
end

View file

@ -82,6 +82,8 @@ class AwsIamUser < Inspec.resource(1)
# TODO: consider returning Inspec AwsIamAccessKey objects
@access_keys = backend.list_access_keys(user_name: username).access_key_metadata
# If the above call fails, we get nil here; but we promise access_keys will be an array.
@access_keys ||= []
end
class Backend

View file

@ -10,15 +10,16 @@ class AwsS3Bucket < Inspec.resource(1)
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :bucket_name, :region
attr_reader :bucket_name, :has_access_logging_enabled, :region
def to_s
"S3 Bucket #{@bucket_name}"
end
def bucket_acl
# This is simple enough to inline it.
@bucket_acl ||= BackendFactory.create(inspec_runner).get_bucket_acl(bucket: bucket_name).grants
catch_aws_errors do
@bucket_acl ||= BackendFactory.create(inspec_runner).get_bucket_acl(bucket: bucket_name).grants
end
end
def bucket_policy
@ -36,8 +37,9 @@ class AwsS3Bucket < Inspec.resource(1)
def has_access_logging_enabled?
return unless @exists
# This is simple enough to inline it.
!BackendFactory.create(inspec_runner).get_bucket_logging(bucket: bucket_name).logging_enabled.nil?
catch_aws_errors do
@has_access_logging_enabled ||= !BackendFactory.create(inspec_runner).get_bucket_logging(bucket: bucket_name).logging_enabled.nil?
end
end
private
@ -72,17 +74,18 @@ class AwsS3Bucket < Inspec.resource(1)
def fetch_bucket_policy
backend = BackendFactory.create(inspec_runner)
begin
# AWS SDK returns a StringIO, we have to read()
raw_policy = backend.get_bucket_policy(bucket: bucket_name).policy
return JSON.parse(raw_policy.read)['Statement'].map do |statement|
lowercase_hash = {}
statement.each_key { |k| lowercase_hash[k.downcase] = statement[k] }
OpenStruct.new(lowercase_hash)
catch_aws_errors do
begin
# AWS SDK returns a StringIO, we have to read()
raw_policy = backend.get_bucket_policy(bucket: bucket_name).policy
return JSON.parse(raw_policy.read)['Statement'].map do |statement|
lowercase_hash = {}
statement.each_key { |k| lowercase_hash[k.downcase] = statement[k] }
@bucket_policy = OpenStruct.new(lowercase_hash)
end
rescue Aws::S3::Errors::NoSuchBucketPolicy
@bucket_policy = []
end
rescue Aws::S3::Errors::NoSuchBucketPolicy
return []
end
end

View file

@ -37,19 +37,40 @@ module Inspec::Resources
@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
resource_group = client.resource_groups.get(opts[:group_name])
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)
# create the methods for the resource group object
dm = AzureResourceDynamicMethods.new
dm.create_methods(self, resource_group)
end
end
def resources
resources = client.resources.list_by_resource_group(opts[:group_name])
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)
@ -58,11 +79,15 @@ module Inspec::Resources
if resources.count == 1
@total = 1
# get the apiversion for the resource, if one has not been specified
apiversion = azure.get_api_version(resources[0].type, opts)
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)
# 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

View file

@ -132,6 +132,7 @@ module Inspec::Resources
# @author Russell Seymour
# @private
def create_has_methods
return if failed_resource?
# Create the has methods for each of the mappings
# This is a quick test to show that the resource group has at least one of these things
mapping.each do |name, type|

View file

@ -54,6 +54,7 @@ module Inspec::Resources
#
# @author Russell Seymour
def datadisk_details
return if failed_resource?
# Iterate around the data disks on the machine
properties.storageProfile.dataDisks.each_with_index.map do |datadisk, index|
# Call function to parse the data disks and return an object based on the parameters