class InspecPlugins::Chef::Input

Constants

VALID_PATTERNS

Attributes

chef_server[R]
inspec_config[W]

Dependency Injection

logger[W]

Dependency Injection

Public Instance Methods

fetch(_profile_name, input_uri) click to toggle source

Fetch method used for Input plugins

# File lib/inspec-chef/input.rb, line 34
def fetch(_profile_name, input_uri)
  logger.trace format("Inspec-Chef received query for input %<uri>s", uri: input_uri)
  return nil unless valid_plugin_input?(input_uri)

  logger.debug format("Inspec-Chef input schema detected")

  connect_to_chef_server

  input = parse_input(input_uri)
  if input[:type] == :databag
    data = get_databag_item(input[:object], input[:item])
  elsif input[:type] == :node && input[:item] == "attributes"
    # Search Chef node name, if no host given explicitly
    input[:object] = get_clientname(scan_target) unless input[:object] || inside_testkitchen?

    data = get_attributes(input[:object])
  end

  # Quote components to allow "-" as part of search query.
  # @see https://github.com/jmespath/jmespath.rb/issues/12
  expression = input[:query].map { |component| '"' + component + '"' }.join(".")
  result = JMESPath.search(expression, data)
  raise format("Could not resolve value for %s, check if databag/item or attribute exist", input_uri) unless result

  stringify(result)
end
inspec_config() click to toggle source
# File lib/inspec-chef/input.rb, line 22
def inspec_config
  @inspec_config ||= Inspec::Config.cached
end
logger() click to toggle source
# File lib/inspec-chef/input.rb, line 26
def logger
  @logger ||= Inspec::Log
end

Private Instance Methods

connect_to_chef_server() click to toggle source

Establish a Chef Server connection

# File lib/inspec-chef/input.rb, line 155
def connect_to_chef_server
  # From within TestKitchen we need no Chef Server connection
  if inside_testkitchen?
    logger.info "Running from TestKitchen, using static settings instead of Chef Server"

  # Only connect once
  elsif !server_connected?
    @plugin_conf = inspec_config.fetch_plugin_config("inspec-chef")

    chef_endpoint = fetch_plugin_setting("endpoint")
    chef_client   = fetch_plugin_setting("client")
    chef_api_key  = fetch_plugin_setting("key")

    unless chef_endpoint && chef_client && chef_api_key
      raise "ERROR: Plugin inspec-chef needs configuration of chef endpoint, client name and api key."
    end

    # @todo: DI this
    @chef_server ||= ChefAPI::Connection.new(
      endpoint: chef_endpoint,
      client:   chef_client,
      key:      chef_api_key
    )

    logger.debug format("Connected to %s as client %s", chef_endpoint, chef_client)
  end
end
data_bags_path() click to toggle source

Calculate lookup path for databags within TestKitchen

# File lib/inspec-chef/input.rb, line 213
def data_bags_path
  # These can occur on suite-level, provisioner-level, verifier or at the default location
  kitchen_provisioner_config[:data_bags_path] || kitchen_verifier_config[:data_bags_path] || File.join('test', 'data_bags')
end
fetch_plugin_setting(setting_name, default = nil) click to toggle source

Get plugin setting via environment, config file or default

# File lib/inspec-chef/input.rb, line 142
def fetch_plugin_setting(setting_name, default = nil)
  env_var_name = "INSPEC_CHEF_#{setting_name.upcase}"
  config_name = "chef_api_#{setting_name.downcase}"
  ENV[env_var_name] || plugin_conf[config_name] || default
end
fqdn?(ip_or_name) click to toggle source

Check if this is an FQDN

# File lib/inspec-chef/input.rb, line 80
def fqdn?(ip_or_name)
  # If it is not an IP but contains a Dot, it is an FQDN
  !ip?(ip_or_name) && ip_or_name.include?(".")
end
get_attributes(node) click to toggle source

Retrieve attributes of a node

# File lib/inspec-chef/input.rb, line 219
def get_attributes(node)
  unless inside_testkitchen?
    data = get_search(:node, "name:#{node}")

    merge_attributes(data)
  else
    kitchen_provisioner_config[:attributes]
  end
end
get_clientname(ip_or_name) click to toggle source

Try to look up Chef Client name by the address requested

# File lib/inspec-chef/input.rb, line 230
def get_clientname(ip_or_name)
  query = "hostname:%<address>s"
  query = "ipaddress:%<address>s" if ip?(ip_or_name)
  query = "fqdn:%<address>s" if fqdn?(ip_or_name)
  result = get_search(:node, format(query, address: ip_or_name))
  logger.debug format("Automatic lookup of node name (IPv4 or hostname) returned: %s", result&.fetch("name") || "(nothing)")

  # Try EC2 lookup, if nothing found (assuming public IP)
  unless result
    query = "ec2_public_ipv4:%<address>s OR ec2_public_hostname:%<address>s"
    result = get_search(:node, format(query, address: ip_or_name))
    logger.debug format("Automatic lookup of node name (EC2 public IPv4 or hostname) returned: %s", result&.fetch("name"))
  end

  # This will fail for cases like trying to connect to IPv6, so it will need extension in the future

  result&.fetch("name") || raise(format("Unable too lookup remote Chef client name from %s", ip_or_name))
end
get_databag_item(databag, item) click to toggle source

Retrieve a Databag item from Chef Server

# File lib/inspec-chef/input.rb, line 194
def get_databag_item(databag, item)
  unless inside_testkitchen?
    unless chef_server.data_bags.any? { |k| k.name == databag }
      raise format('Databag "%s" not found on Chef Infra Server', databag)
    end

    chef_server.data_bag_item.fetch(item, bag: databag).data
  else
    filename = File.join(data_bags_path, databag, item + ".json")

    begin
      return JSON.load(File.read(filename))
    rescue
      raise format("Error accessing databag file %s, check TestKitchen configuration", filename)
    end
  end
end
inside_testkitchen?() click to toggle source

Check if this is called from within TestKitchen

# File lib/inspec-chef/input.rb, line 67
def inside_testkitchen?
  !! defined?(::Kitchen)
end
ip?(ip_or_name) click to toggle source

Check if this is an IP

# File lib/inspec-chef/input.rb, line 72
def ip?(ip_or_name)
  # Get address always returns an IP, so if nothing changes this was one
  Resolv.getaddress(ip_or_name) == ip_or_name
rescue Resolv::ResolvError
  false
end
kitchen() click to toggle source

Access to kitchen data

# File lib/inspec-chef/input.rb, line 121
def kitchen
  require "binding_of_caller"
  binding.callers.find { |b| b.frame_description == "verify" }.receiver
end
kitchen_provisioner_config() click to toggle source

Return provisioner config

# File lib/inspec-chef/input.rb, line 127
def kitchen_provisioner_config
  kitchen.provisioner.send(:provided_config)
end
kitchen_verifier_config() click to toggle source

Return verifier config

# File lib/inspec-chef/input.rb, line 132
def kitchen_verifier_config
  kitchen.verifier.send(:provided_config)
end
merge_attributes(data) click to toggle source

Merge attributes in hierarchy like Chef

# File lib/inspec-chef/input.rb, line 86
def merge_attributes(data)
  data.fetch("default", {})
    .merge(data.fetch("normal", {}))
    .merge(data.fetch("override", {}))
    .merge(data.fetch("automatic", {}))
end
parse_input(input_uri) click to toggle source

Parse InSpec input name into Databag, Item and search query

# File lib/inspec-chef/input.rb, line 99
def parse_input(input_uri)
  uri = URI(input_uri)
  item, *components = uri.path.slice(1..-1).split("/")

  {
    type: uri.scheme.to_sym,
    object: uri.host,
    item: item,
    query: components,
  }
end
plugin_conf() click to toggle source

Get plugin specific configuration

# File lib/inspec-chef/input.rb, line 137
def plugin_conf
  inspec_config.fetch_plugin_config("inspec-chef")
end
scan_target() click to toggle source

Get remote address for this scan from InSpec

# File lib/inspec-chef/input.rb, line 149
def scan_target
  target = inspec_config.final_options["target"]
  URI.parse(target)&.host
end
server_connected?() click to toggle source

Return if connection is established

# File lib/inspec-chef/input.rb, line 184
def server_connected?
  ! chef_server.nil?
end
stringify(result) click to toggle source

Deeply stringify keys of Array/Hash

# File lib/inspec-chef/input.rb, line 112
def stringify(result)
  JSON.parse(JSON.dump(result))
end
valid_plugin_input?(input) click to toggle source

Verify if input is valid for this plugin

# File lib/inspec-chef/input.rb, line 94
def valid_plugin_input?(input)
  VALID_PATTERNS.any? { |regex| regex.match? input }
end