shutter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shutter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Rob Lyon
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
+ # Shutter
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'shutter'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install shutter
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rubygems'
5
+ rescue LoadError
6
+ end
7
+
8
+ require 'shutter'
9
+ config_path = ENV['SHUTTER_CONFIG'] ? ENV['SHUTTER_CONFIG'] : "/etc/shutter.d"
10
+ Shutter::CommandLine.new(config_path).execute
@@ -0,0 +1,7 @@
1
+ require "shutter/version"
2
+ require "shutter/content"
3
+ require "shutter/command_line"
4
+
5
+ module Shutter
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,71 @@
1
+ require 'optparse'
2
+ require 'shutter/iptables'
3
+
4
+ module Shutter
5
+ class CommandLine
6
+ def initialize( path = "/etc/shutter.d")
7
+ @config_path = path
8
+ # Make sure that we have the proper files
9
+ files = %w[
10
+ base.ipt
11
+ iface.dmz
12
+ ip.allow
13
+ ip.deny
14
+ ports.private
15
+ ports.public
16
+ ]
17
+ files.each do |name|
18
+ file = "#{@config_path}/#{name}"
19
+ unless File.exists?(file)
20
+ # puts "Creating: #{file}"
21
+ File.open(file, 'w') do |f|
22
+ f.write(Shutter.const_get(name.upcase.gsub(/\./, "_")))
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def execute
29
+ options = {}
30
+ optparse = OptionParser.new do |opts|
31
+ opts.banner = "Usage: shutter [options]"
32
+ options[:command] = :save
33
+ opts.on( '-s', '--save', 'Output the firewall to stdout.') do
34
+ options[:command] = :save
35
+ end
36
+ opts.on( '-r', '--restore', 'Load the firewall through iptables-restore.') do
37
+ options[:command] = :restore
38
+ end
39
+ options[:debug] = false
40
+ opts.on( '-d', '--debug', 'Be a bit more chatty') do
41
+ options[:debug] = true
42
+ end
43
+ opts.on_tail( '-h', '--help', 'Display this screen' ) do
44
+ puts opts
45
+ exit
46
+ end
47
+ opts.on_tail( '--version', "Show the version") do
48
+ puts Shutter::VERSION
49
+ exit
50
+ end
51
+ end
52
+ optparse.parse!
53
+ puts "* Using config path: #{@config_path}" if options[:debug]
54
+ puts "* Running command: #{options[:command].to_s}" if options[:debug]
55
+ send(options[:command])
56
+ end
57
+
58
+ def save
59
+ @ipt = Shutter::IPTables::Base.new(@config_path).generate
60
+ puts @ipt
61
+ end
62
+
63
+ def restore
64
+ @ipt = Shutter::IPTables::Base.new(@config_path).generate
65
+ IO.popen("#{Shutter::IPTables::IPTABLES_RESTORE}", "r+") do |iptr|
66
+ iptr.puts @ipt ; iptr.close_write
67
+ end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,178 @@
1
+ module Shutter
2
+
3
+ BASE_IPT = %q{# Generated by Shutter
4
+ *filter
5
+ :INPUT DROP [0:0]
6
+ :FORWARD DROP [0:0]
7
+ :OUTPUT ACCEPT [0:0]
8
+ :Dmz - [0:0]
9
+ :ValidCheck - [0:0]
10
+ :Jail - [0:0]
11
+ :Bastards - [0:0]
12
+ :Public - [0:0]
13
+ :AllowIP - [0:0]
14
+ :Allowed - [0:0]
15
+ :Private - [0:0]
16
+ :DropJail - [0:0]
17
+ :DropBastards - [0:0]
18
+ :DropInvalid - [0:0]
19
+ :DropScan - [0:0]
20
+ :DropDDOS - [0:0]
21
+ # [CHAIN:FAIL2BAN]
22
+
23
+ -A INPUT -i lo -j ACCEPT
24
+ -A INPUT -j Jail
25
+ -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
26
+ -A INPUT -j ValidCheck
27
+ -A INPUT -j Dmz
28
+ -A INPUT -j Bastards
29
+ -A INPUT -j Public
30
+ -A INPUT -j AllowIP
31
+ -A INPUT ! -d 0.0.0.255/0.0.0.255 -m limit --limit 1/min -j LOG --log-prefix "iptables: Block:"
32
+ -A INPUT -j DROP
33
+
34
+ ##################################################################
35
+ # Jail goes here. Jail and any fail2ban chains will be
36
+ # taken care of dynamically in locker-restore.
37
+ ##################################################################
38
+ # [RULES:JAIL]
39
+
40
+ ##################################################################
41
+ # Validity/Scanning/DDOS checking
42
+ ##################################################################
43
+ -A ValidCheck -m state --state INVALID -j DropInvalid
44
+ -A ValidCheck -p tcp --tcp-flags ALL FIN,URG,PSH -j DropScan
45
+ -A ValidCheck -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DropScan
46
+ -A ValidCheck -p tcp --tcp-flags ALL ALL -j DropScan
47
+ -A ValidCheck -p tcp --tcp-flags ALL FIN -j DropScan
48
+ -A ValidCheck -p tcp --tcp-flags ACK,FIN FIN -j DropScan
49
+ -A ValidCheck -p tcp --tcp-flags ACK,PSH PSH -j DropScan
50
+ -A ValidCheck -p tcp --tcp-flags ACK,URG URG -j DropScan
51
+ -A ValidCheck -p tcp --tcp-flags FIN,RST FIN,RST -j DropScan
52
+ -A ValidCheck -p tcp --tcp-flags ALL SYN,FIN -j DropScan
53
+ -A ValidCheck -p tcp --tcp-flags ALL URG,PSH,FIN -j DropScan
54
+ -A ValidCheck -p tcp --tcp-flags ALL URG,PSH,SYN,FIN -j DropScan
55
+ -A ValidCheck -p tcp --tcp-flags SYN,RST SYN,RST -j DropScan
56
+ -A ValidCheck -p tcp --tcp-flags SYN,FIN SYN,FIN -j DropScan
57
+ -A ValidCheck -p tcp --tcp-flags ALL NONE -j DropScan
58
+ -A ValidCheck -p tcp --tcp-option 64 -j DropScan
59
+ -A ValidCheck -p tcp --tcp-option 128 -j DropScan
60
+ -A ValidCheck -p tcp ! --dport 2049 -m multiport --sports 20,21,22,23,80,110,143,443,993,995 -j DropDDOS
61
+ -A ValidCheck -p udp ! --dport 2049 -m multiport --sports 20,21,22,23,80,110,143,443,993,995 -j DropDDOS
62
+ -A ValidCheck -j RETURN
63
+
64
+ ##################################################################
65
+ # DMZ. Read from iface.dmz and added as:
66
+ # -A INPUT -i <iface> -j ACCEPT
67
+ ##################################################################
68
+ # [RULES:DMZ]
69
+ -A Dmz -j RETURN
70
+
71
+ ##################################################################
72
+ # All IP address ranges that are permanently banned. If
73
+ # no IP addresses are given, then all will be assumed that no ip
74
+ # addresses are banned and create the following rule
75
+ # -A Bastards -j RETURN
76
+ # otherwise a list of banned ips will be generated from ip.deny
77
+ # and will look like this:
78
+ # -A Bastards -s <ipaddr>/<subnet> -j DropBastards
79
+ ##################################################################
80
+ # [RULES:BASTARDS]
81
+ -A Bastards -j RETURN
82
+
83
+ ##################################################################
84
+ # A list of authorized ports for the public access. If there are
85
+ # entries in the ports.public file then they will be added as:
86
+ # -A Public -m state --state NEW -p <proto> -m <proto> --dport <port> -j ACCEPT
87
+ ##################################################################
88
+ # [RULES:PUBLIC]
89
+ -A Public -j RETURN
90
+
91
+ ##################################################################
92
+ # All IP address ranges that are allowed to access the ports. If
93
+ # no IP addresses are given, then all will be assumed and a rule
94
+ # to jump to the Allowed chain will be created:
95
+ # -A AllowIP -j Allowed
96
+ # otherwise a list of allowed ips will be generated from ip.allow
97
+ # and will look like this:
98
+ # -A AllowIP -s 129.101.159.128/26 -j Allowed
99
+ ##################################################################
100
+ # [RULES:ALLOWIP]
101
+ -A AllowIP -j RETURN
102
+
103
+ ##################################################################
104
+ # Allowed. If a packet has met all the requirements it will end
105
+ # up here. This should be a static chain.
106
+ ##################################################################
107
+ -A Allowed -p icmp -m state --state NEW -m icmp --icmp-type 0 -j ACCEPT
108
+ -A Allowed -p icmp -m state --state NEW -m icmp --icmp-type 3 -j ACCEPT
109
+ -A Allowed -p icmp -m state --state NEW -m icmp --icmp-type 8 -j ACCEPT
110
+ -A Allowed -p icmp -m state --state NEW -m icmp --icmp-type 11 -j ACCEPT
111
+ -A Allowed -j Private
112
+ -A Allowed ! -d 0.0.0.255/0.0.0.255 -m limit --limit 1/min -j LOG --log-prefix "iptables: Authorized:"
113
+ -A Allowed -j ACCEPT
114
+
115
+ ##################################################################
116
+ # A list of authorized ports for the allowed IPs. If there are
117
+ # entries in the ports.private file then they will be added as:
118
+ # -A Private -m state --state NEW -p <proto> -m <proto> --dport <port> -j RETURN
119
+ ##################################################################
120
+ # [RULES:PRIVATE]
121
+ -A Private ! -d 0.0.0.255/0.0.0.255 -m limit --limit 3/min -j LOG --log-prefix "iptables: Unauthorized:"
122
+ -A Private -j DROP
123
+
124
+ ##################################################################
125
+ # Log and Drops
126
+ ##################################################################
127
+ -A DropJail ! -d 0.0.0.255/0.0.0.255 -m limit --limit 3/min -j LOG --log-prefix "iptables: Jail:"
128
+ -A DropJail -j DROP
129
+
130
+ -A DropBastards ! -d 0.0.0.255/0.0.0.255 -m limit --limit 3/min -j LOG --log-prefix "iptables: Bastards:"
131
+ -A DropBastards -j DROP
132
+
133
+ -A DropInvalid ! -d 0.0.0.255/0.0.0.255 -m limit --limit 3/min -j LOG --log-prefix "iptables: Invalid:"
134
+ -A DropInvalid -j DROP
135
+
136
+ -A DropScan ! -d 0.0.0.255/0.0.0.255 -m limit --limit 3/min -j LOG --log-prefix "iptables: Scan detected:"
137
+ -A DropScan -j DROP
138
+
139
+ -A DropDDOS ! -d 0.0.0.255/0.0.0.255 -m limit --limit 3/min -j LOG --log-prefix "iptables: DDOS detected:"
140
+ -A DropDDOS -j DROP
141
+
142
+ ##################################################################
143
+ # Add any additional rules that fail2ban has added
144
+ ##################################################################
145
+ # [RULES:FAIL2BAN]
146
+
147
+ COMMIT
148
+ }
149
+
150
+ IFACE_DMZ = %q{# Generated by Shutter
151
+ # iface
152
+ # eth0
153
+ # eth1
154
+ }
155
+
156
+ IP_ALLOW = %q{# Generated by Shutter
157
+ # ipaddr
158
+ # ipaddr/subnet
159
+ 192.168.0.0/16
160
+ }
161
+
162
+ IP_DENY = %q{# Generated by Shutter
163
+ # ipaddr
164
+ # ipaddr/subnet
165
+ # 192.168.0.0/16
166
+ }
167
+
168
+ PORTS_PUBLIC = %q{
169
+ # proto port
170
+ # 80 tcp
171
+ # 443 tcp
172
+ }
173
+
174
+ PORTS_PRIVATE = %q{
175
+ # proto port
176
+ 22 tcp
177
+ }
178
+ end
@@ -0,0 +1,11 @@
1
+ require 'shutter/iptables/base'
2
+ require 'shutter/iptables/eyepee'
3
+ require 'shutter/iptables/iface'
4
+ require 'shutter/iptables/jail'
5
+ require 'shutter/iptables/port'
6
+
7
+ module Shutter
8
+ module IPTables
9
+ IPTABLES_RESTORE="/sbin/iptables-restore"
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module Shutter
2
+ module IPTables
3
+ class Base
4
+ def initialize( path )
5
+ @path = path
6
+ file = File.open("#{path}/base.ipt", "r")
7
+ @content = file.read
8
+ end
9
+
10
+ def to_s
11
+ @content
12
+ end
13
+
14
+ def generate
15
+ #generate_nat
16
+ generate_filter
17
+ end
18
+
19
+ def generate_filter
20
+ @dmz = Iface.new("#{@path}", :dmz).to_ipt
21
+ @content = @content.gsub(/#\ \[RULES:DMZ\]/, @dmz)
22
+ @bastards = EyePee.new("#{@path}", :deny).to_ipt
23
+ @content = @content.gsub(/#\ \[RULES:BASTARDS\]/, @bastards)
24
+ @public = Port.new("#{@path}", :public).to_ipt
25
+ @content = @content.gsub(/#\ \[RULES:PUBLIC\]/, @public)
26
+ @allow = EyePee.new("#{@path}", :allow).to_ipt
27
+ @content = @content.gsub(/#\ \[RULES:ALLOWIP\]/, @allow)
28
+ @private = Port.new("#{@path}", :private).to_ipt
29
+ @content = @content.gsub(/#\ \[RULES:PRIVATE\]/, @private)
30
+
31
+ # Make sure we are restoring what fail2ban has added
32
+ @f2b_chains = Jail.new.fail2ban_chains
33
+ @content = @content.gsub(/#\ \[CHAIN:FAIL2BAN\]/, @f2b_chains)
34
+ @f2b_rules = Jail.new.fail2ban_rules
35
+ @content = @content.gsub(/#\ \[RULES:FAIL2BAN\]/, @f2b_rules)
36
+ @jail = Jail.new.jail_rules
37
+ @content = @content.gsub(/#\ \[RULES:JAIL\]/, @jail)
38
+
39
+ # Remove the rest of the comments and extra lines
40
+ @content = @content.gsub(/^#.*$/, "")
41
+ @content = @content.gsub(/^$\n/, "")
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ module Shutter
2
+ module IPTables
3
+ class EyePee
4
+ def initialize( path, state )
5
+ @state = state
6
+ file = File.open("#{path}/ip.#{state.to_s}", "r")
7
+ @content = file.read
8
+ end
9
+
10
+ def to_s
11
+ @content
12
+ end
13
+
14
+ def to_ipt
15
+ @rules = ""
16
+ @content.each_line do |ip|
17
+ ip_clean = ip.strip
18
+ if ip_clean =~ /^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}(\/[0-9]{0,2})*$/
19
+ @rules += send(:"#{@state.to_s}_ipt", ip_clean)
20
+ end
21
+ end
22
+ @rules
23
+ end
24
+
25
+ def allow_ipt(ip)
26
+ "-A AllowIP -m state --state NEW -s #{ip} -j Allowed\n"
27
+ end
28
+
29
+ def deny_ipt(ip)
30
+ "-A Bastards -s #{ip} -j DropBastards\n"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ module Shutter
2
+ module IPTables
3
+ class Iface
4
+ def initialize( path, type )
5
+ @type = type
6
+ file = File.open("#{path}/iface.#{type.to_s}", "r")
7
+ @content = file.read
8
+ end
9
+
10
+ def to_s
11
+ @content
12
+ end
13
+
14
+ def to_ipt
15
+ @rules = ""
16
+ @content.each_line do |line|
17
+ line = line.strip
18
+ if line =~ /^[a-z].+$/
19
+ @rules += send(:"#{@type.to_s}_ipt", line)
20
+ end
21
+ end
22
+ @rules
23
+ end
24
+
25
+ def dmz_ipt( iface )
26
+ "-A Dmz -i #{iface} -j ACCEPT\n"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ module Shutter
2
+ module IPTables
3
+ class Jail
4
+ def initialize( iptables = "/sbin/iptables")
5
+ @iptables = iptables
6
+ end
7
+
8
+ def fail2ban_chains
9
+ `/sbin/iptables-save | grep "^:fail2ban"`
10
+ end
11
+
12
+ def fail2ban_rules
13
+ `/sbin/iptables-save | grep "^-A fail2ban"`
14
+ end
15
+
16
+ def jail_rules
17
+ jail = `/sbin/iptables-save | grep "^-A Jail"`
18
+ lines = jail.split('\n')
19
+ unless lines != [] && lines[-1] == "-A Jail -j RETURN\n"
20
+ jail += "-A Jail -j RETURN\n"
21
+ end
22
+ jail
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ module Shutter
2
+ module IPTables
3
+ class Port
4
+ def initialize( path, type )
5
+ @type = type
6
+ file = File.open("#{path}/ports.#{type.to_s}", "r")
7
+ @content = file.read
8
+ end
9
+
10
+ def to_s
11
+ @content
12
+ end
13
+
14
+ def to_ipt
15
+ @rules = ""
16
+ @content.each_line do |line|
17
+ line = line.strip
18
+ if line =~ /^[1-9].+$/
19
+ port,proto = line.split
20
+ @rules += send(:"#{@type.to_s}_ipt", port, proto)
21
+ end
22
+ end
23
+ @rules
24
+ end
25
+
26
+ def private_ipt( port, proto )
27
+ "-A Private -m state --state NEW -p #{proto} -m #{proto} --dport #{port} -j RETURN\n"
28
+ end
29
+
30
+ def public_ipt( port, proto )
31
+ "-A Public -m state --state NEW -p #{proto} -m #{proto} --dport #{port} -j ACCEPT\n"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Shutter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,17 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/shutter/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Rob Lyon"]
6
+ gem.email = ["nosignsoflifehere@gmail.com"]
7
+ gem.description = %q{Shutter helps maintain firewalls}
8
+ gem.summary = %q{Shutter helps maintain firewalls}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "shutter"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Shutter::VERSION
17
+ end
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shutter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rob Lyon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-22 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Shutter helps maintain firewalls
15
+ email:
16
+ - nosignsoflifehere@gmail.com
17
+ executables:
18
+ - shutter
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - .gitignore
23
+ - Gemfile
24
+ - LICENSE
25
+ - README.md
26
+ - Rakefile
27
+ - bin/shutter
28
+ - lib/shutter.rb
29
+ - lib/shutter/command_line.rb
30
+ - lib/shutter/content.rb
31
+ - lib/shutter/iptables.rb
32
+ - lib/shutter/iptables/base.rb
33
+ - lib/shutter/iptables/eyepee.rb
34
+ - lib/shutter/iptables/iface.rb
35
+ - lib/shutter/iptables/jail.rb
36
+ - lib/shutter/iptables/port.rb
37
+ - lib/shutter/version.rb
38
+ - shutter.gemspec
39
+ homepage: ''
40
+ licenses: []
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ! '>='
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubyforge_project:
59
+ rubygems_version: 1.8.24
60
+ signing_key:
61
+ specification_version: 3
62
+ summary: Shutter helps maintain firewalls
63
+ test_files: []