Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
inspec / lib / resources / nginx_conf.rb
Size: Mime:
# encoding: utf-8

require 'utils/nginx_parser'
require 'utils/find_files'
require 'utils/file_reader'
require 'forwardable'

# STABILITY: Experimental
# This resouce needs a proper interace to the underlying data, which is currently missing.
# Until it is added, we will keep it experimental.
#
# TODO: Support it on Windows. To do so, we need to recognize the base os and how
# it combines the file path. Calling `File.join` or similar methods may lead to errors
# when running remotely.
module Inspec::Resources
  class NginxConf < Inspec.resource(1)
    name 'nginx_conf'
    supports platform: 'unix'
    desc 'Use the nginx_conf InSpec resource to test configuration data '\
         'for the NginX web server located in /etc/nginx/nginx.conf on '\
         'Linux and UNIX platforms.'
    example "
      describe nginx_conf.params ...
      describe nginx_conf('/path/to/my/nginx.conf').params ...
    "

    extend Forwardable

    include FindFiles
    include FileReader

    attr_reader :contents

    def initialize(conf_path = nil)
      @conf_path = conf_path || '/etc/nginx/nginx.conf'
      @contents = {}
      return skip_resource 'The `nginx_conf` resource is currently not supported on Windows.' if inspec.os.windows?
      read_content(@conf_path)
    end

    def params
      @params ||= parse_nginx(@conf_path)
    rescue StandardError => e
      skip_resource e.message
      @params = {}
    end

    def http
      NginxConfHttp.new(params['http'], self)
    end

    def_delegators :http, :servers, :locations

    def to_s
      "nginx_conf #{@conf_path}"
    end

    private

    def read_content(path)
      return @contents[path] if @contents.key?(path)
      @contents[path] = read_file_content(path, allow_empty: true)
    end

    def parse_nginx(path)
      return nil if inspec.os.windows?
      content = read_content(path)
      data = NginxConfig.parse(content)
      resolve_references(data, File.dirname(path))
    rescue StandardError => _
      raise "Cannot parse NginX config in #{path}."
    end

    # Cycle through the complete parsed data structure and try to find any
    # calls to `include`. In NginX, this is used to embed data from other
    # files into the current data structure.
    #
    # The method steps through the object structure that is passed in to
    # find any calls to 'include' and returns the object structure with the
    # included data merged in.
    #
    # @param data [Hash] data structure from NginxConfig.parse
    # @param rel_path [String] the relative path from which this config is read
    # @return [Hash] data structure with references included
    def resolve_references(data, rel_path)
      # Walk through all array entries to find more references
      return data.map { |x| resolve_references(x, rel_path) } if data.is_a?(Array)

      # Return any data that we cannot step into to find more `include` calls
      return data unless data.is_a?(Hash)

      # Any call to `include` gets its data read, parsed, and merged back
      # into the current data structure
      if data.key?('include')
        data.delete('include').flatten
            .map { |x| File.expand_path(x, rel_path) }
            .map { |x| find_files(x) }.flatten
            .map { |path| parse_nginx(path) }
            .each { |conf| merge_config!(data, conf) }
      end

      # Walk through the remaining hash fields to find more references
      Hash[data.map { |k, v| [k, resolve_references(v, rel_path)] }]
    end

    # Deep merge fields from NginxConfig.parse.
    # A regular merge would overwrite values so a deep merge is needed.
    # @param data [Hash] data structure from NginxConfig.parse
    # @param conf [Hash] data structure to be deep merged into data
    # @return [Hash] data structure with conf and data deep merged
    def merge_config!(data, conf)
      # Catch edge-cases
      return if data.nil? || conf.nil?
      # Step through all conf items and create combined return value
      data.merge!(conf) do |_, v1, v2|
        if v1.is_a?(Array) && v2.is_a?(Array)
          # If both the data field and the conf field are arrays, then combine them
          v1 + v2
        elsif v1.is_a?(Hash) && v2.is_a?(Hash)
          # If both the data field and the conf field are maps, then deep merge them
          merge_config!(v1, v2)
        else
          # All other cases, just use the new value (regular merge behavior)
          v2
        end
      end
    end
  end

  class NginxConfHttp
    attr_reader :entries
    def initialize(params, parent)
      @parent = parent
      @entries = (params || []).map { |x| NginxConfHttpEntry.new(x, parent) }
    end

    def servers
      @entries.map(&:servers).flatten
    end

    def locations
      servers.map(&:locations).flatten
    end

    def to_s
      @parent.to_s + ', http entries'
    end
    alias inspect to_s
  end

  class NginxConfHttpEntry
    attr_reader :params, :parent
    def initialize(params, parent)
      @params = params || {}
      @parent = parent
    end

    filter = FilterTable.create
    filter.register_column(:servers, field: 'server')
          .install_filter_methods_on_resource(self, :server_table)

    def locations
      servers.map(&:locations).flatten
    end

    def to_s
      @parent.to_s + ', http entry'
    end
    alias inspect to_s

    private

    def server_table
      @server_table ||= (params['server'] || []).map { |x| { 'server' => NginxConfServer.new(x, self) } }
    end
  end

  class NginxConfServer
    attr_reader :params, :parent
    def initialize(params, parent)
      @parent = parent
      @params = params || {}
    end

    filter = FilterTable.create
    filter.register_column(:locations, field: 'location')
          .install_filter_methods_on_resource(self, :location_table)

    def to_s
      server = ''
      name = Array(params['server_name']).flatten.first
      unless name.nil?
        server += name
        listen = Array(params['listen']).flatten.first
        server += ":#{listen}" unless listen.nil?
      end

      # go two levels up: 1. to the http entry and 2. to the root nginx conf
      @parent.parent.to_s + ", server #{server}"
    end
    alias inspect to_s

    private

    def location_table
      @location_table ||= (params['location'] || []).map { |x| { 'location' => NginxConfLocation.new(x, self) } }
    end
  end

  class NginxConfLocation
    attr_reader :params, :parent
    def initialize(params, parent)
      @parent = parent
      @params = params || {}
    end

    def to_s
      location = Array(params['_']).join(' ')
      # go three levels up: 1. to the server entry, 2. http entry and 3. to the root nginx conf
      @parent.parent.parent.to_s + ", location #{location.inspect}"
    end
    alias inspect to_s
  end
end