class Simp::BeakerHelpers::Inspec

Helpers for working with Inspec

Attributes

deps_root[R]
profile[R]
profile_dir[R]

Public Class Methods

enable_repo_on(suts) click to toggle source
# File lib/simp/beaker_helpers/inspec.rb, line 13
def self.enable_repo_on(suts)
  parallel = (ENV['BEAKER_SIMP_parallel'] == 'yes')
  block_on(suts, :run_in_parallel => parallel) do |sut|
    repo_manifest = create_yum_resource(
      'chef-current',
      {
        :baseurl => "https://packages.chef.io/repos/yum/current/el/#{fact_on(sut,'os.release.major')}/$basearch",
        :gpgkeys => ['https://packages.chef.io/chef.asc']
      }
    )

    apply_manifest_on(sut, repo_manifest, :catch_failures => true)
  end
end
new(sut, profile) click to toggle source

Create a new Inspec helper for the specified host against the specified profile

@param sut

The SUT against which to run

@param profile

The name of the profile against which to run
# File lib/simp/beaker_helpers/inspec.rb, line 36
def initialize(sut, profile)
  @inspec_version = ENV['BEAKER_inspec_version'] || 'latest'

  @sut = sut

  @sut.install_package('git')

  if @inspec_version == 'latest'
    @sut.install_package('inspec')
  else
    @sut.install_package("inspec-#{@inspec_version}")
  end

  os = fact_on(@sut, 'operatingsystem')
  os_rel = fact_on(@sut, 'operatingsystemmajrelease')

  @profile = "#{os}-#{os_rel}-#{profile}"
  @profile_dir = '/tmp/inspec/inspec_profiles'
  @deps_root = '/tmp/inspec'

  @test_dir = @profile_dir + "/#{@profile}"

  sut.mkdir_p(@profile_dir)

  output_dir = File.absolute_path('sec_results/inspec')

  unless File.directory?(output_dir)
    FileUtils.mkdir_p(output_dir)
  end

  local_profile = File.join(fixtures_path, 'inspec_profiles', %(#{os}-#{os_rel}-#{profile}))
  local_deps = File.join(fixtures_path, 'inspec_deps')

  @result_file = File.join(output_dir, "#{@sut.hostname}-inspec-#{Time.now.to_i}")

  copy_to(@sut, local_profile, @profile_dir)

  if File.exist?(local_deps)
    copy_to(@sut, local_deps, @deps_root)
  end

  # The results of the inspec scan in Hash form
  @results = {}
end
process_inspec_results(results) click to toggle source

Process the results of an InSpec run

@return [Hash] A Hash of statistics and a formatted report

# File lib/simp/beaker_helpers/inspec.rb, line 158
def self.process_inspec_results(results)
  require 'highline'

  HighLine.colorize_strings

  stats = {
    # Legacy metrics counters for backwards compatibility
    :failed     => 0,
    :passed     => 0,
    :skipped    => 0,
    :overridden => 0,
    # End legacy stuff
    :global   => {
      :failed     => [],
      :passed     => [],
      :skipped    => [],
      :overridden => []
    },
    :score    => 0,
    :report   => nil,
    :profiles => {}
  }

  if results.is_a?(String)
    if File.readable?(results)
      profiles = JSON.load(File.read(results))['profiles']
    else
      fail("Error: Could not read results file at #{results}")
    end
  elsif results.is_a?(Hash)
    profiles = results['profiles']
  else
    fail("Error: first argument must be a String path to a file or a Hash")
  end

  if !profiles || profiles.empty?
    fail("Error: Could not find 'profiles' in the passed results")
  end

  profiles.each do |profile|
    profile_name = profile['name']

    next unless profile_name

    stats[:profiles][profile_name] = {
      :controls => {}
    }

    profile['controls'].each do |control|
      title = control['title']

      next unless title

      base_title = title.scan(/.{1,60}\W|.{1,60}/).map(&:strip).join("\n           ")

      if control['results'] && (control['results'].size > 1)
        control['results'].each do |result|
          control_title = " => { #{result['code_desc']} }"

          full_title = title + control_title
          formatted_title = base_title + control_title

          stats[:profiles][profile_name][:controls][full_title] = {}

          stats[:profiles][profile_name][:controls][full_title][:formatted_title] = formatted_title

          if result['status'] =~ /^fail/
            status = :failed
            color = 'red'
          else
            status = :passed
            color = 'green'
          end

          stats[:global][status] << formatted_title.color

          stats[:profiles][profile_name][:controls][full_title][:status] = status
          stats[:profiles][profile_name][:controls][full_title][:source] = control['source_location']['ref']
        end
      else
        formatted_title = base_title

        stats[:profiles][profile_name][:controls][title] = {}

        stats[:profiles][profile_name][:controls][title][:formatted_title] = formatted_title

        if control['results'] && !control['results'].empty?
          status = :passed
          color = 'green'

          control['results'].each do |result|
            if results['status'] =~ /^fail/
              status = :failed
              color = 'red'
            end
          end

        else
          status = :skipped
        end

        stats[:global][status] << formatted_title.color

        stats[:profiles][profile_name][:controls][title][:status] = status
        stats[:profiles][profile_name][:controls][title][:source] = control['source_location']['ref']
      end
    end
  end

  valid_checks = stats[:global][:failed] + stats[:global][:passed]
  stats[:global][:skipped].dup.each do |skipped|
    if valid_checks.include?(skipped)
      stats[:global][:overridden] << skipped
      stats[:global][:skipped].delete(skipped)
    end
  end

  status_colors = {
    :failed     => 'red',
    :passed     => 'green',
    :skipped    => 'yellow',
    :overridden => 'white'
  }

  report = []

  stats[:profiles].keys.each do |profile|
    report << "Profile: #{profile}"

    stats[:profiles][profile][:controls].each do |control|
      control_info = control.last

      report << "\n  Control: #{control_info[:formatted_title]}"

      if control_info[:status] == :skipped && stats[:global][:overridden].include?(control.first)
        control_info[:status] = :overridden
      end

      report << "    Status: #{control_info[:status].to_s.send(status_colors[control_info[:status]])}"
      report << "    File: #{control_info[:source]}" if control_info[:source]
    end

    report << "\n"
  end

  num_passed     = stats[:global][:passed].count
  num_failed     = stats[:global][:failed].count
  num_skipped    = stats[:global][:skipped].count
  num_overridden = stats[:global][:overridden].count

  # Backwards compat values
  stats[:passed]     = num_passed
  stats[:failed]     = num_failed
  stats[:skipped]    = num_skipped
  stats[:overridden] = num_overridden

  report << "Statistics:"
  report << "  * Passed: #{num_passed.to_s.green}"
  report << "  * Failed: #{num_failed.to_s.red}"
  report << "  * Skipped: #{num_skipped.to_s.yellow}"

  score = 0
  if (stats[:global][:passed].count + stats[:global][:failed].count) > 0
    score = ((stats[:global][:passed].count.to_f/(stats[:global][:passed].count + stats[:global][:failed].count)) * 100.0).round(0)
  end

  report << "\n Score: #{score}%"

  stats[:score] = score
  stats[:report] = report.join("\n")

  return stats
end

Public Instance Methods

process_inspec_results() click to toggle source
# File lib/simp/beaker_helpers/inspec.rb, line 150
def process_inspec_results
  self.class.process_inspec_results(@results)
end
run() click to toggle source

Run the inspec tests and record the results

# File lib/simp/beaker_helpers/inspec.rb, line 82
def run
  sut_inspec_results = '/tmp/inspec_results.json'

  inspec_version = Gem::Version.new(on(@sut, 'inspec --version').output.lines.first.strip)

  # See: https://github.com/inspec/inspec/pull/3935
  if inspec_version <= Gem::Version.new('3.9.0')
    inspec_cmd = "inspec exec '#{@test_dir}' --reporter json > #{sut_inspec_results}"
  else
    inspec_cmd = "inspec exec '#{@test_dir}' --chef-license accept --reporter json > #{sut_inspec_results}"
  end

  result = on(@sut, inspec_cmd, :accept_all_exit_codes => true)

  tmpdir = Dir.mktmpdir
  begin
    Dir.chdir(tmpdir) do
      scp_from(@sut, sut_inspec_results, '.')

      local_inspec_results = File.basename(sut_inspec_results)

      if File.exist?(local_inspec_results)
        begin
          # The output is occasionally broken from past experience. Need to
          # fetch the line that actually looks like JSON
          inspec_json = File.read(local_inspec_results).lines.find do |line|
            line.strip!

            line.start_with?('{') && line.end_with?('}')
          end

          @results = JSON.load(inspec_json) if inspec_json
        rescue JSON::ParserError, JSON::GeneratorError
          @results = nil
        end
      end
    end
  ensure
    FileUtils.remove_entry_secure tmpdir
  end

  if @results.nil? || @results.empty?
    File.open(@result_file + '.err', 'w') do |fh|
      fh.puts(result.stderr.strip)
    end

    err_msg = ["Error running inspec command #{inspec_cmd}"]
    err_msg << "Error captured in #{@result_file}" + '.err'

    fail(err_msg.join("\n"))
  end
end
write_report(report) click to toggle source

Output the report

@param report

The inspec results Hash
# File lib/simp/beaker_helpers/inspec.rb, line 140
def write_report(report)
  File.open(@result_file + '.json', 'w') do |fh|
    fh.puts(JSON.pretty_generate(@results))
  end

  File.open(@result_file + '.report', 'w') do |fh|
    fh.puts(report[:report].uncolor)
  end
end