puffy 0.1.0

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: b53bffbf2f30ba597482608cf4f3e005cf481bd69cbfa4fdaf7a9de14f1b90ca
4
+ data.tar.gz: 5c4fee30b67e7c8cd777d219173473d2e2d4f0b437c9df02e7f0d16ca9341515
5
+ SHA512:
6
+ metadata.gz: 7a23559f7ff420013adaeafa437f13450a78b41bf1757e584c8bea54c7cade3fd51da7fc31497a2c70844c011d13d841edb0091eb801c03eee7737d9f0fe7a19
7
+ data.tar.gz: a3c6db0679f173cd714665e9a884c5eff22df4847562cb188f8d04c18da31ec79d3b9e54f6084d2a9514eb0dfb831337c878b58e92391ed86a7fd125888e181c
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - master
12
+
13
+ jobs:
14
+ unit:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ matrix:
18
+ ruby:
19
+ - "2.6"
20
+ - "2.7"
21
+ - "3.0"
22
+ name: Ruby ${{ matrix.ruby }}
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ - name: Setup ruby
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ - name: Install dependencies
30
+ run: |
31
+ gem install bundler
32
+ bundle install --jobs 4 --retry 3
33
+ - name: Generate the parser
34
+ run: bundle exec rake gen_parser
35
+ - name: Run tests
36
+ run: bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ Gemfile.lock
2
+ coverage
3
+ lib/puffy/parser.tab.rb
4
+ tags
5
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,39 @@
1
+ AllCops:
2
+ TargetRubyVersion: '2.5'
3
+ AllowSymlinksInCacheRootDirectory: true
4
+ NewCops: enable
5
+ Exclude:
6
+ - lib/melt/*.tab.rb
7
+ - tmp/**/*.rb
8
+
9
+ Layout/HashAlignment:
10
+ EnforcedColonStyle: table
11
+ EnforcedHashRocketStyle: table
12
+
13
+ Layout/LineLength:
14
+ Enabled: false
15
+
16
+ Metrics/BlockLength:
17
+ Exclude:
18
+ - spec/**/*.rb
19
+
20
+ Metrics/ClassLength:
21
+ Max: 200
22
+
23
+ Metrics/ModuleLength:
24
+ Exclude:
25
+ - spec/**/*.rb
26
+
27
+ Style/Documentation:
28
+ Exclude:
29
+ - features/support/env.rb
30
+ - spec/**/*.rb
31
+
32
+ Style/TrailingCommaInArrayLiteral:
33
+ EnforcedStyleForMultiline: comma
34
+
35
+ Style/TrailingCommaInHashLiteral:
36
+ EnforcedStyleForMultiline: comma
37
+
38
+ Style/TrailingCommaInArguments:
39
+ EnforcedStyleForMultiline: comma
data/.simplecov ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # vim:set syntax=ruby:
4
+
5
+ SimpleCov.start do
6
+ add_filter '/spec/'
7
+ add_group 'Formatters', 'lib/melt/formatters'
8
+ end
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in puffy.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # Puffy
2
+
3
+ [![Build Status](https://travis-ci.com/opus-codium/puffy.svg?branch=master)](https://travis-ci.com/opus-codium/puffy)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/1d46ac8511718fd284fd/maintainability)](https://codeclimate.com/github/opus-codium/puffy/maintainability)
5
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/1d46ac8511718fd284fd/test_coverage)](https://codeclimate.com/github/opus-codium/puffy/test_coverage)
6
+ [![Inline docs](http://inch-ci.org/github/opus-codium/puffy.svg?branch=master)](http://inch-ci.org/github/opus-codium/puffy)
7
+
8
+ ## Features
9
+
10
+ * Generate rules for [Netfilter](http://www.netfilter.org/) and [PF](http://www.openbsd.org/faq/pf/) (extensible);
11
+ * IPv6 and IPv4 support;
12
+ * Define the configuration of multiple *nodes* in a single file;
13
+ * Define *services* as group of rules to mix-in in *nodes* rules definitions;
14
+ * Handle NAT & port redirection;
15
+
16
+ ## Requirements
17
+
18
+ * Accurate DNS information;
19
+
20
+ ## Syntax
21
+
22
+ The Puffy syntax is inspired by the syntax of the [OpenBSD Packet Filter](http://www.openbsd.org/faq/pf/), with the ability to group rules in reusable blocks in order to describe all rules of a network of nodes in a single file.
23
+
24
+ Rules must appear in either a *node* or *service* definition, *services* being
25
+ reusable blocks of related rules:
26
+
27
+ ~~~
28
+ service base do
29
+ service ntp
30
+ service ssh
31
+ end
32
+
33
+ service ntp do
34
+ pass out proto udp from any to port ntp
35
+ end
36
+
37
+ service ssh do
38
+ pass in proto tcp form any to port ssh
39
+ end
40
+
41
+ node 'db.example.com' do
42
+ service base
43
+ pass in proto tcp from 'www1.example.com' to port postgresql
44
+ end
45
+
46
+ node /www\d+.example.com/ do
47
+ service base
48
+ pass in proto tcp from any to port www
49
+ pass out proto tcp from any to 'db.example.com' port postgresql
50
+ end
51
+ ~~~
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'cucumber'
6
+ require 'cucumber/rake/task'
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+ Cucumber::Rake::Task.new(:features)
10
+
11
+ task test: %i[spec features]
12
+
13
+ task default: :test
14
+
15
+ desc 'Generate the puffy language parser'
16
+ task gen_parser: 'lib/puffy/parser.tab.rb'
17
+
18
+ file 'lib/puffy/parser.tab.rb' => 'lib/puffy/parser.y' do
19
+ `racc -S lib/puffy/parser.y`
20
+ end
data/bin/puffy ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH << File.expand_path('../lib', __dir__)
5
+
6
+ require 'puffy/cli'
7
+
8
+ begin
9
+ cli = Puffy::Cli.new
10
+ cli.execute(ARGV)
11
+ rescue Puffy::SyntaxError => e
12
+ $stderr.puts e.message
13
+ exit 1
14
+ rescue Puffy::ParseError => e
15
+ $stderr.puts e.message
16
+ exit 1
17
+ end
data/lib/core_ext.rb ADDED
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Mixin for combining variations
4
+ #
5
+ # { a: :b }.expand #=> [{ a: :b }]
6
+ # { a: [:b, :c] }.expand #=> [{ a: :b }, { a: :c }]
7
+ # {
8
+ # a: [:b, :c],
9
+ # d: [:e, :f],
10
+ # }.expand
11
+ # #=> [{ a: :b, d: :e }, {a: :b, d: :f}, { a: :c, d: :e }, { a: :c, d: :f }]
12
+ module Expandable
13
+ # Returns an array composed of all possible variation of the object by
14
+ # combining all the items of it's array values.
15
+ #
16
+ # @return [Array]
17
+ def expand
18
+ @expand_res = [{}]
19
+ each do |key, value|
20
+ case value
21
+ when Array then expand_array(key)
22
+ when Hash then expand_hash(key)
23
+ else @expand_res.map! { |hash| hash.merge(key => value) }
24
+ end
25
+ end
26
+ @expand_res
27
+ end
28
+
29
+ private
30
+
31
+ def expand_array(key)
32
+ orig = @expand_res
33
+ @expand_res = []
34
+ fetch(key).each do |value|
35
+ @expand_res += orig.map { |hash| hash.merge(key => value) }
36
+ end
37
+ end
38
+
39
+ def expand_hash(key)
40
+ orig = @expand_res
41
+ @expand_res = []
42
+ fetch(key).expand.each do |value|
43
+ @expand_res += orig.map { |hash| hash.merge(key => value) }
44
+ end
45
+ end
46
+ end
47
+
48
+ class Array # :nodoc:
49
+ def deep_dup
50
+ array = []
51
+ each do |value|
52
+ array << if value.respond_to?(:deep_dup)
53
+ value.deep_dup
54
+ else
55
+ value.dup
56
+ end
57
+ end
58
+ array
59
+ end
60
+ end
61
+
62
+ class Hash # :nodoc:
63
+ include Expandable
64
+
65
+ def deep_dup
66
+ hash = dup
67
+ each_pair do |key, value|
68
+ hash[key.dup] = if value.respond_to?(:deep_dup)
69
+ value.deep_dup
70
+ else
71
+ value.dup
72
+ end
73
+ end
74
+ hash
75
+ end
76
+ end
data/lib/puffy/cli.rb ADDED
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cri'
4
+ require 'puffy'
5
+ require 'fileutils'
6
+
7
+ module Puffy
8
+ # Command-line processing
9
+ class Cli
10
+ def initialize
11
+ cli = self
12
+
13
+ @main = Cri::Command.define do
14
+ name 'puffy'
15
+ usage 'puffy [options] <command>'
16
+ summary 'Network firewall rules made easy!'
17
+
18
+ description <<~DESCRIPTION
19
+ Generate firewall rules for multiple nodes from a single network
20
+ specification file.
21
+ DESCRIPTION
22
+
23
+ flag :h, :help, 'Show help for this command' do |_value, cmd|
24
+ puts cmd.help
25
+ exit 0
26
+ end
27
+
28
+ run do |_opts, _args, cmd|
29
+ puts cmd.help
30
+ exit 0
31
+ end
32
+ end
33
+
34
+ @main.define_command do
35
+ name 'generate'
36
+ usage 'generate [options] <network> <hostname>'
37
+ summary 'Generate the firewall configuration for a node'
38
+
39
+ description <<~DESCRIPTION
40
+ Generate the firewall configuration for the node "hostname" for which
41
+ the configuration is described in the "network" specification file.
42
+ DESCRIPTION
43
+
44
+ required :f, :formatter, 'The formatter to use', default: 'Pf'
45
+
46
+ param('network')
47
+ param('hostname')
48
+
49
+ run do |opts, args|
50
+ parser = cli.load_network(args[:network])
51
+ rules = parser.ruleset_for(args[:hostname])
52
+ policy = parser.policy_for(args[:hostname])
53
+
54
+ formatter = Object.const_get("Puffy::Formatters::#{opts[:formatter]}::Ruleset").new
55
+ puts formatter.emit_ruleset(rules, policy)
56
+ end
57
+ end
58
+
59
+ puppet = @main.define_command do
60
+ name 'puppet'
61
+ usage 'puppet [options] <subcommand>'
62
+ summary 'Run puppet actions'
63
+
64
+ description <<~DESCRIPTION
65
+ Manage a directory of firewall configurations files suitable for Puppet.
66
+ DESCRIPTION
67
+
68
+ required :o, :output, 'Base directory for network firewall rules', default: '.'
69
+
70
+ run do |_opts, _args, cmd|
71
+ puts cmd.help
72
+ exit 0
73
+ end
74
+ end
75
+
76
+ puppet.define_command do
77
+ name 'diff'
78
+ usage 'diff [options] <network>'
79
+ summary 'Show differences between network specification and firewall rules'
80
+
81
+ description <<~DESCRIPTION
82
+ Show the changes that would be introduced by running `puffy puppet
83
+ generate` with the current "network" specification file.
84
+ DESCRIPTION
85
+
86
+ param('network')
87
+
88
+ run do |opts, args|
89
+ parser = cli.load_network(args[:network])
90
+ puppet = Puffy::Puppet.new(opts[:output], parser)
91
+ puppet.diff
92
+ end
93
+ end
94
+
95
+ puppet.define_command do
96
+ name 'generate'
97
+ usage 'generate [options] <network>'
98
+ summary 'Generate network firewall rules according to network specification'
99
+
100
+ description <<~DESCRIPTION
101
+ Generate a tree of configuration files suitable for all supported
102
+ firewalls for all the nodes described in the "network" specification
103
+ file.
104
+ DESCRIPTION
105
+
106
+ param('network')
107
+
108
+ run do |opts, args|
109
+ parser = cli.load_network(args[:network])
110
+ puppet = Puffy::Puppet.new(opts[:output], parser)
111
+ puppet.save
112
+ end
113
+ end
114
+
115
+ help_command = Cri::Command.new_basic_help.modify do
116
+ summary 'Show help for a command'
117
+ end
118
+
119
+ @main.add_command(help_command)
120
+ end
121
+
122
+ def load_network(filename)
123
+ parser = Puffy::Parser.new
124
+ parser.parse_file(filename)
125
+ parser
126
+ end
127
+
128
+ def execute(argv)
129
+ @main.run(argv)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Puffy
4
+ module Formatters # :nodoc:
5
+ module Base # :nodoc:
6
+ # Returns the loopback IPv4 IPAddr
7
+ #
8
+ # @return [IPAddr]
9
+ def self.loopback_ipv4
10
+ IPAddr.new('127.0.0.1')
11
+ end
12
+
13
+ # Returns the loopback IPv6 IPAddr
14
+ #
15
+ # @return [IPAddr]
16
+ def self.loopback_ipv6
17
+ IPAddr.new('::1')
18
+ end
19
+
20
+ # Returns a list of loopback addresses
21
+ #
22
+ # @return [Array<IPAddr>]
23
+ def self.loopback_addresses
24
+ [nil, loopback_ipv4, loopback_ipv6]
25
+ end
26
+
27
+ # Base class for Puffy Formatter Rulesets
28
+ class Ruleset
29
+ def initialize
30
+ @rule_formatter = Class.const_get(self.class.name.sub(/set$/, '')).new
31
+ end
32
+
33
+ def emit_header
34
+ ["# Generated by puffy v#{Puffy::VERSION} on #{Time.now.strftime('%c')}"]
35
+ end
36
+
37
+ # Returns a String representation of the provided +rules+ Array of Puffy::Rule with the +policy+ policy.
38
+ #
39
+ # @param rules [Array<Puffy::Rule>] array of Puffy::Rule.
40
+ # @param _policy [Symbol] ruleset policy.
41
+ # @return [String]
42
+ def emit_ruleset(rules, _policy = nil)
43
+ rules.collect { |rule| @rule_formatter.emit_rule(rule) }.join("\n")
44
+ end
45
+
46
+ # Filename for a firewall configuration fragment emitted by the formatter.
47
+ #
48
+ # @return [Array<String>]
49
+ def filename_fragment
50
+ raise 'Formatters#filename_fragment MUST be overriden'
51
+ end
52
+ end
53
+
54
+ # Base class for Puffy Formatter Rulesets
55
+ class Rule
56
+ protected
57
+
58
+ # Returns the loopback IPAddr of the given +address_family+
59
+ #
60
+ # @param address_family [Symbol] the address family, +:inet+ or +:inet6+
61
+ # @return [IPAddr,nil]
62
+ def loopback_address(address_family)
63
+ case address_family
64
+ when nil then nil
65
+ when :inet then Puffy::Formatters::Base.loopback_ipv4
66
+ when :inet6 then Puffy::Formatters::Base.loopback_ipv6
67
+ else raise "Unsupported address family #{address_family.inspect}"
68
+ end
69
+ end
70
+
71
+ # Return a string representation of the +host+ IPAddr as a host or network.
72
+ # @param host [IPAddr]
73
+ # @return [String] IP address
74
+ def emit_address(host)
75
+ if (host.ipv4? && host.prefix.to_i == 32) || (host.ipv6? && host.prefix.to_i == 128)
76
+ host.to_s
77
+ else
78
+ "#{host}/#{host.prefix}"
79
+ end
80
+ end
81
+
82
+ # Return a string representation of the +port+ port.
83
+ # #param port [Integer,Range]
84
+ # @return [String] Port
85
+ def emit_port(port)
86
+ case port
87
+ when Integer then port.to_s
88
+ when Range then "#{port.begin}:#{port.end}"
89
+ else raise "Unexpected #{port.class.name}"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end