Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Network ACL overlapping port rule (Fixes #487, #483) #491

Merged
merged 2 commits into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Update tests for network acl entry issues (Fixes #487, Fixes #483)
- Test for port values from references
- Test for entries without logical nacl resource
- Test for correctly evaluating metadata to ignore warning
Fix up various linting errors in yaml templates
Refactor nacl grouping logic
  • Loading branch information
arothian committed Oct 12, 2020
commit 215fc5f834a3864d5812eaacb73f1b226621a9c9
Original file line number Diff line number Diff line change
Expand Up @@ -18,79 +18,80 @@ def rule_id
end

def audit_impl(cfn_model)
nacl_entries = cfn_model.resources_by_type('AWS::EC2::NetworkAclEntry')

# Select nacl entries that can be evaluated
nacl_entries.select! do |nacl_entry|
tcp_or_udp_protocol?(nacl_entry) && valid_ports?(nacl_entry)
end

violating_nacl_entries = []
cfn_model.resources_by_type('AWS::EC2::NetworkAcl').each do |nacl|
violating_nacl_entries += violating_nacl_entries(nacl)

# Group entries by nacl id, ip type, and egress/ingress
grouped_nacl_entries = group_nacl_entries(nacl_entries)

grouped_nacl_entries.each do |grouping|
violating_nacl_entries += overlapping_port_entries(grouping)
end
violating_nacl_entries.map(&:logical_resource_id)
end

private

def overlapping_port_entries(nacl_entries)
unique_pairs(nacl_entries).select do |nacl_entry_pair|
tcp_or_udp_protocol?(nacl_entry_pair[0], nacl_entry_pair[1]) && overlap?(nacl_entry_pair[0], nacl_entry_pair[1])
end
def tcp_or_udp_protocol?(entry)
%w[6 17].include?(entry.protocol.to_s)
end

def tcp_or_udp_protocol?(entry1, entry2)
%w[6 17].include?(entry1.protocol.to_s) && %w[6 17].include?(entry2.protocol.to_s)
def valid_ports?(entry)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running the test cfn_nag_scan --input-path spec/test_templates/yaml/ec2_networkaclentry/ec2_networkaclentry_missing_port_range.yml, we fail here with this response:

Traceback (most recent call last):
21: from /Users/stephengoncher/.rvm/gems/ruby-2.6.6/bin/ruby_executable_hooks:24:in <main>' 20: from /Users/stephengoncher/.rvm/gems/ruby-2.6.6/bin/ruby_executable_hooks:24:in eval'
19: from /Users/stephengoncher/.rvm/gems/ruby-2.6.6/bin/cfn_nag_scan:23:in <main>' 18: from /Users/stephengoncher/.rvm/gems/ruby-2.6.6/bin/cfn_nag_scan:23:in load'
17: from /private/tmp/arothian_cfn_nag/bin/cfn_nag_scan:11:in <top (required)>' 16: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/cfn_nag_executor.rb:30:in scan'
15: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/cfn_nag_executor.rb:50:in execute_aggregate_scan' 14: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/cfn_nag.rb:34:in audit_aggregate_across_files_and_render_results'
13: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/cfn_nag.rb:64:in audit_aggregate_across_files' 12: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/cfn_nag.rb:64:in each'
11: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/cfn_nag.rb:67:in block in audit_aggregate_across_files' 10: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/cfn_nag.rb:91:in audit'
9: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rule_loader.rb:64:in execute_custom_rules' 8: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rule_loader.rb:81:in filter_rule_classes'
7: from /Users/stephengoncher/.rvm/rubies/ruby-2.6.6/lib/ruby/2.6.0/set.rb:338:in each' 6: from /Users/stephengoncher/.rvm/rubies/ruby-2.6.6/lib/ruby/2.6.0/set.rb:338:in each_key'
5: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rule_loader.rb:91:in block in filter_rule_classes' 4: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rules/base.rb:19:in audit'
3: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rules/EC2NetworkAclEntryOverlappingPortsRule.rb:24:in audit_impl' 2: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rules/EC2NetworkAclEntryOverlappingPortsRule.rb:24:in select!'
1: from /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rules/EC2NetworkAclEntryOverlappingPortsRule.rb:25:in block in audit_impl' /private/tmp/arothian_cfn_nag/lib/cfn-nag/custom_rules/EC2NetworkAclEntryOverlappingPortsRule.rb:46:in valid_ports?': undefined method `[]' for nil:NilClass (NoMethodError)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, updated!

valid_port_number?(entry.portRange['From']) && valid_port_number?(entry.portRange['To'])
end

def unique_pairs(arr)
pairs_without_dupes = arr.product(arr).select { |pair| pair[0] != pair[1] }
pairs_without_dupes.reduce(Set.new) { |set_of_sets, pair| set_of_sets << Set.new(pair) }.to_a.map(&:to_a)
def valid_port_number?(port)
port.is_a?(Numeric) || (port.is_a?(String) && port.to_i(10) != 0)
end

def overlap?(entry1, entry2)
roverlap?(entry1, entry2) || loverlap?(entry1, entry2)
end
def group_nacl_entries(nacl_entries)
grouped_nacl_entries = []

def roverlap?(entry1, entry2)
entry1.portRange['From'].between?(entry2.portRange['From'], entry2.portRange['To']) ||
entry1.portRange['To'].between?(entry2.portRange['From'], entry2.portRange['To'])
end
# Group by NaclID
nacl_entries.group_by(&:networkAclId).each_value do |entries|
# Split entries by ip type
ipv4_entries, ipv6_entries = entries.partition { |nacl_entry| nacl_entry.ipv6CidrBlock.nil? }

def loverlap?(entry1, entry2)
entry2.portRange['From'].between?(entry1.portRange['From'], entry1.portRange['To']) ||
entry2.portRange['To'].between?(entry1.portRange['From'], entry1.portRange['To'])
end
# Split entries by egress/ingress
egress4, ingress4 = ipv4_entries.partition { |nacl_entry| truthy?(nacl_entry.egress) }
egress6, ingress6 = ipv6_entries.partition { |nacl_entry| truthy?(nacl_entry.egress) }

def egress_entries(nacl_entries)
nacl_entries.select do |nacl_entry|
truthy?(nacl_entry.egress)
grouped_nacl_entries << egress4
grouped_nacl_entries << ingress4
grouped_nacl_entries << egress6
grouped_nacl_entries << ingress6
end
end

def ingress_entries(nacl_entries)
nacl_entries.select do |nacl_entry|
not_truthy?(nacl_entry.egress)
end
grouped_nacl_entries
end

def ip6_entries(nacl_entries)
nacl_entries.select do |nacl_entry|
!nacl_entry.ipv6CidrBlock.nil?
end
def overlapping_port_entries(nacl_entries)
unique_pairs(nacl_entries).select do |nacl_entry_pair|
overlap?(nacl_entry_pair[0], nacl_entry_pair[1])
end.flatten.uniq
end

def ip4_entries(nacl_entries)
nacl_entries.select do |nacl_entry|
nacl_entry.ipv6CidrBlock.nil?
end
def unique_pairs(arr)
pairs_without_dupes = arr.product(arr).select { |pair| pair[0] != pair[1] }
pairs_without_dupes.reduce(Set.new) { |set_of_sets, pair| set_of_sets << Set.new(pair) }.to_a.map(&:to_a)
end

def violating_nacl_entries(nacl)
violating_ip4_nacl_entries(nacl) || violating_ip6_nacl_entries(nacl)
def overlap?(entry1, entry2)
port_overlap?(entry1.portRange, entry2.portRange) || port_overlap?(entry2.portRange, entry1.portRange)
end

def violating_ip4_nacl_entries(nacl)
overlapping_port_entries(egress_entries(ip4_entries(nacl.network_acl_entries))).flatten.uniq &&
overlapping_port_entries(ingress_entries(ip4_entries(nacl.network_acl_entries))).flatten.uniq
def port_overlap?(port_range1, port_range2)
port_number(port_range1['From']).between?(port_number(port_range2['From']), port_number(port_range2['To'])) ||
port_number(port_range1['To']).between?(port_number(port_range2['From']), port_number(port_range2['To']))
end

def violating_ip6_nacl_entries(nacl)
overlapping_port_entries(egress_entries(ip6_entries(nacl.network_acl_entries))).flatten.uniq &&
overlapping_port_entries(ingress_entries(ip6_entries(nacl.network_acl_entries))).flatten.uniq
def port_number(port)
port.to_i
end
end
30 changes: 30 additions & 0 deletions spec/cfn_nag_integration/cfn_nag_ec2_network_acl_entry_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
require 'spec_helper'
require 'cfn-nag/cfn_nag_config'
require 'cfn-nag/cfn_nag'
require 'cfn-nag/cfn_nag_logging'

describe CfnNag do
before(:all) do
CfnNagLogging.configure_logging(debug: false)
@cfn_nag = CfnNag.new(config: CfnNagConfig.new)
end

context 'when nacl entry rule has metadata ignore' do
it 'does not warn' do
template_name = 'yaml/ec2_networkaclentry/metadata_overlapping_ports.yml'

expected_aggregate_results = [
{
filename: test_template_path(template_name),
file_results: {
failure_count: 0,
violations: []
}
}
]

actual_aggregate_results = @cfn_nag.audit_aggregate_across_files input_path: test_template_path(template_name)
expect(actual_aggregate_results).to eq expected_aggregate_results
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,42 @@
expect(actual_logical_resource_ids).to eq expected_logical_resource_ids
end
end

context 'EC2 Network ACLs entries contain references' do
it 'returns an empty list' do
cfn_model = CfnParser.new.parse read_test_template(
'yaml/ec2_networkaclentry/ref_ports.yml'
)

actual_logical_resource_ids = EC2NetworkAclEntryOverlappingPortsRule.new.audit_impl cfn_model
expected_logical_resource_ids = %w[]

expect(actual_logical_resource_ids).to eq expected_logical_resource_ids
end

it 'returns warnings when provided conflicting parameter values' do
cfn_model = CfnParser.new.parse read_test_template(
'yaml/ec2_networkaclentry/ref_ports.yml'
),
JSON.generate({Parameters: { NaclPort: 80 }})

actual_logical_resource_ids = EC2NetworkAclEntryOverlappingPortsRule.new.audit_impl cfn_model
expected_logical_resource_ids = %w[InboundHTTPPublicNetworkAclEntry InboundHTTPPublicNetworkAclEntry2]

expect(actual_logical_resource_ids).to eq expected_logical_resource_ids
end
end

context 'EC2 Network ACLs entries without direct nacl resource' do
it 'returns expected duplicates' do
cfn_model = CfnParser.new.parse read_test_template(
'yaml/ec2_networkaclentry/overlapping_ports_no_nacl.yml'
)

actual_logical_resource_ids = EC2NetworkAclEntryOverlappingPortsRule.new.audit_impl cfn_model
expected_logical_resource_ids = %w[BadNaclEntry1 BadNaclEntry2]

expect(actual_logical_resource_ids).to eq expected_logical_resource_ids
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Resources:
CidrBlock: "10.0.0.0/16"
Egress: false
NetworkAclId: !Ref 'myNetworkAcl'
Protocol: '-1'
Protocol: -1
RuleAction: allow
RuleNumber: 1000
myNetworkAclEntry2:
Expand All @@ -26,6 +26,6 @@ Resources:
CidrBlock: "10.0.0.0/16"
Egress: false
NetworkAclId: !Ref 'myNetworkAcl'
Protocol: '-1'
Protocol: -1
RuleAction: allow
RuleNumber: 2000
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,47 @@ Resources:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref myNetworkAcl
Protocol: "6"
Protocol: 6
RuleAction: "allow"
RuleNumber: "100"
RuleNumber: 100
CidrBlock: "10.0.0.0/16"
Egress: false
PortRange:
From: '400'
To: '500'
From: 400
To: 500
myNetworkAclEntry2:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref myNetworkAcl
Protocol: "6"
Protocol: 6
RuleAction: "deny"
RuleNumber: "200"
RuleNumber: 200
CidrBlock: "0.0.0.0/0"
Egress: false
PortRange:
From: '450'
To: '475'
From: 450
To: 475
myNetworkAclEntry3:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref myNetworkAcl
Protocol: "6"
Protocol: 6
RuleAction: "deny"
RuleNumber: "300"
RuleNumber: 300
CidrBlock: "0.0.0.0/0"
Egress: false
PortRange:
From: '200'
To: '200'
From: 200
To: 200
myNetworkAclEntry4:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref myNetworkAcl
Protocol: "6"
Protocol: 6
RuleAction: "deny"
RuleNumber: "300"
RuleNumber: 300
CidrBlock: "0.0.0.0/0"
Egress: false
PortRange:
From: '300'
To: '400'

From: 300
To: 400
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Test
Parameters:
PublicNetworkAcl:
Type: String
Description: 'Nacl Port'
Resources:
InboundHTTPPublicNetworkAclEntry:
Type: AWS::EC2::NetworkAclEntry
Metadata:
cfn_nag:
rules_to_suppress:
- id: W72
reason: I know what I am doing
Properties:
NetworkAclId: !Ref 'PublicNetworkAcl'
RuleNumber: 100
Protocol: 6
RuleAction: 'allow'
Egress: false
CidrBlock: '0.0.0.0/0'
PortRange:
From: 80
To: 80
InboundHTTPPublicNetworkAclEntry2:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref 'PublicNetworkAcl'
RuleNumber: 101
Protocol: 6
RuleAction: 'allow'
Egress: false
CidrBlock: '0.0.0.0/0'
PortRange:
From: 80
To: 80
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Test
Parameters:
NaclIdOne:
Type: String
Description: 'Id for first Nacl'
NaclIdTwo:
Type: String
Description: 'Id for first Nacl'
Resources:
BadNaclEntry1:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref 'NaclIdOne'
RuleNumber: 100
Protocol: 6
RuleAction: 'allow'
Egress: false
CidrBlock: '0.0.0.0/0'
PortRange:
From: 80
To: 80
BadNaclEntry2:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref 'NaclIdOne'
RuleNumber: 101
Protocol: 6
RuleAction: 'allow'
Egress: false
CidrBlock: '0.0.0.0/0'
PortRange:
From: 80
To: 80
GoodNaclEntry1:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref 'NaclIdTwo'
RuleNumber: 101
Protocol: 6
RuleAction: 'allow'
Egress: false
CidrBlock: '0.0.0.0/0'
PortRange:
From: 80
To: 80
Loading