Add resources for aws_billing_report and aws_billing_reports. (#2838)

Signed-off-by: Miah Johnson <miah@chia-pet.org>
Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Miah Johnson 2019-01-09 16:06:48 -08:00 committed by Clinton Wolfe
parent c2b551e10b
commit 15162bf920
8 changed files with 615 additions and 0 deletions

View file

@ -0,0 +1,114 @@
---
title: About the aws_billing_report Resource
platform: aws
---
# aws\_billing\_report
Use the `aws_billing_report` InSpec audit resource to test properties of a single AWS Cost and Billing report.
<br>
## Syntax
# Verify the time_unit used by the 'inspec1' Billing Report.
describe aws_billing_report('inspec1') do
its('time_unit') { should cmp 'daily' }
end
# Hash Syntax to verify the time_unit used by the 'inspec1' Billing Report.
describe aws_billing_report(report_name: 'inspec1') do
its('time_unit') { should cmp 'daily' }
end
## Properties
`report_name`, `time_unit`, `compression`, `s3_bucket`, `s3_prefix`, `s3_region`, `additional_artifacts`
<br>
## Propery Examples
### report_name
The report's name.
describe aws_billing_report('inspec1') do
its('report_name') { should cmp 'inspec1' }
end
### time_unit
The interval of time covered by the report. Valid values: hourly or daily.
describe aws_billing_report('inspec1') do
its('time_unit') { should cmp 'hourly' }
end
### compression
The reports compression type. Valid values: zip, or gzip.
describe aws_billing_report('inspec1') do
its('compression') { should cmp 'zip' }
end
### s3_bucket
The s3_bucket the report is stored in.
describe aws_billing_report('inspec1') do
its('s3_bucket') { should cmp 'inspec-s3-bucket' }
end
### s3_prefix
The prefix that AWS adds to the report when stored.
describe aws_billing_report('inspec1') do
its('s3_prefix') { should cmp 'inspec1' }
end
### s3_region
The AWS region of the S3 bucket.
describe aws_billing_report('inspec1') do
its('s3_region') { should cmp 'us-east-1' }
end
## Matchers
For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### be_hourly
If true, indicates that the report summarizes usage on a per-hour basis.
describe aws_billing_report('inspec1') do
it { should be_hourly }
end
### be_daily
If true, indicates that the report summarizes usage on a per-day basis.
describe aws_billing_report('inspec1') do
it { should be_daily }
end
### exist
Indicates that the Billing Report provided was found. Use `should_not` to test for Billing Reports that should not exist.
# Verify that the 'inspec1' Billing Report exists.
describe aws_billing_report('inspec1') do
it { should exist }
end
# Verify that the 'inspec2' Billing Report does not exist.
describe aws_billing_report('invalid-inspec') do
it { should_not exist }
end

View file

@ -0,0 +1,93 @@
---
title: About the aws_billing_reports Resource
platform: aws
---
# aws\_billing\_reports
Use the `aws_billing_reports` InSpec audit resource to test properties of a some or all AWS Cost and Billing reports.
<br>
## Syntax
# Verify the number of Billing Reports in the AWS account.
describe aws_billing_reports do
its('count') { should cmp 2 }
end
# Use the .where clause to match a property to one or more rules in the available reports.
describe aws_billing_reports.where { report_name =~ /inspec.*/ } do
its('report_names') { should include 'inspec1' }
its('time_units') { should include 'DAILY' }
its('s3_buckets') { should include 'inspec1-s3-bucket' }
end
## Properties
`report_names`, `time_units`, `compressions`, `s3_buckets`, `s3_prefixs`, `s3_regions`
<br>
## Propery Examples
### report_names
A list of the names of the reports matched by the query.
describe aws_billing_reports do
its('report_names') { should include 'myreport' }
end
### time_units
A list of the time intervals of the reports matched by the query. Valid values: `hourly` or `daily`. This list is de-duplicated, so its count may not match the query count.
describe aws_billing_reports do
its('time_units') { should_not include 'hourly' }
end
### compressions
A list of the compression types of the reports matched by the query. Valid values: `zip`, or `gzip`. This list is de-duplicated, so its count may not match the query count.
describe aws_billing_reports do
its('compressions') { should_not include 'zip' }
end
### s3_buckets
A list of the S3 buckets the reports matched by the query are stored in. This list is de-duplicated, so its count may not match the query count.
describe aws_billing_reports do
its('s3_buckets') { should include 'some-s3-bucket'] }
end
### s3_prefixes
A list of the S3 prefixes (analogous to a directory on a filesystem) that the reports matched by the query are stored in. This list is de-duplicated, so its count may not match the query count.
describe aws_billing_reports do
its('s3_prefixes') { should include '/my/path/here' }
end
### s3_regions
A list of the S3 regions that reports matched by the query are stored in. This list is de-duplicated, so its count may not match the query count.
describe aws_billing_reports do
its('s3_regions') { should_not include 'us-west-1' }
end
## Matchers
For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/).
### exist
Indicates that the query matched at least one report. Use `should_not` to test for Billing Reports that should not exist.
# Verify that at least one Billing Report exists.
describe aws_billing_reports
it { should exist }
end

View file

@ -12,6 +12,8 @@ require 'resource_support/aws/aws_backend_base'
# Load all AWS resources
# TODO: loop over and load entire directory
# for f in ls lib/resources/aws/*; do t=$(echo $f | cut -c 5- | cut -f1 -d. ); echo "require '${t}'"; done
require 'resources/aws/aws_billing_report'
require 'resources/aws/aws_billing_reports'
require 'resources/aws/aws_cloudtrail_trail'
require 'resources/aws/aws_cloudtrail_trails'
require 'resources/aws/aws_cloudwatch_alarm'

View file

@ -0,0 +1,99 @@
class AwsBillingReport < Inspec.resource(1)
name 'aws_billing_report'
supports platform: 'aws'
desc 'Verifies settings for AWS Cost and Billing Reports.'
example "
describe aws_billing_report('inspec1') do
its('report_name') { should cmp 'inspec1' }
its('time_unit') { should cmp 'hourly' }
end
describe aws_billing_report(report: 'inspec1') do
it { should exist }
end"
include AwsSingularResourceMixin
attr_reader :report_name, :time_unit, :format, :compression, :s3_bucket,
:s3_prefix, :s3_region
def to_s
"AWS Billing Report #{report_name}"
end
def hourly?
exists? ? time_unit.eql?('hourly') : nil
end
def daily?
exists? ? time_unit.eql?('daily') : nil
end
def zip?
exists? ? compression.eql?('zip') : nil
end
def gzip?
exists? ? compression.eql?('gzip') : nil
end
private
def validate_params(raw_params)
validated_params = check_resource_param_names(
raw_params: raw_params,
allowed_params: [:report_name],
allowed_scalar_name: :report_name,
allowed_scalar_type: String,
)
if validated_params.empty?
raise ArgumentError, "You must provide the parameter 'report_name' to aws_billing_report."
end
validated_params
end
def fetch_from_api
report = find_report(report_name)
@exists = !report.nil?
if exists?
@time_unit = report.time_unit.downcase
@format = report.format.downcase
@compression = report.compression.downcase
@s3_bucket = report.s3_bucket
@s3_prefix = report.s3_prefix
@s3_region = report.s3_region
end
end
def find_report(report_name)
pagination_opts = {}
found_report_def = nil
while found_report_def.nil?
api_result = backend.describe_report_definitions(pagination_opts)
next_token = api_result.next_token
found_report_def = api_result.report_definitions.find { |report_def| report_def.report_name == report_name }
pagination_opts = { next_token: next_token }
next if found_report_def.nil? && next_token # Loop again: didn't find it, but there are more results
break if found_report_def.nil? && next_token.nil? # Give up: didn't find it, no more results
end
found_report_def
end
def backend
@backend ||= BackendFactory.create(inspec_runner)
end
class Backend
class AwsClientApi < AwsBackendBase
AwsBillingReport::BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::CostandUsageReportService::Client
def describe_report_definitions(query = {})
aws_service_client.describe_report_definitions(query)
end
end
end
end

View file

@ -0,0 +1,69 @@
require 'utils/filter'
class AwsBillingReports < Inspec.resource(1)
name 'aws_billing_reports'
supports platform: 'aws'
desc 'Verifies settings for AWS Cost and Billing Reports.'
example "
describe aws_billing_reports do
its('report_names') { should include 'inspec1' }
its('s3_buckets') { should include 'inspec1-s3-bucket' }
end
describe aws_billing_reports.where { report_name =~ /inspec.*/ } do
its ('report_names') { should include ['inspec1'] }
its ('time_units') { should include ['DAILY'] }
its ('s3_buckets') { should include ['inspec1-s3-bucket'] }
end"
include AwsPluralResourceMixin
filtertable = FilterTable.create
filtertable.register_custom_matcher(:exists?) { |x| !x.entries.empty? }
.register_column(:report_names, field: :report_name)
.register_column(:time_units, field: :time_unit, style: :simple)
.register_column(:formats, field: :format, style: :simple)
.register_column(:compressions, field: :compression, style: :simple)
.register_column(:s3_buckets, field: :s3_bucket, style: :simple)
.register_column(:s3_prefixes, field: :s3_prefix, style: :simple)
.register_column(:s3_regions, field: :s3_region, style: :simple)
filtertable.install_filter_methods_on_resource(self, :table)
def validate_params(resource_params)
unless resource_params.empty?
raise ArgumentError, 'aws_billing_reports does not accept resource parameters.'
end
resource_params
end
def to_s
'AWS Billing Reports'
end
def fetch_from_api
@table = []
pagination_opts = {}
backend = BackendFactory.create(inspec_runner)
loop do
api_result = backend.describe_report_definitions(pagination_opts)
api_result.report_definitions.each do |raw_report|
report = raw_report.to_h
%i(time_unit compression).each { |field| report[field].downcase! }
@table << report
end
pagination_opts = { next_token: api_result.next_token }
break unless api_result.next_token
end
end
class Backend
class AwsClientApi < AwsBackendBase
AwsBillingReports::BackendFactory.set_default_backend(self)
self.aws_client_class = Aws::CostandUsageReportService::Client
def describe_report_definitions(options = {})
aws_service_client.describe_report_definitions(options)
end
end
end
end

View file

@ -0,0 +1,99 @@
module MockAwsBillingReports
class Empty < AwsBackendBase
def describe_report_definitions(_query)
Aws::CostandUsageReportService::Types::DescribeReportDefinitionsResponse.new(report_definitions: [])
end
end
class Basic < AwsBackendBase
def describe_report_definitions(_query)
Aws::CostandUsageReportService::Types::DescribeReportDefinitionsResponse
.new(report_definitions:
[
Aws::CostandUsageReportService::Types::ReportDefinition.new(
report_name: 'inspec1',
time_unit: 'HOURLY',
format: 'textORcsv',
compression: 'ZIP',
s3_bucket: 'inspec1-s3-bucket',
s3_prefix: 'inspec1/accounting',
s3_region: 'us-east-1',
),
Aws::CostandUsageReportService::Types::ReportDefinition.new(
report_name: 'inspec2',
time_unit: 'DAILY',
format: 'textORcsv',
compression: 'GZIP',
s3_bucket: 'inspec2-s3-bucket',
s3_prefix: 'inspec2/accounting',
s3_region: 'us-west-1',
),
])
end
end
# This backend will always repond with 5 reports, as if the `max_results` option was passed to
# `#describe_report_definitions`. I chose 5 because when using `max_results` in the real world
# it seems to only accept a value of 5.
#
# == Returns:
# A Aws::CostandUsageReportService::Types::DescribeReportDefinitionsRespons object with two instance
# attributes:
# `report_definitions` An Array that includes a single page of 5 Reports.
# `next_token` A String set to the start of the next page. When `next_token` is nil, there are no more pages.
#
class Paginated < AwsBackendBase
# Generate a set of report data, and shuffle their order.
def generate_definitions
definitions = []
definitions << Aws::CostandUsageReportService::Types::ReportDefinition.new(
report_name: 'inspec1',
time_unit: 'HOURLY',
format: 'textORcsv',
compression: 'ZIP',
s3_bucket: 'inspec1-s3-bucket',
s3_prefix: 'inspec1/accounting',
s3_region: 'us-east-1')
definitions << Aws::CostandUsageReportService::Types::ReportDefinition.new(
report_name: 'inspec2',
time_unit: 'DAILY',
format: 'textORcsv',
compression: 'GZIP',
s3_bucket: 'inspec2-s3-bucket',
s3_prefix: 'inspec2/accounting',
s3_region: 'us-west-1')
(3..12).each do |i|
definitions <<
Aws::CostandUsageReportService::Types::ReportDefinition.new(
report_name: "inspec#{i}",
time_unit: %w{HOURLY DAILY}.sample,
format: 'textORcsv',
compression: %w{ZIP GZIP}.sample,
s3_bucket: "inspec#{i}-s3-bucket",
s3_prefix: "inspec#{i}",
s3_region: 'us-east-1'
)
end
definitions.shuffle
end
def describe_report_definitions(options = {})
@definitions ||= generate_definitions
starting_position = options.fetch(:next_token, 0)
selected_definitions = @definitions.slice(starting_position, 5).compact
next_token = starting_position + 5
next_token = @definitions.count < next_token ? nil : next_token
response = Aws::CostandUsageReportService::Types::DescribeReportDefinitionsResponse
.new(report_definitions: selected_definitions)
response.next_token = next_token
response
end
end
end

View file

@ -0,0 +1,78 @@
require 'helper'
require_relative 'aws_billing_backend'
class EmptyAwsBillingReportTest < Minitest::Test
def setup
AwsBillingReport::BackendFactory.select(MockAwsBillingReports::Empty)
end
def test_empty_query
assert_raises(ArgumentError) { AwsBillingReport.new }
end
end
class BasicAwsBillingReportTest < Minitest::Test
def setup
AwsBillingReport::BackendFactory.select(MockAwsBillingReports::Basic)
end
def test_search_hit_via_scalar
assert AwsBillingReport.new('inspec1').exists?
end
def test_search_miss_via_scalar
refute AwsBillingReport.new('non-existent').exists?
end
def test_search_hit_via_hash_works
assert AwsBillingReport.new(report_name: 'inspec1').exists?
end
def test_search_miss_is_not_an_exception
refute AwsBillingReport.new(report_name: 'non-existent').exists?
end
def test_search_hit_properties
r = AwsBillingReport.new('inspec1')
assert_equal('inspec1', r.report_name)
assert_equal('hourly', r.time_unit)
assert_equal('zip', r.compression)
assert_equal('inspec1-s3-bucket', r.s3_bucket)
assert_equal('inspec1/accounting', r.s3_prefix)
assert_equal('us-east-1', r.s3_region)
end
def test_hourly?
assert AwsBillingReport.new('inspec1').hourly?
refute AwsBillingReport.new('inspec2').hourly?
end
def test_daily?
assert AwsBillingReport.new('inspec2').daily?
refute AwsBillingReport.new('inspec1').daily?
end
def test_zip?
assert AwsBillingReport.new('inspec1').zip?
refute AwsBillingReport.new('inspec2').zip?
end
def test_gzip?
assert AwsBillingReport.new('inspec2').gzip?
refute AwsBillingReport.new('inspec1').gzip?
end
end
class PaginatedAwsBillingReportTest < Minitest::Test
def setup
AwsBillingReport::BackendFactory.select(MockAwsBillingReports::Paginated)
end
def test_paginated_search_hit_via_scalar
assert AwsBillingReport.new('inspec8').exists?
end
def test_paginated_search_miss_via_scalar
refute AwsBillingReport.new('non-existent').exists?
end
end

View file

@ -0,0 +1,61 @@
require 'helper'
require_relative 'aws_billing_backend'
class ConstructorAwsBillingReportsTest < Minitest::Test
def setup
AwsBillingReports::BackendFactory.select(MockAwsBillingReports::Empty)
end
def test_empty_params_ok
assert AwsBillingReports.new
end
def test_rejects_unrecognized_params
assert_raises(ArgumentError) { AwsBillingReports.new(unrecognized_param: 1) }
end
end
class EmptyAwsBillingReportsTest < Minitest::Test
def setup
AwsBillingReports::BackendFactory.select(MockAwsBillingReports::Empty)
end
def test_search_miss_reports_empty
refute AwsBillingReports.new.exists?
end
end
class BasicAwsBillingReportsTest < Minitest::Test
def setup
AwsBillingReports::BackendFactory.select(MockAwsBillingReports::Basic)
end
def test_search_hit_via_empty_filter
assert AwsBillingReports.new.exists?
end
def test_search_hit_properties
assert AwsBillingReports.new.report_names.include?('inspec1')
end
def test_where_hit
abr = AwsBillingReports.new.where { report_name =~ /inspec.*/ }
assert_includes abr.time_units, 'daily'
assert_includes abr.compressions, 'zip'
assert_includes abr.s3_buckets, 'inspec1-s3-bucket'
end
end
class PaginatedAwsBillingReportsTest < Minitest::Test
def setup
AwsBillingReports::BackendFactory.select(MockAwsBillingReports::Paginated)
end
def test_paginated_search_hit_via_scalar
assert AwsBillingReports.new.report_names.include?('inspec12')
end
def test_paginated_search_miss_via_scalar
refute AwsBillingReports.new.report_names.include?('non-existent')
end
end