diff --git a/docs/aws_vpc_subnet.md b/docs/aws_vpc_subnet.md new file mode 100644 index 000000000..1dc8b2104 --- /dev/null +++ b/docs/aws_vpc_subnet.md @@ -0,0 +1,148 @@ +--- +title: About the aws_vpc_subnet Resource +--- + +# aws_vpc_subnet + +Use the `aws_vpc_subnet` InSpec audit resource to test properties of a vpc subnet. + +To test properties of a single VPC subnet, use the `aws_vpc_subnet` resource. + +To test properties of all or a group of VPC subnets, use the `aws_vpc_subnets` resource. + +
+ +## Syntax + +An `aws_vpc_subnet` resource block uses the parameter to select a VPC and a subnet in the VPC. + + describe aws_vpc_subnet(vpc_id: 'vpc-01234567', subnet_id: 'subnet-1234567') do + it { should exist } + its('cidr_block') { should eq ['10.0.1.0/24'] } + end + +
+ +## Resource Parameters + +This InSpec resource accepts the following parameters, which are used to search for the VPCs subnet. + +### vpc_id + +A string identifying the VPC which contains zero or more subnets. + + # This will error if there is more than the default SG + describe aws_vpc_subnet(vpc_id: 'vpc-12345678', 'subnet-1234567') do + it { should exist } + end + +### subnet_id + +A string identifying the subnet that the VPC contains. + + # This will error if there is more than the default SG + describe aws_vpc_subnet(vpc_id: 'vpc-12345678', subnet_id: 'subnet-12345678') do + it { should exist } + end + +
+ +## Properties + +### assign_ipv_6_address_on_creation + +Detects whether the network interface on the subnet accepts IPv6 addresses. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('assign_ipv_6_address_on_creation') { should eq false } + end + +### availability_zone + +Provides the Availability Zone of the subnet. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('availability_zone') { should eq 'us-east-1c' } + end + +### available_ip_address_count + +Provides the number of available IPv4 addresses on the subnet. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('available_ip_address_count') { should eq 251 } + end + +### cidr_block + +Provides the block of ip addresses specified to the subnet. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('cidr_block') { should eq '10.0.1.0/24' } + end + +### default_for_az + +Detects if this is the default subnet for the Availability Zone. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('default_for_az') { should eq false } + end + +### ipv_6_cidr_block_association_set + +Provides information about the IPv6 cidr_block associatied with the subnet. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('ipv_6_cidr_block_association_set') { should eq [ + { + "Ipv6CidrBlock": "2001:db8:1234:a101::/64", + "AssociationId": "subnet-cidr-assoc-30e7e348", + "Ipv6CidrBlockState": { + "State": "ASSOCIATED" + } + } + ] } + end + +### map_public_ip_on_launch + +Provides the ID of the VPC the subnet is in. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('map_public_ip_on_launch') { should eq false } + end + +### state + +Provides the ID of the VPC the subnet is in. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('state') { should eq 'available' } + end + +### subnet_id + +Provides the ID of the VPC the subnet is in. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('subnet_id') { should eq 'subnet-12345678' } + end + +### vpc_id + +Provides the ID of the VPC the subnet is in. + + describe aws_vpc_subnet(vpc_id: 'vpc-12345678' , subnet_id: 'subnet-12345678') do + its('vpc_id') { should eq 'vpc-12345678' } + end + +## Matchers + +### exist + +The `exist` matcher indicates that a subnet exists for the specified vpc. + + describe aws_vpc_subnet(vpc_id: 'vpc-1234567', subnet_id: 'subnet-12345678') do + it { should exist } + end diff --git a/docs/resources/aws_vpc_subnet.md b/docs/resources/aws_vpc_subnet.md new file mode 100644 index 000000000..702925bf7 --- /dev/null +++ b/docs/resources/aws_vpc_subnet.md @@ -0,0 +1,123 @@ +--- +title: About the aws_vpc_subnet Resource +--- + +# aws_vpc_subnet + +Use the `aws_vpc_subnet` InSpec audit resource to test properties of a vpc subnet. + +To test properties of a single VPC subnet, use the `aws_vpc_subnet` resource. + +To test properties of all or a group of VPC subnets, use the `aws_vpc_subnets` resource. + +
+ +## Syntax + +An `aws_vpc_subnet` resource block uses the parameter to select a VPC and a subnet in the VPC. + + describe aws_vpc_subnet(subnet_id: 'subnet-1234567') do + it { should exist } + its('cidr_block') { should eq '10.0.1.0/24' } + end + +
+ +## Resource Parameters + +This InSpec resource accepts the following parameters, which are used to search for the VPCs subnet. + +### subnet_id + +A string identifying the subnet that the VPC contains. + + # This will error if there is more than the default SG + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + it { should exist } + end + +
+ +## Matchers + +### assigning_ipv_6_address_on_creation + +Detects whether the network interface on the subnet accepts IPv6 addresses. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + it { should be_assigning_ipv_6_address_on_creation } + end + +### available + +Provides the current state of the subnet. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + it { should be_available } + end + +### default_for_az + +Detects if this is the default subnet for the Availability Zone. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + it { should be_default_for_az } + end + +### exist + +The `exist` matcher indicates that a subnet exists for the specified vpc. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + it { should exist } + end + +### mapping_public_ip_on_launch + +Provides the ID of the VPC the subnet is in. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + it { should be_mapping_public_ip_on_launch } + end + +## Properties + +### availability_zone + +Provides the Availability Zone of the subnet. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + its('availability_zone') { should eq 'us-east-1c' } + end + +### available_ip_address_count + +Provides the number of available IPv4 addresses on the subnet. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + its('available_ip_address_count') { should eq 251 } + end + +### cidr_block + +Provides the block of ip addresses specified to the subnet. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + its('cidr_block') { should eq '10.0.1.0/24' } + end + +### subnet_id + +Provides the ID of the Subnet. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + its('subnet_id') { should eq 'subnet-12345678' } + end + +### vpc_id + +Provides the ID of the VPC the subnet is in. + + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + its('vpc_id') { should eq 'vpc-12345678' } + end diff --git a/libraries/aws_vpc_subnet.rb b/libraries/aws_vpc_subnet.rb new file mode 100644 index 000000000..8d9c1ad90 --- /dev/null +++ b/libraries/aws_vpc_subnet.rb @@ -0,0 +1,89 @@ +# author: Matthew Dromazos + +require '_aws' + +class AwsVpcSubnet < Inspec.resource(1) + name 'aws_vpc_subnet' + desc 'This resource is used to test the attributes of a VPC subnet' + example " + describe aws_vpc_subnet(subnet_id: 'subnet-12345678') do + it { should exist } + its('cidr_block') { should eq '10.0.1.0/24' } + end + " + + include AwsResourceMixin + attr_reader :vpc_id, :subnet_id, :cidr_block, :availability_zone, :available_ip_address_count, + :default_for_az, :mapping_public_ip_on_launch, :available, :ipv_6_cidr_block_association_set, + :assigning_ipv_6_address_on_creation + alias available? available + alias default_for_az? default_for_az + alias mapping_public_ip_on_launch? mapping_public_ip_on_launch + alias assigning_ipv_6_address_on_creation? assigning_ipv_6_address_on_creation + + def to_s + "VPC Subnet #{@subnet_id}" + end + + private + + def validate_params(raw_params) + validated_params = check_resource_param_names( + raw_params: raw_params, + allowed_params: [:subnet_id], + allowed_scalar_name: :subnet_id, + allowed_scalar_type: String, + ) + + # Make sure the subnet_id parameter was specified and in the correct form. + if validated_params.key?(:subnet_id) && validated_params[:subnet_id] !~ /^subnet\-[0-9a-f]{8}/ + raise ArgumentError, 'aws_vpc_subnet Subnet ID must be in the format "subnet-" followed by 8 hexadecimal characters.' + end + + if validated_params.empty? + raise ArgumentError, 'You must provide a subnet_id to aws_vpc_subnet.' + end + + validated_params + end + + def fetch_from_aws + backend = AwsVpcSubnet::BackendFactory.create + + # Transform into filter format expected by AWS + filters = [] + filters.push({ name: 'subnet-id', values: [@subnet_id] }) + ds_response = backend.describe_subnets(filters: filters) + + # If no subnets exist in the VPC, exist is false. + if ds_response.subnets.empty? + @exists = false + return + end + @exists = true + assign_properties(ds_response) + end + + def assign_properties(ds_response) + @vpc_id = ds_response.subnets[0].vpc_id + @subnet_id = ds_response.subnets[0].subnet_id + @cidr_block = ds_response.subnets[0].cidr_block + @availability_zone = ds_response.subnets[0].availability_zone + @available_ip_address_count = ds_response.subnets[0].available_ip_address_count + @default_for_az = ds_response.subnets[0].default_for_az + @mapping_public_ip_on_launch = ds_response.subnets[0].map_public_ip_on_launch + @available = ds_response.subnets[0].state == 'available' + @ipv_6_cidr_block_association_set = ds_response.subnets[0].ipv_6_cidr_block_association_set + @assigning_ipv_6_address_on_creation = ds_response.subnets[0].assign_ipv_6_address_on_creation + end + + # Uses the SDK API to really talk to AWS + class Backend + class AwsClientApi + BackendFactory.set_default_backend(self) + def describe_subnets(query) + AWSConnection.new.ec2_client.describe_subnets(query) + end + end + end +end diff --git a/test/integration/default/build/ec2.tf b/test/integration/default/build/ec2.tf index e9571f41f..a2ed72648 100644 --- a/test/integration/default/build/ec2.tf +++ b/test/integration/default/build/ec2.tf @@ -198,6 +198,19 @@ output "ec2_security_group_alpha_group_id" { value = "${aws_security_group.alpha.id}" } +#============================================================# +# VPC Subnets +#============================================================# + +resource "aws_subnet" "subnet_01" { + vpc_id = "${data.aws_vpc.default.id}" + cidr_block = "172.31.96.0/20" +} + +output "ec2_default_vpc_subnet_01_id" { + value = "${aws_subnet.subnet_01.id}" +} + output "ec2_security_group_alpha_group_name" { value = "${aws_security_group.alpha.name}" } diff --git a/test/integration/default/verify/controls/aws_vpc_subnet.rb b/test/integration/default/verify/controls/aws_vpc_subnet.rb new file mode 100644 index 000000000..c41d9ca0f --- /dev/null +++ b/test/integration/default/verify/controls/aws_vpc_subnet.rb @@ -0,0 +1,47 @@ +fixtures = {} +[ + 'ec2_security_group_default_vpc_id', + 'ec2_default_vpc_subnet_01_id', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/ec2.tf', + ) +end + +control "aws_vpc_subnet recall of subnet_01" do + # Test hash given subnet_id + describe aws_vpc_subnet(subnet_id: fixtures['ec2_default_vpc_subnet_01_id']) do + it { should exist } + end + + # Test scalar works + describe aws_vpc_subnet(fixtures['ec2_default_vpc_subnet_01_id']) do + it { should exist } + end + + describe aws_vpc_subnet(subnet_id: 'subnet-00000000') do + it { should_not exist } + end +end + +control "aws_vpc_subnet properties of subnet_01" do + describe aws_vpc_subnet(subnet_id: fixtures['ec2_default_vpc_subnet_01_id']) do + its('vpc_id') { should eq fixtures['ec2_security_group_default_vpc_id'] } + its('subnet_id') { should eq fixtures['ec2_default_vpc_subnet_01_id'] } + its('cidr_block') { should eq '172.31.96.0/20' } + its('available_ip_address_count') { should eq 4091 } + its('availability_zone') { should eq 'us-east-1c' } + its('ipv_6_cidr_block_association_set') { should eq [] } + end +end + +control "aws_vpc_subnet matchers of subnet_01" do + describe aws_vpc_subnet(subnet_id: fixtures['ec2_default_vpc_subnet_01_id']) do + it { should be_available } + it { should_not be_mapping_public_ip_on_launch } + it { should_not be_default_for_az } + it { should_not be_assigning_ipv_6_address_on_creation } + end +end diff --git a/test/unit/resources/aws_vpc_subnet_test.rb b/test/unit/resources/aws_vpc_subnet_test.rb new file mode 100644 index 000000000..5995ed910 --- /dev/null +++ b/test/unit/resources/aws_vpc_subnet_test.rb @@ -0,0 +1,160 @@ +# encoding: utf-8 +require 'helper' +require 'aws_vpc_subnet' + +# MVSSB = MockVpcSubnetSingleBackend +# Abbreviation not used outside this file + +#=============================================================================# +# Constructor Tests +#=============================================================================# +class AwsVpcSubnetConstructorTest < Minitest::Test + def setup + AwsVpcSubnet::BackendFactory.select(AwsMVSSB::Basic) + end + + def test_constructor_no_args_raises + assert_raises(ArgumentError) { AwsVpcSubnet.new } + end + + def test_constructor_expected_well_formed_args + AwsVpcSubnet.new(subnet_id: 'subnet-12345678') + end + + def test_constructor_reject_unknown_resource_params + assert_raises(ArgumentError) { AwsVpcSubnet.new(bla: 'blabla') } + end +end + +#=============================================================================# +# Recall +#=============================================================================# + +class AwsVpcSubnetRecallTest < Minitest::Test + def setup + AwsVpcSubnet::BackendFactory.select(AwsMVSSB::Basic) + end + + def test_search_hit_via_hash_with_vpc_id_and_subnet_id_works + assert AwsVpcSubnet.new(subnet_id: 'subnet-12345678').exists? + end + + def test_search_miss_is_not_an_exception + refute AwsVpcSubnet.new(subnet_id: 'subnet-00000000').exists? + end +end + +#=============================================================================# +# properties +#=============================================================================# + +class AwsVpcSubnetPropertiesTest < Minitest::Test + def setup + AwsVpcSubnet::BackendFactory.select(AwsMVSSB::Basic) + end + + def test_property_subnet_id + assert_equal('subnet-12345678', AwsVpcSubnet.new(subnet_id: 'subnet-12345678').subnet_id) + end + + def test_property_vpc_id + assert_equal('vpc-12345678', AwsVpcSubnet.new(subnet_id: 'subnet-12345678').vpc_id) + end + + def test_property_cidr_block + assert_equal('10.0.1.0/24', AwsVpcSubnet.new(subnet_id: 'subnet-12345678').cidr_block) + assert_nil(AwsVpcSubnet.new(subnet_id: 'subnet-00000000').cidr_block) + end + + def test_property_availability_zone + assert_equal('us-east-1', AwsVpcSubnet.new(subnet_id: 'subnet-12345678').availability_zone) + assert_nil(AwsVpcSubnet.new(subnet_id: 'subnet-00000000').availability_zone) + end + + def test_property_available_ip_address_count + assert_equal(251, AwsVpcSubnet.new(subnet_id: 'subnet-12345678').available_ip_address_count) + assert_nil(AwsVpcSubnet.new(subnet_id: 'subnet-00000000').available_ip_address_count) + end + + def test_property_ipv_6_cidr_block_association_set + assert_equal([], AwsVpcSubnet.new(subnet_id: 'subnet-12345678').ipv_6_cidr_block_association_set) + assert_nil(AwsVpcSubnet.new(subnet_id: 'subnet-00000000').ipv_6_cidr_block_association_set) + end +end + +#=============================================================================# +# Test Matchers +#=============================================================================# +class AwsVpcSubnetPropertiesTest < Minitest::Test + def test_matcher_assign_ipv_6_address_on_creation + assert AwsVpcSubnet.new(subnet_id: 'subnet-12345678').assigning_ipv_6_address_on_creation + refute AwsVpcSubnet.new(subnet_id: 'subnet-87654321').assigning_ipv_6_address_on_creation + end + + def test_matcher_available + assert AwsVpcSubnet.new(subnet_id: 'subnet-12345678').available? + refute AwsVpcSubnet.new(subnet_id: 'subnet-87654321').available? + end + + def test_matcher_default_for_az + assert AwsVpcSubnet.new(subnet_id: 'subnet-12345678').default_for_az? + refute AwsVpcSubnet.new(subnet_id: 'subnet-87654321').default_for_az? + end + + def test_matcher_map_public_ip_on_launch + assert AwsVpcSubnet.new(subnet_id: 'subnet-12345678').mapping_public_ip_on_launch + refute AwsVpcSubnet.new(subnet_id: 'subnet-87654321').mapping_public_ip_on_launch + end +end + + +#=============================================================================# +# Test Fixtures +#=============================================================================# + +module AwsMVSSB + class Basic < AwsVpcSubnet::Backend + def describe_subnets(query) + subnets = { + 'subnet-12345678' => OpenStruct.new({ + :subnets => [ + OpenStruct.new({ + availability_zone: "us-east-1", + available_ip_address_count: 251, + cidr_block: "10.0.1.0/24", + default_for_az: true, + map_public_ip_on_launch: true, + state: "available", + subnet_id: "subnet-12345678", + vpc_id: "vpc-12345678", + ipv_6_cidr_block_association_set: [], + assign_ipv_6_address_on_creation: true, + }), + ] + }), + 'subnet-87654321' => OpenStruct.new({ + :subnets => [ + OpenStruct.new({ + availability_zone: "us-east-1", + available_ip_address_count: 251, + cidr_block: "10.0.1.0/24", + default_for_az: false, + map_public_ip_on_launch: false, + state: "pending", + subnet_id: "subnet-87654321", + vpc_id: "vpc-87654321", + ipv_6_cidr_block_association_set: [], + assign_ipv_6_address_on_creation: false, + }), + ] + }), + 'empty' => OpenStruct.new({ + :subnets => [] + }) + } + + return subnets[query[:filters][0][:values][0]] unless subnets[query[:filters][0][:values][0]].nil? + subnets['empty'] + end + end +end