Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Kascic committed Feb 11, 2016
0 parents commit 3da80c2
Show file tree
Hide file tree
Showing 20 changed files with 715 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .rspec
@@ -0,0 +1,2 @@
--color
--require spec_helper
6 changes: 6 additions & 0 deletions Gemfile
@@ -0,0 +1,6 @@
source 'https://rubygems.org'

group :test do
gem 'rspec'
gem 'simplecov'
end
34 changes: 34 additions & 0 deletions Gemfile.lock
@@ -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
9 changes: 9 additions & 0 deletions bin/cfn_nag
@@ -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])
17 changes: 17 additions & 0 deletions cfn-nag.gemspec
@@ -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
5 changes: 5 additions & 0 deletions deploy_local.sh
@@ -0,0 +1,5 @@
#!/bin/bash

gem uninstall cfn-nag -x
gem build cfn-nag.gemspec
gem install cfn-nag-0.0.0.gem
56 changes: 56 additions & 0 deletions lib/cfn_nag.rb
@@ -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
5 changes: 5 additions & 0 deletions lib/custom_rules/security_group_egress.rb
@@ -0,0 +1,5 @@
class SecurityGroupEgressRule
def audit(cfn_model)
true
end
end
6 changes: 6 additions & 0 deletions lib/json_rules/basic_rules.rb
@@ -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
32 changes: 32 additions & 0 deletions lib/json_rules/open_cidr.rb
@@ -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
62 changes: 62 additions & 0 deletions lib/model/cfn_model.rb
@@ -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

74 changes: 74 additions & 0 deletions lib/model/security_group_parser.rb
@@ -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
43 changes: 43 additions & 0 deletions lib/rule.rb
@@ -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

0 comments on commit 3da80c2

Please sign in to comment.