Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Eric Kascic
committed
Feb 11, 2016
0 parents
commit 3da80c2
Showing
20 changed files
with
715 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
--color | ||
--require spec_helper |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
source 'https://rubygems.org' | ||
|
||
group :test do | ||
gem 'rspec' | ||
gem 'simplecov' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
GEM | ||
remote: https://rubygems.org/ | ||
specs: | ||
diff-lcs (1.2.5) | ||
docile (1.1.5) | ||
json (1.8.3) | ||
rspec (3.4.0) | ||
rspec-core (~> 3.4.0) | ||
rspec-expectations (~> 3.4.0) | ||
rspec-mocks (~> 3.4.0) | ||
rspec-core (3.4.1) | ||
rspec-support (~> 3.4.0) | ||
rspec-expectations (3.4.0) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.4.0) | ||
rspec-mocks (3.4.0) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.4.0) | ||
rspec-support (3.4.1) | ||
simplecov (0.11.1) | ||
docile (~> 1.1.0) | ||
json (~> 1.8) | ||
simplecov-html (~> 0.10.0) | ||
simplecov-html (0.10.0) | ||
|
||
PLATFORMS | ||
ruby | ||
|
||
DEPENDENCIES | ||
rspec | ||
simplecov | ||
|
||
BUNDLED WITH | ||
1.10.6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
#!/usr/bin/env ruby | ||
require 'trollop' | ||
require 'cfn_nag' | ||
|
||
opts = Trollop::options do | ||
opt :input_json, 'Cloudformation template to nag on', type: :string, required: true | ||
end | ||
|
||
exit CfnNag.new.audit(opts[:input_json]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
require 'rake' | ||
|
||
Gem::Specification.new do |s| | ||
s.name = 'cfn-nag' | ||
s.version = '0.0.0' | ||
s.bindir = 'bin' | ||
s.executables = %w(cfn_nag) | ||
s.authors = %w(someguy) | ||
s.summary = 'cfn-nag' | ||
s.description = 'Auditing tool for Cloudformation templates' | ||
s.files = FileList[ 'lib/**/*.rb' ] | ||
|
||
s.require_paths << 'lib' | ||
|
||
|
||
s.add_runtime_dependency('trollop', '2.1.2') | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
#!/bin/bash | ||
|
||
gem uninstall cfn-nag -x | ||
gem build cfn-nag.gemspec | ||
gem install cfn-nag-0.0.0.gem |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
require_relative 'rule' | ||
require_relative 'custom_rules/security_group_egress' | ||
require_relative 'model/cfn_model' | ||
|
||
class CfnNag | ||
include Rule | ||
|
||
def audit(input_json_path) | ||
fail 'not even legit JSON' unless legal_json?(input_json_path) | ||
|
||
@failure_count = 0 | ||
@warning_count = 0 | ||
|
||
generic_json_rules input_json_path | ||
|
||
#custom_rules | ||
|
||
puts "failure_count: #{@failure_count}" | ||
puts "warning_count: #{@warning_count}" | ||
@failure_count | ||
end | ||
|
||
private | ||
|
||
def legal_json?(input_json_path) | ||
begin | ||
JSON.parse(IO.read(input_json_path)) | ||
true | ||
rescue JSON::ParserError | ||
return false | ||
end | ||
end | ||
|
||
def command?(command) | ||
system("which #{command} > /dev/null 2>&1") | ||
end | ||
|
||
def generic_json_rules(input_json_path) | ||
unless command? 'jq' | ||
fail 'jq executable must be available in PATH' | ||
end | ||
|
||
Dir[File.join(__dir__, 'json_rules', '*.rb')].each do |rule_file| | ||
@input_json_path = input_json_path | ||
eval IO.read(rule_file) | ||
end | ||
end | ||
|
||
def custom_rules | ||
cfn_model = CfnModel.new.parse(IO.read(@input_json_path)) | ||
rules = [SecurityGroupEgressRule] | ||
rules.each do |rule_class| | ||
rule_class.new.audit(cfn_model) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class SecurityGroupEgressRule | ||
def audit(cfn_model) | ||
true | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
assertion '.Resources|length > 0' do | ||
puts 'Must have at least 1 resource' | ||
end | ||
|
||
|
||
#use recurse to find all Ref and then cross-reference against .Resources|keys |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
warning '.Resources[] | select(.Type == "AWS::EC2::SecurityGroup")|select(.Properties.SecurityGroupIngress.CidrIp? == "0.0.0.0/0")' do |security_groups| | ||
puts "WARNING: Security Groups found with open cidr on ingress: #{security_groups}" | ||
end | ||
|
||
warning '.Resources[] | select(.Type == "AWS::EC2::SecurityGroup")|select(.Properties.SecurityGroupIngress|type == "array")|select(.Properties.SecurityGroupIngress[].CidrIp == "0.0.0.0/0")' do |security_groups| | ||
puts "WARNING: Security Groups found with open cidr on ingress: #{security_groups}" | ||
end | ||
|
||
warning '.Resources[] | select(.Type == "AWS::EC2::SecurityGroupIngress")|select(.Properties.CidrIp == "0.0.0.0/0")' do |ingress_rules| | ||
puts "WARNING: Security Group Standalone Ingress json_rules found with open cidr: #{ingress_rules}" | ||
end | ||
|
||
warning '.Resources[] | select(.Type == "AWS::EC2::SecurityGroup")|select(.Properties.SecurityGroupEgress.CidrIp? == "0.0.0.0/0")' do |security_groups| | ||
puts "WARNING: Security Groups found with open cidr on egress: #{security_groups}" | ||
end | ||
|
||
warning '.Resources[] | select(.Type == "AWS::EC2::SecurityGroup")|select(.Properties.SecurityGroupEgress|type == "array")|select(.Properties.SecurityGroupEgress[].CidrIp == "0.0.0.0/0")' do |security_groups| | ||
puts "WARNING: Security Groups found with open cidr on ingress: #{security_groups}" | ||
end | ||
|
||
warning '.Resources[] | select(.Type == "AWS::EC2::SecurityGroupEgress")|select(.Properties.CidrIp == "0.0.0.0/0")' do |egress_rules| | ||
puts "WARNING: Security Group Standalone Egress json_rules found with open cidr: #{egress_rules}" | ||
end | ||
|
||
|
||
#for inline, this covers it, but with an externalized egress rule... the expression gets real evil | ||
#i guess the ideal would be to do a join of ingress and egress rules with the parent sg | ||
#but it gets real hairy with FnGetAtt for GroupId and all that.... think it best to | ||
#write some imperative code in custom rules to take care of things | ||
# violation '.Resources[]|select(.Type == "AWS::EC2::SecurityGroup")|select(.Properties.SecurityGroupEgress == null or .Properties.SecurityGroupEgress.length? == 0)' do |security_groups| | ||
# puts "Security Groups found without egress json_rules: #{security_groups}" | ||
# end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
require 'json' | ||
require_relative 'security_group_parser' | ||
|
||
class CfnModel | ||
def initialize | ||
@parser_registry = { | ||
'AWS::EC2::SecurityGroup' => SecurityGroupParser, | ||
'AWS::EC2::SecurityGroupIngress' => SecurityGroupIngressParser | ||
} | ||
end | ||
|
||
def parse(cfn_json_string) | ||
@json_hash = JSON.load cfn_json_string | ||
self | ||
end | ||
|
||
def security_groups | ||
security_groups = resources_by_type('AWS::EC2::SecurityGroup') | ||
wire_ingress_rules_to_security_groups(security_groups) | ||
#dangling ingress rules | ||
security_groups.values | ||
end | ||
|
||
private | ||
|
||
def wire_ingress_rules_to_security_groups(security_groups_hash) | ||
resources_by_type('AWS::EC2::SecurityGroupIngress').each do |ingress_rules| | ||
if not properties['GroupId'].nil? | ||
if not properties['GroupId']['Ref'].nil? | ||
unless @security_groups_hash[properties['GroupId']['Ref']].nil? | ||
@security_groups_hash[properties['GroupId']['Ref']].add_ingress_rule security_group_ingress_rule | ||
end | ||
else | ||
#dangling ingress rule | ||
end | ||
else | ||
p 'hi!' | ||
end | ||
end | ||
end | ||
|
||
def resources | ||
@json['Resources'] | ||
end | ||
|
||
def resources_by_type(resource_type) | ||
# resources.values.select do |resource| | ||
# resource['Type'] == resource_type | ||
# end | ||
|
||
resources_map = {} | ||
resources.each do |resource_name, resource| | ||
if resource['Type'] == resource_type | ||
resource_parser = @parser_registry[resource_type].new | ||
resources_map[resource_name] = resource_parser.parse(resource) | ||
end | ||
end | ||
resources_map | ||
end | ||
|
||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
class SecurityGroupParser | ||
|
||
def parse(resource_json) | ||
properties = resource_json['Properties'] | ||
security_group = SecurityGroup.new | ||
|
||
#pre-validation of structure somehow to make life easier here? | ||
#unless properties.nil? | ||
|
||
unless properties['SecurityGroupIngress'].nil? | ||
if properties['SecurityGroupIngress'].is_a? Array | ||
properties['SecurityGroupIngress'].each do |ingress_json| | ||
security_group.add_ingress_rule ingress_json | ||
end | ||
elsif properties['SecurityGroupIngress'].is_a? Hash | ||
security_group.add_ingress_rule properties['SecurityGroupIngress'] | ||
end | ||
end | ||
|
||
#generalize? | ||
security_group.vpc_id = properties['VpcId'] | ||
security_group.group_description = properties['GroupDescription'] | ||
|
||
security_group | ||
end | ||
end | ||
|
||
class SecurityGroupIngressParser | ||
|
||
def parse(resource_json) | ||
security_group_ingress_rule = SecurityGroupIngressRule.new | ||
|
||
properties = resource_json['Properties'] | ||
|
||
unless properties['GroupName'].nil? | ||
fail 'GroupName is only allowed in EC2-Classic, and we dont play that!' | ||
end | ||
|
||
security_group_ingress_rule.group_id = properties['GroupId'] | ||
security_group_ingress_rule.to_port = properties['ToPort'] | ||
security_group_ingress_rule.from_port = properties['FromPort'] | ||
security_group_ingress_rule.ip_protocol = properties['IpProtocol'] | ||
security_group_ingress_rule.cidr_ip = properties['CidrIp'] | ||
security_group_ingress_rule | ||
end | ||
end | ||
|
||
class SecurityGroupIngressRule | ||
attr_accessor :to_port, :ip_protocol, :from_port, :cidr_ip, :group_id | ||
end | ||
|
||
class SecurityGroup | ||
attr_accessor :group_description, :vpc_id | ||
attr_reader :ingress_rules, :egress_rules | ||
|
||
def initialize | ||
@ingress_rules = [] | ||
@egress_rules = [] | ||
end | ||
|
||
def add_ingress_rule(ingress_rule) | ||
@ingress_rules << ingress_rule | ||
end | ||
|
||
def add_egress_rule(egress_rule) | ||
@egress_rules << egress_rule | ||
end | ||
end | ||
|
||
# def method_missing(name) | ||
# return self[name] if key? name | ||
# self.each { |k,v| return v if k.to_s.to_sym == name } | ||
# super.method_missing name | ||
# end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
module Rule | ||
attr_accessor :input_json_path, :failure_count | ||
|
||
def warning(jq_expression) | ||
stdout = jq_command(@input_json_path, jq_expression) | ||
result = $?.exitstatus | ||
if result == 0 | ||
@warning_count ||= 0 | ||
@warning_count += 1 | ||
yield stdout | ||
end | ||
end | ||
|
||
def violation(jq_expression, &action) | ||
failing_rule(jq_expression: jq_expression, | ||
fail_if_found: true, | ||
&action) | ||
end | ||
|
||
def assertion(jq_expression, &action) | ||
failing_rule(jq_expression: jq_expression, | ||
fail_if_found: false, | ||
&action) | ||
end | ||
|
||
private | ||
|
||
def failing_rule(jq_expression:, fail_if_found:) | ||
stdout = jq_command(@input_json_path, jq_expression) | ||
result = $?.exitstatus | ||
if (fail_if_found and result == 0) or | ||
(not fail_if_found and result != 0) | ||
@failure_count ||= 0 | ||
@failure_count += 1 | ||
yield stdout | ||
end | ||
end | ||
|
||
def jq_command(input_json_path, jq_expression) | ||
`cat #{input_json_path} | jq '#{jq_expression}' -e` | ||
end | ||
end | ||
|
Oops, something went wrong.