ec2-security-czar 1.0.0

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.
@@ -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