mirror of
https://github.com/inspec/inspec
synced 2024-11-10 07:04:15 +00:00
Catch cloud exceptions and document connection info (#2636)
Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
parent
59fd0e8775
commit
4f341acfbc
13 changed files with 287 additions and 59 deletions
|
@ -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
111
docs/platforms.md
Normal 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
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue