diff --git a/docs/resources/aws_vpcs.md.erb b/docs/resources/aws_vpcs.md.erb index 55d411d01..a3818b9f7 100644 --- a/docs/resources/aws_vpcs.md.erb +++ b/docs/resources/aws_vpcs.md.erb @@ -20,23 +20,94 @@ Every AWS account has at least one VPC, the "default" VPC, in every region. An `aws_vpcs` resource block uses an optional filter to select a group of VPCs and then tests that group. # The control will pass if the filter returns at least one result. Use `should_not` if you expect zero matches. + + # Since you always have at least one VPC, this will always pass. describe aws_vpcs do it { should exist } end + # Insist that all VPCs use the same DHCP option set. + describe aws_vpcs.where { dhcp_options_id != 'dopt-12345678' } do + it { should_not exist } + end +
## Examples The following examples show how to use this InSpec audit resource. -As this is the initial release of `aws_vpcs`, its limited functionality precludes examples. +### Check for a Particular VPC ID + + describe aws_vpcs do + its('vpc_ids') { should include 'vpc-12345678' } + end + +### Use the VPC IDs to Get a List of Default Security Groups + + aws_vpcs.vpc_ids.each do |vpc_id| + describe aws_security_group(vpc_id: vpc_id, group_name: 'default') do + it { should_not allow_in(port: 22) } + end + end
+## Filter Criteria + +### cidr_block + +Filters the results to include only those VPCs that match the given IPv4 range. This is a string value. + + # We shun the 10.0.0.0/8 space + describe aws_vpcs.where { cidr_block.start_with?('10') } do + it { should_not exist } + end + +### dhcp_option_id + +Filters the results to include only those VPCs that have the given DHCP Option Set. + + # Insist on one DHCP option set for all VPCs. + describe aws_vpcs.where { dhcp_options_id != 'dopt-12345678' } do + it { should_not exist } + end + +## Properties + +### cidr_blocks + +The cidr_blocks property provides a list of the CIDR blocks that the matched VPCs serve as strings. + + describe aws_vpcs do + # This is simple array membership checking - not subnet membership + its('cidr_blocks') { should include '179.0.0.0/16' } + end + +### dhcp_options_ids + +The dhcp_option_set_ids property provides a de-duplicated list of the DHCP Option Set IDs that the matched VPCs use when assigning IPs to resources. + + describe aws_vpcs do + its('dhcp_options_ids') { should include 'dopt-12345678' } + end + +### vpc_ids + +The vpc_ids property provides a list of the IDs of the matched VPCs. + + describe aws_vpcs do + its('vpc_ids') { should include 'vpc-12345678' } + end + + # Get a list of all VPC IDs + aws_vpcs.vpc_ids.each do |vpc_id| + # Do something with vpc_id + end + ## Matchers -This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [matchers page](https://www.inspec.io/docs/reference/matchers/). +This InSpec audit resource has the following special matchers. For a full list of available matchers, please visit our [Universal Matchers page](https://www.inspec.io/docs/reference/matchers/). ### exists diff --git a/lib/resources/aws/aws_vpc.rb b/lib/resources/aws/aws_vpc.rb index 23a172a20..fbdf09933 100644 --- a/lib/resources/aws/aws_vpc.rb +++ b/lib/resources/aws/aws_vpc.rb @@ -15,11 +15,8 @@ class AwsVpc < Inspec.resource(1) "VPC #{vpc_id}" end - [:cidr_block, :dhcp_options_id, :state, :vpc_id, :instance_tenancy, :is_default].each do |property| - define_method(property) do - @vpc[property] - end - end + attr_reader :cidr_block, :dhcp_options_id, :instance_tenancy, :is_default,\ + :state, :vpc_id alias default? is_default @@ -51,9 +48,16 @@ class AwsVpc < Inspec.resource(1) resp = backend.describe_vpcs({ filters: [filter] }) - @vpc = resp.vpcs[0].to_h - @vpc_id = @vpc[:vpc_id] - @exists = !@vpc.empty? + vpc = resp.vpcs[0].to_h + @exists = !vpc.empty? + return unless @exists + + @cidr_block = vpc[:cidr_block] + @dhcp_options_id = vpc[:dhcp_options_id] + @instance_tenancy = vpc[:instance_tenancy] + @is_default = vpc[:is_default] + @state = vpc[:state] + @vpc_id = vpc[:vpc_id] end class Backend diff --git a/lib/resources/aws/aws_vpcs.rb b/lib/resources/aws/aws_vpcs.rb index 45967bb51..13e4e6272 100644 --- a/lib/resources/aws/aws_vpcs.rb +++ b/lib/resources/aws/aws_vpcs.rb @@ -13,7 +13,13 @@ class AwsVpcs < Inspec.resource(1) # Underlying FilterTable implementation. filter = FilterTable.create filter.add_accessor(:entries) + .add_accessor(:where) .add(:exists?) { |x| !x.entries.empty? } + .add(:cidr_blocks, field: :cidr_block) + .add(:vpc_ids, field: :vpc_id) + # We need a dummy here, so FilterTable will define and populate the dhcp_options_id field + filter.add(:dummy, field: :dhcp_options_id) + .add(:dhcp_options_ids) { |obj| obj.entries.map(&:dhcp_options_id).uniq } filter.connect(self, :table) def validate_params(raw_params) @@ -29,7 +35,8 @@ class AwsVpcs < Inspec.resource(1) end def fetch_from_api - @table = BackendFactory.create(inspec_runner).describe_vpcs.to_h[:vpcs] + describe_vpcs_response = BackendFactory.create(inspec_runner).describe_vpcs + @table = describe_vpcs_response.to_h[:vpcs].map(&:to_h) end class Backend diff --git a/test/integration/aws/default/build/ec2.tf b/test/integration/aws/default/build/ec2.tf index 062310727..5d062c35c 100644 --- a/test/integration/aws/default/build/ec2.tf +++ b/test/integration/aws/default/build/ec2.tf @@ -10,24 +10,23 @@ # alpha | debian | N | t2.micro # beta | centos | Y | t2.small - resource "aws_instance" "alpha" { ami = "${data.aws_ami.debian.id}" instance_type = "t2.micro" tags { - Name = "${terraform.env}.alpha" + Name = "${terraform.env}.alpha" X-Project = "inspec" } } resource "aws_instance" "beta" { - ami = "${data.aws_ami.centos.id}" - instance_type = "t2.small" + ami = "${data.aws_ami.centos.id}" + instance_type = "t2.small" iam_instance_profile = "${aws_iam_instance_profile.profile_for_ec2_with_role.name}" tags { - Name = "${terraform.env}.beta" + Name = "${terraform.env}.beta" X-Project = "inspec" } } @@ -76,7 +75,7 @@ EOF } resource "aws_iam_instance_profile" "profile_for_ec2_with_role" { - name = "${terraform.env}.profile_for_ec2_with_role" + name = "${terraform.env}.profile_for_ec2_with_role" role = "${aws_iam_role.role_for_ec2_with_role.name}" } @@ -88,6 +87,7 @@ output "ec2_instance_has_role_id" { output "ec2_instance_type_t2_micro_id" { value = "${aws_instance.alpha.id}" } + output "ec2_instance_type_t2_small_id" { value = "${aws_instance.beta.id}" } @@ -96,8 +96,8 @@ output "ec2_instance_type_t2_small_id" { # Debian data "aws_ami" "debian" { - most_recent = true - owners = ["679593333241"] + most_recent = true + owners = ["679593333241"] filter { name = "name" @@ -110,21 +110,23 @@ data "aws_ami" "debian" { } filter { - name = "root-device-type" + name = "root-device-type" values = ["ebs"] } } + output "ec2_ami_id_debian" { value = "${data.aws_ami.debian.id}" } + output "ec2_instance_debian_id" { value = "${aws_instance.alpha.id}" } # Centos data "aws_ami" "centos" { - most_recent = true - owners = ["679593333241"] + most_recent = true + owners = ["679593333241"] filter { name = "name" @@ -137,13 +139,15 @@ data "aws_ami" "centos" { } filter { - name = "root-device-type" + name = "root-device-type" values = ["ebs"] } } + output "ec2_ami_id_centos" { value = "${data.aws_ami.centos.id}" } + output "ec2_instance_centos_id" { value = "${aws_instance.beta.id}" } @@ -159,7 +163,7 @@ data "aws_vpc" "default" { data "aws_security_group" "default" { vpc_id = "${data.aws_vpc.default.id}" - name = "default" + name = "default" } output "ec2_security_group_default_vpc_id" { @@ -174,6 +178,18 @@ resource "aws_vpc" "non_default" { cidr_block = "172.32.0.0/16" } +output "vpc_default_vpc_id" { + value = "${data.aws_vpc.default.id}" +} + +output "vpc_default_vpc_cidr_block" { + value = "${data.aws_vpc.default.cidr_block}" +} + +output "vpc_default_dhcp_options_id" { + value = "${data.aws_vpc.default.dhcp_options_id}" +} + output "vpc_non_default_id" { value = "${aws_vpc.non_default.id}" } @@ -186,6 +202,10 @@ output "vpc_non_default_instance_tenancy" { value = "${aws_vpc.non_default.instance_tenancy}" } +output "vpc_non_default_dhcp_options_id" { + value = "${aws_vpc.non_default.dhcp_options_id}" +} + # Create a security group with a known description # in the default VPC resource "aws_security_group" "alpha" { @@ -207,43 +227,42 @@ output "ec2_security_group_alpha_group_name" { # Populate SG Alpha with some rules resource "aws_security_group_rule" "alpha_http_world" { - type = "ingress" - from_port = "80" - to_port = "80" - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] + type = "ingress" + from_port = "80" + to_port = "80" + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] security_group_id = "${aws_security_group.alpha.id}" } resource "aws_security_group_rule" "alpha_ssh_in" { - type = "ingress" - from_port = "22" - to_port = "22" - protocol = "tcp" - cidr_blocks = ["10.1.2.0/24"] + type = "ingress" + from_port = "22" + to_port = "22" + protocol = "tcp" + cidr_blocks = ["10.1.2.0/24"] security_group_id = "${aws_security_group.alpha.id}" } resource "aws_security_group_rule" "alpha_x11" { - description = "Only allow X11 out for some reason" - type = "egress" - from_port = "6000" - to_port = "6007" - protocol = "tcp" - cidr_blocks = ["10.1.2.0/24", "10.3.2.0/24"] + description = "Only allow X11 out for some reason" + type = "egress" + from_port = "6000" + to_port = "6007" + protocol = "tcp" + cidr_blocks = ["10.1.2.0/24", "10.3.2.0/24"] security_group_id = "${aws_security_group.alpha.id}" } resource "aws_security_group_rule" "alpha_all_ports" { - type = "ingress" - from_port = "0" - to_port = "65535" - protocol = "tcp" - cidr_blocks = ["10.1.2.0/24"] + type = "ingress" + from_port = "0" + to_port = "65535" + protocol = "tcp" + cidr_blocks = ["10.1.2.0/24"] security_group_id = "${aws_security_group.alpha.id}" } - #============================================================# # VPC Subnets #============================================================# diff --git a/test/integration/aws/default/verify/controls/aws_vpc.rb b/test/integration/aws/default/verify/controls/aws_vpc.rb index 4fff46542..0a229aa7c 100644 --- a/test/integration/aws/default/verify/controls/aws_vpc.rb +++ b/test/integration/aws/default/verify/controls/aws_vpc.rb @@ -1,9 +1,12 @@ fixtures = {} [ - 'ec2_security_group_default_vpc_id', + 'vpc_default_vpc_id', + 'vpc_default_cidr_block', + 'vpc_default_dhcp_options_id', 'vpc_non_default_id', 'vpc_non_default_cidr_block', - 'vpc_non_default_instance_tenancy' + 'vpc_non_default_instance_tenancy', + 'vpc_non_default_dhcp_options_id', ].each do |fixture_name| fixtures[fixture_name] = attribute( fixture_name, @@ -13,7 +16,7 @@ fixtures = {} end control "aws_vpc recall" do - describe aws_vpc(fixtures['ec2_security_group_default_vpc_id']) do + describe aws_vpc(fixtures['vpc_default_vpc_id']) do it { should exist} end @@ -36,11 +39,11 @@ control "aws_vpc properties" do its('state') { should eq 'available' } its('cidr_block') { should eq fixtures['vpc_non_default_cidr_block']} its('instance_tenancy') { should eq fixtures['vpc_non_default_instance_tenancy']} - # TODO: figure out how to access the dhcp_options_id + its('dhcp_options_id') { should eq fixtures['vpc_non_default_dhcp_options_id']} end describe aws_vpc do - its('vpc_id') { should eq fixtures['ec2_security_group_default_vpc_id'] } + its('vpc_id') { should eq fixtures['vpc_default_vpc_id'] } end end @@ -49,7 +52,7 @@ control "aws_vpc matchers" do it { should be_default } end - describe aws_vpc(fixtures['ec2_security_group_default_vpc_id']) do + describe aws_vpc(fixtures['vpc_default_vpc_id']) do it { should be_default } end diff --git a/test/integration/aws/default/verify/controls/aws_vpcs.rb b/test/integration/aws/default/verify/controls/aws_vpcs.rb index bc6461dea..1bff2aa48 100644 --- a/test/integration/aws/default/verify/controls/aws_vpcs.rb +++ b/test/integration/aws/default/verify/controls/aws_vpcs.rb @@ -1,5 +1,46 @@ +fixtures = {} +[ + 'vpc_default_vpc_id', + 'vpc_default_cidr_block', + 'vpc_default_dhcp_options_id', + 'vpc_non_default_id', + 'vpc_non_default_cidr_block', + 'vpc_non_default_instance_tenancy', + 'vpc_non_default_dhcp_options_id', +].each do |fixture_name| + fixtures[fixture_name] = attribute( + fixture_name, + default: "default.#{fixture_name}", + description: 'See ../build/ec2.tf', + ) +end + control "aws_vpcs recall" do describe aws_vpcs do it { should exist } + its('vpc_ids') { should include fixtures['vpc_default_vpc_id'] } + its('vpc_ids') { should include fixtures['vpc_non_default_vpc_id'] } + its('entries.count') { should cmp 2 } end end + +control "aws_vpcs filtering" do + describe aws_vpcs.where { cidr_block.starts_with? '179'} do + its('vpc_ids.first') { should cmp fixtures['vpc_non_default_vpc_id'] } + end + + describe aws_vpcs.where { dhcp_option_set_id == fixtures['vpc_default_dhcp_option_set_id']} do + its('vpc_ids') { should include fixtures['vpc_default_vpc_id']} + end +end + +control "aws_vpcs properties" do + describe aws_vpcs do + its('vpc_ids') { should include fixtures['vpc_default_vpc_id'] } + its('vpc_ids') { should include fixtures['vpc_non_default_vpc_id'] } + its('cidr_blocks') { should include fixtures['vpc_default_cidr_block']} + its('cidr_blocks') { should include fixtures['vpc_non_default_cidr_block']} + its('dhcp_option_set_ids') { should include fixtures['vpc_default_dhcp_option_set_id']} + its('dhcp_option_set_ids') { should include fixtures['vpc_non_default_dhcp_option_set_id']} + end +end diff --git a/test/unit/resources/aws_vpc_test.rb b/test/unit/resources/aws_vpc_test.rb index 5444a31fb..a64d1b025 100644 --- a/test/unit/resources/aws_vpc_test.rb +++ b/test/unit/resources/aws_vpc_test.rb @@ -71,7 +71,8 @@ class AwsVpcPropertiesTest < Minitest::Test def test_property_vpc_id assert_equal('vpc-aaaabbbb', AwsVpc.new('vpc-aaaabbbb').vpc_id) - assert_nil(AwsVpc.new(vpc_id: 'vpc-00000000').vpc_id) + # Even on a miss, identifiers should be preserved + assert_equal('vpc-00000000', AwsVpc.new(vpc_id: 'vpc-00000000').vpc_id) end def test_property_cidr_block diff --git a/test/unit/resources/aws_vpcs_test.rb b/test/unit/resources/aws_vpcs_test.rb index 86e7b664e..b41e8bae3 100644 --- a/test/unit/resources/aws_vpcs_test.rb +++ b/test/unit/resources/aws_vpcs_test.rb @@ -1,4 +1,5 @@ require 'helper' +require 'ipaddr' # MAVPB = MockAwsVpcsPluralBackend # Abbreviation not used outside this file @@ -23,30 +24,77 @@ end #=============================================================================# -# Search / Recall +# Filter Criteria #=============================================================================# -class AwsVpcsRecallEmptyTest < Minitest::Test - - def setup - AwsVpcs::BackendFactory.select(MAVPB::Empty) - end - - def test_search_miss_via_empty_vpcs - refute AwsVpcs.new.exists? - end -end - -class AwsVpcsRecallBasicTest < Minitest::Test +class AwsVpcsFilterCriteriaTest < Minitest::Test def setup AwsVpcs::BackendFactory.select(MAVPB::Basic) end - def test_search_hit_via_empty_filter + def test_filter_with_no_criteria assert AwsVpcs.new.exists? end + + def test_filter_with_vpc_id + hit = AwsVpcs.new.where(:vpc_id => 'vpc-aaaabbbb') + assert(hit.exists?) + + miss = AwsVpcs.new.where(:vpc_id => 'vpc-09876543') + refute(miss.exists?) + end + + def test_filter_with_cidr_block + hit = AwsVpcs.new.where(:cidr_block => '10.0.0.0/16') + assert(hit.exists?) + + # This triggers a bug/misfeature in FilterTable - see https://github.com/chef/inspec/issues/2929 + # hit = AwsVpcs.new.where { IPAddr.new('10.0.0.0/8').include? cidr_block } + hit = AwsVpcs.new.where { cidr_block.start_with? '10' } + assert(hit.exists?) + assert_equal(2, hit.entries.count) + + miss = AwsVpcs.new.where(:cidr_block => '11.0.0.0/16') + refute(miss.exists?) + end + + def test_filter_with_dhcp_options_id + hit = AwsVpcs.new.where(:dhcp_options_id => 'dopt-aaaabbbb') + assert(hit.exists?) + + miss = AwsVpcs.new.where(:dhcp_options_id => 'dopt-12345678') + refute(miss.exists?) + end end +#=============================================================================# +# Properties +#=============================================================================# +class AwsVpcsProperties < Minitest::Test + + def setup + AwsVpcs::BackendFactory.select(MAVPB::Basic) + @vpcs = AwsVpcs.new + end + + def test_property_vpc_ids + assert_includes(@vpcs.vpc_ids, 'vpc-aaaabbbb') + assert_includes(@vpcs.vpc_ids, 'vpc-12344321') + refute_includes(@vpcs.vpc_ids, nil) + end + + def test_property_cidr_blocks + assert_includes(@vpcs.cidr_blocks, '10.0.0.0/16') + assert_includes(@vpcs.cidr_blocks, '10.1.0.0/16') + refute_includes(@vpcs.cidr_blocks, nil) + end + + def test_property_dhcp_options_ids + assert_includes(@vpcs.dhcp_options_ids, 'dopt-aaaabbbb') + # Should be de-duplicated + assert_equal(1, @vpcs.dhcp_options_ids.count) + end +end #=============================================================================# # Test Fixtures #=============================================================================# @@ -70,7 +118,7 @@ module MAVPB }), OpenStruct.new({ cidr_block: '10.1.0.0/16', - dhcp_options_id: 'dopt-43211234', + dhcp_options_id: 'dopt-aaaabbbb', # Same as vpc-aaaabbbb state: 'available', vpc_id: 'vpc-12344321', instance_tenancy: 'default',