lib/airbrake-ruby/filters/sql_filter.rb
module Airbrake
module Filters
# SqlFilter filters out sensitive data from {Airbrake::Query}. Sensitive
# data is everything that is not table names or fields (e.g. column values
# and such).
#
# Supports the following SQL dialects:
# * PostgreSQL
# * MySQL
# * SQLite
# * Cassandra
# * Oracle
#
# @api private
# @since v3.2.0
class SqlFilter
# @return [String] the label to replace real values of filtered query
FILTERED = '?'.freeze
# @return [String] the string that will replace the query in case we
# cannot filter it
ERROR_MSG = 'Error: Airbrake::Query was not filtered'.freeze
# @return [Hash{Symbol=>Regexp}] matchers for certain features of SQL
ALL_FEATURES = {
# rubocop:disable Layout/LineLength
single_quotes: /'(?:[^']|'')*?(?:\\'.*|'(?!'))/,
double_quotes: /"(?:[^"]|"")*?(?:\\".*|"(?!"))/,
dollar_quotes: /(\$(?!\d)[^$]*?\$).*?(?:\1|$)/,
uuids: /\{?(?:[0-9a-fA-F]-*){32}\}?/,
numeric_literals: /\b-?(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/,
boolean_literals: /\b(?:true|false|null)\b/i,
hexadecimal_literals: /0x[0-9a-fA-F]+/,
comments: /(?:#|--).*?(?=\r|\n|$)/i,
multi_line_comments: %r{/\*(?:[^/]|/[^*])*?(?:\*/|/\*.*)},
oracle_quoted_strings: /q'\[.*?(?:\]'|$)|q'\{.*?(?:\}'|$)|q'<.*?(?:>'|$)|q'\(.*?(?:\)'|$)/,
# rubocop:enable Layout/LineLength
}.freeze
# @return [Regexp] the regexp that is applied after the feature regexps
# were used
POST_FILTER = /(?<=[values|in ]\().+(?=\))/i.freeze
# @return [Hash{Symbol=>Array<Symbol>}] a set of features that corresponds
# to a certain dialect
DIALECT_FEATURES = {
default: ALL_FEATURES.keys,
mysql: %i[
single_quotes double_quotes numeric_literals boolean_literals
hexadecimal_literals comments multi_line_comments
].freeze,
postgres: %i[
single_quotes dollar_quotes uuids numeric_literals boolean_literals
comments multi_line_comments
].freeze,
sqlite: %i[
single_quotes numeric_literals boolean_literals hexadecimal_literals
comments multi_line_comments
].freeze,
oracle: %i[
single_quotes oracle_quoted_strings numeric_literals comments
multi_line_comments
].freeze,
cassandra: %i[
single_quotes uuids numeric_literals boolean_literals
hexadecimal_literals comments multi_line_comments
].freeze,
}.freeze
# @return [Hash{Symbol=>Regexp}] a set of regexps to check for unmatches
# quotes after filtering (should be none)
UNMATCHED_PAIR = {
mysql: %r{'|"|/\*|\*/},
mysql2: %r{'|"|/\*|\*/},
postgres: %r{'|/\*|\*/|\$(?!\?)},
sqlite: %r{'|/\*|\*/},
cassandra: %r{'|/\*|\*/},
oracle: %r{'|/\*|\*/},
oracle_enhanced: %r{'|/\*|\*/},
}.freeze
# @return [Array<Regexp>] the list of queries to be ignored
IGNORED_QUERIES = [
/\ACOMMIT/i,
/\ABEGIN/i,
/\ASET/i,
/\ASHOW/i,
/\AWITH/i,
/FROM pg_attribute/i,
/FROM pg_index/i,
/FROM pg_class/i,
/FROM pg_type/i,
].freeze
def initialize(dialect)
@dialect =
case dialect
when /mysql/i then :mysql
when /postgres/i then :postgres
when /sqlite/i then :sqlite
when /oracle/i then :oracle
when /cassandra/i then :cassandra
else
:default
end
features = DIALECT_FEATURES[@dialect].map { |f| ALL_FEATURES[f] }
@regexp = Regexp.union(features)
end
# @param [Airbrake::Query] metric
def call(metric)
return unless metric.respond_to?(:query)
query = metric.query
if IGNORED_QUERIES.any? { |q| q =~ query }
metric.ignore!
return
end
q = query.gsub(@regexp, FILTERED)
q.gsub!(POST_FILTER, FILTERED) if q =~ POST_FILTER
q = ERROR_MSG if UNMATCHED_PAIR[@dialect] =~ q
metric.query = q
end
end
end
end