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,8 +3,10 @@ module AwsResourceMixin
validate_params(resource_params).each do |param, value|
instance_variable_set(:"@#{param}", value)
end
catch_aws_errors do
fetch_from_api
end
end
# Default implementation of validate params accepts everything.
def validate_params(resource_params)
@ -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,6 +52,7 @@ EOX
def id
return @instance_id if defined?(@instance_id)
catch_aws_errors do
if @opts.is_a?(Hash)
first = @ec2_resource.instances(
{
@ -52,6 +68,7 @@ EOX
@instance_id = @opts
end
end
end
alias instance_id id
def exists?
@ -61,8 +78,10 @@ EOX
# returns the instance state
def state
catch_aws_errors do
instance&.state&.name
end
end
# helper methods for each state
%w{
@ -82,25 +101,32 @@ EOX
instance_type image_id vpc_id
}.each do |attribute|
define_method attribute do
catch_aws_errors do
instance.send(attribute) if instance
end
end
end
def security_groups
catch_aws_errors do
@security_groups ||= instance.security_groups.map { |sg|
{ id: sg.group_id, name: sg.group_name }
}
end
end
def tags
catch_aws_errors do
@tags ||= instance.tags.map { |tag| { key: tag.key, value: tag.value } }
end
end
def to_s
"EC2 Instance #{@display_name}"
end
def has_roles?
catch_aws_errors do
instance_profile = instance.iam_instance_profile
if instance_profile
@ -113,11 +139,12 @@ EOX
roles && !roles.empty?
end
end
private
def instance
@instance ||= @ec2_resource.instance(id)
catch_aws_errors { @instance ||= @ec2_resource.instance(id) }
end
end

View file

@ -54,8 +54,10 @@ class AwsIamAccessKey < Inspec.resource(1)
return nil unless exists?
return @last_used_date if defined? @last_used_date
backend = BackendFactory.create(inspec_runner)
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
backend = BackendFactory.create(inspec_runner)

View file

@ -16,12 +16,29 @@ EOX
# TODO: rewrite to avoid direct injection, match other resources, use AwsSingularResourceMixin
def initialize(conn = nil)
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 = 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
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,16 +10,17 @@ 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.
catch_aws_errors do
@bucket_acl ||= BackendFactory.create(inspec_runner).get_bucket_acl(bucket: bucket_name).grants
end
end
def bucket_policy
@bucket_policy ||= fetch_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)
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] }
OpenStruct.new(lowercase_hash)
@bucket_policy = OpenStruct.new(lowercase_hash)
end
rescue Aws::S3::Errors::NoSuchBucketPolicy
return []
@bucket_policy = []
end
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
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)
@ -58,11 +79,15 @@ module Inspec::Resources
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

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