spacelift-policy 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: 2e03df63a02ecd9be71ce542073170a680ca8700a2d83b6ecb377918a7477c6b
4
+ data.tar.gz: d98721507e5b61471d68ddacf87f381279e92156ebba8f1604ab8c4923fdfa70
5
+ SHA512:
6
+ metadata.gz: 78fbe029ec9c679ad3a4451897c2eb5030e391699bee1d7026fdab0c9d1315736067a52f392e1fed821d3c83a7aef697987eed61a9cf85562a7efdcb1da62520
7
+ data.tar.gz: 0cc1dd62ce8281c9fc22692a179665ff07669446edf73066cf06870b59b8487cafb413745b998ceb643638fa5b49d523eb125f3ae2e8121ec641ef10fecf3f38
data/.gitignore ADDED
@@ -0,0 +1,13 @@
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
12
+ Gemfile.lock
13
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in spacelift-policy.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "rspec", "~> 3.0"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Spacelift
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.
data/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Spacelift::Policy
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/spacelift/policy`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'spacelift-policy'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install spacelift-policy
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/spacelift-policy.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/spacelift-policy'
4
+
5
+ exit(1) unless Spacelift::Policy::CLI.run
@@ -0,0 +1,6 @@
1
+ require_relative 'spacelift/policy/cli'
2
+ require_relative 'spacelift/policy/error'
3
+ require_relative 'spacelift/policy/policy'
4
+ require_relative 'spacelift/policy/rule'
5
+ require_relative 'spacelift/policy/version'
6
+ require_relative 'spacelift/policy/violation'
@@ -0,0 +1,70 @@
1
+ require 'optparse'
2
+
3
+ module Spacelift
4
+ module Policy
5
+ # CLI implements the logic required to configure, run and report policy
6
+ # checks.
7
+ class CLI
8
+ attr_reader :json, :policies
9
+
10
+ DEFAULT_PLAN = 'spacelift.plan.json'.freeze
11
+ DEFAULT_POLICIES = '/spacelift/project/**/*.policy.rb'.freeze
12
+
13
+ def self.run(argv: ARGV)
14
+ new.parse(argv).run
15
+ end
16
+
17
+ def initialize
18
+ @json = DEFAULT_PLAN
19
+ @policies = DEFAULT_POLICIES
20
+ end
21
+
22
+ # This method reeks of :reek:NestedIterators and :reek:TooManyStatements
23
+ # rubocop:disable Metrics/LineLength, Metrics/MethodLength
24
+ def parse(options)
25
+ parser = OptionParser.new do |opts|
26
+ opts.banner = 'Usage: spacelift-policy [options]'
27
+
28
+ opts.on('-jJSON', '--json=JSON', 'Path to the Terraform JSON plan') do |json|
29
+ @json = json.freeze
30
+ end
31
+
32
+ opts.on('-pPOLICIES', '--policies=POLICIES', 'Glob expression capturing policy files') do |policies|
33
+ @policies = policies.freeze
34
+ end
35
+
36
+ opts.on('-h', '--help', 'Prints this help') do
37
+ puts opts
38
+ exit
39
+ end
40
+ end
41
+ parser.parse!(options)
42
+ self
43
+ end
44
+ # rubocop:enable Metrics/LineLength, Metrics/MethodLength
45
+
46
+ # This method reeks of :reek:TooManyStatements.
47
+ def run
48
+ # List and validate policy paths.
49
+ paths = Dir.glob(@policies)
50
+ raise Error, "no policy files matched by #{@policies}" if paths.empty?
51
+
52
+ # Validate state file path.
53
+ raise Error, "state file '#{json}' not present" unless File.file?(@json)
54
+
55
+ # Load policy files.
56
+ paths.each { |path| load path }
57
+
58
+ # Apply rules against the plan JSON file.
59
+ violations = Spacelift::Policy.enforce(File.read(@json))
60
+
61
+ # Print out violations, if any.
62
+ violations.each { |violation| warn violation.to_s }
63
+
64
+ # In the end, report if there were any violations. Based on that the
65
+ # caller will be able to decide whether ther run was successful or not.
66
+ violations.empty?
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,6 @@
1
+ module Spacelift
2
+ module Policy
3
+ # Error is a simple class for declaring policy errors.
4
+ class Error < StandardError; end
5
+ end
6
+ end
@@ -0,0 +1,56 @@
1
+ require 'json'
2
+ require 'ostruct'
3
+ require 'singleton'
4
+
5
+ module Spacelift
6
+ # Policy is the module that hosts all the other resources in this library,
7
+ # and provides helper methods to deal with the Collection singleton.
8
+ module Policy
9
+ module_function
10
+
11
+ def define
12
+ yield Collection.instance
13
+ end
14
+
15
+ def enforce(source)
16
+ input = JSON.parse(source, object_class: OpenStruct)
17
+
18
+ changes = input.resource_changes
19
+ changes ? Collection.instance.process(changes) : []
20
+ end
21
+
22
+ # Collection is a singleton instance combining multiple rules. It's defined
23
+ # this way to allow pulling policy from multiple files in no particular
24
+ # order.
25
+ class Collection
26
+ include Singleton
27
+
28
+ def initialize
29
+ @rules = []
30
+ @violations = []
31
+ end
32
+
33
+ def ensure(name, &block)
34
+ raise Error, "definition not provided for rule '#{name}'" unless block
35
+
36
+ @rules << Rule.new(name, &block)
37
+ end
38
+
39
+ def process(resources)
40
+ raise Error, 'no rules defined' if @rules.empty?
41
+
42
+ resources.each { |resource| process_resource(resource) }
43
+ @violations
44
+ end
45
+
46
+ private
47
+
48
+ def process_resource(resource)
49
+ @rules.each do |rule|
50
+ ok, violation = rule.process(resource)
51
+ @violations << violation unless ok
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,69 @@
1
+ require 'colorize'
2
+ require 'set'
3
+
4
+ module Spacelift
5
+ module Policy
6
+ # Rule represents a single rule applied to all resources.
7
+ class Rule
8
+ attr_reader :name
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ @matchers = []
13
+ @check = nil
14
+ yield self
15
+ freeze
16
+ validate
17
+ end
18
+
19
+ def process(resource)
20
+ return [true, nil] if ok?(resource)
21
+
22
+ [false, Violation.new(address: resource.address, rule: name)]
23
+ end
24
+
25
+ def then(&block)
26
+ raise Error, "check already defined on rule '#{name}'" if check
27
+
28
+ self.check = block
29
+ end
30
+
31
+ def when(&block)
32
+ matchers << block
33
+ self
34
+ end
35
+
36
+ def when_action_is(*actions)
37
+ required = Set.new(actions)
38
+
39
+ self.when do |resource|
40
+ Set.new(resource.change.actions).intersect?(required)
41
+ end
42
+ end
43
+
44
+ def when_managed
45
+ self.when { |resource| resource.mode == 'managed' }
46
+ end
47
+
48
+ def when_type_is(*types)
49
+ self.when { |resource| types.include?(resource.type) }
50
+ end
51
+
52
+ private
53
+
54
+ attr_accessor :matchers, :check
55
+
56
+ def ok?(resource)
57
+ return true unless matchers.all? { |matcher| matcher.call(resource) }
58
+
59
+ change = resource.change
60
+ check.call(change.before, change.after)
61
+ end
62
+
63
+ def validate
64
+ raise Error, "no matchers defined on rule '#{name}'" if matchers.empty?
65
+ raise Error, "no check defined on rule '#{name}'" unless check
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,5 @@
1
+ module Spacelift
2
+ module Policy
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ require 'colorize'
2
+
3
+ module Spacelift
4
+ module Policy
5
+ # Violation encapsulates a resource's violation of a single Rule.
6
+ class Violation
7
+ attr_reader :address, :rule
8
+
9
+ def initialize(address:, rule:)
10
+ @address = address
11
+ @rule = rule
12
+ end
13
+
14
+ def to_s
15
+ [address.red.bold, 'failed rule', rule.cyan.bold].join(' ')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'lib/spacelift/policy/version'
2
+
3
+ # rubocop:disable Metrics/LineLength
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'spacelift-policy'
6
+ spec.version = Spacelift::Policy::VERSION
7
+ spec.authors = ['Marcin Wyszynski']
8
+ spec.email = ['marcinw@spacelift.io']
9
+
10
+ spec.summary = 'Ruby microframework for defining rich policy-as-code for Terraform'
11
+ spec.description = <<-SUMMARY
12
+ SUMMARY
13
+ spec.homepage = 'https://github.com/spacelift-io/policy'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/spacelift-io/policy'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/spacelift-io/policy'
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+
27
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ spec.add_dependency 'colorize', '~> 0.8'
31
+ end
32
+ # rubocop:enable Metrics/LineLength
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spacelift-policy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marcin Wyszynski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.8'
27
+ description: ''
28
+ email:
29
+ - marcinw@spacelift.io
30
+ executables:
31
+ - spacelift-policy
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - ".rspec"
37
+ - Gemfile
38
+ - LICENSE
39
+ - README.md
40
+ - Rakefile
41
+ - bin/spacelift-policy
42
+ - lib/spacelift-policy.rb
43
+ - lib/spacelift/policy/cli.rb
44
+ - lib/spacelift/policy/error.rb
45
+ - lib/spacelift/policy/policy.rb
46
+ - lib/spacelift/policy/rule.rb
47
+ - lib/spacelift/policy/version.rb
48
+ - lib/spacelift/policy/violation.rb
49
+ - spacelift-policy.gemspec
50
+ homepage: https://github.com/spacelift-io/policy
51
+ licenses:
52
+ - MIT
53
+ metadata:
54
+ homepage_uri: https://github.com/spacelift-io/policy
55
+ source_code_uri: https://github.com/spacelift-io/policy
56
+ changelog_uri: https://github.com/spacelift-io/policy
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 2.3.0
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.0.2
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Ruby microframework for defining rich policy-as-code for Terraform
76
+ test_files: []