2019-06-11 22:24:35 +00:00
require " set "
require " ipaddr "
2018-04-06 18:22:25 +00:00
2019-06-11 22:24:35 +00:00
require " resource_support/aws/aws_singular_resource_mixin "
require " resource_support/aws/aws_backend_base "
require " aws-sdk-ec2 "
2019-05-25 08:33:26 +00:00
2018-02-08 04:23:05 +00:00
class AwsSecurityGroup < Inspec . resource ( 1 )
2019-06-11 22:24:35 +00:00
name " aws_security_group "
desc " Verifies settings for an individual AWS Security Group. "
2019-03-19 14:17:32 +00:00
example << ~ EXAMPLE
2019-03-19 16:30:35 +00:00
describe aws_security_group ( 'sg-12345678' ) do
it { should exist }
end
2019-03-19 14:17:32 +00:00
EXAMPLE
2019-06-11 22:24:35 +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 )
2019-07-09 00:20:30 +00:00
2018-11-13 18:25:33 +00:00
if criteria . key? ( :security_group )
if criteria . key? ( :position )
2019-06-11 22:24:35 +00:00
pos = criteria [ :position ] - 1
2018-11-13 18:25:33 +00:00
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?
2019-07-09 00:20:30 +00:00
raise ArgumentError , " Unrecognized security group rule 'allow' criteria ' #{ raw_criteria . keys . join ( " , " ) } '. Expected criteria: #{ allowed_criteria . join ( " , " ) } "
2018-04-06 18:22:25 +00:00
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
2019-06-11 22:24:35 +00:00
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 } . "
2018-04-06 18:22:25 +00:00
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?
2019-07-09 00:20:30 +00:00
2018-04-06 18:22:25 +00:00
# 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?
2019-07-09 00:20:30 +00:00
2018-04-06 18:22:25 +00:00
# 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 )
2019-07-09 00:20:30 +00:00
2018-04-06 18:22:25 +00:00
prot = criteria [ :protocol ]
# We provide a "fluency alias" for -1 (any).
2019-06-11 22:24:35 +00:00
prot = " -1 " if prot == " any "
2018-04-06 18:22:25 +00:00
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 )
2019-07-09 00:20:30 +00:00
2018-09-18 20:21:41 +00:00
match_ipv4_or_6_range ( rule , criteria )
end
def allow__match_ipv6_range ( rule , criteria )
return true unless criteria . key? ( :ipv6_range )
2019-07-09 00:20:30 +00:00
2018-09-18 20:21:41 +00:00
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 )
2019-07-09 00:20:30 +00:00
2018-11-13 18:25:33 +00:00
query = criteria [ :security_group ]
return false unless rule [ :user_id_group_pairs ]
2019-07-09 00:20:30 +00:00
2018-11-13 18:25:33 +00:00
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 ,
2019-07-09 00:20:30 +00:00
allowed_params : % i { id group_id group_name vpc_id } ,
2017-12-14 14:28:29 +00:00
allowed_scalar_name : :group_id ,
2019-06-11 22:24:35 +00:00
allowed_scalar_type : String
2017-12-14 14:28:29 +00:00
)
# 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?
2019-06-11 22:24:35 +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
2019-07-09 00:20:30 +00:00
2017-12-14 14:28:29 +00:00
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 |
2019-07-09 00:20:30 +00:00
% i { ip_ranges ipv_6_ranges user_id_group_pairs } . each do | key |
2018-09-18 20:21:41 +00:00
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 = [ ]
2019-07-09 00:20:30 +00:00
% i {
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 )
2019-07-09 00:20:30 +00:00
2018-02-01 02:54:47 +00:00
val = instance_variable_get ( instance_var )
2017-12-14 14:28:29 +00:00
next if val . nil?
2019-07-09 00:20:30 +00:00
2017-12-14 14:28:29 +00:00
filters . push (
{
2019-06-11 22:24:35 +00:00
name : criterion_name . to_s . tr ( " _ " , " - " ) ,
2017-12-14 14:28:29 +00:00
values : [ val ] ,
2019-06-11 22:24:35 +00:00
}
2017-12-14 14:28:29 +00:00
)
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