class Confiner::Cli

Attributes

action[RW]
parser[R]
plugin_arguments[R]
plugins[RW]

Public Class Methods

new(*options) click to toggle source
# File lib/confiner/cli.rb, line 13
    def initialize(*options)
      @plugin_arguments = [] # arguments to be passed to the plugin(s)
      @rules_files = [] # which files were loaded
      @rules = []
      @plugin_options = { debug: false, dry_run: false }

      # default logging to standard out
      Logger.log_to = $stdout

      if options.include?('--')
        @plugin_arguments = options[options.index('--')..]
        options = options[0..options.index('--')]
      end

      @parser = OptionParser.new do |opts|
        opts.banner = <<~USAGE
          Usage: #{$PROGRAM_NAME} [options] [-- plugin_options]

          Examples:
            Run all rules within .confiner directory outputting to a log file:
              #{$PROGRAM_NAME} -o confiner.log
            Run a specific rule file overriding specific arguments for plugins:
              #{$PROGRAM_NAME} -r rules.yml -- --arg1=foo --arg2=bar
            Run all all_rules within a specific directory:
              #{$PROGRAM_NAME} -r ./all_rules

          Options:
        USAGE

        opts.on('-h', '--help', 'Show the help') do
          puts opts
          exit 0
        end

        opts.on('-r RULES', '--rules RULES', 'Path to rule yaml file or directory of rules') do |rule|
          rule.strip! # strip any trailing/leading spacess
          rules = File.expand_path(rule)

          raise "Rule file or directory `#{rules}` does not exist" unless File.exist?(rules)

          @rules = if File.directory?(rules)
                     Dir[File.join(rules, '**', '*.yml')].each_with_object([]) do |definitions, all_rules|
                       all_rules << load_yaml(definitions)
                     end
                   else
                     [load_yaml(rules)]
                   end
        end

        opts.on('-v', '--version', 'Show the version') do
          $stdout.puts "#{$PROGRAM_NAME} version #{VERSION}"
          exit(0)
        end

        opts.on('--dry-run', 'Dry run') do
          @plugin_options[:dry_run] = true
        end

        opts.on('--debug', 'Toggle debug mode') do
          @plugin_options[:debug] = true
        end

        opts.on('-o OUTPUT', '--output-to OUTPUT', 'File to output the log to') do |output_to|
          Logger.log_to = output_to
        end
      end

      @parser.parse!(options)

      log :confiner, 'Program Start'

      if @rules.empty?
        # load any and all rules within .confiner
        raise 'No rules to run. Are you missing a .confiner directory or -r argument?' unless Dir.exist?('.confiner')

        @rules = Dir[File.join('.confiner', '**', '*.yml')].each_with_object([]) do |definitions, rules|
          rules << load_yaml(definitions)
        end
      end

      log :rules, 'Using rule files:'
      @rules_files.each do |file|
        log :loaded, file, 2
      end
    end
run(*argv) click to toggle source
# File lib/confiner/cli.rb, line 110
def self.run(*argv)
  new(*argv).run
end

Public Instance Methods

run() click to toggle source

Run the confiner

# File lib/confiner/cli.rb, line 100
def run
  @rules.each do |rules|
    rules.each do |rule|
      process_rule(rule)
    end
  end

  log :confiner, 'Done'
end

Private Instance Methods

load_yaml(file) click to toggle source

Load yaml file and validate all rules

# File lib/confiner/cli.rb, line 175
def load_yaml(file)
  raise 'File is not a YAML file' unless File.extname(file).match(/yml|yaml/i)

  @rules_files << file

  validate_rules(YAML.load_file(file), file)
end
process_rule(rule) click to toggle source

Process a singular rule @param [Hash] rule

# File lib/confiner/cli.rb, line 118
def process_rule(rule)
  log :rule, rule.keys.map { |k| "\t#{k}=#{rule[k]}" }.join(',')

  rule['plugin']['args'] ||= {}
  rule['plugin']['args'].transform_keys!(&:to_sym) # ruby 2.5 compatability

  plugin = Plugins.const_get(translate_plugin_name(rule['plugin']['name'])).new(@plugin_options, **rule['plugin']['args'])

  # perform verification of actions before execution
  rule['actions'].each do |action|
    raise "YAML is invalid. Action `#{action}` does not exist." unless plugin.respond_to?(action)
  end

  # execute each action
  rule['actions'].each do |action|
    plugin.run(action) { |p| p.public_send(action) }
  end
end
translate_plugin_name(plugin_name) click to toggle source

Translate a plugin name from snake_case to PascalCase

# File lib/confiner/cli.rb, line 170
def translate_plugin_name(plugin_name)
  plugin_name.split('_').map(&:capitalize).join
end
validate_rules(rules, file) click to toggle source

Ensure that the rules are well-formed

# File lib/confiner/cli.rb, line 138
def validate_rules(rules, file)
  raise "YAML is invalid. Rules must be an array (from #{file})." unless rules.is_a? Array

  rules.each do |rule|
    # name is required
    raise "YAML is invalid. Rule must have a name. (from #{file})" unless rule['name']

    # actions are required
    raise "YAML is invalid. Rule `#{rule['name']}` must have actions and it must be an Array (from #{file})" unless rule['actions']&.is_a? Array

    # plugin is required and must be well-formed
    raise "YAML is invalid. Rule `#{rule['name']}` must have a plugin and it must have a name (from #{file})" unless rule['plugin'] && rule['plugin']['name']

    # Plugin must exist
    plugin = begin
              Plugins.const_get(translate_plugin_name(rule['plugin']['name']))
            rescue NameError
              raise "YAML is invalid. Rule `#{rule['name']}` does not have plugin `#{rule['plugin']['name']}` (from #{file})"
            end

    # Validate the actions
    rule['actions'].each do |action|
      begin
        plugin.instance_method(action)
      rescue NameError
        raise "YAML is invalid. Rule `#{rule['name']}` plugin `#{rule['plugin']['name']}` has no action `#{action}` (from #{file})"
      end
    end
  end
end