squelch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3882fdd525cffcfe62b3ac46dac3af8ebce60ea3c83e69315be51ac211dce232
4
+ data.tar.gz: 7e82cc70e6341afd6de48aa8d67c55d4ce618d06f62e52aff7c4fdd6f1c202cc
5
+ SHA512:
6
+ metadata.gz: 6bd3863b88e5ed602d2f632c4f67c6f339032d743d47281b8a0041dfb2e0ff47b335426c8dfbb88cc15e1ff8d7b2c82b37c7d0e58e102e597a6cc4a0a08021b0
7
+ data.tar.gz: 3c48c016f881d621db4ddad51883e29b5cf5ac8aa2db58baaf225904d1a46633e26489b8f6de6c274556f401d931082296911f423f3e62c2f74040e95a209fda
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Alex Vondrak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Squelch
2
+
3
+ [![build](https://github.com/ajvondrak/squelch/workflows/build/badge.svg)](https://github.com/ajvondrak/squelch/actions?query=workflow%3Abuild)
4
+ [![coverage](https://coveralls.io/repos/github/ajvondrak/squelch/badge.svg?branch=main)](https://coveralls.io/github/ajvondrak/squelch?branch=main)
5
+
6
+ Squelch squelches SQL!
7
+
8
+ ```sql
9
+ -- Before
10
+ INSERT INTO users(name, address, phone) VALUES ("John Doe", "1600 Pennsylvania Ave", "867-5309");
11
+
12
+ -- After
13
+ INSERT INTO users(name, address, phone) VALUES (?, ?, ?);
14
+ ```
15
+
16
+ This gem is a purposefully simple string obfuscator. It aims to replace every data literal in a SQL query with a `?` placeholder, as though it were a prepared statement. The result should still be readable SQL, but without the risk of leaking potentially sensitive information.
17
+
18
+ The code was originally adapted from the [`NewRelic::Agent::Database::ObfuscationHelpers`](https://github.com/newrelic/newrelic-ruby-agent/blob/f0290ab6468ad205dd014d63c794883dc47eebe7/lib/new_relic/agent/database/obfuscation_helpers.rb) in the [newrelic\_rpm](https://rubygems.org/gems/newrelic_rpm) gem. By abstracting out these low-level implementation details, the hope is that Squelch can empower other libraries to easily sanitize their SQL logs.
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem "squelch"
26
+ ```
27
+
28
+ and then install it with `bundle install`.
29
+
30
+ Alternatively, you could install it to your system's gems with:
31
+
32
+ ```console
33
+ $ gem install squelch
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Basic interface
39
+
40
+ The main API is the `Squelch.obfuscate` method, which takes in your SQL string and returns an obfuscated version of it.
41
+
42
+ ```ruby
43
+ Squelch.obfuscate("SELECT * FROM social_security_cards WHERE number = 'pii';")
44
+
45
+ #=> "SELECT * FROM social_security_cards WHERE number = ?;"
46
+ ```
47
+
48
+ This method is powered by regular expression patterns, some of which correspond to particular database systems. For example, Postgres supports a unique [dollar quoting](https://www.postgresql.org/docs/13/sql-syntax-lexical.html#SQL-SYNTAX-DOLLAR-QUOTING) syntax, while Oracle has its own [Q quoting](https://livesql.oracle.com/apex/livesql/file/content_CIREYU9EA54EOKQ7LAMZKRF6P.html) syntax. If possible, try to always supply the optional `db:` keyword parameter with a symbol corresponding to your RDMS. The currently supported options are `:mysql`, `:postgres`, `:sqlite`, `:oracle`, and `:cassandra`, but any other option will fall back safely to a generic default pattern.
49
+
50
+ ```ruby
51
+ Squelch.obfuscate("SELECT * FROM credit_cards WHERE number = $pii$ ... $pii$;", db: :postgres)
52
+
53
+ #=> "SELECT * FROM credit_cards WHERE number = ?;"
54
+ ```
55
+
56
+ ```ruby
57
+ Squelch.obfuscate("SELECT * FROM phones WHERE number = q'<pii>';", db: :oracle)
58
+
59
+ #=> "SELECT * FROM phones WHERE number = ?;"
60
+ ```
61
+
62
+ ### Handling errors
63
+
64
+ When there's an issue with squelching the SQL, we don't want to risk of using the problematic results that might still be leaking PII. The error-safe `Squelch.obfuscate` method returns a single `?` placeholder in the event of an issue, but Squelch has the error-raising variant `Squelch.obfuscate!` as well.
65
+
66
+ ```ruby
67
+ Squelch.obfuscate("SELECT * FROM table WHERE pii = 'a string missing a closing quote;")
68
+
69
+ #=> "?"
70
+ ```
71
+
72
+ ```ruby
73
+ Squelch.obfuscate!("SELECT * FROM table WHERE pii = 'a string missing a closing quote;")
74
+
75
+ #=> Squelch::Error: Failed to squelch SQL, delimiter ' remained after obfuscation
76
+ ```
77
+
78
+ If you rescue the `Squelch::Error`, you can access the problematic obfuscation result in `Squelch::Error#obfuscation`.
79
+
80
+ ```ruby
81
+ begin
82
+ Squelch.obfuscate!("SELECT * FROM users WHERE id = 12345 AND name = 'Mister Danglin' Quote';")
83
+ rescue Squelch::Error => e
84
+ e.obfuscation
85
+ end
86
+
87
+ #=> "SELECT * FROM users WHERE id = ? AND name = ? Quote';"
88
+ ```
data/lib/squelch.rb ADDED
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "squelch/version"
4
+ require "squelch/pairs"
5
+ require "squelch/patterns"
6
+ require "squelch/database"
7
+ require "squelch/error"
8
+
9
+ # A simple SQL obfuscator.
10
+ #
11
+ # The goal of {Squelch} is to replace every data literal in any given SQL
12
+ # string with a placeholder `?`. This removes any potentially sensitive values
13
+ # by removing *all* values, making the SQL read like it was a prepared
14
+ # statement all along.
15
+ #
16
+ # However, this process might produce bad results if there's a bug or you give
17
+ # a malformed SQL query. One heuristic we use to try to catch these cases is to
18
+ # look at the end result: are there any unmatched delimiters left over? Since
19
+ # obfuscation should remove *all* strings and comments, any dangling quotes or
20
+ # `/*` markers or such would indicate that either:
21
+ #
22
+ # * the SQL query had mismatched delimiter pairs in the first place, or
23
+ # * the obfuscation patterns failed to parse all the data literals correctly.
24
+ #
25
+ # Either way, we should be cautious of using either the original or the
26
+ # improperly-obfuscated query, since both may still contain sensitive
27
+ # information. To deal with these issues, we have both the error-safe
28
+ # {Squelch.obfuscate} and the error-raising {Squelch.obfuscate!}.
29
+ #
30
+ # Both methods not only accept a string of SQL, but optionally a specific
31
+ # database driver. This is because certain databases have their own special
32
+ # syntax. For example, Postgres uses double quotes around table names and
33
+ # supports `$$dollar quoted$$` strings, whereas MySQL uses backticks around
34
+ # table names and supports `"double quoted"` strings.
35
+ #
36
+ # To stand the best chance of scrubbing all sensitive values from your SQL, you
37
+ # should provide the specific database that you're using. That way, {Squelch}
38
+ # can make tweaks to its internal pattern matching. The default just tries to
39
+ # match all possible special cases, but that may wind up obfuscating too much
40
+ # or even being less performant than a more specific option.
41
+ #
42
+ # @example Basic obfuscation
43
+ # Squelch.obfuscate("SELECT * FROM examples WHERE name = 'basic';")
44
+ # #=> "SELECT * FROM examples WHERE name = ?;"
45
+ #
46
+ # @example Malformed query
47
+ # Squelch.obfuscate("SELECT * FROM examples WHERE name = ''malformed';")
48
+ # #=> "?"
49
+ #
50
+ # begin
51
+ # Squelch.obfuscate!("SELECT * FROM examples WHERE name = ''malformed';")
52
+ # rescue Squelch::Error => e
53
+ # puts e.message
54
+ # puts
55
+ # puts e.obfuscation
56
+ # end
57
+ # # Failed to squelch SQL, delimiter ' remained after obfuscation
58
+ # #
59
+ # # SELECT * FROM examples WHERE name = ?malformed';
60
+ #
61
+ # @example Using database-specific syntax
62
+ # Squelch.obfuscate(
63
+ # 'SELECT "examples".name FROM examples WHERE db = $$postgres$$;',
64
+ # db: :mysql,
65
+ # )
66
+ # #=> "SELECT ?.name FROM examples WHERE db = $$postgres$$;"
67
+ #
68
+ # Squelch.obfuscate(
69
+ # 'SELECT "examples".name FROM examples WHERE db = $$postgres$$;',
70
+ # db: :postgres,
71
+ # )
72
+ # #=> 'SELECT "examples".name FROM examples WHERE db = ?;'
73
+ module Squelch
74
+ # Obfuscates a SQL query.
75
+ #
76
+ # If the resulting obfuscation still has dangling delimiters left over, we
77
+ # return a single placeholder for the whole query, `?`. In order to get more
78
+ # information about such errors, you should use {.obfuscate!}.
79
+ #
80
+ # @param sql [String] an unobfuscated SQL query
81
+ #
82
+ # @param db [Symbol] the specific database syntax being used; supports
83
+ # `:mysql`, `:postgres`, `:sqlite`, `:oracle`, `:oracle`, or `:cassandra`,
84
+ # while anything else will be treated as `:default`
85
+ #
86
+ # @return [String] the obfuscated SQL query
87
+ def self.obfuscate(sql, db: :default)
88
+ obfuscate!(sql, db: db)
89
+ rescue Error
90
+ "?"
91
+ end
92
+
93
+ # Obfuscates a SQL query, raising an error if there are issues.
94
+ #
95
+ # If the resulting obfuscation still has dangling delimiters left over, we
96
+ # raise {Squelch::Error} with more information about what went wrong. If you
97
+ # don't care about the details and just want some canonical string output,
98
+ # you should use {.obfuscate}.
99
+ #
100
+ # @param (see .obfuscate)
101
+ # @return (see .obfuscate)
102
+ # @raise [Squelch::Error] if obfuscation didn't remove all delimiters
103
+ def self.obfuscate!(sql, db: :default)
104
+ sql.gsub(Database.pattern(db), "?").tap do |obfuscated|
105
+ mismatched = obfuscated.match(Database.pairs(db))
106
+ raise Error.new(sql, mismatched) if mismatched
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squelch
4
+ # @private
5
+ module Database
6
+ PATTERNS = {
7
+ mysql: Regexp.union(
8
+ Patterns::SINGLE_QUOTED,
9
+ Patterns::DOUBLE_QUOTED,
10
+ Patterns::NUMBER,
11
+ Patterns::BOOLEAN,
12
+ Patterns::HEXADECIMAL,
13
+ Patterns::LINE_COMMENT,
14
+ Patterns::BLOCK_COMMENT,
15
+ ).freeze,
16
+ postgres: Regexp.union(
17
+ Patterns::SINGLE_QUOTED,
18
+ Patterns::DOLLAR_QUOTED,
19
+ Patterns::UUID,
20
+ Patterns::NUMBER,
21
+ Patterns::BOOLEAN,
22
+ Patterns::LINE_COMMENT,
23
+ Patterns::BLOCK_COMMENT,
24
+ ).freeze,
25
+ sqlite: Regexp.union(
26
+ Patterns::SINGLE_QUOTED,
27
+ Patterns::NUMBER,
28
+ Patterns::BOOLEAN,
29
+ Patterns::HEXADECIMAL,
30
+ Patterns::LINE_COMMENT,
31
+ Patterns::BLOCK_COMMENT,
32
+ ).freeze,
33
+ oracle: Regexp.union(
34
+ Patterns::SINGLE_QUOTED,
35
+ Patterns::ORACLE_QUOTED,
36
+ Patterns::NUMBER,
37
+ Patterns::LINE_COMMENT,
38
+ Patterns::BLOCK_COMMENT,
39
+ ).freeze,
40
+ cassandra: Regexp.union(
41
+ Patterns::SINGLE_QUOTED,
42
+ Patterns::UUID,
43
+ Patterns::NUMBER,
44
+ Patterns::BOOLEAN,
45
+ Patterns::HEXADECIMAL,
46
+ Patterns::LINE_COMMENT,
47
+ Patterns::BLOCK_COMMENT,
48
+ ).freeze,
49
+ default: Regexp.union(
50
+ Patterns::SINGLE_QUOTED,
51
+ Patterns::DOUBLE_QUOTED,
52
+ Patterns::DOLLAR_QUOTED,
53
+ Patterns::UUID,
54
+ Patterns::NUMBER,
55
+ Patterns::BOOLEAN,
56
+ Patterns::HEXADECIMAL,
57
+ Patterns::LINE_COMMENT,
58
+ Patterns::BLOCK_COMMENT,
59
+ Patterns::ORACLE_QUOTED,
60
+ ).freeze,
61
+ }.freeze
62
+
63
+ def self.pattern(db)
64
+ PATTERNS[db] || PATTERNS[:default]
65
+ end
66
+
67
+ PAIRS = {
68
+ mysql: Regexp.union(
69
+ Pairs::SINGLE_QUOTED,
70
+ Pairs::DOUBLE_QUOTED,
71
+ Pairs::BLOCK_COMMENT,
72
+ ),
73
+ postgres: Regexp.union(
74
+ Pairs::SINGLE_QUOTED,
75
+ Pairs::DOLLAR_QUOTED,
76
+ Pairs::BLOCK_COMMENT,
77
+ ),
78
+ sqlite: Regexp.union(
79
+ Pairs::SINGLE_QUOTED,
80
+ Pairs::BLOCK_COMMENT,
81
+ ),
82
+ cassandra: Regexp.union(
83
+ Pairs::SINGLE_QUOTED,
84
+ Pairs::BLOCK_COMMENT,
85
+ ),
86
+ oracle: Regexp.union(
87
+ Pairs::SINGLE_QUOTED,
88
+ Pairs::BLOCK_COMMENT,
89
+ ),
90
+ default: Regexp.union(
91
+ Pairs::SINGLE_QUOTED,
92
+ Pairs::DOUBLE_QUOTED,
93
+ Pairs::BLOCK_COMMENT,
94
+ ),
95
+ }.freeze
96
+
97
+ def self.pairs(db)
98
+ PAIRS[db] || PAIRS[:default]
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squelch
4
+ # Raised by {Squelch.obfuscate!} if obfuscation seems to have failed.
5
+ #
6
+ # This might be raised either because the SQL was malformed in the first
7
+ # place or because of a bug in our parsing. See {Squelch} for more
8
+ # discussion.
9
+ class Error < StandardError
10
+ # @return [String] the original SQL input
11
+ attr_reader :sql
12
+
13
+ # @return [String] the invalid result of obfuscating {#sql}
14
+ attr_reader :obfuscation
15
+
16
+ # @return [String] the left over delimiter detected in {#obfuscation}
17
+ attr_reader :delimiter
18
+
19
+ def initialize(sql, mismatched)
20
+ @sql = sql
21
+ @obfuscation = mismatched.string
22
+ @delimiter = mismatched.to_s
23
+ super(<<~MSG.strip)
24
+ Failed to squelch SQL, delimiter #{delimiter} remained after obfuscation
25
+ MSG
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squelch
4
+ # @private
5
+ module Pairs
6
+ SINGLE_QUOTED = "'"
7
+ DOUBLE_QUOTED = '"'
8
+ DOLLAR_QUOTED = /\$(?!\?)/.freeze
9
+ BLOCK_COMMENT = Regexp.union("/*", "*/").freeze
10
+ end
11
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squelch
4
+ # @private
5
+ module Patterns
6
+ ORACLE_QUOTED = Regexp.union(
7
+ /q'\<.*?\>'/m, # q'<text>'
8
+ /q'\[.*?\]'/m, # q'[text]'
9
+ /q'\{.*?\}'/m, # q'{text}'
10
+ /q'\(.*?\)'/m, # q'(text)'
11
+ ).freeze
12
+
13
+ SINGLE_QUOTED = %r{
14
+ ' # a single quote
15
+ (?: # followed by zero or more
16
+ [^'] # non-quote characters
17
+ | # or
18
+ '' # escaped quotes
19
+ )*? #
20
+ (?: # and closed by either
21
+ \\'.* # a literal backslash at the end of the string
22
+ | # or
23
+ '(?!') # one non-escaped quote
24
+ ) #
25
+ }mx.freeze
26
+
27
+ DOUBLE_QUOTED = %r{
28
+ " # a double quote
29
+ (?: # followed by zero or more
30
+ [^"] # non-quote characters
31
+ | # or
32
+ "" # escaped quotes
33
+ )*? #
34
+ (?: # and closed by either
35
+ \\".* # a literal backslash at the end of the string
36
+ | # or
37
+ "(?!") # one non-escaped quote
38
+ ) #
39
+ }mx.freeze
40
+
41
+ DOLLAR_QUOTED = %r{
42
+ ( # a dollar quote is defined dynamically
43
+ \$ # the quote mark begins with a dollar sign
44
+ (?!\d) # unless followed by a digit, which denotes a variable
45
+ [^$]*? # otherwise it can can optionally contain any characters
46
+ \$ # up until another dollar sign
47
+ ) #
48
+ .*? # there's some amount of intervening text being quoted
49
+ \1 # until the dollar quote mark is repeated
50
+ }mx.freeze
51
+
52
+ LINE_COMMENT = %r{
53
+ (?:\#|--) # single-line comments start with a hash or double hyphen
54
+ .*? # they contain arbitrary text
55
+ (?=\r|\n|$) # up until, but not including, the newline/carriage-return
56
+ }x.freeze
57
+
58
+ BLOCK_COMMENT = %r{
59
+ /\* # block comments open with a slash-star
60
+ (?> # they contain zero or more
61
+ [^/*] # non-star or -slash characters
62
+ | # or
63
+ /(?!\*) # a slash as long as it's not followed by a star
64
+ | # or
65
+ \*(?!/) # a star as long as it's not followed by a slash
66
+ | # or
67
+ \g<0> # recursively, a nested block comment
68
+ )* #
69
+ /? # slash followed by star is ok if star is part of close
70
+ \*/ # block comments close with a star-slash
71
+ }mx.freeze
72
+
73
+ UUID = /\{?(?:[0-9a-fA-F]-*){32}\}?/.freeze
74
+
75
+ NUMBER = /-?\b(?:[0-9]+\.)?[0-9]+([eE][+-]?[0-9]+)?\b/.freeze
76
+
77
+ BOOLEAN = /\b(?:true|false|null)\b/i.freeze
78
+
79
+ HEXADECIMAL = /0x\h+/.freeze
80
+ end
81
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squelch
4
+ # The current version of the gem.
5
+ VERSION = "0.1.0"
6
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: squelch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alex Vondrak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-03-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ Squelch squelches SQL!
15
+
16
+ This gem is a purposefully simple string obfuscator. It aims to replace
17
+ every data literal in a SQL query with a `?` placeholder, as though it were
18
+ a prepared statement. The result should still be readable SQL, but without
19
+ the risk of leaking potentially sensitive information.
20
+ email: ajvondrak@gmail.com
21
+ executables: []
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - LICENSE
26
+ - README.md
27
+ - lib/squelch.rb
28
+ - lib/squelch/database.rb
29
+ - lib/squelch/error.rb
30
+ - lib/squelch/pairs.rb
31
+ - lib/squelch/patterns.rb
32
+ - lib/squelch/version.rb
33
+ homepage: https://github.com/ajvondrak/squelch
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ bug_tracker_uri: https://github.com/ajvondrak/squelch/issues
38
+ documentation_uri: https://rubydoc.info/github/ajvondrak/squelch/main
39
+ homepage_uri: https://github.com/ajvondrak/squelch
40
+ source_code_uri: https://github.com/ajvondrak/squelch
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: 2.6.0
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.2.3
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: A simple SQL obfuscator
60
+ test_files: []