Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
inspec / lib / inspec / shell.rb
Size: Mime:
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann

require 'pry'

module Inspec
  # A pry based shell for inspec. Given a runner (with a configured backend and
  # all that jazz), this shell will produce a pry shell from which you can run
  # inspec/ruby commands that will be run within the context of the runner.
  class Shell
    def initialize(runner)
      @runner = runner
    end

    def start
      # This will hold a single evaluation binding context as opened within
      # the instance_eval context of the anonymous class that the profile
      # context creates to evaluate each individual test file. We want to
      # pretend like we are constantly appending to the same file and want
      # to capture the local variable context from inside said class.
      @ctx_binding = @runner.eval_with_virtual_profile('binding')
      configure_pry
      @ctx_binding.pry
    end

    def configure_pry # rubocop:disable Metrics/AbcSize
      # Delete any before_session, before_eval, and after_eval hooks so we can
      # replace them with our own. Pry 0.10 used to have a single method to clear
      # all hooks, but this was removed in Pry 0.11.
      [:before_session, :before_eval, :after_eval].each do |event|
        Pry.hooks.get_hooks(event).keys.map { |hook| Pry.hooks.delete_hook(event, hook) }
      end

      that = self

      # Add the help command
      Pry::Commands.block_command 'help', 'Show examples' do |resource|
        that.help(resource)
      end

      # configure pry shell prompt
      Pry.config.prompt_name = 'inspec'
      Pry.prompt = [proc { "#{readline_ignore("\e[1m\e[32m")}#{Pry.config.prompt_name}> #{readline_ignore("\e[0m")}" }]

      # Add a help menu as the default intro
      Pry.hooks.add_hook(:before_session, 'inspec_intro') do
        intro
        print_target_info
      end

      # Track the rules currently registered and what their merge count is.
      Pry.hooks.add_hook(:before_eval, 'inspec_before_eval') do
        @runner.reset
      end

      # After pry has evaluated a commanding within the binding context of a
      # test file, register all the rules it discovered.
      Pry.hooks.add_hook(:after_eval, 'inspec_after_eval') do
        @runner.load
        @runner.run_tests if !@runner.all_rules.empty?
      end

      # Don't print out control class inspection when the user uses DSL methods.
      # Instead produce a result of evaluating their control.
      Pry.config.print = proc do |_output_, value, pry|
        next if !@runner.all_rules.empty?
        pry.pager.open do |pager|
          pager.print pry.config.output_prefix
          Pry::ColorPrinter.pp(value, pager, Pry::Terminal.width! - 1)
        end
      end
    end

    def readline_ignore(code)
      "\001#{code}\002"
    end

    def mark(x)
      "\e[1m\e[39m#{x}\e[0m"
    end

    def print_example(example)
      # determine min whitespace that can be removed
      min = nil
      example.lines.each do |line|
        if !line.strip.empty? # ignore empty lines
          line_whitespace = line.length - line.lstrip.length
          min = line_whitespace if min.nil? || line_whitespace < min
        end
      end
      # remove whitespace from each line
      example.gsub(/\n\s{#{min}}/, "\n")
    end

    def intro
      puts 'Welcome to the interactive InSpec Shell'
      puts "To find out how to use it, type: #{mark 'help'}"
      puts
    end

    def print_target_info
      ctx = @runner.backend
      puts <<~EOF
        You are currently running on:

        #{Inspec::BaseCLI.detect(params: ctx.platform.params, indent: 4, color: 39)}
      EOF
    end

    def help(topic = nil)
      if topic.nil?

        puts <<~EOF
          Available commands:

              `[resource]` - run resource on target machine
              `help resources` - show all available resources that can be used as commands
              `help [resource]` - information about a specific resource
              `help matchers` - show information about common matchers
              `exit` - exit the InSpec shell

          You can use resources in this environment to test the target machine. For example:

              command('uname -a').stdout
              file('/proc/cpuinfo').content => "value"

          #{print_target_info}
        EOF
      elsif topic == 'resources'
        resources.sort.each do |resource|
          puts " - #{resource}"
        end
      elsif topic == 'matchers'
        print_matchers_help
      elsif !Inspec::Resource.registry[topic].nil?
        topic_info = Inspec::Resource.registry[topic]
        info = "#{mark 'Name:'} #{topic}\n\n"
        unless topic_info.desc.nil?
          info += "#{mark 'Description:'}\n\n"
          info += "#{topic_info.desc}\n\n"
        end

        unless topic_info.example.nil?
          info += "#{mark 'Example:'}\n"
          info += "#{print_example(topic_info.example)}\n\n"
        end

        info += "#{mark 'Web Reference:'}\n\n"
        info += "https://www.inspec.io/docs/reference/resources/#{topic}\n\n"
        puts info
      else
        puts "The resource #{topic} does not exist. For a list of valid resources, type: help resources"
      end
    end

    def resources
      Inspec::Resource.registry.keys
    end

    def print_matchers_help
      puts <<~EOL
        Matchers are used to compare resource values to expectations. While some
        resources implement their own custom matchers, the following matchers are
        common amongst all resources:

        #{mark 'be'}

        The #{mark 'be'} matcher can be used to compare numeric values.

          its('size') { should be >= 10 }

        #{mark 'cmp'}

        The #{mark 'cmp'} matcher is like #{mark 'eq'} but less restrictive. It will try
        to fit the resource value to the expectation.

        "Protocol" likely returns a string, but cmp will ensure it's a number before
        comparing:

          its('Protocol') { should cmp 2 }
          its('Protocol') { should cmp '2' }

        "users" may return an array, but if it contains only one item, cmp will compare
        it as a string or number as needed:

          its('users') { should cmp 'root' }

        cmp is not case-sensitive:

          its('log_format') { should cmp 'raw' }
          its('log_format') { should cmp 'RAW' }

        #{mark 'eq'}

        The #{mark 'eq'} matcher tests for exact equality of two values. Value type
        (string, number, etc.) is important and must be the same. For a less-restrictive
        comparison matcher, use the #{mark 'cmp'} matcher.

          its('RSAAuthentication') { should_not eq 'no' }

        #{mark 'include'}

        The #{mark 'include'} matcher tests to see if a value is included in a list.

          its('users') { should include 'my_user' }

        #{mark 'match'}

        The #{mark 'match'} matcher can be used to test a string for a match using a
        regular expression.

          its('content') { should_not match /^MyKey:\\s+some value/ }

        For more examples, see: https://www.inspec.io/docs/reference/matchers/

      EOL
    end
  end
end