banacle 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 62eb7e77755ab01a7f402528bd3d4740bf57849519fecdcf9fcb2d2360c369aa
4
+ data.tar.gz: afa51477a9e6f8d19669b766fecc2a2b0e09e709247ef03da9ca96ecd19fc4e3
5
+ SHA512:
6
+ metadata.gz: 99a11e4010d7663b2d991cca2a461bcbd2722f5897e5f9e4a199ab04ec353c1b38688400f4d4fdd260b68f6e4c315f0fc5cb0925cecf1bde79c9dcc73d7b2cab
7
+ data.tar.gz: 1ee71620bf8b1c67bbda63b9680585f62b526bc79d62f52474a059b2985e1f83dddb6f92ed5071e73728993c7264fb9b4b69bf011efd7fd631c8eb49f0ef7d8e
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install: gem install bundler -v 1.16.2
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in banacle.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,81 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ banacle (0.1.1)
5
+ aws-sdk-ec2
6
+ sinatra
7
+ unicorn
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ aws-eventstream (1.0.1)
13
+ aws-partitions (1.127.0)
14
+ aws-sdk-core (3.44.1)
15
+ aws-eventstream (~> 1.0)
16
+ aws-partitions (~> 1.0)
17
+ aws-sigv4 (~> 1.0)
18
+ jmespath (~> 1.0)
19
+ aws-sdk-ec2 (1.65.0)
20
+ aws-sdk-core (~> 3, >= 3.39.0)
21
+ aws-sigv4 (~> 1.0)
22
+ aws-sigv4 (1.0.3)
23
+ backports (3.11.4)
24
+ coderay (1.1.2)
25
+ diff-lcs (1.3)
26
+ jmespath (1.4.0)
27
+ kgio (2.11.2)
28
+ method_source (0.9.2)
29
+ multi_json (1.13.1)
30
+ mustermann (1.0.3)
31
+ pry (0.12.2)
32
+ coderay (~> 1.1.0)
33
+ method_source (~> 0.9.0)
34
+ rack (2.0.6)
35
+ rack-protection (2.0.5)
36
+ rack
37
+ raindrops (0.19.0)
38
+ rake (10.5.0)
39
+ rspec (3.8.0)
40
+ rspec-core (~> 3.8.0)
41
+ rspec-expectations (~> 3.8.0)
42
+ rspec-mocks (~> 3.8.0)
43
+ rspec-core (3.8.0)
44
+ rspec-support (~> 3.8.0)
45
+ rspec-expectations (3.8.2)
46
+ diff-lcs (>= 1.2.0, < 2.0)
47
+ rspec-support (~> 3.8.0)
48
+ rspec-mocks (3.8.0)
49
+ diff-lcs (>= 1.2.0, < 2.0)
50
+ rspec-support (~> 3.8.0)
51
+ rspec-support (3.8.0)
52
+ sinatra (2.0.5)
53
+ mustermann (~> 1.0)
54
+ rack (~> 2.0)
55
+ rack-protection (= 2.0.5)
56
+ tilt (~> 2.0)
57
+ sinatra-contrib (2.0.5)
58
+ backports (>= 2.8.2)
59
+ multi_json
60
+ mustermann (~> 1.0)
61
+ rack-protection (= 2.0.5)
62
+ sinatra (= 2.0.5)
63
+ tilt (>= 1.3, < 3)
64
+ tilt (2.0.9)
65
+ unicorn (5.4.1)
66
+ kgio (~> 2.6)
67
+ raindrops (~> 0.7)
68
+
69
+ PLATFORMS
70
+ ruby
71
+
72
+ DEPENDENCIES
73
+ banacle!
74
+ bundler (~> 1.16)
75
+ pry
76
+ rake (~> 10.0)
77
+ rspec (~> 3.0)
78
+ sinatra-contrib
79
+
80
+ BUNDLED WITH
81
+ 1.16.2
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Takuya Kosugiyama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Banacle: Create or delete DENY NACL entries on AWS VPC as ChatOps (Slack Slash Command)
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'banacle'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ ```
14
+ $ bundle
15
+ ```
16
+
17
+ Or install it yourself as:
18
+
19
+ ```
20
+ $ gem install banacle
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Banacle is supposed to be run as a Sinatra server. You can run it simply by `rackup` command. Banacle has two endpoints for Slack as follows:
26
+
27
+ - `/slack/command`: handle Slash Command
28
+ - `/slack/message`: handle Interactive Message
29
+
30
+ ### Example: ban 1.2.3.4 from my VPC
31
+
32
+ Execute an command that create a DENY NACL entry for 1.2.3.4 on a VPC named "test" in ap-northeast-1.
33
+
34
+ ![](./docs/demo1.png)
35
+
36
+ Then an approval request appears. Someone else can review the request and decide to approve or reject it. The requester can cancel the request. In this case, the request looks good so the reviewer clicks "Approve" button.
37
+
38
+ ![](./docs/demo2.png)
39
+
40
+ After approving the request, Banacle executes creating the NACL entry on the target VPC through the AWS API as follows.
41
+
42
+ ![](./docs/nacl.png)
43
+
44
+
45
+ ## Development
46
+
47
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
48
+
49
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
50
+
51
+ ## Contributing
52
+
53
+ Bug reports and pull requests are welcome on GitHub at https://github.com/itkq/banacle.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/banacle.gemspec ADDED
@@ -0,0 +1,34 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "banacle/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "banacle"
8
+ spec.version = Banacle::VERSION
9
+ spec.authors = ["Takuya Kosugiyama"]
10
+ spec.email = ["re@itkq.jp"]
11
+
12
+ spec.summary = %q{Create or delete DENY NACL entries on AWS VPC as ChatOps (Slack Slash Command)}
13
+ spec.description = %q{Create or delete DENY NACL entries on AWS VPC as ChatOps (Slack Slash Command)}
14
+ spec.homepage = "https://github.com/itkq/banacle"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.16"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "sinatra-contrib"
29
+ spec.add_development_dependency "pry"
30
+
31
+ spec.add_dependency "sinatra"
32
+ spec.add_dependency "unicorn"
33
+ spec.add_dependency "aws-sdk-ec2"
34
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "banacle"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/config.ru ADDED
@@ -0,0 +1,3 @@
1
+ require 'banacle'
2
+
3
+ run Banacle::App
data/docs/demo1.png ADDED
Binary file
data/docs/demo2.png ADDED
Binary file
data/docs/nacl.png ADDED
Binary file
@@ -0,0 +1,21 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/reloader'
3
+ require 'banacle/handler'
4
+
5
+ module Banacle
6
+ class App < Sinatra::Base
7
+ configure :development do
8
+ register Sinatra::Reloader
9
+ end
10
+
11
+ post '/slack/command' do
12
+ content_type :json
13
+ Handler.handle_slash_command(request)
14
+ end
15
+
16
+ post '/slack/message' do
17
+ content_type :json
18
+ Handler.handle_interactive_message(request)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module Banacle
2
+ module AwsWrapper
3
+ class Error < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,127 @@
1
+ require 'aws-sdk-ec2'
2
+ require 'banacle/aws_wrapper/error'
3
+ require 'banacle/aws_wrapper/result'
4
+
5
+ module Banacle
6
+ module AwsWrapper
7
+ class Nacl
8
+ class EntryDuplicatedError < AwsWrapper::Error; end
9
+ class EntryNotFoundError < AwsWrapper::Error; end
10
+
11
+ DEFAULT_RULE_NUMBER = 100
12
+
13
+ def self.create_network_acl_ingress_entries(region:, vpc_id:, cidr_blocks:)
14
+ new(region, vpc_id, cidr_blocks).create_network_acl_ingress_entries
15
+ end
16
+
17
+ def self.delete_network_acl_entries(region:, vpc_id:, cidr_blocks:)
18
+ new(region, vpc_id, cidr_blocks).delete_network_acl_entries
19
+ end
20
+
21
+ def initialize(region, vpc_id, cidr_blocks)
22
+ @region = region
23
+ @vpc_id = vpc_id
24
+ @cidr_blocks = cidr_blocks
25
+ @rule_numbers = ingress_rules.map(&:rule_number).sort
26
+ end
27
+
28
+ attr_reader :action, :region, :vpc_id, :cidr_blocks
29
+ attr_accessor :rule_numbers
30
+
31
+ def create_network_acl_ingress_entries
32
+ cidr_blocks.map do |cidr_block|
33
+ result = begin
34
+ create_network_acl_ingress_entry(cidr_block)
35
+ AwsWrapper::Result.new(status: true)
36
+ rescue AwsWrapper::Error => e
37
+ AwsWrapper::Result.new(status: false, error: e)
38
+ end
39
+ [cidr_block, result]
40
+ end.to_h
41
+ end
42
+
43
+ def delete_network_acl_entries
44
+ cidr_blocks.map do |cidr_block|
45
+ result = begin
46
+ delete_network_acl_entry(cidr_block)
47
+ AwsWrapper::Result.new(status: true)
48
+ rescue AwsWrapper::Error => e
49
+ AwsWrapper::Result.new(status: false, error: e)
50
+ end
51
+ [cidr_block, result]
52
+ end.to_h
53
+ end
54
+
55
+ private
56
+
57
+ def create_network_acl_ingress_entry(cidr_block)
58
+ duplicated_rule = ingress_rules.select { |e|
59
+ e.cidr_block == cidr_block
60
+ }.first
61
+
62
+ if duplicated_rule
63
+ raise EntryDuplicatedError.new("entry already exists (rule_number: #{duplicated_rule.rule_number})")
64
+ end
65
+
66
+ next_min_rule_number = nil
67
+ (0..rule_numbers.size - 1).each do |i|
68
+ if rule_numbers[i + 1] - rule_numbers[i] > 1
69
+ next_min_rule_number = rule_numbers[i] + 1
70
+ break
71
+ end
72
+ end
73
+ next_min_rule_number = DEFAULT_RULE_NUMBER unless next_min_rule_number
74
+
75
+ ec2.create_network_acl_entry(
76
+ cidr_block: cidr_block,
77
+ egress: false,
78
+ network_acl_id: network_acl_id,
79
+ protocol: "-1", # all protocols
80
+ rule_action: "deny",
81
+ rule_number: next_min_rule_number,
82
+ )
83
+
84
+ add_rule_number(next_min_rule_number)
85
+ end
86
+
87
+ def delete_network_acl_entry(cidr_block)
88
+ target = ingress_rules.select { |e| !e.egress && e.cidr_block == cidr_block }.first
89
+ if target
90
+ ec2.delete_network_acl_entry(
91
+ egress: false,
92
+ network_acl_id: network_acl_id,
93
+ rule_number: target.rule_number,
94
+ )
95
+ else
96
+ raise EntryDuplicatedError.new("not found")
97
+ end
98
+ end
99
+
100
+ def add_rule_number(num)
101
+ rule_numbers << num
102
+ rule_numbers.sort!
103
+ num
104
+ end
105
+
106
+ def network_acl_id
107
+ acl.network_acl_id
108
+ end
109
+
110
+ def ingress_rules
111
+ @ingress_rules ||= acl.entries.select { |e| !e.egress }
112
+ end
113
+
114
+ def acl
115
+ @acl ||= ec2.describe_network_acls(
116
+ filters: [
117
+ { name: 'vpc-id', values: [vpc_id] },
118
+ ],
119
+ ).network_acls.first
120
+ end
121
+
122
+ def ec2
123
+ @ec2 ||= ::Aws::EC2::Client.new(region: region)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,6 @@
1
+ module Banacle
2
+ module AwsWrapper
3
+ Result = Struct.new(:status, :error, keyword_init: true) do
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,51 @@
1
+ require 'aws-sdk-ec2'
2
+ require 'banacle/aws_wrapper/error'
3
+ require 'banacle/aws_wrapper/result'
4
+
5
+ module Banacle
6
+ module AwsWrapper
7
+ class Vpc
8
+ class InvalidRegionError < AwsWrapper::Error; end
9
+
10
+ def self.resolve_vpc_id(region, vpc_id_or_name)
11
+ new(region, vpc_id_or_name).resolve_vpc_id
12
+ end
13
+
14
+ def initialize(region, vpc_id_or_name)
15
+ @region = region
16
+ @vpc_id_or_name = vpc_id_or_name
17
+ end
18
+
19
+ attr_reader :region, :vpc_id_or_name
20
+
21
+ def resolve_vpc_id
22
+ begin
23
+ vpc_list = ec2.describe_vpcs.each.flat_map(&:vpcs).map do |vpc|
24
+ name_tag = vpc.tags.find { |t| t.key == "Name" }
25
+ [
26
+ name_tag.value,
27
+ vpc.vpc_id,
28
+ ]
29
+ end.sort_by { |e| e[0] }.to_h
30
+ rescue Aws::Errors::NoSuchEndpointError
31
+ raise InvalidRegionError.new("region: #{region} is invalid")
32
+ end
33
+
34
+ vpc_id = nil
35
+ if vpc_list.values.include?(vpc_id_or_name)
36
+ vpc_id = vpc_id_or_name
37
+ else
38
+ vpc_id = vpc_list[vpc_id_or_name]
39
+ end
40
+
41
+ vpc_id
42
+ end
43
+
44
+ private
45
+
46
+ def ec2
47
+ @ec2 ||= ::Aws::EC2::Client.new(region: region)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,61 @@
1
+ require 'banacle/slash_command/error'
2
+ require 'banacle/slash_command/parser'
3
+ require 'banacle/slash_command/renderer'
4
+
5
+ require 'banacle/interactive_message/parser'
6
+ require 'banacle/interactive_message/renderer'
7
+
8
+ require 'banacle/slack_validator'
9
+
10
+ module Banacle
11
+ class Handler
12
+ def self.handle_slash_command(request)
13
+ new(request).handle_slash_command
14
+ end
15
+
16
+ def self.handle_interactive_message(request)
17
+ new(request).handle_interactive_message
18
+ end
19
+
20
+ def initialize(request)
21
+ @request = request
22
+ end
23
+
24
+ attr_reader :request
25
+
26
+ def handle_slash_command
27
+ unless skip_validation? || SlackValidator.valid_signature?(request)
28
+ return [401, {}, "invalid request"]
29
+ end
30
+
31
+ begin
32
+ command = SlashCommand::Parser.parse(request_text)
33
+ rescue SlashCommand::Error => e
34
+ return SlashCommand::Renderer.render_error(e)
35
+ end
36
+
37
+ SlashCommand::Renderer.render(request.params, command)
38
+ end
39
+
40
+ def handle_interactive_message
41
+ unless skip_validation? || SlackValidator.valid_signature?(request)
42
+ return [401, {}, "invalid request"]
43
+ end
44
+
45
+ command = InteractiveMessage::Parser.parse(JSON.parse(request_payload))
46
+ InteractiveMessage::Renderer.render(request.params, command)
47
+ end
48
+
49
+ def request_text
50
+ request.params["text"]
51
+ end
52
+
53
+ def request_payload
54
+ request.params["payload"]
55
+ end
56
+
57
+ def skip_validation?
58
+ request.params["skip_validation"] || ENV["BANACLE_SKIP_VALIDATION"]
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ require 'banacle/slash_command/command'
2
+
3
+ module Banacle
4
+ module InteractiveMessage
5
+ class Parser
6
+ def self.parse(payload)
7
+ new.parse(payload)
8
+ end
9
+
10
+ def parse(payload)
11
+ original_text = payload["original_message"]["text"]
12
+ original_json = JSON.parse(
13
+ original_text.match(command_json_regex)[1].strip, symbolize_names: true,
14
+ )
15
+ command = SlashCommand::Command.new(**original_json)
16
+ end
17
+
18
+ # TODO: sync slash_command/renderer
19
+ def command_json_regex
20
+ /```([^`]+)```/.freeze
21
+ end
22
+ end
23
+ end
24
+ end