cfn-nag 0.3.36 → 0.3.37
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 +23 -15
- data/lib/cfn-nag/custom_rule_loader.rb +16 -10
- data/lib/cfn-nag/custom_rules/CloudFormationAuthenticationRule.rb +2 -1
- data/lib/cfn-nag/custom_rules/CloudFrontDistributionAccessLoggingRule.rb +5 -3
- data/lib/cfn-nag/custom_rules/EbsVolumeHasSseRule.rb +4 -3
- data/lib/cfn-nag/custom_rules/ElasticLoadBalancerAccessLoggingRule.rb +6 -3
- data/lib/cfn-nag/custom_rules/IamManagedPolicyNotActionRule.rb +5 -3
- data/lib/cfn-nag/custom_rules/IamManagedPolicyNotResourceRule.rb +5 -3
- data/lib/cfn-nag/custom_rules/IamManagedPolicyWildcardActionRule.rb +5 -3
- data/lib/cfn-nag/custom_rules/IamManagedPolicyWildcardResourceRule.rb +5 -3
- data/lib/cfn-nag/custom_rules/IamPolicyNotActionRule.rb +5 -3
- data/lib/cfn-nag/custom_rules/IamPolicyNotResourceRule.rb +5 -3
- data/lib/cfn-nag/custom_rules/SecurityGroupIngressOpenToWorldRule.rb +4 -2
- data/lib/cfn-nag/custom_rules/ebs_volumes_jmespath.rb +4 -1
- data/lib/cfn-nag/ip_addr.rb +12 -6
- data/lib/cfn-nag/profile_loader.rb +2 -1
- data/lib/cfn-nag/result_view/json_results.rb +2 -1
- data/lib/cfn-nag/rule_dumper.rb +2 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 679f328149572244f9d7716508fb21ec61d51b82
|
4
|
+
data.tar.gz: 5fe1047c08e2b48a6e7370ffb65ac7aa877a0503
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cf850041ccbb24904d252e21153c84a843bdfd05eec85fd87f2752ac1707b731b06ba81ffa1b5b81fea4bf77c71c209d4898f1ef6fb4d4b192dc92cd29a0ae6
|
7
|
+
data.tar.gz: 2d2ed30c52bdd934748b423982daba740ed52b427ee186d9c95ab7cda8996be33ff2d1410e81f8e2bc8cdf786dc630bff9bb43c572d7e49e2a089cf8147f0a59
|
data/lib/cfn-nag/cfn_nag.rb
CHANGED
@@ -14,10 +14,11 @@ class CfnNag
|
|
14
14
|
print_suppression: false,
|
15
15
|
isolate_custom_rule_exceptions: false)
|
16
16
|
@rule_directory = rule_directory
|
17
|
-
@custom_rule_loader = CustomRuleLoader.new(
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
@custom_rule_loader = CustomRuleLoader.new(
|
18
|
+
rule_directory: rule_directory, allow_suppression: allow_suppression,
|
19
|
+
print_suppression: print_suppression,
|
20
|
+
isolate_custom_rule_exceptions: isolate_custom_rule_exceptions
|
21
|
+
)
|
21
22
|
@profile_definition = profile_definition
|
22
23
|
end
|
23
24
|
|
@@ -26,10 +27,12 @@ class CfnNag
|
|
26
27
|
#
|
27
28
|
# Return an aggregate failure count (for exit code usage)
|
28
29
|
#
|
29
|
-
def audit_aggregate_across_files_and_render_results(
|
30
|
-
|
31
|
-
|
32
|
-
aggregate_results =
|
30
|
+
def audit_aggregate_across_files_and_render_results(
|
31
|
+
input_path:, output_format: 'txt', parameter_values_path: nil
|
32
|
+
)
|
33
|
+
aggregate_results = \
|
34
|
+
audit_aggregate_across_files input_path: input_path,
|
35
|
+
parameter_values_path: parameter_values_path
|
33
36
|
|
34
37
|
render_results(aggregate_results: aggregate_results,
|
35
38
|
output_format: output_format)
|
@@ -43,7 +46,8 @@ class CfnNag
|
|
43
46
|
# Given a file or directory path, return aggregate results
|
44
47
|
#
|
45
48
|
def audit_aggregate_across_files(input_path:, parameter_values_path: nil)
|
46
|
-
parameter_values_string =
|
49
|
+
parameter_values_string = \
|
50
|
+
parameter_values_path.nil? ? nil : IO.read(parameter_values_path)
|
47
51
|
templates = TemplateDiscovery.new.discover_templates(input_path)
|
48
52
|
aggregate_results = []
|
49
53
|
templates.each do |template|
|
@@ -59,7 +63,8 @@ class CfnNag
|
|
59
63
|
##
|
60
64
|
# Given cloudformation json/yml, run all the rules against it
|
61
65
|
#
|
62
|
-
# Optionally include JSON with Parameters key to substitute into
|
66
|
+
# Optionally include JSON with Parameters key to substitute into
|
67
|
+
# cfn_model.parameters
|
63
68
|
#
|
64
69
|
# Return a hash with failure count
|
65
70
|
#
|
@@ -68,7 +73,8 @@ class CfnNag
|
|
68
73
|
violations = []
|
69
74
|
|
70
75
|
begin
|
71
|
-
cfn_model = CfnParser.new.parse cloudformation_string,
|
76
|
+
cfn_model = CfnParser.new.parse cloudformation_string,
|
77
|
+
parameter_values_string
|
72
78
|
rescue Psych::SyntaxError, ParserError => parser_error
|
73
79
|
violations << Violation.new(id: 'FATAL',
|
74
80
|
type: Violation::FAILING_VIOLATION,
|
@@ -76,9 +82,10 @@ class CfnNag
|
|
76
82
|
stop_processing = true
|
77
83
|
end
|
78
84
|
|
79
|
-
|
80
|
-
|
81
|
-
|
85
|
+
unless stop_processing == true
|
86
|
+
violations += @custom_rule_loader.execute_custom_rules(cfn_model)
|
87
|
+
violations = filter_violations_by_profile violations
|
88
|
+
end
|
82
89
|
|
83
90
|
{
|
84
91
|
failure_count: Violation.count_failures(violations),
|
@@ -102,7 +109,8 @@ class CfnNag
|
|
102
109
|
def filter_violations_by_profile(violations)
|
103
110
|
profile = nil
|
104
111
|
unless @profile_definition.nil?
|
105
|
-
profile = ProfileLoader.new(@custom_rule_loader.rule_definitions)
|
112
|
+
profile = ProfileLoader.new(@custom_rule_loader.rule_definitions)
|
113
|
+
.load(profile_definition: @profile_definition)
|
106
114
|
end
|
107
115
|
|
108
116
|
violations.reject do |violation|
|
@@ -49,9 +49,9 @@ class CustomRuleLoader
|
|
49
49
|
|
50
50
|
discover_rule_classes(@rule_directory).each do |rule_class|
|
51
51
|
begin
|
52
|
-
filtered_cfn_model = cfn_model_with_suppressed_resources_removed
|
53
|
-
|
54
|
-
|
52
|
+
filtered_cfn_model = cfn_model_with_suppressed_resources_removed \
|
53
|
+
cfn_model: cfn_model, rule_id: rule_class.new.rule_id,
|
54
|
+
allow_suppression: @allow_suppression
|
55
55
|
audit_result = rule_class.new.audit(filtered_cfn_model)
|
56
56
|
violations << audit_result unless audit_result.nil?
|
57
57
|
rescue Exception => exception
|
@@ -76,7 +76,10 @@ class CustomRuleLoader
|
|
76
76
|
private
|
77
77
|
|
78
78
|
def rules_to_suppress(resource)
|
79
|
-
if resource.metadata &&
|
79
|
+
if resource.metadata &&
|
80
|
+
resource.metadata['cfn_nag'] &&
|
81
|
+
resource.metadata['cfn_nag']['rules_to_suppress']
|
82
|
+
|
80
83
|
resource.metadata['cfn_nag']['rules_to_suppress']
|
81
84
|
else
|
82
85
|
nil
|
@@ -102,7 +105,8 @@ class CustomRuleLoader
|
|
102
105
|
logical_resource_id = mangled_metadata.first
|
103
106
|
mangled_rules = mangled_metadata[1]
|
104
107
|
|
105
|
-
STDERR.puts "#{logical_resource_id} has missing cfn_nag
|
108
|
+
STDERR.puts "#{logical_resource_id} has missing cfn_nag " \
|
109
|
+
"suppression rule id: #{mangled_rules}"
|
106
110
|
end
|
107
111
|
end
|
108
112
|
|
@@ -112,7 +116,8 @@ class CustomRuleLoader
|
|
112
116
|
rule_to_suppress['id'] == rule_id
|
113
117
|
end
|
114
118
|
if found_suppression_rule && @print_suppression
|
115
|
-
STDERR.puts "Suppressing #{rule_id} on #{logical_resource_id}
|
119
|
+
STDERR.puts "Suppressing #{rule_id} on #{logical_resource_id} " \
|
120
|
+
"for reason: #{found_suppression_rule['reason']}"
|
116
121
|
end
|
117
122
|
!found_suppression_rule.nil?
|
118
123
|
end
|
@@ -136,9 +141,8 @@ class CustomRuleLoader
|
|
136
141
|
end
|
137
142
|
|
138
143
|
def validate_extra_rule_directory(rule_directory)
|
139
|
-
|
140
|
-
|
141
|
-
end
|
144
|
+
return true if rule_directory.nil? || File.directory?(rule_directory)
|
145
|
+
raise "Not a real directory #{rule_directory}"
|
142
146
|
end
|
143
147
|
|
144
148
|
def discover_rule_filenames(rule_directory)
|
@@ -172,7 +176,9 @@ class CustomRuleLoader
|
|
172
176
|
unless rule_directory.nil?
|
173
177
|
rule_filenames += Dir[File.join(rule_directory, '*jmespath.rb')].sort
|
174
178
|
end
|
175
|
-
rule_filenames += Dir[File.join(__dir__,
|
179
|
+
rule_filenames += Dir[File.join(__dir__,
|
180
|
+
'custom_rules',
|
181
|
+
'*jmespath.rb')].sort
|
176
182
|
Logging.logger['log'].debug "jmespath_filenames: #{rule_filenames}"
|
177
183
|
rule_filenames
|
178
184
|
end
|
@@ -3,7 +3,8 @@ require_relative 'base'
|
|
3
3
|
|
4
4
|
class CloudFormationAuthenticationRule < BaseRule
|
5
5
|
def rule_text
|
6
|
-
'Specifying credentials in the template itself
|
6
|
+
'Specifying credentials in the template itself ' \
|
7
|
+
'is probably not the safest thing'
|
7
8
|
end
|
8
9
|
|
9
10
|
def rule_type
|
@@ -15,9 +15,11 @@ class CloudFrontDistributionAccessLoggingRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_distributions =
|
19
|
-
|
20
|
-
|
18
|
+
violating_distributions = \
|
19
|
+
cfn_model.resources_by_type('AWS::CloudFront::Distribution')
|
20
|
+
.select do |distribution|
|
21
|
+
distribution.distributionConfig['Logging'].nil?
|
22
|
+
end
|
21
23
|
|
22
24
|
violating_distributions.map(&:logical_resource_id)
|
23
25
|
end
|
@@ -15,9 +15,10 @@ class EbsVolumeHasSseRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_volumes =
|
19
|
-
|
20
|
-
|
18
|
+
violating_volumes = \
|
19
|
+
cfn_model.resources_by_type('AWS::EC2::Volume').select do |volume|
|
20
|
+
volume.encrypted.nil? || volume.encrypted.to_s.downcase == 'false'
|
21
|
+
end
|
21
22
|
|
22
23
|
violating_volumes.map(&:logical_resource_id)
|
23
24
|
end
|
@@ -15,9 +15,12 @@ class ElasticLoadBalancerAccessLoggingRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_elbs =
|
19
|
-
|
20
|
-
|
18
|
+
violating_elbs = \
|
19
|
+
cfn_model.resources_by_type('AWS::ElasticLoadBalancing::LoadBalancer')
|
20
|
+
.select do |elb|
|
21
|
+
elb.accessLoggingPolicy.nil? ||
|
22
|
+
elb.accessLoggingPolicy['Enabled'] != true
|
23
|
+
end
|
21
24
|
|
22
25
|
violating_elbs.map(&:logical_resource_id)
|
23
26
|
end
|
@@ -15,9 +15,11 @@ class IamManagedPolicyNotActionRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_policies =
|
19
|
-
|
20
|
-
|
18
|
+
violating_policies = \
|
19
|
+
cfn_model.resources_by_type('AWS::IAM::ManagedPolicy')
|
20
|
+
.select do |policy|
|
21
|
+
!policy.policy_document.allows_not_action.empty?
|
22
|
+
end
|
21
23
|
|
22
24
|
violating_policies.map(&:logical_resource_id)
|
23
25
|
end
|
@@ -15,9 +15,11 @@ class IamManagedPolicyNotResourceRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_policies =
|
19
|
-
|
20
|
-
|
18
|
+
violating_policies = \
|
19
|
+
cfn_model.resources_by_type('AWS::IAM::ManagedPolicy')
|
20
|
+
.select do |policy|
|
21
|
+
!policy.policy_document.allows_not_resource.empty?
|
22
|
+
end
|
21
23
|
|
22
24
|
violating_policies.map(&:logical_resource_id)
|
23
25
|
end
|
@@ -15,9 +15,11 @@ class IamManagedPolicyWildcardActionRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_policies =
|
19
|
-
|
20
|
-
|
18
|
+
violating_policies = \
|
19
|
+
cfn_model.resources_by_type('AWS::IAM::ManagedPolicy')
|
20
|
+
.select do |policy|
|
21
|
+
!policy.policy_document.wildcard_allowed_actions.empty?
|
22
|
+
end
|
21
23
|
|
22
24
|
violating_policies.map(&:logical_resource_id)
|
23
25
|
end
|
@@ -15,9 +15,11 @@ class IamManagedPolicyWildcardResourceRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_policies =
|
19
|
-
|
20
|
-
|
18
|
+
violating_policies = \
|
19
|
+
cfn_model.resources_by_type('AWS::IAM::ManagedPolicy')
|
20
|
+
.select do |policy|
|
21
|
+
!policy.policy_document.wildcard_allowed_resources.empty?
|
22
|
+
end
|
21
23
|
|
22
24
|
violating_policies.map(&:logical_resource_id)
|
23
25
|
end
|
@@ -15,9 +15,11 @@ class IamPolicyNotActionRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_policies =
|
19
|
-
|
20
|
-
|
18
|
+
violating_policies = \
|
19
|
+
cfn_model.resources_by_type('AWS::IAM::Policy')
|
20
|
+
.select do |policy|
|
21
|
+
!policy.policy_document.allows_not_action.empty?
|
22
|
+
end
|
21
23
|
|
22
24
|
violating_policies.map(&:logical_resource_id)
|
23
25
|
end
|
@@ -15,9 +15,11 @@ class IamPolicyNotResourceRule < BaseRule
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def audit_impl(cfn_model)
|
18
|
-
violating_policies =
|
19
|
-
|
20
|
-
|
18
|
+
violating_policies = \
|
19
|
+
cfn_model.resources_by_type('AWS::IAM::Policy')
|
20
|
+
.select do |policy|
|
21
|
+
!policy.policy_document.allows_not_resource.empty?
|
22
|
+
end
|
21
23
|
|
22
24
|
violating_policies.map(&:logical_resource_id)
|
23
25
|
end
|
@@ -6,7 +6,8 @@ class SecurityGroupIngressOpenToWorldRule < BaseRule
|
|
6
6
|
include IpAddr
|
7
7
|
|
8
8
|
def rule_text
|
9
|
-
'Security Groups found with cidr open to world on ingress.
|
9
|
+
'Security Groups found with cidr open to world on ingress. ' \
|
10
|
+
'This should never be true on instance. Permissible on ELB'
|
10
11
|
end
|
11
12
|
|
12
13
|
def rule_type
|
@@ -18,7 +19,8 @@ class SecurityGroupIngressOpenToWorldRule < BaseRule
|
|
18
19
|
end
|
19
20
|
|
20
21
|
##
|
21
|
-
# This will behave slightly different than the legacy jq based rule
|
22
|
+
# This will behave slightly different than the legacy jq based rule
|
23
|
+
# which was targeted against inline ingress only
|
22
24
|
def audit_impl(cfn_model)
|
23
25
|
logical_resource_ids = []
|
24
26
|
cfn_model.security_groups.each do |security_group|
|
@@ -1,4 +1,7 @@
|
|
1
1
|
|
2
2
|
# failure(id: 'F8888',
|
3
|
-
# jmespath:
|
3
|
+
# jmespath:
|
4
|
+
# "Resources.*|[?Type == 'AWS::EC2::Volume' && " \
|
5
|
+
# "(Properties.Encrypted == `false` || " \
|
6
|
+
# "Properties.Encrypted == `null`)].id",
|
4
7
|
# message: 'Found a naughty EBS volume')
|
data/lib/cfn-nag/ip_addr.rb
CHANGED
@@ -2,7 +2,8 @@ require 'netaddr'
|
|
2
2
|
|
3
3
|
module IpAddr
|
4
4
|
def ip4_open?(ingress)
|
5
|
-
# only care about literals. if a Hash/Ref not going to chase it down
|
5
|
+
# only care about literals. if a Hash/Ref not going to chase it down
|
6
|
+
# given likely a Parameter with external val
|
6
7
|
ingress.cidrIp.is_a?(String) && ingress.cidrIp == '0.0.0.0/0'
|
7
8
|
end
|
8
9
|
|
@@ -10,8 +11,10 @@ module IpAddr
|
|
10
11
|
normalized_cidr_ip6 = normalize_cidr_ip6(ingress)
|
11
12
|
return false if normalized_cidr_ip6.nil?
|
12
13
|
|
13
|
-
# only care about literals. if a Hash/Ref not going to chase it down
|
14
|
-
|
14
|
+
# only care about literals. if a Hash/Ref not going to chase it down
|
15
|
+
# given likely a Parameter with external val
|
16
|
+
(NetAddr::CIDRv6.create(normalized_cidr_ip6) ==
|
17
|
+
NetAddr::CIDRv6.create('::/0'))
|
15
18
|
end
|
16
19
|
|
17
20
|
def ip4_cidr_range?(ingress)
|
@@ -22,14 +25,17 @@ module IpAddr
|
|
22
25
|
normalized_cidr_ip6 = normalize_cidr_ip6(ingress)
|
23
26
|
return false if normalized_cidr_ip6.nil?
|
24
27
|
|
25
|
-
# only care about literals. if a Hash/Ref not going to chase it down
|
28
|
+
# only care about literals. if a Hash/Ref not going to chase it down
|
29
|
+
# given likely a Parameter with external val
|
26
30
|
!NetAddr::CIDRv6.create(normalized_cidr_ip6).to_s.end_with?('/128')
|
27
31
|
end
|
28
32
|
|
29
33
|
##
|
30
34
|
# If it's a string, just pass through
|
31
|
-
# If it's a symbol - probably because the YAML.load call treats an unquoted
|
32
|
-
#
|
35
|
+
# If it's a symbol - probably because the YAML.load call treats an unquoted
|
36
|
+
# ::/0 as a the symbol :':/0'
|
37
|
+
# Otherwise it's probably a Ref or whatever and we aren't going to do
|
38
|
+
# anything with it
|
33
39
|
#
|
34
40
|
def normalize_cidr_ip6(ingress)
|
35
41
|
if ingress.cidrIpv6.is_a?(Symbol)
|
@@ -18,7 +18,8 @@ class ProfileLoader
|
|
18
18
|
if !rule_line_match.nil?
|
19
19
|
rule_id = rule_line_match.captures.first
|
20
20
|
if @rules_registry.by_id(rule_id) == nil
|
21
|
-
raise "#{rule_id} is not a legal rule identifier from:
|
21
|
+
raise "#{rule_id} is not a legal rule identifier from: " \
|
22
|
+
"#{@rules_registry.rules.map { |rule| rule.id }}"
|
22
23
|
else
|
23
24
|
new_profile.add_rule rule_id
|
24
25
|
end
|
@@ -3,7 +3,8 @@ require 'json'
|
|
3
3
|
class JsonResults
|
4
4
|
def render(results)
|
5
5
|
hashified_results = results.each do |result|
|
6
|
-
result[:file_results][:violations] =
|
6
|
+
result[:file_results][:violations] = \
|
7
|
+
result[:file_results][:violations].map(&:to_h)
|
7
8
|
end
|
8
9
|
|
9
10
|
puts JSON.pretty_generate(hashified_results)
|
data/lib/cfn-nag/rule_dumper.rb
CHANGED
@@ -15,7 +15,8 @@ class CfnNagRuleDumper
|
|
15
15
|
|
16
16
|
profile = nil
|
17
17
|
unless @profile_definition.nil?
|
18
|
-
profile = ProfileLoader.new(rule_registry)
|
18
|
+
profile = ProfileLoader.new(rule_registry)
|
19
|
+
.load(profile_definition: @profile_definition)
|
19
20
|
end
|
20
21
|
|
21
22
|
RulesView.new.emit(rule_registry, profile)
|
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.3.
|
4
|
+
version: 0.3.37
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Kascic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-03-
|
11
|
+
date: 2018-03-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|