ec2-security-czar 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 259408a1effb7996e0508c8ee6425d5b699a2f07
4
+ data.tar.gz: 9ac56b7f5a1bb903ebcaeb4ce9c8be47d4c2e4cd
5
+ SHA512:
6
+ metadata.gz: 0fe4dbf7f7f0af332c2776de3fbfa09db26491582befe0bfe2de2faab1b7378fd0e35623186b954ad9964836458eeed80f469a3f4d2272f436ddf29f829881ae
7
+ data.tar.gz: 81fb5b7d917f1d5fe930861f770f5fcc65bca346844dc05618dd9a4cf62afa010c8f0cdfd9c2e884cc61c3b2f99e111a339394f0df233ee11442af99a346e405
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+
24
+ config/*
@@ -0,0 +1,4 @@
1
+ github_repo: sportngin/ec2-security-czar
2
+ semantic_versioning: true
3
+ branches_to_keep:
4
+ - master
@@ -0,0 +1,2 @@
1
+ ec2-security-czar
2
+
@@ -0,0 +1,2 @@
1
+ 2.1
2
+
@@ -0,0 +1,13 @@
1
+ defaults:
2
+ deploy_cmd: gem push *.gem
3
+ before_deploy_cmds:
4
+ - op tag-release
5
+ - sed -i '' -e "s/\".*/\"$(git tag | tail -1 | sed s/v//)\"/" lib/ec2-security-czar/version.rb
6
+ - git add lib/ec2-security-czar/version.rb
7
+ - git commit -m "Version Bump" && git push
8
+ - gem build ec2-security-czar.gemspec
9
+ after_deploy_cmds:
10
+ - rm *.gem
11
+ environments:
12
+ -
13
+ rubygems: {}
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0
4
+ - 2.1
5
+ - 1.9.3
6
+ script: bundle exec rspec
7
+ matrix:
8
+ include:
9
+ - rvm: jruby-19mode # JRuby in 1.9 mode
10
+ env: JRUBY_OPTS="--1.9 -Xcext.enabled=true"
11
+ allow_failures:
12
+ - rvm: jruby-19mode
@@ -0,0 +1 @@
1
+ #### v1.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ec2-security-czar.gemspec
4
+ gemspec
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: 'bundle exec rspec', all_on_start: true, all_after_pass: true do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ian Ehlert
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Ec2SecurityCzar
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'ec2-security-czar'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install ec2-security-czar
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/ec2-security-czar/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,35 @@
1
+ #! /usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'gli'
4
+ require 'ec2_security_czar'
5
+
6
+ include GLI::App
7
+
8
+ program_desc 'Command Line Ec2 Security Group Manager'
9
+ version Ec2SecurityCzar::VERSION
10
+
11
+ wrap_help_text :verbatim
12
+
13
+ switch :verbose, :desc => 'Enable Verbose mode for more logging', :negatable => false
14
+ switch :debug, :desc => 'Enable Debug mode for detailed logs and backtraces', :negatable => false
15
+
16
+ pre do |global_options, command, options, args|
17
+ $verbose = global_options[:verbose]
18
+ $debug = global_options[:debug]
19
+ ENV['GLI_DEBUG'] = $debug.to_s
20
+ true
21
+ end
22
+
23
+ desc "Update your ec2 security groups"
24
+ arg_name '<environment>'
25
+ command :update do |c|
26
+ c.flag [:t, :token], :desc => "AWS MFA Auth Token, Requires mfa_serial_number to be set in the aws config"
27
+ c.flag [:r, :region], :desc => "AWS Region to apply updates in, ex. 'us-west-2', defaults to 'us-east-1'"
28
+ c.action do |global_options, options, args|
29
+ Ec2SecurityCzar::Base.new(args.first, global_options.merge(options)).update_security_groups
30
+ end
31
+ end
32
+
33
+ default_command :help
34
+
35
+ exit run(ARGV)
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ec2-security-czar/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ec2-security-czar"
8
+ spec.version = Ec2SecurityCzar::VERSION
9
+ spec.authors = ["Ian Ehlert"]
10
+ spec.email = ["ehlertij@gmail.com"]
11
+ spec.summary = %q{Rule manager for EC2 Security Groups.}
12
+ spec.description = %q{Manages your EC2 security groups using YAML config files.}
13
+ spec.homepage = "https://github.com/sportngin/ec2-security-czar"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "aws-sdk", "~> 1.38"
22
+ spec.add_dependency "gli"
23
+ spec.add_dependency "hashie"
24
+ spec.add_dependency "highline"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.6"
27
+ spec.add_development_dependency "rake"
28
+ spec.add_development_dependency "rspec", "3.0.0.beta2"
29
+ spec.add_development_dependency "guard"
30
+ spec.add_development_dependency "guard-rspec"
31
+ spec.add_development_dependency "terminal-notifier-guard"
32
+ spec.add_development_dependency "simplecov"
33
+ end
@@ -0,0 +1,52 @@
1
+ require 'aws-sdk'
2
+ require 'yaml'
3
+ require 'hashie'
4
+
5
+ module Ec2SecurityCzar
6
+ class AwsConfig < Hash
7
+ include Hashie::Extensions::IndifferentAccess
8
+ end
9
+
10
+ class Base
11
+ attr_accessor :ec2
12
+
13
+ def initialize(environment=nil, args={})
14
+ raise MissingConfig.new("Missing aws_keys.yml config file") unless File.exists?(config_filename)
15
+ @environment = environment
16
+ load_config(args[:region])
17
+ AWS.config(access_key_id: @config[:access_key], secret_access_key: @config[:secret_key], region: @config[:region])
18
+ if @config[:mfa_serial_number]
19
+ @ec2 = mfa_auth(args[:token])
20
+ else
21
+ @ec2 = AWS.ec2
22
+ end
23
+ end
24
+
25
+ def update_security_groups
26
+ SecurityGroup.update_security_groups(ec2, @environment, @config[:region])
27
+ end
28
+
29
+ def load_config(region)
30
+ return @config if @config
31
+ @config = AwsConfig[YAML.load_file(config_filename)]
32
+ @config = @config[@environment] if @environment
33
+ @config[:region] = region || 'us-east-1'
34
+ @config
35
+ end
36
+
37
+ private
38
+ def mfa_auth(mfa_token)
39
+ raise MFATokenMissing.new("MFA token is required as an argument!") unless mfa_token
40
+ sts = AWS::STS.new(access_key_id: @config[:access_key], secret_access_key: @config[:secret_key])
41
+ session = sts.new_session(duration: @config[:mfa_duration] || 900, serial_number: @config[:mfa_serial_number], token_code: mfa_token)
42
+ AWS::EC2.new(session.credentials)
43
+ end
44
+
45
+ def config_filename
46
+ 'config/aws_keys.yml'
47
+ end
48
+ end
49
+
50
+ MFATokenMissing = Class.new(StandardError)
51
+ MissingConfig = Class.new(StandardError)
52
+ end
@@ -0,0 +1,87 @@
1
+ require 'highline/import'
2
+
3
+ module Ec2SecurityCzar
4
+ class Rule
5
+
6
+ attr_accessor :protocol, :port_range, :ip, :group, :egress
7
+
8
+ def initialize(options)
9
+ @egress = options[:direction] == :outbound
10
+ @ip = options[:ip_range]
11
+ @group = group_id(options[:group])
12
+ @protocol = options[:protocol] || :tcp
13
+ @port_range = options[:port_range] || (0..65535)
14
+ @api_object = options[:api_object]
15
+ end
16
+
17
+ def equal?(rule)
18
+ rule.protocol.to_s == protocol.to_s &&
19
+ Array(rule.port_range) == Array(port_range) &&
20
+ rule.ip == ip &&
21
+ rule.group == group &&
22
+ rule.egress == egress
23
+ end
24
+
25
+ def authorize!(security_group_api)
26
+ sources = ip.nil? ? { group_id: group } : ip
27
+ if egress
28
+ security_group_api.authorize_egress(sources, protocol: protocol, ports: port_range)
29
+ else
30
+ security_group_api.authorize_ingress(protocol, port_range, sources)
31
+ end
32
+ say "<%= color('Authorized - #{pretty_print}', :green) %>"
33
+ rescue StandardError => e
34
+ say "<%= color('#{e.class} - #{e.message}', :red) %>"
35
+ say "<%= color('#{pretty_print}', :red) %>"
36
+ end
37
+
38
+ def revoke!
39
+ @api_object.revoke
40
+ say "<%= color('Revoked - #{pretty_print}', :cyan) %>"
41
+ rescue StandardError => e
42
+ say "<%= color('#{e.class} - #{e.message}', :red) %>"
43
+ say "<%= color('#{pretty_print}', :red) %>"
44
+ end
45
+
46
+ def group_id(group)
47
+ if group.is_a? Hash
48
+ group[:group_id] || SecurityGroup.lookup(group[:group_name]).id
49
+ else
50
+ group
51
+ end
52
+ end
53
+
54
+ def self.rules_from_api(api_rules, direction)
55
+ rules = []
56
+ Array(api_rules).map do |api_rule|
57
+ rules << api_rule.ip_ranges.map do |ip|
58
+ Rule.new(ip_range: ip, port_range: api_rule.port_range, protocol: api_rule.protocol, direction: direction, api_object: api_rule)
59
+ end
60
+ rules << api_rule.groups.map do |group|
61
+ Rule.new(group: group.id, port_range: api_rule.port_range, protocol: api_rule.protocol, direction: direction, api_object: api_rule)
62
+ end
63
+ end
64
+ rules.flatten
65
+ end
66
+
67
+ def self.rules_from_config(config, direction)
68
+ rules = []
69
+ Array(config[direction]).map do |zone|
70
+ rules << Array(zone[:ip_ranges]).map do |ip|
71
+ Rule.new(ip_range: ip, port_range: zone[:port_range], protocol: zone[:protocol], direction: direction)
72
+ end
73
+ rules << Array(zone[:groups]).map do |group|
74
+ Rule.new(group: group, port_range: zone[:port_range], protocol: zone[:protocol], direction: direction)
75
+ end
76
+ end
77
+ rules.flatten
78
+ end
79
+
80
+ def pretty_print
81
+ direction = egress ? "Outbound" : "Inbound"
82
+ ip_or_group = ip ? ip : SecurityGroup.lookup(group).name
83
+ port = port_range.is_a?(Range) ? "ports #{port_range}" : "port #{port_range}"
84
+ "#{direction} traffic on #{port} for #{ip_or_group} using #{protocol}"
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,209 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'hashie'
4
+ require 'highline/import'
5
+
6
+ module Ec2SecurityCzar
7
+ class SecurityGroupConfig < Hash
8
+ include Hashie::Extensions::IndifferentAccess
9
+ end
10
+
11
+ class SecurityGroup
12
+
13
+ attr_accessor :name, :config, :diff
14
+
15
+ def initialize(name, environment)
16
+ @name = name
17
+ @environment = environment
18
+ load_rules
19
+ end
20
+
21
+ # Public: Creates missing security groups, updates all security groups
22
+ #
23
+ # ec2: ec2 instance created in base.rb
24
+ # environment: environment passed in from commandline
25
+ # region: the region loaded in from aws_keys.yml, defaults to 'us-east-1'
26
+ def self.update_security_groups(ec2, environment, region)
27
+ @ec2 = ec2
28
+ @environment = environment
29
+ @region = region
30
+ create_missing_security_groups(environment)
31
+ update_rules
32
+ end
33
+
34
+ def self.update_rules
35
+ security_groups.each do |sg|
36
+ security_group = SecurityGroup.new(sg.name, @environment)
37
+ security_group.update_rules
38
+ end
39
+ end
40
+
41
+ # Public: Creates a hash mapping security_group.name to security_group, and looks up security_group by name or id
42
+ #
43
+ # name: the name of the security group to lookup
44
+ #
45
+ # Returns - SecurityGroup object
46
+ def self.lookup(query)
47
+ @security_group_hash ||= security_groups.inject({}) do |hash, security_group|
48
+ hash[security_group.name] = security_group
49
+ hash[security_group.id] = security_group
50
+ hash
51
+ end
52
+ @security_group_hash[query]
53
+ end
54
+
55
+ # Private: Gets all security groups from AWS
56
+ #
57
+ # Returns - SecurityGroupCollection
58
+ def self.from_aws
59
+ @security_groups = ec2.security_groups
60
+ end
61
+
62
+ # Private: Gets all the security groups with YAML files
63
+ #
64
+ # Returns - Array of all security group names
65
+ def self.config_security_groups
66
+ security_group_definition_files
67
+ .reject { |file| get_security_group_region(file) != region}
68
+ .map { |file| File.basename(file,File.extname(file)) }
69
+ end
70
+ private_class_method :config_security_groups
71
+
72
+ def self.security_group_definition_files
73
+ Dir["config/*.yml"].reject!{ |file| file == "config/aws_keys.yml" }
74
+ end
75
+ private_class_method :security_group_definition_files
76
+
77
+ # Private: Gets the security group region
78
+ #
79
+ # Returns - The region in which the security group should be made
80
+ def self.get_security_group_region(file)
81
+ SecurityGroupConfig[YAML.load(ERB.new(File.read(file)).result(binding))][:region] || 'us-east-1'
82
+ end
83
+ private_class_method :get_security_group_region
84
+
85
+ # Public: Finds security groups with YAML files not on AWS
86
+ #
87
+ # Returns - Array of all security group names not on AWS
88
+ def self.missing_security_groups
89
+ config_security_groups - from_aws.map(&:name)
90
+ end
91
+ private_class_method :missing_security_groups
92
+
93
+ # Public: Creates missing security groups
94
+ #
95
+ # Returns - nil
96
+ def self.create_missing_security_groups(environment)
97
+ unless (missing_groups = missing_security_groups).empty?
98
+ say "================================================="
99
+ say "Creating security groups for #{environment}:"
100
+ say "================================================="
101
+ missing_groups.each do |name|
102
+ security_group = SecurityGroup.new(name, environment)
103
+ config = security_group.config
104
+ ec2.security_groups.create(name, vpc: config[:vpc], description: config[:description])
105
+ say "<%= color('#{name}', :green) %>"
106
+ end
107
+ say "\n"
108
+ end
109
+ end
110
+ private_class_method :create_missing_security_groups
111
+
112
+ # Private: @security_groups accessor
113
+ #
114
+ # Returns - @security_groups
115
+ def self.security_groups
116
+ @security_groups
117
+ end
118
+ private_class_method :security_groups
119
+
120
+ # Private: @ec2 accessor
121
+ #
122
+ # Returns - @ec2
123
+ def self.ec2
124
+ @ec2
125
+ end
126
+ private_class_method :ec2
127
+
128
+ def self.environment
129
+ @environment
130
+ end
131
+ private_class_method :environment
132
+
133
+ def self.region
134
+ @region
135
+ end
136
+ private_class_method :region
137
+
138
+ def update_rules
139
+ if config
140
+ say "================================================="
141
+ say "Applying changes for #{name}:"
142
+ say "================================================="
143
+
144
+ # Apply deletions first
145
+ rules_diff
146
+ [:outbound, :inbound].each do |direction|
147
+ diff[:deletions][direction].each{ |rule| rule.revoke! }
148
+ end
149
+
150
+ # Re-calculate the diff after performing deletions to make sure we add
151
+ # back any that got removed because of the way AWS groups rules together.
152
+ rules_diff
153
+ [:outbound, :inbound].each do |direction|
154
+ diff[:additions][direction].each{ |rule| rule.authorize!(self.class.lookup(name)) }
155
+ end
156
+ say "\n"
157
+ else
158
+ say "No config file for #{name}, skipping...\n\n"
159
+ end
160
+ end
161
+
162
+ def rules_diff
163
+ @diff = { deletions: {}, additions: {} }
164
+
165
+ [:outbound, :inbound].each do |direction|
166
+ @diff[:deletions][direction] = []
167
+ @diff[:additions][direction] = new_rules(direction)
168
+
169
+ current_rules(direction).each do |current_rule|
170
+ unless rule_exists?(direction, current_rule)
171
+ @diff[:deletions][direction] << current_rule
172
+ end
173
+ end
174
+ end
175
+ diff
176
+ end
177
+ private :rules_diff
178
+
179
+ def load_rules
180
+ if File.exists? config_filename
181
+ environment = @environment
182
+ @config = SecurityGroupConfig[YAML.load(ERB.new(File.read(config_filename)).result(binding))]
183
+ end
184
+ end
185
+
186
+ def config_filename
187
+ "config/#{name}.yml"
188
+ end
189
+ private :config_filename
190
+
191
+ def rule_exists?(direction, current_rule)
192
+ @diff[:additions][direction].reject!{ |rule| rule.equal?(current_rule) }
193
+ end
194
+ private :config_filename
195
+
196
+ def current_rules(direction)
197
+ security_group = self.class.lookup(name)
198
+ aws_security_group_rules = direction == :outbound ? security_group.egress_ip_permissions : security_group.ingress_ip_permissions
199
+ Rule.rules_from_api(aws_security_group_rules, direction)
200
+ end
201
+ private :current_rules
202
+
203
+ def new_rules(direction)
204
+ Rule.rules_from_config(config, direction)
205
+ end
206
+ private :new_rules
207
+
208
+ end
209
+ end