aws_security_group: Query against other security group ids in allow_* matchers (#3576)

* add security-group to security-group rules
* update docs
* Add integration tests for security-group to security-group rules
* rubocop fix
*     Add one security group rule, with position.
* make control fit description

Signed-off-by: Timothy van Zadelhoff <timothy.inspec@theothersolution.nl
This commit is contained in:
Timothy van Zadelhoff 2018-11-13 19:25:33 +01:00 committed by Jared Quick
parent 8033134ebe
commit 5739cb2d6b
5 changed files with 297 additions and 5 deletions

View file

@ -12,7 +12,6 @@ SGs are a networking construct which contain ingress and egress rules for networ
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:
* References to other Security Groups
* References to VPC peers or other AWS services (that is, no support for searches based on 'prefix lists').
<br>
@ -85,12 +84,36 @@ The following examples show how to use this InSpec audit resource.
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' ] }
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
# Ensure that the canary_deployments Security Group only allows access from one specific security group id on port 443.
describe aws_security_group(group_name: 'canary_deployments') do
it { should allow_in_only(port: 443, security_group: "sg-33334444") }
end
# Ensure that one of the security groups in a list allows access from one of the groups in another list.
# If you have two lists of groups, check if one of the groups in the first list allows access for one of the
# groups in the second list.
control 'master to node access security group' do
desc 'one of the node security groups should allow all access from:
one of the master security groups, so masters can reach all nodes'
describe.one do
[ 'sg-11112222', 'sg-33334444' ].each do |nodeSG|
[ 'sg-55556666', 'sg-77778888' ].each do |masterSG|
describe aws_security_group(id: nodeSG) do
it { should allow_in(security_group: masterSG) }
end
end
end
end
end
<br>
## Resource Parameters
@ -260,6 +283,7 @@ The matchers accept a key-value list of search criteria. For a rule to match, i
* 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.
* security_group - Specifies a security-group id, to be checked as permissible origin (for `allow_in`) or destination (for `allow_out`) for traffic. Each AWS Security Group rule may have multiple allowed source or destination security groups.
describe aws_security_group(group_name: 'mixed-functionality-group') do
# Allow RDP from defined range
@ -283,6 +307,9 @@ The matchers accept a key-value list of search criteria. For a rule to match, i
# Do not allow unrestricted IPv4 access.
it { should_not allow_in(ipv4_range: '0.0.0.0/0') }
# Allow unrestricted access from security-group.
it { should allow_in(security_group: 'sg-11112222') }
end
# Suppose you have a Group that should allow SSH and RDP from

View file

@ -41,9 +41,18 @@ class AwsSecurityGroup < Inspec.resource(1)
private
def allow_only(rules, criteria)
rules = allow__focus_on_position(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)
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
criteria[:exact] = true
allow(rules, criteria)
end
@ -58,6 +67,7 @@ class AwsSecurityGroup < Inspec.resource(1)
matched &&= allow__match_protocol(rule, criteria)
matched &&= allow__match_ipv4_range(rule, criteria)
matched &&= allow__match_ipv6_range(rule, criteria)
matched &&= allow__match_security_group(rule, criteria)
matched
end
end
@ -67,6 +77,7 @@ class AwsSecurityGroup < Inspec.resource(1)
:from_port,
:ipv4_range,
:ipv6_range,
:security_group,
:port,
:position,
:protocol,
@ -187,6 +198,13 @@ class AwsSecurityGroup < Inspec.resource(1)
match_ipv4_or_6_range(rule, criteria)
end
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
def validate_params(raw_params)
recognized_params = check_resource_param_names(
raw_params: raw_params,

View file

@ -222,6 +222,38 @@ output "ec2_security_group_alpha_group_name" {
value = "${aws_security_group.alpha.name}"
}
# Create another security group
# in the default VPC
resource "aws_security_group" "beta" {
name = "${terraform.env}-beta"
description = "SG beta"
vpc_id = "${data.aws_vpc.default.id}"
}
output "ec2_security_group_beta_group_id" {
value = "${aws_security_group.beta.id}"
}
output "ec2_security_group_beta_group_name" {
value = "${aws_security_group.beta.name}"
}
# Create third security group
# in the default VPC
resource "aws_security_group" "gamma" {
name = "${terraform.env}-gamma"
description = "SG gamma"
vpc_id = "${data.aws_vpc.default.id}"
}
output "ec2_security_group_gamma_group_id" {
value = "${aws_security_group.gamma.id}"
}
output "ec2_security_group_gamma_group_name" {
value = "${aws_security_group.gamma.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.
@ -273,6 +305,44 @@ resource "aws_security_group_rule" "alpha_piv6_all_ports" {
security_group_id = "${aws_security_group.alpha.id}"
}
# Populate SG Beta with some rules
resource "aws_security_group_rule" "beta_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.beta.id}"
}
resource "aws_security_group_rule" "beta_ssh_in_alfa" {
type = "ingress"
from_port = "22"
to_port = "22"
protocol = "tcp"
source_security_group_id = "${aws_security_group.alpha.id}"
security_group_id = "${aws_security_group.beta.id}"
}
resource "aws_security_group_rule" "beta_all_ports_in_gamma" {
type = "ingress"
from_port = "0"
to_port = "65535"
protocol = "tcp"
source_security_group_id = "${aws_security_group.gamma.id}"
security_group_id = "${aws_security_group.beta.id}"
}
# Populate SG Gamma with a rule
resource "aws_security_group_rule" "gamma_ssh_in_alfa" {
type = "ingress"
from_port = "22"
to_port = "22"
protocol = "tcp"
source_security_group_id = "${aws_security_group.alpha.id}"
security_group_id = "${aws_security_group.gamma.id}"
}
#============================================================#
# VPC Subnets
#============================================================#

View file

@ -3,6 +3,8 @@ fixtures = {}
'ec2_security_group_default_vpc_id',
'ec2_security_group_default_group_id',
'ec2_security_group_alpha_group_id',
'ec2_security_group_beta_group_id',
'ec2_security_group_gamma_group_id',
'ec2_security_group_alpha_group_name',
].each do |fixture_name|
fixtures[fixture_name] = attribute(
@ -74,4 +76,11 @@ control "aws_security_group matchers" do
it { should allow_out(ipv6_range: ["2001:db8::/122"])}
it { should allow_out(ipv6_range: ["2001:db8::/122"], from_port: 6000, to_port: 6007)}
end
describe aws_security_group(fixtures['ec2_security_group_beta_group_id']) do
it { should allow_in(port: 22, security_group: fixtures['ec2_security_group_alpha_group_id']) }
it { should allow_in(security_group: fixtures['ec2_security_group_gamma_group_id']) }
end
describe aws_security_group(fixtures['ec2_security_group_gamma_group_id']) do
it { should allow_in_only(port: 22, security_group: fixtures['ec2_security_group_alpha_group_id']) }
end
end

View file

@ -123,6 +123,7 @@ class AwsSGSProperties < Minitest::Test
:position,
:protocol,
:to_port,
:security_group,
].each do |criterion|
# No errors here
sg.allow_in?(criterion => 'dummy')
@ -133,7 +134,7 @@ class AwsSGSProperties < Minitest::Test
sg = AwsSecurityGroup.new('sg-aaaabbbb')
rules = sg.inbound_rules
assert_equal(0, rules.count)
refute(sg.allow_in?()) # Should we test this - "open" crieria?
refute(sg.allow_in?()) # Should we test this - "open" criteria?
end
def test_matcher_allow_inbound_complex
@ -200,14 +201,40 @@ class AwsSGSProperties < Minitest::Test
# Test _only with a single rule group for IPv6
sg = AwsSecurityGroup.new('sg-33334444')
assert_equal(1, sg.inbound_rules.count, "count the number of rules for 1-rule ipv6 group")
assert_equal(1, sg.inbound_rules_count, "Count the number of rule variants for 1-rule gipv6 roup")
assert_equal(1, sg.inbound_rules_count, "Count the number of rule variants for 1-rule ipv6 group")
assert(sg.allow_in_only?(ipv6_range: "::/0"), "Match IP range using _only on 1-rule ipv6 group")
assert(sg.allow_in_only?(protocol: 'any'), "Match protocol using _only on 1-rule ipv6 group")
refute(sg.allow_in_only?(port: 22), "no match port using _only on 1-rule ipv6 group")
# security-group
sg = AwsSecurityGroup.new('sg-55556666')
assert(sg.allow_in?(security_group: "sg-33334441"), "match on group-id")
assert(sg.allow_in?(security_group: "sg-33334441", port: 22), "match on group-id, numeric port")
assert(sg.allow_in?(security_group: "sg-33334441", port: "22"), "match on group-id, string port")
assert(sg.allow_in?(security_group: "sg-33334441", to_port: "22", from_port: "22"), "match on group-id, 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")
refute(sg.allow_in_only?(security_group: "sg-33334441",), "no matching on group with allow_in_only when multiple group rules")
assert(sg.allow_in_only?(from_port: 9001, to_port: 9003, position: 3), "exact range matching on port with allow_in_only")
# Test _only with a single rule group for security-group
sg = AwsSecurityGroup.new('sg-33334441')
assert_equal(1, sg.inbound_rules.count, "count the number of rules for 1-rule security-group")
assert_equal(1, sg.inbound_rules_count, "Count the number of rule variants for 1-rule security-group")
assert(sg.allow_in_only?(security_group: "sg-33334444"), "Match security-group using _only on 1-rule security-group")
assert(sg.allow_in_only?(protocol: 'any',security_group: "sg-33334444"), "Match protocol using _only on 1-rule security-group")
refute(sg.allow_in_only?(port: 22, security_group: "sg-33334444"), "no match port using _only on 1-rule security-group")
# Test _only with a single rule group for security-group with position pinning
sg = AwsSecurityGroup.new('sg-33334442')
assert(sg.allow_in_only?(security_group: "sg-33334444", position: 2), "Match security-group using _only with numerical position")
assert(sg.allow_in_only?(protocol: 'any',security_group: "sg-33334444", position: 2), "Match protocol using _only on 1-rule security-group with numerical position")
refute(sg.allow_in_only?(port: 22, security_group: "sg-33334444", position: 2), "no match port using _only on 1-rule security-group with numerical position")
assert(sg.allow_in_only?(security_group: "sg-33334444", position: "2"), "Match security-group using _only with string position")
assert(sg.allow_in_only?(security_group: "sg-33334444", position: :last), "Match security-group using _only with last position")
end
end
#=============================================================================#
# Test Fixtures
#=============================================================================#
@ -320,6 +347,147 @@ module AwsMESGSB
}),
],
ip_permissions_egress: [],
}),
OpenStruct.new({
description: 'Open for group one group rule second position',
group_id: 'sg-33334442',
group_name: 'etha',
vpc_id: 'vpc-12345678',
ip_permissions: [
OpenStruct.new({
from_port: nil,
to_port: nil,
ip_protocol: "-1",
ipv_6_ranges: [
{cidr_ipv_6:"::/0"},
]
}),
OpenStruct.new({
from_port: nil,
to_port: nil,
ip_protocol: "-1",
user_id_group_pairs: [
OpenStruct.new({
description: 'Open for group one rule second position',
group_id: 'sg-33334444',
group_name: 'delta',
peering_status: "",
user_id: "123456789012",
vpc_id: "",
vpc_peering_connection_id: ""
}),
]
}),
],
ip_permissions_egress: [],
}),
OpenStruct.new({
description: 'Open for group one rule',
group_id: 'sg-33334441',
group_name: 'zeta',
vpc_id: 'vpc-12345678',
ip_permissions: [
OpenStruct.new({
from_port: nil,
to_port: nil,
ip_protocol: "-1",
user_id_group_pairs: [
OpenStruct.new({
description: 'Open for group one rule',
group_id: 'sg-33334444',
group_name: 'delta',
peering_status: "",
user_id: '123456789012',
vpc_id: "",
vpc_peering_connection_id: ""
}),
]
}),
],
ip_permissions_egress: [],
}),
OpenStruct.new({
description: 'Open for group',
group_id: 'sg-55556666',
group_name: 'epsilon',
vpc_id: 'vpc-12345678',
ip_permissions: [
OpenStruct.new({
from_port: 80,
to_port: 443,
ip_protocol: "-1",
ip_ranges: [
{cidr_ip:"0.0.0.0/0"},
]
}),
OpenStruct.new({
from_port: 22,
to_port: 22,
ip_protocol: "-1",
user_id_group_pairs: [
OpenStruct.new({
description: 'Open for group rule 2',
group_id: 'sg-33334441',
group_name: 'zeta',
peering_status: "",
user_id: '123456789012',
vpc_id: "",
vpc_peering_connection_id: ""
}),
]
}),
OpenStruct.new({
from_port: 9001,
to_port: 9003,
ip_protocol: "-1",
user_id_group_pairs: [
OpenStruct.new({
description: 'Open for group rule 3',
group_id: 'sg-33334441',
group_name: 'zeta',
peering_status: "",
user_id: '123456789012',
vpc_id: "",
vpc_peering_connection_id: ""
}),
]
}),
OpenStruct.new({
from_port: nil,
to_port: nil,
ip_protocol: "-1",
user_id_group_pairs: [
OpenStruct.new({
description: 'allow all from multiple sg',
group_id: 'sg-33334441',
group_name: 'zeta',
peering_status: "",
user_id: '123456789012',
vpc_id: "",
vpc_peering_connection_id: ""
}),
OpenStruct.new({
description: 'allow all from multiple sg[2]',
group_id: 'sg-33334442',
group_name: 'etha',
peering_status: "",
user_id: '123456789012',
vpc_id: "",
vpc_peering_connection_id: ""
}),
OpenStruct.new({
description: 'allow all from multiple sg[3]',
group_id: 'sg-11112222',
group_name: 'theta',
peering_status: "",
user_id: '123456789012',
vpc_id: "",
vpc_peering_connection_id: ""
}),
]
}),
],
ip_permissions_egress: [],
}), ]
selected = fixtures.select do |sg|