puffy 0.1.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.
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