cfn-nag 0.6.8 → 0.6.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/cfn-nag/cfn_nag.rb +1 -2
- data/lib/cfn-nag/cfn_nag_config.rb +2 -8
- data/lib/cfn-nag/custom_rule_loader.rb +1 -1
- data/lib/cfn-nag/custom_rules/DynamoDBBackupRule.rb +28 -0
- data/lib/cfn-nag/custom_rules/EC2NetworkAclEntryOverlappingPortsRule.rb +47 -46
- data/lib/cfn-nag/custom_rules/ElasticLoadBalancerV2AccessLoggingRule.rb +1 -2
- data/lib/cfn-nag/custom_rules/ElasticsearchDomainEncryptionAtRestOptionsRule.rb +0 -1
- data/lib/cfn-nag/custom_rules/IamRolePassRoleWildcardResourceRule.rb +1 -1
- data/lib/cfn-nag/custom_rules/RDSInstanceDeletionProtectionRule.rb +1 -1
- data/lib/cfn-nag/custom_rules/SPCMRule.rb +1 -0
- data/lib/cfn-nag/custom_rules/SecurityGroupEgressOpenToWorldRule.rb +8 -2
- data/lib/cfn-nag/custom_rules/SecurityGroupIngressCidrNon32Rule.rb +8 -2
- data/lib/cfn-nag/custom_rules/SecurityGroupIngressOpenToWorldRule.rb +8 -2
- data/lib/cfn-nag/custom_rules/SecurityGroupRuleDescriptionRule.rb +1 -1
- data/lib/cfn-nag/custom_rules/boolean_base_rule.rb +1 -1
- data/lib/cfn-nag/custom_rules/passrole_base_rule.rb +1 -1
- data/lib/cfn-nag/iam_complexity_metric/condition_metric.rb +3 -2
- data/lib/cfn-nag/ip_addr.rb +3 -2
- data/lib/cfn-nag/metadata.rb +2 -2
- data/lib/cfn-nag/result_view/stdout_results.rb +2 -2
- data/lib/cfn-nag/rule_repos/gem_based_rule_repo.rb +1 -1
- data/lib/cfn-nag/rule_repository_loader.rb +1 -1
- data/lib/cfn-nag/util/enforce_reference_parameter.rb +1 -1
- data/lib/cfn-nag/util/enforce_string_or_dynamic_reference.rb +5 -6
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9b5274ea37c43e66281bd7e21513c650ca7dac661c5ce943e5fce39dad25506c
|
4
|
+
data.tar.gz: 22355ae1a48c603a4f4672d7887134f5087973aab0fda5eda92bcb794f975fbe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8d30927728b0b9ea80774a83c937629ede960143b02b69e51ad6a55ce770e15805e6b8adb733d5c461a346d66b4c11c6fc19fd59f90bf0ee614718375c1ac22
|
7
|
+
data.tar.gz: 4043b47cd7b3b9c19a5d1259134346768febabd2418a96c160a27f644e09cdb7a630f57013828e0d5d8bd1d49f3b19b414b6893bd1082e8ad9cfd4f9e27c6bcd
|
data/lib/cfn-nag/cfn_nag.rb
CHANGED
@@ -135,12 +135,11 @@ class CfnNag
|
|
135
135
|
)
|
136
136
|
|
137
137
|
# this must come after - blacklist should always win
|
138
|
-
|
138
|
+
filter_violations_by_blacklist(
|
139
139
|
blacklist_definition: @config.blacklist_definition,
|
140
140
|
rule_definitions: @config.custom_rule_loader.rule_definitions,
|
141
141
|
violations: violations
|
142
142
|
)
|
143
|
-
violations
|
144
143
|
rescue StandardError => blacklist_or_profile_parse_error
|
145
144
|
violations << fatal_violation(blacklist_or_profile_parse_error.to_s)
|
146
145
|
violations
|
@@ -29,12 +29,6 @@ class CfnNagConfig
|
|
29
29
|
end
|
30
30
|
# rubocop:enable Metrics/ParameterLists
|
31
31
|
|
32
|
-
attr_reader :rule_arguments
|
33
|
-
|
34
|
-
attr_reader :custom_rule_loader
|
35
|
-
attr_reader :profile_definition
|
36
|
-
attr_reader :blacklist_definition
|
37
|
-
attr_reader :fail_on_warnings
|
38
|
-
attr_reader :rule_repositories
|
39
|
-
attr_reader :ignore_fatal
|
32
|
+
attr_reader :rule_arguments, :rule_directory, :custom_rule_loader, :profile_definition, :blacklist_definition, \
|
33
|
+
:fail_on_warnings, :rule_repositories, :ignore_fatal
|
40
34
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cfn-nag/violation'
|
4
|
+
require 'cfn-nag/util/truthy'
|
5
|
+
require_relative 'base'
|
6
|
+
|
7
|
+
class DynamoDBBackupRule < BaseRule
|
8
|
+
def rule_text
|
9
|
+
'DynamoDB table should have backup enabled, should be set using PointInTimeRecoveryEnabled'
|
10
|
+
end
|
11
|
+
|
12
|
+
def rule_type
|
13
|
+
Violation::WARNING
|
14
|
+
end
|
15
|
+
|
16
|
+
def rule_id
|
17
|
+
'W78'
|
18
|
+
end
|
19
|
+
|
20
|
+
def audit_impl(cfn_model)
|
21
|
+
violating_ddb_tables = cfn_model.resources_by_type('AWS::DynamoDB::Table').select do |table|
|
22
|
+
table.pointInTimeRecoverySpecification.nil? ||
|
23
|
+
!truthy?(table.pointInTimeRecoverySpecification['PointInTimeRecoveryEnabled'].to_s)
|
24
|
+
end
|
25
|
+
|
26
|
+
violating_ddb_tables.map(&:logical_resource_id)
|
27
|
+
end
|
28
|
+
end
|
@@ -18,79 +18,80 @@ class EC2NetworkAclEntryOverlappingPortsRule < BaseRule
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def audit_impl(cfn_model)
|
21
|
+
nacl_entries = cfn_model.resources_by_type('AWS::EC2::NetworkAclEntry')
|
22
|
+
|
23
|
+
# Select nacl entries that can be evaluated
|
24
|
+
nacl_entries.select! do |nacl_entry|
|
25
|
+
tcp_or_udp_protocol?(nacl_entry) && valid_ports?(nacl_entry)
|
26
|
+
end
|
27
|
+
|
21
28
|
violating_nacl_entries = []
|
22
|
-
|
23
|
-
|
29
|
+
|
30
|
+
# Group entries by nacl id, ip type, and egress/ingress
|
31
|
+
grouped_nacl_entries = group_nacl_entries(nacl_entries)
|
32
|
+
|
33
|
+
grouped_nacl_entries.each do |grouping|
|
34
|
+
violating_nacl_entries += overlapping_port_entries(grouping)
|
24
35
|
end
|
25
36
|
violating_nacl_entries.map(&:logical_resource_id)
|
26
37
|
end
|
27
38
|
|
28
39
|
private
|
29
40
|
|
30
|
-
def
|
31
|
-
|
32
|
-
tcp_or_udp_protocol?(nacl_entry_pair[0], nacl_entry_pair[1]) && overlap?(nacl_entry_pair[0], nacl_entry_pair[1])
|
33
|
-
end
|
41
|
+
def tcp_or_udp_protocol?(entry)
|
42
|
+
%w[6 17].include?(entry.protocol.to_s)
|
34
43
|
end
|
35
44
|
|
36
|
-
def
|
37
|
-
|
45
|
+
def valid_ports?(entry)
|
46
|
+
!entry.portRange.nil? && valid_port_number?(entry.portRange['From']) && valid_port_number?(entry.portRange['To'])
|
38
47
|
end
|
39
48
|
|
40
|
-
def
|
41
|
-
|
42
|
-
pairs_without_dupes.reduce(Set.new) { |set_of_sets, pair| set_of_sets << Set.new(pair) }.to_a.map(&:to_a)
|
49
|
+
def valid_port_number?(port)
|
50
|
+
port.is_a?(Numeric) || (port.is_a?(String) && port.to_i(10) != 0)
|
43
51
|
end
|
44
52
|
|
45
|
-
def
|
46
|
-
|
47
|
-
end
|
53
|
+
def group_nacl_entries(nacl_entries)
|
54
|
+
grouped_nacl_entries = []
|
48
55
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
56
|
+
# Group by NaclID
|
57
|
+
nacl_entries.group_by(&:networkAclId).each_value do |entries|
|
58
|
+
# Split entries by ip type
|
59
|
+
ipv4_entries, ipv6_entries = entries.partition { |nacl_entry| nacl_entry.ipv6CidrBlock.nil? }
|
53
60
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
61
|
+
# Split entries by egress/ingress
|
62
|
+
egress4, ingress4 = ipv4_entries.partition { |nacl_entry| truthy?(nacl_entry.egress) }
|
63
|
+
egress6, ingress6 = ipv6_entries.partition { |nacl_entry| truthy?(nacl_entry.egress) }
|
58
64
|
|
59
|
-
|
60
|
-
|
61
|
-
|
65
|
+
grouped_nacl_entries << egress4
|
66
|
+
grouped_nacl_entries << ingress4
|
67
|
+
grouped_nacl_entries << egress6
|
68
|
+
grouped_nacl_entries << ingress6
|
62
69
|
end
|
63
|
-
end
|
64
70
|
|
65
|
-
|
66
|
-
nacl_entries.select do |nacl_entry|
|
67
|
-
not_truthy?(nacl_entry.egress)
|
68
|
-
end
|
71
|
+
grouped_nacl_entries
|
69
72
|
end
|
70
73
|
|
71
|
-
def
|
72
|
-
nacl_entries.select do |
|
73
|
-
|
74
|
-
end
|
74
|
+
def overlapping_port_entries(nacl_entries)
|
75
|
+
unique_pairs(nacl_entries).select do |nacl_entry_pair|
|
76
|
+
overlap?(nacl_entry_pair[0], nacl_entry_pair[1])
|
77
|
+
end.flatten.uniq
|
75
78
|
end
|
76
79
|
|
77
|
-
def
|
78
|
-
|
79
|
-
|
80
|
-
end
|
80
|
+
def unique_pairs(arr)
|
81
|
+
pairs_without_dupes = arr.product(arr).select { |pair| pair[0] != pair[1] }
|
82
|
+
pairs_without_dupes.reduce(Set.new) { |set_of_sets, pair| set_of_sets << Set.new(pair) }.to_a.map(&:to_a)
|
81
83
|
end
|
82
84
|
|
83
|
-
def
|
84
|
-
|
85
|
+
def overlap?(entry1, entry2)
|
86
|
+
port_overlap?(entry1.portRange, entry2.portRange) || port_overlap?(entry2.portRange, entry1.portRange)
|
85
87
|
end
|
86
88
|
|
87
|
-
def
|
88
|
-
|
89
|
-
|
89
|
+
def port_overlap?(port_range1, port_range2)
|
90
|
+
port_number(port_range1['From']).between?(port_number(port_range2['From']), port_number(port_range2['To'])) ||
|
91
|
+
port_number(port_range1['To']).between?(port_number(port_range2['From']), port_number(port_range2['To']))
|
90
92
|
end
|
91
93
|
|
92
|
-
def
|
93
|
-
|
94
|
-
overlapping_port_entries(ingress_entries(ip6_entries(nacl.network_acl_entries))).flatten.uniq
|
94
|
+
def port_number(port)
|
95
|
+
port.to_i
|
95
96
|
end
|
96
97
|
end
|
@@ -29,10 +29,9 @@ class ElasticLoadBalancerV2AccessLoggingRule < BaseRule
|
|
29
29
|
private
|
30
30
|
|
31
31
|
def access_logging_is_false?(load_balancer)
|
32
|
-
|
32
|
+
load_balancer.loadBalancerAttributes.find do |load_balancer_attribute|
|
33
33
|
load_balancer_attribute['Key'] == 'access_logs.s3.enabled' && not_truthy?(load_balancer_attribute['Value'])
|
34
34
|
end
|
35
|
-
false_access_log_attribute
|
36
35
|
end
|
37
36
|
|
38
37
|
def missing_access_logs?(load_balancer)
|
@@ -5,7 +5,7 @@ require_relative 'base'
|
|
5
5
|
require 'cfn-nag/util/wildcard_patterns'
|
6
6
|
|
7
7
|
class IamRolePassRoleWildcardResourceRule < BaseRule
|
8
|
-
IAM_ACTION_PATTERNS = wildcard_patterns('PassRole').map! { |x|
|
8
|
+
IAM_ACTION_PATTERNS = wildcard_patterns('PassRole').map! { |x| "iam:#{x}" } + ['*']
|
9
9
|
|
10
10
|
def rule_text
|
11
11
|
'IAM role should not allow * resource with PassRole action on its permissions policy'
|
@@ -25,16 +25,22 @@ class SecurityGroupEgressOpenToWorldRule < BaseRule
|
|
25
25
|
def audit_impl(cfn_model)
|
26
26
|
violating_security_groups = cfn_model.security_groups.select do |security_group|
|
27
27
|
violating_egresses = security_group.egresses.select do |egress|
|
28
|
-
|
28
|
+
violating_egress(egress)
|
29
29
|
end
|
30
30
|
|
31
31
|
!violating_egresses.empty?
|
32
32
|
end
|
33
33
|
|
34
34
|
violating_egresses = cfn_model.standalone_egress.select do |standalone_egress|
|
35
|
-
|
35
|
+
violating_egress(standalone_egress)
|
36
36
|
end
|
37
37
|
|
38
38
|
violating_security_groups.map(&:logical_resource_id) + violating_egresses.map(&:logical_resource_id)
|
39
39
|
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def violating_egress(egress)
|
44
|
+
ip4_open?(egress) || ip6_open?(egress)
|
45
|
+
end
|
40
46
|
end
|
@@ -25,16 +25,22 @@ class SecurityGroupIngressCidrNon32Rule < BaseRule
|
|
25
25
|
def audit_impl(cfn_model)
|
26
26
|
violating_security_groups = cfn_model.security_groups.select do |security_group|
|
27
27
|
violating_ingresses = security_group.ingresses.select do |ingress|
|
28
|
-
|
28
|
+
violating_ingress(ingress)
|
29
29
|
end
|
30
30
|
|
31
31
|
!violating_ingresses.empty?
|
32
32
|
end
|
33
33
|
|
34
34
|
violating_ingresses = cfn_model.standalone_ingress.select do |standalone_ingress|
|
35
|
-
|
35
|
+
violating_ingress(standalone_ingress)
|
36
36
|
end
|
37
37
|
|
38
38
|
violating_security_groups.map(&:logical_resource_id) + violating_ingresses.map(&:logical_resource_id)
|
39
39
|
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def violating_ingress(ingress)
|
44
|
+
ip4_cidr_range?(ingress) || ip6_cidr_range?(ingress)
|
45
|
+
end
|
40
46
|
end
|
@@ -26,16 +26,22 @@ class SecurityGroupIngressOpenToWorldRule < BaseRule
|
|
26
26
|
def audit_impl(cfn_model)
|
27
27
|
violating_security_groups = cfn_model.security_groups.select do |security_group|
|
28
28
|
violating_ingresses = security_group.ingresses.select do |ingress|
|
29
|
-
|
29
|
+
violating_ingress(ingress)
|
30
30
|
end
|
31
31
|
|
32
32
|
!violating_ingresses.empty?
|
33
33
|
end
|
34
34
|
|
35
35
|
violating_ingresses = cfn_model.standalone_ingress.select do |standalone_ingress|
|
36
|
-
|
36
|
+
violating_ingress(standalone_ingress)
|
37
37
|
end
|
38
38
|
|
39
39
|
violating_security_groups.map(&:logical_resource_id) + violating_ingresses.map(&:logical_resource_id)
|
40
40
|
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def violating_ingress(ingress)
|
45
|
+
ip4_open?(ingress) || ip6_open?(ingress)
|
46
|
+
end
|
41
47
|
end
|
@@ -5,7 +5,7 @@ require_relative 'base'
|
|
5
5
|
require 'cfn-nag/util/wildcard_patterns'
|
6
6
|
|
7
7
|
class PassRoleBaseRule < BaseRule
|
8
|
-
IAM_ACTION_PATTERNS = wildcard_patterns('PassRole').map { |pattern|
|
8
|
+
IAM_ACTION_PATTERNS = wildcard_patterns('PassRole').map { |pattern| "iam:#{pattern}" } + ['*']
|
9
9
|
|
10
10
|
def policy_type
|
11
11
|
raise 'must implement in subclass'
|
@@ -41,9 +41,10 @@ class ConditionMetric
|
|
41
41
|
result = []
|
42
42
|
conditions.each do |_, expression|
|
43
43
|
expression.each do |_, value|
|
44
|
-
|
44
|
+
case value
|
45
|
+
when String
|
45
46
|
result << value
|
46
|
-
|
47
|
+
when Array
|
47
48
|
result += value
|
48
49
|
end
|
49
50
|
end
|
data/lib/cfn-nag/ip_addr.rb
CHANGED
@@ -47,9 +47,10 @@ module IpAddr
|
|
47
47
|
# anything with it
|
48
48
|
#
|
49
49
|
def normalize_cidr_ip6(ingress)
|
50
|
-
|
50
|
+
case ingress.cidrIpv6
|
51
|
+
when Symbol
|
51
52
|
":#{ingress.cidrIpv6}"
|
52
|
-
|
53
|
+
when String
|
53
54
|
ingress.cidrIpv6
|
54
55
|
end
|
55
56
|
end
|
data/lib/cfn-nag/metadata.rb
CHANGED
@@ -13,7 +13,7 @@ module Metadata
|
|
13
13
|
logical_resource_id = mangled_metadata.first
|
14
14
|
mangled_rules = mangled_metadata[1]
|
15
15
|
|
16
|
-
|
16
|
+
$stderr.puts "#{logical_resource_id} has missing cfn_nag suppression rule id: #{mangled_rules}"
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
@@ -46,7 +46,7 @@ module Metadata
|
|
46
46
|
end
|
47
47
|
if found_suppression_rule && print_suppression
|
48
48
|
message = "Suppressing #{rule_id} on #{logical_resource_id} for reason: #{found_suppression_rule['reason']}"
|
49
|
-
|
49
|
+
$stderr.puts message
|
50
50
|
end
|
51
51
|
!found_suppression_rule.nil?
|
52
52
|
end
|
@@ -27,7 +27,7 @@ class StdoutResults
|
|
27
27
|
def render(results)
|
28
28
|
results.each do |result|
|
29
29
|
60.times { print '-' }
|
30
|
-
puts "\n
|
30
|
+
puts "\n#{result[:filename]}"
|
31
31
|
60.times { print '-' }
|
32
32
|
|
33
33
|
violations = result[:file_results][:violations]
|
@@ -45,6 +45,6 @@ class StdoutResults
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def indent_multiline_string_with_prefix(prefix, multiline_string)
|
48
|
-
prefix
|
48
|
+
"#{prefix} #{multiline_string.gsub(/\n/, "\n#{prefix} ")}"
|
49
49
|
end
|
50
50
|
end
|
@@ -53,7 +53,7 @@ class GemBasedRuleRepo
|
|
53
53
|
require rule_gem_name
|
54
54
|
true
|
55
55
|
rescue LoadError
|
56
|
-
|
56
|
+
$stderr.puts "Could not require #{rule_gem_name} - does the rule gem have a top level entry point?"
|
57
57
|
false
|
58
58
|
end
|
59
59
|
|
@@ -33,7 +33,7 @@ class RuleRepositoryLoader
|
|
33
33
|
end
|
34
34
|
|
35
35
|
repo_class = class_from_name(rule_repository_definition['repo_class_name'])
|
36
|
-
if rule_repository_definition['repo_arguments']
|
36
|
+
if rule_repository_definition['repo_arguments'].is_a?(Hash)
|
37
37
|
repo_class.new(**to_sym_keys(rule_repository_definition['repo_arguments']))
|
38
38
|
else
|
39
39
|
repo_class.new
|
@@ -9,17 +9,16 @@ def insecure_string_or_dynamic_reference?(_cfn_model, key_to_check)
|
|
9
9
|
|
10
10
|
# Check if string starts with a Dynamic Reference pointing to SecretsManager
|
11
11
|
# or SSM Secure
|
12
|
+
# &&
|
13
|
+
# Verify that the secure string ends properly with the double curly braces
|
12
14
|
if key_to_check.start_with?(
|
13
15
|
'{{resolve:secretsmanager:',
|
14
16
|
'{{resolve:ssm-secure:'
|
15
|
-
)
|
16
|
-
|
17
|
-
if key_to_check.end_with? '}}'
|
18
|
-
return false
|
19
|
-
end
|
17
|
+
) && key_to_check.end_with?('}}')
|
18
|
+
return false
|
20
19
|
end
|
21
20
|
|
22
|
-
#
|
21
|
+
# Return true if key_to_check is a string and is not calling a secured
|
23
22
|
# dynamic reference pattern (Secrets Manager or SSM-Secure)
|
24
23
|
true
|
25
24
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cfn-nag
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.13
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Kascic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - '='
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.5.
|
75
|
+
version: 0.5.4
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - '='
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.5.
|
82
|
+
version: 0.5.4
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: logging
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -203,6 +203,7 @@ files:
|
|
203
203
|
- lib/cfn-nag/custom_rules/DirectoryServiceMicrosoftADPasswordRule.rb
|
204
204
|
- lib/cfn-nag/custom_rules/DirectoryServiceSimpleADPasswordRule.rb
|
205
205
|
- lib/cfn-nag/custom_rules/DocDBDBClusterMasterUserPasswordRule.rb
|
206
|
+
- lib/cfn-nag/custom_rules/DynamoDBBackupRule.rb
|
206
207
|
- lib/cfn-nag/custom_rules/DynamoDBBillingModeRule.rb
|
207
208
|
- lib/cfn-nag/custom_rules/DynamoDBEncryptionRule.rb
|
208
209
|
- lib/cfn-nag/custom_rules/EC2NetworkAclEntryDuplicateRule.rb
|