probium 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0d8175b7893bcfa662a261942ea892c4d8be515f
4
+ data.tar.gz: 4e91a34fe46154dfc218d1b69346fe069a98eb90
5
+ SHA512:
6
+ metadata.gz: 3ab68bd317fcf6788e044010caf43134e84963d56e0f39d8cddf62bbea5dc25f3029a0bbeff49a5b51dae20088c32f2028be51f04e064e1e33cc62dffb53afcb
7
+ data.tar.gz: ebac34f2db1e54128678caf76b93c81226be2e0898a96904463ab30d28f22bb3f18364a0c250f538cbb4662e2c5e9d2cd3f69d29bda7311f980daec1f878d92d
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'runner'
4
+
5
+ begin
6
+ runner = Runner.new
7
+ puts runner.run
8
+ exit runner.exit_code
9
+ rescue StandardError => e
10
+ puts e
11
+ exit 1
12
+ end
@@ -0,0 +1,46 @@
1
+ require 'optparse'
2
+
3
+ class CommandLine
4
+ VALID_OUTPUT_FORMATS = [:graphic, :json, :yaml, :csv].freeze
5
+
6
+ def self.parse!(options)
7
+ OptionParser.new do |opts|
8
+ opts.banner = 'Usage: probium my_policy.yaml [options]'
9
+
10
+ opts.on('-o', '--output-format=FORMAT', 'Format in which to display policy report (graphic, json, yaml, csv)') do |of|
11
+ if VALID_OUTPUT_FORMATS.include?(f = of.downcase.to_sym)
12
+ options[:output_format] = f
13
+ else
14
+ options[:message] = "Invalid output-format '#{of}'. Options are #{VALID_OUTPUT_FORMATS.join(', ')}"
15
+ options[:state] = :fail
16
+ end
17
+ end
18
+
19
+ opts.on('-e', '--extension-dir=PATH', 'Location of extension files') do |path|
20
+ options[:extensions_path] = path
21
+ end
22
+
23
+ opts.on('-d', '--debug', 'Enable debug output') do
24
+ options[:debug] = true
25
+ end
26
+
27
+ opts.on('--no-color', 'Disable color in output') do
28
+ options[:color] = false
29
+ end
30
+
31
+ opts.on('-h', '--help', 'Print this help') do
32
+ options[:message] = opts
33
+ options[:state] = :exit
34
+ end
35
+ end.parse!
36
+ options
37
+ end
38
+
39
+ def self.policy!
40
+ policy_file = ARGV.shift
41
+ unless policy_file
42
+ raise StandardError, 'Missing required policy file as argument'
43
+ end
44
+ policy_file
45
+ end
46
+ end
@@ -0,0 +1,50 @@
1
+ require 'log'
2
+
3
+ class Extensions
4
+ @@extensions = {}
5
+
6
+ def self.[](extension_name)
7
+ @@extensions[extension_name]
8
+ end
9
+
10
+ def initialize(location = File.join(File.dirname(__FILE__), 'extensions'))
11
+ validate_location(location)
12
+ @location = File.join(location, '*.rb')
13
+ load_extensions
14
+ end
15
+
16
+ private
17
+
18
+ def load_extensions
19
+ extension_files = Dir.glob(@location)
20
+ extension_files.each do |extension_file|
21
+ next if File.directory?(extension_file)
22
+ begin
23
+ Log.debug { "Loading extension file - #{extension_file}"}
24
+ instance_eval(File.read(extension_file))
25
+ Log.debug { "Successfully loaded extension file - #{extension_file}"}
26
+ rescue Exception => e
27
+ Log.debug { "Error in extension #{extension_file} - #{e}" }
28
+ raise "Cannot load extension file '#{extension_file}'"
29
+ end
30
+ end
31
+ end
32
+
33
+ def create_resource(name, &block)
34
+ @@extensions[name] = { :resource => block }
35
+ end
36
+
37
+ def compare_fn(name, &block)
38
+ @@extensions[name] = { :compare_fn => block }
39
+ end
40
+
41
+ def validate_location(location)
42
+ unless File.exist?(location)
43
+ raise "Extensions directory '#{location}' does not exist"
44
+ end
45
+
46
+ unless File.directory?(location)
47
+ raise "Extensions direcotry '#{location}' is not a directory"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,103 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+
4
+ def is_ssl_enabled?(tcp_socket)
5
+ ctx = OpenSSL::SSL::SSLContext.new
6
+ ctx.set_params({ :options=>OpenSSL::SSL::OP_ALL })
7
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
8
+ enabled = true
9
+
10
+ OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx).tap do |socket|
11
+ begin
12
+ socket.sync_close = true
13
+ socket.connect_nonblock
14
+ rescue IO::WaitReadable
15
+ if IO.select([socket], nil, nil, 1)
16
+ retry
17
+ else
18
+ enabled = false
19
+ end
20
+ rescue IO::WaitWritable
21
+ if IO.select([socket], nil, nil, 1)
22
+ retry
23
+ else
24
+ enabled = false
25
+ end
26
+ rescue OpenSSL::SSL::SSLError
27
+ enabled = false
28
+ end
29
+
30
+ return enabled
31
+ end
32
+ end
33
+
34
+ def connect_to_port(port)
35
+ begin
36
+ TCPSocket.new('0.0.0.0', port)
37
+ rescue StandardError # Errno::ECONNREFUSED mainly but covering for timeouts
38
+ nil
39
+ end
40
+ end
41
+
42
+ def get_port_state(port)
43
+ state = { :open => false,
44
+ :ssl => "unknown" }
45
+
46
+ tcp_socket = connect_to_port(port)
47
+
48
+ return state unless tcp_socket # couldn't connect, can't figure anything out
49
+
50
+ state[:open] = true
51
+ state[:ssl] = is_ssl_enabled?(tcp_socket)
52
+
53
+ tcp_socket.close
54
+
55
+ state
56
+ end
57
+
58
+ def combine_port_states(states)
59
+ states.reduce({}) do |old_state, state|
60
+ old_state[:open] ||= state[:open]
61
+ old_state[:ssl] ||= state[:ssl]
62
+
63
+ old_state[:open] &&= state[:open]
64
+ old_state[:ssl] &&= state[:open]
65
+
66
+ old_state
67
+ end
68
+ end
69
+
70
+ # TODO(ploubser): Write custom compare function
71
+ #compare_fn(:port) do |name, expected, actual|
72
+ #end
73
+
74
+ create_resource(:port) do |port|
75
+ resource = Puppet::Resource.new('ssl', port.to_s)
76
+ state = {}
77
+
78
+ if port =~ /^(\d+)-(\d+)$/
79
+ port_states = []
80
+ ports = ($1.to_i..$2.to_i).to_a
81
+ threads = (0...10).map do # god help us all
82
+ Thread.new do
83
+ while p = ports.pop
84
+ port_states << get_port_state(p)
85
+ end
86
+ end
87
+ end
88
+ threads.map(&:join)
89
+ state = combine_port_states(port_states)
90
+ elsif port =~ /^(\d+)$/
91
+ state = get_port_state(port)
92
+ else
93
+ state = { :open => 'unknown',
94
+ :ssl => 'unknown' }
95
+ end
96
+
97
+ # add the state keys to the resource
98
+ state.each do |key, val|
99
+ resource[key] = val
100
+ end
101
+
102
+ resource
103
+ end
@@ -0,0 +1,18 @@
1
+ require 'logger'
2
+
3
+ class Log
4
+ @@log = Logger.new(STDOUT)
5
+ @@log.level = Logger::WARN
6
+
7
+ def self.initialize
8
+ @@log.level = Logger::DEBUG
9
+ @@log.formatter = proc do |severity, datetime, progname, msg|
10
+ calling_position = caller[5].split(/^.+\//).last.split(/:/)[0,2].join(':') # gross
11
+ "[#{datetime}] #{calling_position}: - #{msg}\n"
12
+ end
13
+ end
14
+
15
+ def self.debug(&blk)
16
+ @@log.debug &blk
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ require 'rule'
2
+ require 'facter'
3
+ require 'log'
4
+
5
+ class Policy
6
+ include Enumerable
7
+ extend Forwardable
8
+ def_delegators :@rules, :each
9
+
10
+ class PolicyError < StandardError
11
+ def initialize(message)
12
+ super("Invalid policy - #{message}")
13
+ end
14
+ end
15
+
16
+ VALID_KEYS = [:name, 'name', :rules, 'rules', :confine, 'confine'].freeze
17
+
18
+ attr_reader :rules
19
+ attr_reader :name
20
+ attr_reader :confines
21
+
22
+ def initialize(policy)
23
+ if (invalid_keys = policy.keys - VALID_KEYS).size > 0
24
+ raise PolicyError, "invalid field(s) '#{invalid_keys.join(',')}'"
25
+ end
26
+
27
+ @name = policy[:name] or policy['name'] or raise PolicyError, 'missing required field "name"'
28
+ @rules = policy[:rules] or policy['rules'] or raise PolicyError, 'missing required field "rules"'
29
+ @confines = policy[:confine] or policy['confine']
30
+ @confines ||= {}
31
+
32
+ unless @rules.is_a?(Array)
33
+ raise PolicyError, 'rules field must be an Array'
34
+ end
35
+
36
+ unless @rules.size > 0
37
+ raise PolicyError, 'rules Array must contain at least one rule'
38
+ end
39
+ end
40
+
41
+ def enabled?
42
+ Log.debug { "Checking confine rules for policy - #{@name}" }
43
+
44
+ @confines.each do |fact_name, value|
45
+ if (fact_value = Facter.value(fact_name)) != value
46
+ Log.debug { "Skipping policy '#{@name} - #{fact_name}: #{fact_value.inspect} != #{value.inspect}"}
47
+ return false
48
+ end
49
+ end
50
+
51
+ Log.debug { "Policy '#{@name}' passed all confine rules." }
52
+ true
53
+ end
54
+
55
+ def check_rules
56
+ # Delay loading rules until Policy is checked. Puppet resources are expensive
57
+ # and we avoid it incase enabled? = false
58
+ @rules.map! { |r| Rule.new(r) }
59
+
60
+ result = { :name => @name,
61
+ :success => true,
62
+ :rules => [] }
63
+ @rules.each do |rule|
64
+ rule_result = rule.check_resources
65
+ result[:rules] << rule_result
66
+ result[:success] = false unless rule_result[:success]
67
+ end
68
+
69
+ result
70
+ end
71
+ end
@@ -0,0 +1,90 @@
1
+ require 'extensions'
2
+ require 'log'
3
+ require 'puppet'
4
+
5
+ class Resource
6
+ class ResourceError < StandardError
7
+ def initialize(msg)
8
+ super("Invalid resource - #{msg}")
9
+ end
10
+ end
11
+
12
+ attr_reader :title
13
+ attr_reader :type
14
+ attr_reader :name
15
+ attr_reader :expected_properties
16
+ attr_reader :puppet_resource
17
+
18
+ def initialize(title, properties)
19
+ if title.to_s =~ /^([A-Z].+)\[(.+)\]$/
20
+ @type = $1.to_s.downcase.to_sym
21
+ @name =$2.to_s.gsub(/'|"/, '')
22
+ else
23
+ raise ResourceError, "invalid resource title - #{title}"
24
+ end
25
+
26
+ @title = title
27
+ @expected_properties = properties
28
+
29
+ @compare_fn = lambda do |name, expected, actual|
30
+ if name == :ensure
31
+ if expected == 'present'
32
+ return actual != :absent
33
+ end
34
+
35
+ if expected == 'absent'
36
+ expected = expected.to_sym # user can express it as 'absent' or :absent
37
+ end
38
+ end
39
+
40
+ expected == actual
41
+ end
42
+
43
+ create_resource
44
+ end
45
+
46
+ def check_properties
47
+ result = { :success => true,
48
+ :title => @title,
49
+ :properties => [] }
50
+ @expected_properties.each do |property, value|
51
+ status = @compare_fn.call(property, value, @puppet_resource[property])
52
+ result[:success] = false unless status
53
+ result[:properties] << { :name => property,
54
+ :expected => value,
55
+ :actual => @puppet_resource[property].to_s,
56
+ :success => status }
57
+ end
58
+
59
+ result
60
+ end
61
+
62
+ private
63
+
64
+ def create_resource
65
+ Log.debug { "Loading resource #{@title}"}
66
+ start_time = Time.now
67
+ if extension = Extensions[@type]
68
+ Log.debug { "Found resource type '#{@type}' in extensions." }
69
+ @puppet_resource = extension[:resource].call(@name)
70
+ if extension[:compare_fn]
71
+ Log.debug { "Loading custom compare function for resource - #{@title}"}
72
+ @compare_fn = extension[:compare_fn]
73
+ end
74
+ else
75
+ begin
76
+ Log.debug { "Couldn't find resource type '#{@type}' in extensions. Creating Puppet resource." }
77
+ @puppet_resource = Puppet::Resource.indirection.find("#{@type}/#{@name}")
78
+ rescue Puppet::Error => e
79
+ msg = "Cannot create resource type - '#{@type}'. Unkown error - #{e}"
80
+ if e.message =~ /^.*Permission denied.*$/
81
+ msg = "Insufficient permissions to create resource - #{@title}"
82
+ elsif e.message =~ /^.*Could not find type.*$/
83
+ msg = "Cannot create unknown resource type - '#{@type}'"
84
+ end
85
+ raise Resource::ResourceError, msg
86
+ end
87
+ end
88
+ Log.debug { "Loaded resource #{@title} in #{Time.now - start_time}" }
89
+ end
90
+ end
@@ -0,0 +1,99 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require 'csv'
4
+ require 'rainbow'
5
+
6
+ class ResultViewer
7
+ attr_reader :run_state
8
+
9
+ def initialize(policies)
10
+ @run_state = { :policies => policies,
11
+ :total => policies.size,
12
+ :successes => policies.reduce(0) { |count, policy| policy[:success] ? count + 1 : count } }
13
+ end
14
+
15
+ def to_s
16
+ string_buffer = []
17
+ string_buffer << "Total Policies: #{@run_state[:total]}"
18
+
19
+ @run_state[:policies].each do |policy|
20
+ string_buffer << stringify_policy(policy)
21
+ end
22
+
23
+ string_buffer << "Passed: #{@run_state[:successes]}/#{@run_state[:total]}"
24
+ string_buffer.join("\n\n")
25
+ end
26
+
27
+ def to_yaml
28
+ @run_state.to_yaml
29
+ end
30
+
31
+ def to_json
32
+ @run_state.to_json
33
+ end
34
+
35
+ def to_csv
36
+ ## CSV.generate do |csv|
37
+ # csv << ['Rule', 'Property', 'Expected Value', 'Actual Value', 'Success']
38
+ # rule_results.each do |rule, result|
39
+ # result.each do |r|
40
+ # csv << [rule, r[:property], r[:expected], r[:actual], r[:success]]
41
+ # end
42
+ # end
43
+ # end
44
+
45
+ "TODO(ploubser): Implement"
46
+ end
47
+
48
+ private
49
+
50
+ def stringify_policy(policy)
51
+ string_buffer = []
52
+ color = policy[:success] ? :green : :red
53
+ string_buffer << "Policy: #{Rainbow(policy[:name]).send(color)}"
54
+ policy[:rules].each do |rule|
55
+ string_buffer << stringify_rule(rule)
56
+ end
57
+ string_buffer.join("\n\n")
58
+ end
59
+
60
+ def stringify_rule(rule)
61
+ string_buffer = []
62
+ color = rule[:success] ? :green : :red
63
+ string_buffer << "Description: #{Rainbow(rule[:description]).send(color)}"
64
+
65
+ if rule[:severity]
66
+ string_buffer << "Severity: #{Rainbow(rule[:severity]).send(color)}"
67
+ end
68
+
69
+ rule[:resources].each do |resource|
70
+ string_buffer << stringify_resource(resource)
71
+ end
72
+
73
+ indent_buffer(string_buffer).join("\n")
74
+ end
75
+
76
+ def stringify_resource(resource)
77
+ string_buffer = []
78
+ string_buffer << resource[:title]
79
+
80
+ resource[:properties].each do |property|
81
+ string_buffer << stringify_property(property)
82
+ end
83
+ indent_buffer(string_buffer).join("\n")
84
+ end
85
+
86
+ def stringify_property(property)
87
+ string_buffer = []
88
+ color = property[:success] ? :green : :red
89
+ string_buffer << "#{property[:name]}: #{property[:expected]} -> #{Rainbow(property[:actual]).send(color)}"
90
+ indent_buffer(string_buffer, 8).join("\n")
91
+ end
92
+
93
+
94
+ def indent_buffer(buffer, count=4)
95
+ buffer.map do |s|
96
+ ' ' * count + s.to_s
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,51 @@
1
+ require 'log'
2
+ require 'resource'
3
+
4
+ class Rule
5
+ class RuleError < StandardError
6
+ def initialize(msg)
7
+ super("Invalid rule - #{msg}")
8
+ end
9
+ end
10
+
11
+ VALID_KEYS = [:resources, 'resources',
12
+ :description, 'description',
13
+ :severity, 'severity'].freeze
14
+
15
+ attr_reader :resources
16
+ attr_reader :description
17
+ attr_reader :severity
18
+
19
+ def initialize(rule)
20
+ if (invalid_keys = rule.keys - VALID_KEYS).size > 0
21
+ raise RuleError, "invalid field(s) '#{invalid_keys.join(',')}'"
22
+ end
23
+
24
+ tmp_resources = rule[:resources] or rule['resources'] or
25
+ raise RuleError, 'missing required field "resources"'
26
+ @description = rule[:description] or rule['description'] or
27
+ raise RuleError, 'missing required field "description"'
28
+ @severity = rule[:severity] or rule['severity']
29
+
30
+ unless tmp_resources.is_a? Hash
31
+ raise RuleError, 'resources field must be an Hash'
32
+ end
33
+
34
+ @resources = tmp_resources.map do |title, properties|
35
+ Resource.new(title, properties)
36
+ end
37
+ end
38
+
39
+ def check_resources
40
+ result = { :description => @description,
41
+ :severity => @severity,
42
+ :success => true,
43
+ :resources => [] }
44
+ @resources.each do |resource|
45
+ resource_result = resource.check_properties
46
+ result[:resources] << resource_result
47
+ result[:success] = false unless resource_result[:success]
48
+ end
49
+ result
50
+ end
51
+ end
@@ -0,0 +1,117 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ require 'command_line'
4
+ require 'extensions'
5
+ require 'result_viewer'
6
+ require 'policy'
7
+ require 'log'
8
+
9
+ class Runner
10
+ attr_reader :exit_code
11
+
12
+ def initialize
13
+ @defaults = { :output_format => :graphic,
14
+ :extensions_path => File.join(File.dirname(__FILE__), 'extensions'),
15
+ :debug => false,
16
+ :color => true,
17
+ :state => :run,
18
+ :msg => nil }
19
+ @options = CommandLine.parse!(@defaults)
20
+
21
+ if msg = @options.delete(:message)
22
+ puts msg
23
+ end
24
+
25
+ case @options.delete(:state)
26
+ when :exit
27
+ exit 0
28
+ when :fail
29
+ exit 1
30
+ end
31
+
32
+ if @options[:debug]
33
+ Log.initialize
34
+ end
35
+
36
+ unless @options[:color]
37
+ Rainbow.enabled = false
38
+ end
39
+
40
+ Log.debug { "Starting policy check with configured option - #{@options.inspect}" }
41
+
42
+ @extensions = Extensions.new(@options[:extensions_path])
43
+ policy_file = CommandLine.policy!
44
+ @policies = load_policies(policy_file).map { |policy| Policy.new(policy) }
45
+ @exit_code = 0
46
+ end
47
+
48
+ def run
49
+ processed_policies = []
50
+
51
+ @policies.each do |policy|
52
+ if policy.enabled?
53
+ processed_policies << policy.check_rules
54
+ end
55
+ end
56
+
57
+ result_viewer = ResultViewer.new(processed_policies)
58
+
59
+ if result_viewer.run_state[:successes] != result_viewer.run_state[:total]
60
+ @exit_code = 1
61
+ end
62
+
63
+ case @options[:output_format]
64
+ when :graphic
65
+ result_viewer.to_s
66
+ when :json
67
+ result_viewer.to_json
68
+ when :yaml
69
+ result_viewer.to_yaml
70
+ # TODO(ploubser): Fix this
71
+ #when :csv
72
+ # result_viewer.to_csv
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def load_policies(target_location)
79
+ policies = []
80
+
81
+ if File.directory?(target_location)
82
+ Dir.glob(target_location + '/*').each do |policy_file|
83
+ policies << load_policy_file(policy_file)
84
+ end
85
+ else
86
+ policies << load_policy_file(target_location)
87
+ end
88
+
89
+ policies
90
+ end
91
+
92
+ def load_policy_file(policy_file)
93
+ Log.debug { "Loading policy file - #{policy_file}" }
94
+
95
+ unless File.exist?(policy_file)
96
+ raise "Cannot find policy file - '#{policy_file}'"
97
+ end
98
+
99
+ policy = {}
100
+
101
+ begin
102
+ if policy_file =~ /^.*\.erb$/
103
+ template = ERB.new(File.read(policy_file, 3, '>')).result
104
+ policy = YAML.load(template)
105
+ else
106
+ policy = YAML.load_file(policy_file)
107
+ end
108
+ rescue StandardError => e
109
+ Log.debug { e.message }
110
+ raise "Invalid Policy file - '#{policy_file}'"
111
+ end
112
+
113
+ Log.debug { "Successfully loaded policy file - #{policy_file}"}
114
+
115
+ policy
116
+ end
117
+ end
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: probium
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Pieter Loubser
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rainbow
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.2.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.2.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: puppet
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.3'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 4.3.2
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '4.3'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 4.3.2
53
+ description: A CLI tool that uses Puppet resources to validate YAML or JSON policies.
54
+ email: ploubser@gmail.com
55
+ executables:
56
+ - probium
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - bin/probium
61
+ - lib/command_line.rb
62
+ - lib/extensions.rb
63
+ - lib/extensions/port.rb
64
+ - lib/log.rb
65
+ - lib/policy.rb
66
+ - lib/resource.rb
67
+ - lib/result_viewer.rb
68
+ - lib/rule.rb
69
+ - lib/runner.rb
70
+ homepage: https://github.com/ploubser/probium
71
+ licenses:
72
+ - Apache-2.0
73
+ metadata: {}
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 2.6.8
91
+ signing_key:
92
+ specification_version: 4
93
+ summary: CLI policy checking tool
94
+ test_files: []