AWS Security Group Rules properties and matchers (#2876)

Provides low-, and mid-level properties and matchers for examining rules on aws_security_group.

* Second draft of docs for SG rules interface; need to clarify semantics of reject
* First cut at unit tests
* Cleanup test fixtures
* Implementation for allow, with plausible unit tests
* Doc updates based on reality
* Add integration tests; move allow to allow_ / out; several docs updates
* Add be_open_to_the_world and be_open_to_the_world_on_port
* Update docs to reflect adding allow_only
* Update docs to reflect use of position to allow multiple rules with 'only'
* Implement allow_only with unit tests; still need integration tests
* Add integration tests for allow_only

Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Clinton Wolfe 2018-04-06 14:22:25 -04:00 committed by Jared Quick
parent 4a80cf936e
commit 4200fdd779
5 changed files with 558 additions and 31 deletions

View file

@ -8,31 +8,79 @@ Use the `aws_security_group` InSpec audit resource to test detailed properties o
SGs are a networking construct which contain ingress and egress rules for network communications. SGs may be attached to EC2 instances, as well as certain other AWS resources. Along with Network Access Control Lists, SGs are one of the two main mechanisms of enforcing network-level security.
## Limitations
While this resource provides facilities for searching inbound and outbound rules on a variety of criteria, there is currently no support for performing matches based on:
* IPv6 ranges
* References to other Security Groups
* References to VPC peers or other AWS services (that is, no support for searches based on 'prefix lists').
<br>
## Syntax
An `aws_security_group` resource block uses resource parameters to search for a Security Group and then tests that Security Group. If no SGs match, no error is raised, but the `exists` matcher returns `false` and all properties will be `nil`. If more than one SG matches (due to vague search parameters), an error is raised.
Resource parameters: group_id, group_name, id, vpc_id
# Ensure you have a security group with a certain ID
An `aws_security_group` resource block uses resource parameters to search for and then test a Security Group. If no SGs match, no error is raised, but the `exists` matcher returns `false`, and all scalar properties are `nil`. List properties returned under these conditions are empty lists. If more than one SG matches (due to vague search parameters), an error is raised.
# Ensure you have a Security Group with a specific ID
# This is "safe" - SG IDs are unique within an account
describe aws_security_group('sg-12345678') do
it { should exist }
end
# Ensure you have a security group with a certain ID
# Ensure you have a Security Group with a specific ID
# This uses hash syntax
describe aws_security_group(id: 'sg-12345678') do
it { should exist }
end
# Ensure you have a Security Group with a specific name. Names are
# unique within a VPC but not across VPCs.
# Using only Group returns an error if multiple SGs match.
describe aws_security_group(group_name: 'my-group') do
it { should exist }
end
# Add vpc_id to ensure uniqueness.
describe aws_security_group(group_name: 'my-group', vpc_id: 'vpc-12345678') do
it { should exist }
end
<br>
## Examples
The following examples show how to use this InSpec audit resource.
As this is the initial release of `aws_security_group`, its limited functionality precludes examples.
# Ensure that the linux_servers Security Group permits
# SSH from the 10.5.0.0/16 range, but not the world.
describe aws_security_group(group_name: linux_servers) do
# This passes if any inbound rule exists that specifies
# port 22 and the given IP range, regardless of protocol, etc.
it { should allow_in(port: 22, ipv4_range: '10.5.0.0/16') }
# This passes so long as no inbound rule that specifies port 22 exists
# with a source IP range of 0.0.0.0/0. Other properties are ignored.
it { should_not allow_in(port: 22, ipv4_range: '0.0.0.0/0') }
end
# Ensure that the careful_updates Security Group may only initiate contact with specific IPs.
describe aws_security_group(group_name: 'careful_updates') do
# If you have two rules, with one CIDR each:
[ '10.7.23.12/32', '10.8.23.12/32' ].each do |allowed_destination|
# This doesn't care about which ports are enabled
it { should allow_out(ipv4_range: allowed_destination) }
end
# If you have one rule with two CIDRs:
it { should allow_out(ipv4_range: [ '10.7.23.12/32', '10.8.23.12/32' ] }
# Expect exactly three rules.
its('outbound_rules.count') { should cmp 3 }
end
<br>
@ -42,7 +90,7 @@ This InSpec resource accepts the following parameters, which are used to search
### id, group\_id
The Security Group ID of the Security Group. This is of the format `sg-` followed by 8 hexadecimal characters. The ID is unique within your AWS account; using ID ensures that you will never match more than one SG. The ID is also the default resource parameter, so you may omit the hash syntax.
The Security Group ID of the Security Group. This is of the format `sg-` followed by 8 hexadecimal characters. The ID is unique within your AWS account; using ID ensures a match of only one SG. The ID is also the default resource parameter, so you may omit the hash syntax.
# Using Hash syntax
describe aws_security_group(id: 'sg-12345678') do
@ -61,23 +109,23 @@ The Security Group ID of the Security Group. This is of the format `sg-` followe
### group\_name
The string name of the Security Group. Every VPC has a security group named 'default'. Names are unique within a VPC, but not within an AWS account.
The string name of the Security Group. Every VPC has a Security Group named 'default'. Names are unique within a VPC, but not within an AWS account.
# Get default security group for a certain VPC
# Get default Security Group for a specific VPC
describe aws_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678') do
it { should exist }
end
# This will throw an error if there is a 'backend' SG in more than one VPC.
# This throws an error if more than one VPC has a 'backend' SG.
describe aws_security_group(group_name: 'backend') do
it { should exist }
end
### vpc\_id
A string identifying the VPC that contains the security group. Since VPCs commonly contain many SGs, you should add additional parameters to ensure you find exactly one SG.
A string identifying the VPC that contains the Security Group. Since VPCs commonly contain many SGs, you should add additional parameters to ensure you find exactly one SG.
# This will error if there is more than the default SG
# This throws an error if more than the default SG exists
describe aws_security_group(vpc_id: 'vpc-12345678') do
it { should exist }
end
@ -85,7 +133,7 @@ A string identifying the VPC that contains the security group. Since VPCs common
<br>
## Properties
* `description`, `group_id', `group_name`, `vpc_id`
* [`description`](#description), [`group_id`](#group_id), [`group_name`](#group_name), [`inbound_rules`](#inbound_rules), [`outbound_rules`](#outbound_rules), [`vpc_id`](#vpc_id)
<br>
@ -95,7 +143,7 @@ A string identifying the VPC that contains the security group. Since VPCs common
A String reflecting the human-meaningful description that was given to the SG at creation time.
# Require a description of a particular group
# Require a description of a particular Security Group
describe aws_security_group('sg-12345678') do
its('description') { should_not be_empty }
end
@ -104,28 +152,52 @@ A String reflecting the human-meaningful description that was given to the SG at
Provides the Security Group ID.
# Inspect the group ID of the default group
# Inspect the Security group ID of the default Group
describe aws_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678') do
its('group_id') { should cmp 'sg-12345678' }
end
# Store the group ID in a Ruby variable for use elsewhere
# Store the Group ID in a Ruby variable for use elsewhere
sg_id = aws_security_group(group_name: 'default', vpc_id: vpc_id: 'vpc-12345678').group_id
### group\_name
A String reflecting the name that was given to the SG at creation time.
# Inspect the group name of a particular group
# Inspect the Group name of a particular Group
describe aws_security_group('sg-12345678') do
its('group_name') { should cmp 'my_group' }
end
### inbound\_rules
A list of the rules that the Security Group applies to incoming network traffic. This is a low-level property that is used by the [`allow_in`](#allow_in) and [`allow_in_only`](#allow_in_only) matchers; see them for detailed examples. `inbound_rules` is provided here for those wishing to use Ruby code to inspect the rules directly, instead of using higher-level matchers.
Order is critical in these rules, as the sequentially first rule to match is applied to network traffic. By default, AWS includes a reject-all rule as the last inbound rule. This implicit rule does not appear in the inbound_rules list.
If the Security Group could not be found (that is, `exists` is false), `inbound_rules` returns an empty list.
describe aws_security_group(group_name: linux_servers) do
its('inbound_rules.first') { should include(from_port: '22', ip_ranges: ['10.2.17.0/24']) }
end
### outbound\_rules
A list of the rules that the Security Group applies to outgoing network traffic initiated by the AWS resource in the Security Group. This is a low-level property that is used by the [`allow_out`](#allow_out) matcher; see it for detailed examples. `outbound_rules` is provided here for those wishing to use Ruby code to inspect the rules directly, instead of using higher-level matchers.
Order is critical in these rules, as the sequentially first rule to match is applied to network traffic. Outbound rules are typically used when it is desirable to restrict which portions of the internet, if any, a resource may access. By default, AWS includes an allow-all rule as the last outbound rule; note that Terraform removes this implicit rule.
If the Security Group could not be found (that is, `exists` is false), `outbound_rules` returns an empty list.
describe aws_security_group(group_name: isolated_servers) do
its('outbound_rules.last') { should_not include(ip_ranges:['0.0.0.0/0']) }
end
### vpc\_id
A String in the format 'vpc-' followed by 8 hexadecimal characters reflecting VPC that contains the security group.
A String in the format 'vpc-' followed by 8 hexadecimal characters reflecting VPC that contains the Security Group.
# Inspec the VPC ID of a particular group
# Inspec the VPC ID of a particular Group
describe aws_security_group('sg-12345678') do
its('vpc_id') { should cmp 'vpc-12345678' }
end
@ -134,18 +206,85 @@ A String in the format 'vpc-' followed by 8 hexadecimal characters reflecting VP
## 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 additional available matchers, please visit our [Universal Matchers page](https://www.inspec.io/docs/reference/matchers/).
* [`allow_in`](#allow_in), [`allow_in_only`](#allow_in_only), [`allow_out`](#allow_out), [`allow_out_only`](#allow_out_only)
### allow\_in
### allow\_out
### allow\_in\_only
### allow\_out\_only
The `allow` series of matchers enable you to perform queries about what network traffic would be permitted through the Security Group rule set.
`allow_in` and `allow_in_exactly` examine inbound rules, and `allow_out` and `allow_out_exactly` examine outbound rules.
`allow_in` and `allow_out` examine if at least one rule that matches the criteria exists. `allow_in` and `allow_out` also perform inexact (ie, range-based or subset-based) matching on ports and IP addresses ranges, allowing you to specify a candidate port or IP address and determine if it is covered by a rule.
`allow_in_only` and `allow_out_only` examines if exactly one rule exists (but see `position`, below), and if it matches the criteria (this is useful for ensuring no unexpected rules have been added). Additionally, `allow_in_only` and `allow_out_only` do _not_ perform inexact matching; you must specify exactly the port range or IP address(es) you wish to match.
The matchers accept a key-value list of search criteria. For a rule to match, it must match all provided criteria.
* from_port - Determines if a rule exists whose port range begins at the specified number. The word 'from_' does *not* relate to inbound/outbound directionality; it relates to the port range ("counting _from_"). `from_port` is an exact criterion; so if the rule allows 1000-2000 and you specify a `from_port` of 1001, it does not match.
* ipv4_range - Specifies an IPv4 address or subnet as a CIDR, or a list of them, to be checked as a permissible origin (for `allow_in`) or destination (for `allow_out`) for traffic. Each AWS Security Group rule may have multiple allowed source IP ranges.
* port - Determines if a particular TCP/IP port is reachable. allow_in and allow_out examine whether the specified port is included in the port range of a rule, while allow_in. You may specify the port as a string (`'22'`) or as a number.
* position - A one-based index into the list of rules. If provided, this restricts the evaluation to the rule at that position. You may also use the special values `:first` and `:last`. `position` may also be used to enable `allow_in_only` and `allow_out_only` to work with multi-rule Security Groups.
* protocol - Specifies the IP protocol. 'tcp', 'udp', and 'icmp' are some typical values. The string "-1" or 'any' is used to indicate any protocol.
* to_port - Determines if a rule exists whose port range ends at the specified number. The word 'to_' does *not* relate to inbound/outbound directionality; it relates to the port range ("counting _to_"). `to_port` is an exact criterion; so if the rule allows 1000-2000 and you specify a `to_port` of 1999, it does not match.
describe aws_security_group(group_name: 'mixed-functionality-group') do
# Allow RDP from defined range
it { should allow_in(port: 3389, ipv4_range: '10.5.0.0/16') }
# Allow SSH from two ranges
it { should allow_in(port: 22, ipv4_range: ['10.5.0.0/16', '10.2.3.0/24']) }
# Check Bacula port range
it { should allow_in(from_port: 9101, to_port: 9103, ipv4_range: '10.6.7.0/24') }
# Assuming the AWS SG allows 9001-9003, use inexact matching to check 9002
it { should allow_in(port: 9002) }
# Assuming the AWS SG allows 10.2.1.0/24, use inexact matching to check 10.2.1.33/32
it { should allow_in(ipv4_range: '10.2.1.33/32') }
# Ensure the 3rd outbound rule is TCP-based
it { should allow_in(protocol: 'tcp', position: 3') }
# Do not allow unrestricted IPv4 access.
it { should_not allow_in(ipv4_range: '0.0.0.0/0') }
end
# Suppose you have a Group that should allow SSH and RDP from
# the admin network, 10.5.0.0/16. The resource has 2 rules to
# allow this, and you want to ensure no others have been added.
describe aws_security_group(group_name: 'admin-group') do
# Allow RDP from a defined range and nothing else
# The SG must have this rule in position 1 and it must match this exactly
it { should allow_in_only(port: 3389, ipv4_range: '10.5.0.0/16', position: 1) }
# Specify position 2 for the SSH rule. Without `position`,
# allow_in_only only allows one rule, total.
it { should allow_in_only(port: 22, ipv4_range: '10.5.0.0/16', position: 2) }
# Because this is an _only matcher, this fails - _only matchers
# use exact IP matching.
it { should allow_in_only(port: 3389, ipv4_range: '10.5.1.34/32', position: 1) }
end
### exists
The control will pass if the specified SG was found. Use `should_not` if you want to verify that the specified SG does not exist.
The control passes if the specified Security Group was found. Use `should_not` if you want to verify that the specified SG does not exist.
# You will always have at least one SG, the VPC default SG
# You always have at least one SG, the VPC default SG
describe aws_security_group(group_name: 'default')
it { should exist }
end
# Make sure we don't have any security groups with the name 'nogood'
# Make sure we don't have any Security Groups with the name 'nogood'
describe aws_security_group(group_name: 'nogood')
it { should_not exist }
end

View file

@ -1,22 +1,174 @@
require 'set'
require 'ipaddr'
class AwsSecurityGroup < Inspec.resource(1)
name 'aws_security_group'
desc 'Verifies settings for an individual AWS Security Group.'
example '
describe aws_security_group("sg-12345678") do
example "
describe aws_security_group('sg-12345678') do
it { should exist }
end
'
"
supports platform: 'aws'
include AwsSingularResourceMixin
attr_reader :description, :group_id, :group_name, :vpc_id
attr_reader :description, :group_id, :group_name, :vpc_id, :inbound_rules, :outbound_rules
def to_s
"EC2 Security Group #{@group_id}"
end
def allow_in?(criteria = {})
allow(inbound_rules, criteria)
end
RSpec::Matchers.alias_matcher :allow_in, :be_allow_in
def allow_out?(criteria = {})
allow(outbound_rules, criteria)
end
RSpec::Matchers.alias_matcher :allow_out, :be_allow_out
def allow_in_only?(criteria = {})
allow_only(inbound_rules, criteria)
end
RSpec::Matchers.alias_matcher :allow_in_only, :be_allow_in_only
def allow_out_only?(criteria = {})
allow_only(outbound_rules, criteria)
end
RSpec::Matchers.alias_matcher :allow_out_only, :be_allow_out_only
private
def allow_only(rules, criteria)
# allow_{in_out}_only require either a single-rule group, or you
# to select a rule using position.
return false unless rules.count == 1 || criteria.key?(:position)
criteria[:exact] = true
allow(rules, criteria)
end
def allow(rules, criteria)
criteria = allow__check_criteria(criteria)
rules = allow__focus_on_position(rules, criteria)
rules.any? do |rule|
matched = true
matched &&= allow__match_port(rule, criteria)
matched &&= allow__match_protocol(rule, criteria)
matched &&= allow__match_ipv4_range(rule, criteria)
matched
end
end
def allow__check_criteria(raw_criteria)
allowed_criteria = [
:from_port,
:ipv4_range,
:port,
:position,
:protocol,
:to_port,
:exact, # Internal
]
recognized_criteria = {}
allowed_criteria.each do |expected_criterion|
if raw_criteria.key?(expected_criterion)
recognized_criteria[expected_criterion] = raw_criteria.delete(expected_criterion)
end
end
# Any leftovers are unwelcome
unless raw_criteria.empty?
raise ArgumentError, "Unrecognized security group rule 'allow' criteria '#{raw_criteria.keys.join(',')}'. Expected criteria: #{allowed_criteria.join(', ')}"
end
recognized_criteria
end
def allow__focus_on_position(rules, criteria)
return rules unless criteria.key?(:position)
idx = criteria.delete(:position)
# Normalize to a zero-based numeric index
case # rubocop: disable Style/EmptyCaseCondition
when idx.is_a?(Symbol) && idx == :first
idx = 0
when idx.is_a?(Symbol) && idx == :last
idx = rules.count - 1
when idx.is_a?(String)
idx = idx.to_i - 1 # We document this as 1-based, so adjust to be zero-based.
when idx.is_a?(Numeric)
idx -= 1 # We document this as 1-based, so adjust to be zero-based.
else
raise ArgumentError, "aws_security_group 'allow' 'position' criteria must be an integer or the symbols :first or :last"
end
unless idx < rules.count
raise ArgumentError, "aws_security_group 'allow' 'position' criteria #{idx+1} is out of range - there are only #{rules.count} rules for security group #{group_id}."
end
[rules[idx]]
end
def allow__match_port(rule, criteria) # rubocop: disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
if criteria[:exact] || criteria[:from_port] || criteria[:to_port]
# Exact match mode
# :port is shorthand for a single-valued port range.
criteria[:to_port] = criteria[:from_port] = criteria[:port] if criteria[:port]
to = criteria[:to_port]
from = criteria[:from_port]
# It's a match if neither criteria was specified
return true if to.nil? && from.nil?
# Normalize to integers
to = to.to_i unless to.nil?
from = from.to_i unless from.nil?
# It's a match if either was specified and the other was not
return true if rule[:to_port] == to && from.nil?
return true if rule[:from_port] == from && to.nil?
# Finally, both must match.
rule[:to_port] == to && rule[:from_port] == from
elsif !criteria[:port]
# port not specified, match anything
true
else
# Range membership mode
rule_from = rule[:from_port] || 0
rule_to = rule[:to_port] || 65535
(rule_from..rule_to).cover?(criteria[:port].to_i)
end
end
def allow__match_protocol(rule, criteria)
return true unless criteria.key?(:protocol)
prot = criteria[:protocol]
# We provide a "fluency alias" for -1 (any).
prot = '-1' if prot == 'any'
rule[:ip_protocol] == prot
end
def allow__match_ipv4_range(rule, criteria)
return true unless criteria.key?(:ipv4_range)
query = criteria[:ipv4_range]
query = [query] unless query.is_a?(Array)
ranges = rule[:ip_ranges].map { |rng| rng[:cidr_ip] }
if criteria[:exact]
Set.new(query) == Set.new(ranges)
else
# CIDR subset mode
# "Each of the provided IP ranges must be a member of one of the rule's listed IP ranges"
query.all? do |candidate|
candidate = IPAddr.new(candidate)
ranges.any? do |range|
range = IPAddr.new(range)
range.include?(candidate)
end
end
end
end
def validate_params(raw_params)
recognized_params = check_resource_param_names(
raw_params: raw_params,
@ -44,7 +196,7 @@ class AwsSecurityGroup < Inspec.resource(1)
validated_params
end
def fetch_from_api
def fetch_from_api # rubocop: disable Metrics/AbcSize
backend = BackendFactory.create(inspec_runner)
# Transform into filter format expected by AWS
@ -70,6 +222,8 @@ class AwsSecurityGroup < Inspec.resource(1)
if dsg_response.security_groups.empty?
@exists = false
@inbound_rules = []
@outbound_rules = []
return
end
@ -78,6 +232,8 @@ class AwsSecurityGroup < Inspec.resource(1)
@group_id = dsg_response.security_groups[0].group_id
@group_name = dsg_response.security_groups[0].group_name
@vpc_id = dsg_response.security_groups[0].vpc_id
@inbound_rules = dsg_response.security_groups[0].ip_permissions.map(&:to_h)
@outbound_rules = dsg_response.security_groups[0].ip_permissions_egress.map(&:to_h)
end
class Backend

View file

@ -202,6 +202,48 @@ output "ec2_security_group_alpha_group_name" {
value = "${aws_security_group.alpha.name}"
}
# NOTE: AWS (in the console and CLI) creates SGs with a default
# allow all egress. Terraform removes that rule, unless you specify it here.
# 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"]
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"]
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"]
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"]
security_group_id = "${aws_security_group.alpha.id}"
}
#============================================================#
# VPC Subnets
#============================================================#

View file

@ -37,6 +37,36 @@ control "aws_security_group properties" do
its('group_name') { should cmp fixtures['ec2_security_group_alpha_group_name'] }
its('vpc_id') { should cmp fixtures['ec2_security_group_default_vpc_id'] }
its('description') { should cmp 'SG alpha' }
its('inbound_rules') { should be_a_kind_of(Array)}
its('inbound_rules.first') { should be_a_kind_of(Hash)}
its('inbound_rules.count') { should cmp 3 } # 3 explicit, one implicit
its('outbound_rules') { should be_a_kind_of(Array)}
its('outbound_rules.first') { should be_a_kind_of(Hash)}
its('outbound_rules.count') { should cmp 1 } # 1 explicit
end
end
control "aws_security_group matchers" do
describe aws_security_group(fixtures['ec2_security_group_alpha_group_id']) do
it { should allow_in(port: 22) }
it { should_not allow_in(port: 631, ipv4_range: "0.0.0.0/0") }
it { should allow_in(ipv4_range: "0.0.0.0/0", port: 80)}
it { should_not allow_in(ipv4_range: "0.0.0.0/0", port: 22)}
it { should allow_in(ipv4_range: "10.1.2.0/24", port: 22)}
it { should allow_in(ipv4_range: ["10.1.2.0/24"], port: 22)}
it { should allow_in({ipv4_range: "10.1.2.32/32", position: 2}) }
it { should_not allow_in_only({ipv4_range: "10.1.2.32/32", position: 2}) }
it { should allow_in_only({ipv4_range: "10.1.2.0/24", position: 2}) }
# Fixture allows out 6000-6007, with one rule
it { should allow_out(port: 6003)}
it { should_not allow_out_only(port: 6003)}
it { should allow_out_only(from_port: 6000, to_port: 6007)}
it { should allow_out(ipv4_range: ["10.1.2.0/24", "10.3.2.0/24"])}
it { should allow_out(ipv4_range: ["10.1.2.0/24", "10.3.2.0/24"], from_port: 6000, to_port: 6007)}
it { should allow_out(ipv4_range: ["10.1.2.0/24", "10.3.2.0/24"], from_port: 6000, to_port: 6007, position: 1)}
end
end

View file

@ -74,8 +74,110 @@ class AwsSGSProperties < Minitest::Test
assert_nil(AwsSecurityGroup.new('sg-87654321').description)
end
def test_property_inbound_rules
assert_empty(AwsSecurityGroup.new('sg-87654321').inbound_rules)
rules = AwsSecurityGroup.new('sg-12345678').inbound_rules
assert_kind_of(Array, rules)
assert_kind_of(Hash, rules[0])
end
def test_property_outbound_rules
assert_empty(AwsSecurityGroup.new('sg-87654321').outbound_rules)
rules = AwsSecurityGroup.new('sg-12345678').outbound_rules
assert_kind_of(Array, rules)
assert_kind_of(Hash, rules[0])
end
end
#=============================================================================#
# Matchers
#=============================================================================#
class AwsSGSProperties < Minitest::Test
def setup
AwsSecurityGroup::BackendFactory.select(AwsMESGSB::Basic)
end
def test_matcher_allow_criteria_validation
sg = AwsSecurityGroup.new('sg-aaaabbbb')
rules = sg.inbound_rules
assert_raises(ArgumentError, "allow should reject unrecognized criteria") { sg.allow_in?(shoe_size: 9) }
[
:from_port,
:ipv4_range,
:port,
:position,
:protocol,
:to_port,
].each do |criterion|
# No errors here
sg.allow_in?(criterion => 'dummy')
end
end
def test_matcher_allow_inbound_empty
sg = AwsSecurityGroup.new('sg-aaaabbbb')
rules = sg.inbound_rules
assert_equal(0, rules.count)
refute(sg.allow_in?()) # Should we test this - "open" crieria?
end
def test_matcher_allow_inbound_complex
sg = AwsSecurityGroup.new('sg-12345678')
assert_equal(3, sg.inbound_rules.count, "count the number of rules for 3-rule group")
# Position pinning
assert(sg.allow_in?(ipv4_range: "10.1.4.0/24", position: 2), "use numeric position")
assert(sg.allow_in?(ipv4_range: "10.1.4.0/24", position: "2"), "use string position")
assert(sg.allow_in?(ipv4_range: "10.2.0.0/16", position: :last), "use :last position")
assert(sg.allow_in?(port: 22, position: :first), "use :first position")
# Port
assert(sg.allow_in?(port: 22), "match on a numeric port")
assert(sg.allow_in?(port: "22"), "match on a string port")
assert(sg.allow_in?(to_port: "22", from_port: "22"), "match on to/from port")
assert(sg.allow_in?(port: 9002, position: 3), "range matching on port with allow_in")
refute(sg.allow_in_only?(port: 9002, position: 3), "no range matching on port with allow_in_only")
assert(sg.allow_in_only?(from_port: 9001, to_port: 9003, position: 3), "exact range matching on port with allow_in_only")
# Protocol
assert(sg.allow_in?(protocol: 'tcp'), "match on tcp protocol, unpinned")
assert(sg.allow_in?(protocol: 'tcp', position: 1), "match on tcp protocol")
assert(sg.allow_in?(protocol: 'any', position: 2), "match on our 'any' alias protocol")
assert(sg.allow_in?(protocol: '-1', position: 2), "match on AWS spec '-1 for any' protocol")
# IPv4 range testing
assert(sg.allow_in?(ipv4_range: ["10.1.4.0/24"]), "match on 1 ipv4 range as array")
assert(sg.allow_in?(ipv4_range: ["10.1.4.0/24"]), "match on 1 ipv4 range as array")
assert(sg.allow_in?(ipv4_range: ["10.1.4.33/32"]), "match on 1 ipv4 range subnet membership")
assert(sg.allow_in?(ipv4_range: ["10.1.4.33/32", "10.1.4.82/32"]), "match on 2 addrs ipv4 range subnet membership")
assert(sg.allow_in?(ipv4_range: ["10.1.4.0/25", "10.1.4.128/25"]), "match on 2 subnets ipv4 range subnet membership")
assert(sg.allow_in_only?(ipv4_range: "10.1.4.0/24", position: 2), "exact match on 1 ipv4 range with _only")
refute(sg.allow_in_only?(ipv4_range: "10.1.4.33/32", position: 2), "no range membership ipv4 range with _only")
assert(sg.allow_in?(ipv4_range: "10.1.2.0/24"), "match on a list ipv4 range when providing only one value (first)")
assert(sg.allow_in?(ipv4_range: "10.1.3.0/24"), "match on a list ipv4 range when providing only one value (last)")
assert(sg.allow_in?(ipv4_range: ["10.1.2.33/32", "10.1.3.33/32"]), "match on a list of single IPs against a list of subnets")
assert(sg.allow_in?(ipv4_range: ["10.1.2.0/24", "10.1.3.0/24"]))
refute(sg.allow_in?(ipv4_range: ["10.1.22.0/24", "10.1.33.0/24"]))
assert(sg.allow_in?(ipv4_range: ["10.1.3.0/24", "10.1.2.0/24"])) # Order is ignored
assert(sg.allow_in_only?(ipv4_range: ["10.1.2.0/24", "10.1.3.0/24"], position: 1))
refute(sg.allow_in_only?(ipv4_range: ["10.1.2.0/24"], position: 1))
refute(sg.allow_in_only?(ipv4_range: ["10.1.3.0/24"], position: 1))
# Test _only with a 3-rule group, but omitting position
refute(sg.allow_in_only?(port: 22), "_only will fail a multi-rule SG even if it has matching criteria")
refute(sg.allow_in_only?(), "_only will fail a multi-rule SG even if it has match-any criteria")
# Test _only with a single rule group (ie, omitting position)
sg = AwsSecurityGroup.new('sg-22223333')
assert_equal(1, sg.inbound_rules.count, "count the number of rules for 1-rule group")
assert(sg.allow_in_only?(ipv4_range: "0.0.0.0/0"), "Match IP range using _only on 1-rule group")
assert(sg.allow_in_only?(protocol: 'any'), "Match protocol using _only on 1-rule group")
refute(sg.allow_in_only?(port: 22), "no match port using _only on 1-rule group")
end
end
#=============================================================================#
# Test Fixtures
#=============================================================================#
@ -97,12 +199,70 @@ module AwsMESGSB
group_id: 'sg-aaaabbbb',
group_name: 'alpha',
vpc_id: 'vpc-aaaabbbb',
ip_permissions: [],
ip_permissions_egress: [],
}),
OpenStruct.new({
description: 'Awesome Group',
group_id: 'sg-12345678',
group_name: 'beta',
vpc_id: 'vpc-12345678',
ip_permissions: [
OpenStruct.new({
from_port: 22,
to_port: 22,
ip_protocol: 'tcp',
ip_ranges: [
# Apparently AWS returns these as plain hashes,
# nested in two levels of Structs.
{cidr_ip:"10.1.2.0/24"},
{cidr_ip:"10.1.3.0/24"},
]
}),
OpenStruct.new({
from_port: nil,
to_port: nil,
ip_protocol: "-1",
ip_ranges: [
{cidr_ip:"10.1.4.0/24"},
]
}),
OpenStruct.new({
from_port: 9001,
to_port: 9003,
ip_protocol: "udp",
ip_ranges: [
{cidr_ip:"10.2.0.0/16"},
]
}),
],
ip_permissions_egress: [
OpenStruct.new({
from_port: 123,
to_port: 123,
ip_protocol: "udp",
ip_ranges: [
{cidr_ip:"128.138.140.44/32"},
]
}),
],
}),
OpenStruct.new({
description: 'Open Group',
group_id: 'sg-22223333',
group_name: 'gamma',
vpc_id: 'vpc-12345678',
ip_permissions: [
OpenStruct.new({
from_port: nil,
to_port: nil,
ip_protocol: "-1",
ip_ranges: [
{cidr_ip:"0.0.0.0/0"},
]
}),
],
ip_permissions_egress: [],
}),
]