class InspecPlugins::Chef::Input
Constants
- VALID_PATTERNS
Attributes
Public Instance Methods
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
# File lib/inspec-chef/input.rb, line 22 def inspec_config @inspec_config ||= Inspec::Config.cached end
# File lib/inspec-chef/input.rb, line 26 def logger @logger ||= Inspec::Log end
Private Instance Methods
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
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
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
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
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
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
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
Low-level Chef
search expression
# File lib/inspec-chef/input.rb, line 189 def get_search(index, expression) chef_server.search.query(index, expression, rows: 1).rows.first end
Check if this is called from within TestKitchen
# File lib/inspec-chef/input.rb, line 67 def inside_testkitchen? !! defined?(::Kitchen) end
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
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
Return provisioner config
# File lib/inspec-chef/input.rb, line 127 def kitchen_provisioner_config kitchen.provisioner.send(:provided_config) end
Return verifier config
# File lib/inspec-chef/input.rb, line 132 def kitchen_verifier_config kitchen.verifier.send(:provided_config) end
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 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
Get plugin specific configuration
# File lib/inspec-chef/input.rb, line 137 def plugin_conf inspec_config.fetch_plugin_config("inspec-chef") end
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
Return if connection is established
# File lib/inspec-chef/input.rb, line 184 def server_connected? ! chef_server.nil? end
Deeply stringify keys of Array/Hash
# File lib/inspec-chef/input.rb, line 112 def stringify(result) JSON.parse(JSON.dump(result)) end
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