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