2018-04-06 18:22:25 +00:00
require 'set'
require 'ipaddr'
2018-02-08 04:23:05 +00:00
class AwsSecurityGroup < Inspec . resource ( 1 )
name 'aws_security_group'
2017-12-14 14:28:29 +00:00
desc 'Verifies settings for an individual AWS Security Group.'
2018-04-06 18:22:25 +00:00
example "
describe aws_security_group ( 'sg-12345678' ) do
it { should exist }
end
"
2018-02-08 04:26:37 +00:00
supports platform : 'aws'
2017-12-14 14:28:29 +00:00
2018-02-08 04:26:37 +00:00
include AwsSingularResourceMixin
2018-09-18 20:21:41 +00:00
attr_reader :description , :group_id , :group_name , :vpc_id , :inbound_rules , :outbound_rules , :inbound_rules_count , :outbound_rules_count
2017-12-14 14:28:29 +00:00
def to_s
2018-01-05 18:12:06 +00:00
" EC2 Security Group #{ @group_id } "
2017-12-14 14:28:29 +00:00
end
2018-04-06 18:22:25 +00:00
def allow_in? ( criteria = { } )
2018-05-16 17:16:57 +00:00
allow ( inbound_rules , criteria . dup )
2018-04-06 18:22:25 +00:00
end
RSpec :: Matchers . alias_matcher :allow_in , :be_allow_in
def allow_out? ( criteria = { } )
2018-05-16 17:16:57 +00:00
allow ( outbound_rules , criteria . dup )
2018-04-06 18:22:25 +00:00
end
RSpec :: Matchers . alias_matcher :allow_out , :be_allow_out
def allow_in_only? ( criteria = { } )
2018-05-16 17:16:57 +00:00
allow_only ( inbound_rules , criteria . dup )
2018-04-06 18:22:25 +00:00
end
RSpec :: Matchers . alias_matcher :allow_in_only , :be_allow_in_only
def allow_out_only? ( criteria = { } )
2018-05-16 17:16:57 +00:00
allow_only ( outbound_rules , criteria . dup )
2018-04-06 18:22:25 +00:00
end
RSpec :: Matchers . alias_matcher :allow_out_only , :be_allow_out_only
2017-12-14 14:28:29 +00:00
private
2018-04-06 18:22:25 +00:00
def allow_only ( rules , criteria )
2018-11-13 18:25:33 +00:00
rules = allow__focus_on_position ( rules , criteria )
2018-04-06 18:22:25 +00:00
# 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 )
2018-11-13 18:25:33 +00:00
if criteria . key? ( :security_group )
if criteria . key? ( :position )
pos = criteria [ :position ] - 1
else
pos = 0
end
return false unless rules [ pos ] . key? ( :user_id_group_pairs ) && rules [ pos ] [ :user_id_group_pairs ] . count == 1
end
2018-04-06 18:22:25 +00:00
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 )
2018-09-18 20:21:41 +00:00
matched && = allow__match_ipv6_range ( rule , criteria )
2018-11-13 18:25:33 +00:00
matched && = allow__match_security_group ( rule , criteria )
2018-04-06 18:22:25 +00:00
matched
end
end
def allow__check_criteria ( raw_criteria )
allowed_criteria = [
:from_port ,
:ipv4_range ,
2018-09-18 20:21:41 +00:00
:ipv6_range ,
2018-11-13 18:25:33 +00:00
:security_group ,
2018-04-06 18:22:25 +00:00
: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
2018-09-18 20:21:41 +00:00
def match_ipv4_or_6_range ( rule , criteria )
if criteria . key? ( :ipv4_range )
query = criteria [ :ipv4_range ]
query = [ query ] unless query . is_a? ( Array )
ranges = rule [ :ip_ranges ] . map { | rng | rng [ :cidr_ip ] }
else # IPv6
query = criteria [ :ipv6_range ]
query = [ query ] unless query . is_a? ( Array )
ranges = rule [ :ipv_6_ranges ] . map { | rng | rng [ :cidr_ipv_6 ] }
end
2018-04-06 18:22:25 +00:00
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
2018-09-18 20:21:41 +00:00
def allow__match_ipv4_range ( rule , criteria )
return true unless criteria . key? ( :ipv4_range )
match_ipv4_or_6_range ( rule , criteria )
end
def allow__match_ipv6_range ( rule , criteria )
return true unless criteria . key? ( :ipv6_range )
match_ipv4_or_6_range ( rule , criteria )
end
2018-11-13 18:25:33 +00:00
def allow__match_security_group ( rule , criteria )
return true unless criteria . key? ( :security_group )
query = criteria [ :security_group ]
return false unless rule [ :user_id_group_pairs ]
rule [ :user_id_group_pairs ] . any? { | group | query == group [ :group_id ] }
end
2017-12-14 14:28:29 +00:00
def validate_params ( raw_params )
recognized_params = check_resource_param_names (
raw_params : raw_params ,
allowed_params : [ :id , :group_id , :group_name , :vpc_id ] ,
allowed_scalar_name : :group_id ,
allowed_scalar_type : String ,
)
# id is an alias for group_id
recognized_params [ :group_id ] = recognized_params . delete ( :id ) if recognized_params . key? ( :id )
if recognized_params . key? ( :group_id ) && recognized_params [ :group_id ] !~ / ^sg \ -[0-9a-f]{8} /
2018-02-08 04:23:05 +00:00
raise ArgumentError , 'aws_security_group security group ID must be in the format "sg-" followed by 8 hexadecimal characters.'
2017-12-14 14:28:29 +00:00
end
if recognized_params . key? ( :vpc_id ) && recognized_params [ :vpc_id ] !~ / ^vpc \ -[0-9a-f]{8} /
2018-02-08 04:23:05 +00:00
raise ArgumentError , 'aws_security_group VPC ID must be in the format "vpc-" followed by 8 hexadecimal characters.'
2017-12-14 14:28:29 +00:00
end
validated_params = recognized_params
if validated_params . empty?
2018-02-08 04:23:05 +00:00
raise ArgumentError , 'You must provide parameters to aws_security_group, such as group_name, group_id, or vpc_id.g_group.'
2017-12-14 14:28:29 +00:00
end
validated_params
end
2018-09-18 20:21:41 +00:00
def count_sg_rules ( ip_permissions )
rule_count = 0
ip_permissions . each do | ip_permission |
[ :ip_ranges , :ipv_6_ranges , :user_id_group_pairs ] . each do | key |
if ip_permission . key? key
rule_count += ip_permission [ key ] . length
end
end
end
rule_count
end
2018-04-06 18:22:25 +00:00
def fetch_from_api # rubocop: disable Metrics/AbcSize
2018-02-08 04:26:37 +00:00
backend = BackendFactory . create ( inspec_runner )
2017-12-14 14:28:29 +00:00
# Transform into filter format expected by AWS
filters = [ ]
[
:description ,
:group_id ,
:group_name ,
:vpc_id ,
] . each do | criterion_name |
2018-02-01 02:54:47 +00:00
instance_var = " @ #{ criterion_name } " . to_sym
next unless instance_variable_defined? ( instance_var )
val = instance_variable_get ( instance_var )
2017-12-14 14:28:29 +00:00
next if val . nil?
filters . push (
{
name : criterion_name . to_s . tr ( '_' , '-' ) ,
values : [ val ] ,
} ,
)
end
dsg_response = backend . describe_security_groups ( filters : filters )
if dsg_response . security_groups . empty?
@exists = false
2018-04-06 18:22:25 +00:00
@inbound_rules = [ ]
@outbound_rules = [ ]
2017-12-14 14:28:29 +00:00
return
end
@exists = true
@description = dsg_response . security_groups [ 0 ] . description
@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
2018-04-06 18:22:25 +00:00
@inbound_rules = dsg_response . security_groups [ 0 ] . ip_permissions . map ( & :to_h )
2018-09-18 20:21:41 +00:00
@inbound_rules_count = count_sg_rules ( dsg_response . security_groups [ 0 ] . ip_permissions . map ( & :to_h ) )
2018-04-06 18:22:25 +00:00
@outbound_rules = dsg_response . security_groups [ 0 ] . ip_permissions_egress . map ( & :to_h )
2018-09-18 20:21:41 +00:00
@outbound_rules_count = count_sg_rules ( dsg_response . security_groups [ 0 ] . ip_permissions_egress . map ( & :to_h ) )
2017-12-14 14:28:29 +00:00
end
class Backend
2018-02-08 04:26:37 +00:00
class AwsClientApi < AwsBackendBase
2018-02-09 05:56:28 +00:00
BackendFactory . set_default_backend self
2018-02-08 04:26:37 +00:00
self . aws_client_class = Aws :: EC2 :: Client
2017-12-14 14:28:29 +00:00
def describe_security_groups ( query )
2018-02-08 04:26:37 +00:00
aws_service_client . describe_security_groups ( query )
2017-12-14 14:28:29 +00:00
end
end
end
end