# encoding: utf-8 require 'hashie/mash' require 'utils/database_helpers' require 'htmlentities' require 'rexml/document' require 'csv' module Inspec::Resources # STABILITY: Experimental # This resource needs further testing and refinement # class OracledbSession < Inspec.resource(1) name 'oracledb_session' supports platform: 'unix' supports platform: 'windows' desc 'Use the oracledb_session InSpec resource to test commands against an Oracle database' example " sql = oracledb_session(user: 'my_user', pass: 'password') describe sql.query(\"SELECT UPPER(VALUE) AS VALUE FROM V$PARAMETER WHERE UPPER(NAME)='AUDIT_SYS_OPERATIONS'\").row(0).column('value') do its('value') { should eq 'TRUE' } end " attr_reader :user, :password, :host, :service, :as_os_user, :as_db_role # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity def initialize(opts = {}) @user = opts[:user] @password = opts[:password] || opts[:pass] if opts[:pass] warn '[DEPRECATED] use `password` option to supply password instead of `pass`' end @host = opts[:host] || 'localhost' @port = opts[:port] || '1521' @service = opts[:service] # connection as sysdba stuff return skip_resource "Option 'as_os_user' not available in Windows" if inspec.os.windows? && opts[:as_os_user] @su_user = opts[:as_os_user] @db_role = opts[:as_db_role] # we prefer sqlci although it is way slower than sqlplus, but it understands csv properly @sqlcl_bin = 'sql' unless opts.key?(:sqlplus_bin) # don't use it if user specified sqlplus_bin option @sqlplus_bin = opts[:sqlplus_bin] || 'sqlplus' return fail_resource "Can't run Oracle checks without authentication" if @su_user.nil? && (@user.nil? || @password.nil?) return fail_resource 'You must provide a service name for the session' if @service.nil? end def query(q) escaped_query = q.gsub(/\\/, '\\\\').gsub(/"/, '\\"') # escape tables with $ escaped_query = escaped_query.gsub('$', '\\$') p = nil # use sqlplus if sqlcl is not available if @sqlcl_bin and inspec.command(@sqlcl_bin).exist? bin = @sqlcl_bin opts = "set sqlformat csv\nSET FEEDBACK OFF" p = :parse_csv_result else bin = @sqlplus_bin opts = "SET MARKUP HTML ON\nSET PAGESIZE 32000\nSET FEEDBACK OFF" p = :parse_html_result end query = verify_query(escaped_query) query += ';' unless query.end_with?(';') if @db_role.nil? command = %{#{bin} "#{@user}"/"#{@password}"@#{@host}:#{@port}/#{@service} <', '').gsub('

', '').gsub('
', '') doc = REXML::Document.new result table = doc.elements['table'] hash = [] if !table.nil? rows = table.elements.to_a headers = rows[0].elements.to_a('th').map { |entry| entry.text.strip } rows.delete_at(0) # iterate over each row, first row is header hash = [] if !rows.nil? && !rows.empty? hash = rows.map { |row| res = {} entries = row.elements.to_a('td') # ignore if we have empty entries, oracle is adding th rows in between return nil if entries.empty? headers.each_with_index { |header, index| # we need htmlentities since we do not have nokogiri coder = HTMLEntities.new val = coder.decode(entries[index].text).strip res[header.downcase] = val } Hashie::Mash.new(res) }.compact end end hash end end end