require "json" unless defined?(JSON) module Inspec::Plugin::V2 # Represents the plugin config file on disk. class ConfigFile include Enumerable attr_reader :path def initialize(path = nil) @path = path || self.class.default_path @data = blank_structure read_and_validate_file if File.exist?(@path) end # Returns the defaut path for a config file. # This respects ENV['INSPEC_CONFIG_DIR']. def self.default_path File.join(Inspec.config_dir, "plugins.json") end # Implement Enumerable. All Enumerable methds act # on the plugins list, and yield Hashes that represent # an entry. def each(&block) @data[:plugins].each(&block) end # Look for a plugin by name. def plugin_by_name(name) detect { |entry| entry[:name] == name.to_sym } end # Check for a plugin def existing_entry?(name) !plugin_by_name(name).nil? end # Add an entry with full validation. def add_entry(proposed_entry) unless proposed_entry.keys.all? { |field| field.is_a? Symbol } raise Inspec::Plugin::V2::ConfigError, "All keys to ConfigFile#add_entry must be symbols" end validate_entry(proposed_entry) if existing_entry?(proposed_entry[:name]) raise Inspec::Plugin::V2::ConfigError, "Duplicate plugin name in call to ConfigFile#add_entry: '#{proposed_entry[:name]}'" end @data[:plugins] << proposed_entry end # Removes an entry specified by plugin name. def remove_entry(name) unless existing_entry?(name) raise Inspec::Plugin::V2::ConfigError, "No such entry with plugin name '#{name}'" end @data[:plugins].delete_if { |entry| entry[:name] == name.to_sym } end # Save the file to disk as a JSON structure at the path. def save dir = File.dirname(path) FileUtils.mkdir_p(dir) File.write(path, JSON.pretty_generate(@data)) end private def blank_structure { plugins_config_version: "1.0.0", plugins: [], } end def read_and_validate_file @data = JSON.parse(File.read(path), symbolize_names: true) validate_file rescue JSON::ParserError => e raise Inspec::Plugin::V2::ConfigError, "Failed to load plugins JSON configuration from #{path}:\n#{e}" end def validate_file # rubocop: disable Metrics/AbcSize unless @data[:plugins_config_version] raise Inspec::Plugin::V2::ConfigError, "Missing 'plugins_config_version' entry at #{path} - currently support versions: 1.0.0" end unless @data[:plugins_config_version] == "1.0.0" raise Inspec::Plugin::V2::ConfigError, "Unsupported plugins.json file version #{@data[:plugins_config_version]} at #{path} - currently support versions: 1.0.0" end plugin_entries = @data[:plugins] if plugin_entries.nil? raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file at #{path} - missing top-level key named 'plugins', whose value should be an array" end unless plugin_entries.is_a?(Array) raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file at #{path} - top-level key named 'plugins' should be an array" end plugin_entries.each_with_index do |plugin_entry, idx| begin validate_entry(plugin_entry) rescue Inspec::Plugin::V2::ConfigError => ex # append some context to the message raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - " + ex.message + " at index #{idx}" end # Check for duplicates plugin_entries.each_with_index do |other_entry, other_idx| next if idx == other_idx next unless other_entry.is_a? Hash # We'll catch that invalid entry later next if plugin_entry[:name] != other_entry[:name] indices = [idx, other_idx].sort raise Inspec::Plugin::V2::ConfigError, "Malformed plugins.json file - duplicate plugin entry '#{plugin_entry[:name]}' detected at index #{indices[0]} and #{indices[1]}" end end end def validate_entry(plugin_entry) unless plugin_entry.is_a? Hash raise Inspec::Plugin::V2::ConfigError, "each 'plugins' entry should be a Hash / JSON object" end unless plugin_entry.key? :name raise Inspec::Plugin::V2::ConfigError, "'plugins' entry missing 'name' field" end # Symbolize the name. plugin_entry[:name] = plugin_entry[:name].to_sym if plugin_entry.key? :installation_type seen_type = plugin_entry[:installation_type] unless %i{gem path}.include? seen_type.to_sym raise Inspec::Plugin::V2::ConfigError, "'plugins' entry with unrecognized installation_type (must be one of 'gem' or 'path')" end plugin_entry[:installation_type] = seen_type.to_sym if plugin_entry[:installation_type] == :path && !plugin_entry.key?(:installation_path) raise Inspec::Plugin::V2::ConfigError, "'plugins' entry with a 'path' installation_type missing installation path" end end end end end