Class: Aspera::SecretHider
- Inherits:
-
Object
- Object
- Aspera::SecretHider
- Includes:
- Singleton
- Defined in:
- lib/aspera/secret_hider.rb
Overview
remove secret from logs and output
Constant Summary collapse
- ADDITIONAL_KEYS_TO_HIDE =
configurable:
[]
- ALL_SECRETS2 =
(KEY_SECRETS + HTTP_SECRETS).freeze
- SECRET_LENGTH =
min length of hidden secrets, use
+or{n,} '{5,}'
Instance Attribute Summary collapse
-
#log_secrets ⇒ Object
Returns the value of attribute log_secrets.
Class Method Summary collapse
-
.instance ⇒ SecretHider
Returns the singleton instance of SecretHider.
Instance Method Summary collapse
-
#deep_remove_secret(obj) ⇒ Object
Hides recursively secrets in Hash or Array of Hash.
- #hide_secrets_in_string(value) ⇒ Object
-
#log_formatter(original_formatter) ⇒ Object
New log formatter that hides secrets.
-
#secret?(keyword, value) ⇒ Boolean
True if the key denotes a secret.
Instance Attribute Details
#log_secrets ⇒ Object
Returns the value of attribute log_secrets.
46 47 48 |
# File 'lib/aspera/secret_hider.rb', line 46 def log_secrets @log_secrets end |
Class Method Details
.instance ⇒ SecretHider
Returns the singleton instance of SecretHider
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/aspera/secret_hider.rb', line 13 class SecretHider include Singleton # configurable: ADDITIONAL_KEYS_TO_HIDE = [] # display string for hidden secrets HIDDEN_PASSWORD = '🔑' # env vars for ascp with secrets ASCP_ENV_SECRETS = %w[ASPERA_SCP_PASS ASPERA_SCP_KEY ASPERA_SCP_FILEPASS ASPERA_PROXY_PASS ASPERA_SCP_TOKEN].freeze # keys in hash that contain secrets KEY_SECRETS = %w[password secret passphrase _key apikey crn token].freeze HTTP_SECRETS = %w[Authorization].freeze ALL_SECRETS = (ASCP_ENV_SECRETS + KEY_SECRETS + HTTP_SECRETS).freeze ALL_SECRETS2 = (KEY_SECRETS + HTTP_SECRETS).freeze KEY_FALSE_POSITIVES = [/^access_key$/, /^fallback_private_key$/].freeze # min length of hidden secrets, use `+` or `{n,}` SECRET_LENGTH = '{5,}' # regex that define named captures :begin and :end REGEX_LOG_REPLACES = [ # private key values (place first) /(?<begin>--+BEGIN [^-]+ KEY--+)[[:ascii:]]+?(?<end>--+?END [^-]+ KEY--+)\n*/, # CLI manager get/set options /(?<begin>[sg]et (?:#{KEY_SECRETS.join('|')})=).*(?<end>)/, # env var ascp exec /(?<begin> (?:#{ASCP_ENV_SECRETS.join('|')})=)[^ ]+(?<end> )/, # rendered JSON or Ruby /(?<begin>(?:(?<quote>["'])|:)[^"':=]*(?:#{ALL_SECRETS.join('|')})[^"':=]*\k<quote>?(?:=>|:) *")[^"]+(?<end>")/, # logged data /(?<begin>(?:#{ALL_SECRETS2.join('|')})[ =:]+)[^ "]#{SECRET_LENGTH}(?<end>$)/, # cred in http dump /(?<begin>(?:#{HTTP_SECRETS.join('|')}): )[^\\]+(?<end>\\)/i ].freeze private_constant :HIDDEN_PASSWORD, :ASCP_ENV_SECRETS, :KEY_SECRETS, :HTTP_SECRETS, :ALL_SECRETS, :KEY_FALSE_POSITIVES, :REGEX_LOG_REPLACES attr_accessor :log_secrets # @return new log formatter that hides secrets def log_formatter(original_formatter) original_formatter ||= Logger::Formatter.new # NOTE: that @log_secrets may be set AFTER this init is done, so it's done at runtime return lambda do |severity, date_time, program_name, msg| if msg.is_a?(String) && !@log_secrets REGEX_LOG_REPLACES.each do |reg_ex| msg = msg.gsub(reg_ex){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"} end end original_formatter.call(severity, date_time, program_name, msg) end end def hide_secrets_in_string(value) return value.gsub(REGEX_LOG_REPLACES.first){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"} end # @return true if the key denotes a secret def secret?(keyword, value) keyword = keyword.to_s if keyword.is_a?(Symbol) # only Strings can be secrets, not booleans, or hash, arrays return false unless keyword.is_a?(String) && value.is_a?(String) # those are not secrets return false if KEY_FALSE_POSITIVES.any?{ |f| f.match?(keyword)} return true if ADDITIONAL_KEYS_TO_HIDE.include?(keyword) # check if keyword (name) contains an element that designate it as a secret ALL_SECRETS.any?{ |kw| keyword.include?(kw)} end # Hides recursively secrets in Hash or Array of Hash def deep_remove_secret(obj) case obj when Array obj.each{ |i| deep_remove_secret(i)} when Hash obj.each do |k, v| if secret?(k, v) obj[k] = HIDDEN_PASSWORD elsif obj[k].is_a?(Hash) deep_remove_secret(obj[k]) end end end return obj end private def initialize @log_secrets = false end end |
Instance Method Details
#deep_remove_secret(obj) ⇒ Object
Hides recursively secrets in Hash or Array of Hash
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/aspera/secret_hider.rb', line 79 def deep_remove_secret(obj) case obj when Array obj.each{ |i| deep_remove_secret(i)} when Hash obj.each do |k, v| if secret?(k, v) obj[k] = HIDDEN_PASSWORD elsif obj[k].is_a?(Hash) deep_remove_secret(obj[k]) end end end return obj end |
#hide_secrets_in_string(value) ⇒ Object
62 63 64 |
# File 'lib/aspera/secret_hider.rb', line 62 def hide_secrets_in_string(value) return value.gsub(REGEX_LOG_REPLACES.first){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"} end |
#log_formatter(original_formatter) ⇒ Object
Returns new log formatter that hides secrets.
49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/aspera/secret_hider.rb', line 49 def log_formatter(original_formatter) original_formatter ||= Logger::Formatter.new # NOTE: that @log_secrets may be set AFTER this init is done, so it's done at runtime return lambda do |severity, date_time, program_name, msg| if msg.is_a?(String) && !@log_secrets REGEX_LOG_REPLACES.each do |reg_ex| msg = msg.gsub(reg_ex){"#{Regexp.last_match(:begin)}#{HIDDEN_PASSWORD}#{Regexp.last_match(:end)}"} end end original_formatter.call(severity, date_time, program_name, msg) end end |
#secret?(keyword, value) ⇒ Boolean
Returns true if the key denotes a secret.
67 68 69 70 71 72 73 74 75 76 |
# File 'lib/aspera/secret_hider.rb', line 67 def secret?(keyword, value) keyword = keyword.to_s if keyword.is_a?(Symbol) # only Strings can be secrets, not booleans, or hash, arrays return false unless keyword.is_a?(String) && value.is_a?(String) # those are not secrets return false if KEY_FALSE_POSITIVES.any?{ |f| f.match?(keyword)} return true if ADDITIONAL_KEYS_TO_HIDE.include?(keyword) # check if keyword (name) contains an element that designate it as a secret ALL_SECRETS.any?{ |kw| keyword.include?(kw)} end |