# encoding: utf-8 # author: Christoph Hartmann # author: Dominik Richter require 'utils/object_traversal' module Inspec::Resources class JsonConfig < Inspec.resource(1) name 'json' desc 'Use the json InSpec audit resource to test data in a JSON file.' example " describe json('policyfile.lock.json') do its(['cookbook_locks','omnibus','version']) { should eq('2.2.0') } end describe json({ command: 'retrieve_data.py --json' }) do its('state') { should eq('open') } end describe json({ content: '{\"item1\": { \"status\": \"available\" } }' }) do its(['item1', 'status']) { should cmp 'available' } end " include ObjectTraverser # make params readable attr_reader :params, :raw_content def initialize(opts) # pre-initialize @params to an empty hash. In the event that reading/parsing the data # throws an exception, this allows the resource to still be called outside of a # describe/test and not throw errors when a caller attempts to fetch a value from the params. @params = {} # load the raw content from the source, and then parse it @raw_content = load_raw_content(opts) @params = parse(@raw_content) end # Shorthand to retrieve a parameter name via `#its`. # Example: describe json('file') { its('paramX') { should eq 'Y' } } # # @param [String] name name of the field to retrieve # @return [Object] the value stored at this position def method_missing(*keys) # catch bahavior of rspec its implementation # @see https://github.com/rspec/rspec-its/blob/master/lib/rspec/its.rb#L110 keys.shift if keys.is_a?(Array) && keys[0] == :[] value(keys) end def value(key) # uses ObjectTraverser.extract_value to walk the hash looking for the key, # which may be an Array of keys for a nested Hash. extract_value(key, params) end def to_s "#{resource_base_name} #{@resource_name_supplement || 'content'}" end private def parse(content) require 'json' JSON.parse(content) rescue => e raise Inspec::Exceptions::ResourceFailed, "Unable to parse JSON: #{e.message}" end def load_raw_content(opts) # if the opts isn't a hash, we assume it's a path to a file unless opts.is_a?(Hash) @resource_name_supplement = opts return load_raw_from_file(opts) end if opts.key?(:command) @resource_name_supplement = "from command: #{opts[:command]}" load_raw_from_command(opts[:command]) elsif opts.key?(:content) opts[:content] else raise Inspec::Exceptions::ResourceFailed, 'No JSON content; must specify a file, command, or raw JSON content' end end def load_raw_from_file(path) file = inspec.file(path) # these are currently ResourceSkipped to maintain consistency with the resource # pre-refactor (which used skip_resource). These should likely be changed to # ResourceFailed during a major version bump. raise Inspec::Exceptions::ResourceSkipped, "No such file: #{path}" unless file.file? raise Inspec::Exceptions::ResourceSkipped, "File #{path} is empty or is not readable by current user" if file.content.nil? || file.content.empty? file.content end def load_raw_from_command(command) command_output = inspec.command(command).stdout raise Inspec::Exceptions::ResourceSkipped, "No output from command: #{command}" if command_output.nil? || command_output.empty? command_output end # for resources the subclass JsonConfig, this allows specification of the resource # base name in each subclass so we can build a good to_s method def resource_base_name 'JSON' end end end