lib/gdk/config_redactor.rb (62 lines of code) (raw):
# frozen_string_literal: true
module GDK
# ConfigRedactor provides functionality to securely redact sensitive
# information from configuration data. It processes YAML-compatible Hash
# structures and masks values based on key patterns and value content.
#
# The redactor identifies sensitive data through:
# 1. Key pattern matching (e.g. keys ending in _secret, _token) case-insensitive
# 2. Value pattern matching (e.g. GitLab/GitHub tokens, UUIDs)
# 3. Explicit allowlist exceptions
#
# @example Basic usage with a hash
# config = {
# 'api_token' => 'secret123',
# 'public_url' => 'http://example.com'
# }
# redacted = GDK::ConfigRedactor.redact(config)
# redacted['api_token'] # => "[redacted]"
# redacted['public_url'] # => "http://example.com"
#
# @example Handling nested structures
# nested_config = {
# 'credentials' => {
# 'gitlab_token' => 'glpat-abc123',
# 'github_key' => 'gh_xyz789'
# }
# }
# redacted = GDK::ConfigRedactor.redact(nested_config)
# # Results in:
# # {
# # 'credentials' => {
# # 'gitlab_token' => '[redacted]',
# # 'github_key' => '[redacted]'
# # }
# # }
class ConfigRedactor
# Block the config keys. String or Regexp.
BLOCK_KEYS = [
# Inspired by Rails' filter_parameters
/_key$/i,
/_pass(?:word)?$/i,
/_secret$/i,
/token$/i
].freeze
# Explicitly allow the config keys. String or Regexp.
ALLOW_KEYS = %w[
cookie_key
version
].freeze
# Block the config values. String or Regexp.
BLOCK_VALUES = [
# https://docs.gitlab.com/security/tokens/#token-prefixes
/^gl\w+-/,
# https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/#identifiable-prefixes
/^gh\w_/,
# Hex hashes
/^\h{8,}+$/,
# UUIDs
/^\h+-([\h-]+)$/
].freeze
REDACT_WITH = '[redacted]'
HOME_REDACT_WITH = '$HOME'
def self.redact(yaml)
new.redact(yaml)
end
def redact(yaml, redacted = {})
yaml.each do |key, value|
redact_kv!(key, value, redacted)
end
redacted
end
def redact_logfile(content)
content.gsub(Dir.home, '$HOME')
end
private
def redact_kv!(key, value, redacted)
new_value =
case value
when Hash
value.each { |k, v| redact_kv!(k, v, value) }
when Array
value.each.with_index { |v, i| redact_kv!(i, v, value) }
else
redact_single!(key, value)
end
redacted[key] = new_value
end
def redact_single!(key, value)
if value.is_a?(String)
return REDACT_WITH if redact?(key, value)
value.gsub(Dir.home, HOME_REDACT_WITH)
else
value
end
end
def redact?(key, value)
return false if value.empty?
key = key.to_s
(ALLOW_KEYS.none? { |allow| allow === key } &&
BLOCK_KEYS.any? { |block| block === key }) ||
BLOCK_VALUES.any? { |block| block === value }
end
end
end