From 215ecede31a53db34d2f9e499ea5ff3672ef83e3 Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Wed, 7 May 2025 20:48:29 +0700 Subject: [PATCH 01/10] KRED-2061 add json masker --- lib/xendit_api/client.rb | 18 ++-- lib/xendit_api/json_masker.rb | 60 ++++++++++++++ .../middleware/faraday_log_formatter.rb | 35 ++++++++ lib/xendit_api/url_master.rb | 51 ++++++++++++ spec/xendit_api/json_masker_spec.rb | 83 +++++++++++++++++++ 5 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 lib/xendit_api/json_masker.rb create mode 100644 lib/xendit_api/middleware/faraday_log_formatter.rb create mode 100644 lib/xendit_api/url_master.rb create mode 100644 spec/xendit_api/json_masker_spec.rb diff --git a/lib/xendit_api/client.rb b/lib/xendit_api/client.rb index b33b789..cd3cdde 100644 --- a/lib/xendit_api/client.rb +++ b/lib/xendit_api/client.rb @@ -1,5 +1,6 @@ require 'faraday_middleware' require 'xendit_api/middleware/handle_response_exception' +require 'xendit_api/middleware/faraday_log_formatter' require 'xendit_api/api/virtual_account' require 'xendit_api/api/ewallet' require 'xendit_api/api/credit_card' @@ -16,7 +17,6 @@ require 'logger' module XenditApi - # rubocop:disable Metrics/ClassLength class Client BASE_URL = 'https://api.xendit.co'.freeze @@ -30,16 +30,11 @@ def initialize(authorization = nil, options = {}) logger = find_logger(options[:logger]) if logger - connection.response :logger, logger, { headers: false, bodies: true, errors: true } do |log| - filtered_logs = options[:filtered_logs] - if filtered_logs.respond_to?(:each) - filtered_logs.each do |filter| - log.filter(%r{(#{filter}=)([\w+-.?@:/]+)}, '\1[FILTERED]') - log.filter(/(#{filter}":\s*")(.*?)(")/i, '\1[FILTERED]\3') - log.filter(/(#{filter}":\s*)(\d+(?:\.\d+)?|true|false)/i, '\1[FILTERED]') - log.filter(/(#{filter}":\s*)(\[.*?\])/i, '\1[FILTERED]') - end - end + connection.response :logger, logger, { headers: false, bodies: true, errors: true } do |_log| + faraday.response :logger, { + full_hide_params: options[:filtered_logs] || [], + mask_params: options[:mask_params] || [] + }, formatter: FaradayLogFormatter end end connection.use XenditApi::Middleware::HandleResponseException, logger @@ -142,5 +137,4 @@ def find_logger(logger_option) logger_option || XenditApi.configuration&.logger end end - # rubocop:enable Metrics/ClassLength end diff --git a/lib/xendit_api/json_masker.rb b/lib/xendit_api/json_masker.rb new file mode 100644 index 0000000..846eaa6 --- /dev/null +++ b/lib/xendit_api/json_masker.rb @@ -0,0 +1,60 @@ +module XenditApi + class JsonMasker + def self.mask(json, options = {}) + return json unless json.is_a?(String) + return json if json.empty? + + output = JSON.parse(json) + JsonMasker.new(output, options).to_hash + rescue JSON::ParserError + json + end + + def initialize(data, options = {}) + @data = data + @options = options + @mask_params = options[:mask_params] || [] + @full_hide_params = options[:full_hide_params] || [] + end + + def to_hash + return @data if @mask_params.empty? && @full_hide_params.empty? + + filter(@data) + end + + private + + # rubocop:disable Style/CaseLikeIf + def filter(output) + output.each do |key, value| + output[key] = if value.is_a?(Hash) + JsonMasker.new(value, @options).to_hash + elsif value.is_a?(Array) + value.map do |item| + if item.is_a?(Hash) + JsonMasker.new(item, @options).to_hash + else + item + end + end + else + mask_value(key, value) + end + end + end + # rubocop:enable Style/CaseLikeIf + + def mask_value(key, value) + return '*****' if @full_hide_params.include?(key) + return value if @mask_params.include?(key) == false + + value = value.to_s + return '*****' if value.length <= 5 + + unmasked = value[0..2] + masked = value[3..-1].gsub(/./, '*') + "#{unmasked}#{masked}" + end + end +end diff --git a/lib/xendit_api/middleware/faraday_log_formatter.rb b/lib/xendit_api/middleware/faraday_log_formatter.rb new file mode 100644 index 0000000..1b7b11f --- /dev/null +++ b/lib/xendit_api/middleware/faraday_log_formatter.rb @@ -0,0 +1,35 @@ +require 'faraday/logging/formatter' + +class FaradayLogFormatter < Faraday::Logging::Formatter + MAX_LOG_SIZE = 10_000 + + def initialize(env = {}) + @log_opts = env[:logger] || {} + super(logger: env[:logger], options: env[:options]) + end + + def request(env) + masked_url = UrlMasker.mask(env[:url].to_s, @log_opts) + Rails.logger.info "#{env[:method].upcase} #{masked_url}" + return if env[:request_body].blank? + return if env[:request_body].size > MAX_LOG_SIZE + + message = { + body: JsonMasker.mask(env[:request_body], @log_opts) + } + Rails.logger.info({ request: message }.to_json) + end + + def response(env) + return if env[:response_body].blank? + return if env[:request_body].to_s.size > MAX_LOG_SIZE + + message = { + status: env[:status], + body: JsonMasker.mask(env[:response_body], @log_opts) + } + Rails.logger.info({ response: message }.to_json) + end + + def exception(exc); end +end diff --git a/lib/xendit_api/url_master.rb b/lib/xendit_api/url_master.rb new file mode 100644 index 0000000..9263679 --- /dev/null +++ b/lib/xendit_api/url_master.rb @@ -0,0 +1,51 @@ +module XenditApi + class UrlMasker + def self.mask(url, options = {}) + return url unless url.is_a?(String) + return url if url.blank? + + url = URI.parse(url) + UrlMasker.new(url, options).to_s + rescue URI::Error + url + end + + def initialize(url, options = {}) + @url = url + @mask_params = options[:mask_params] || [] + @full_hide_params = options[:full_hide_params] || [] + end + + def to_s + filter(@url) + end + + private + + def filter(url) + query_params = URI.decode_www_form(url.query || '').to_h + return url.to_s if query_params.blank? + + query_params.each do |key, value| + if @full_hide_params.include?(key) + query_params[key] = '*****' + elsif @mask_params.include?(key) + value = value.to_s + if value.length <= 5 + query_params[key] = '*****' + next + end + + unmasked = value[0..2] + masked = value[3..-1].gsub(/./, '*') + query_params[key] = "#{unmasked}#{masked}" + end + end + # Rebuild the URL with the masked query parameters + masked_query = URI.encode_www_form(query_params) + masked_url = url.dup + masked_url.query = masked_query + masked_url.to_s + end + end +end diff --git a/spec/xendit_api/json_masker_spec.rb b/spec/xendit_api/json_masker_spec.rb new file mode 100644 index 0000000..812c0f8 --- /dev/null +++ b/spec/xendit_api/json_masker_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' +require 'xendit_api/json_masker' + +RSpec.describe XenditApi::JsonMasker do + describe '.mask' do + it 'returns nil when input is nil' do + expect(described_class.mask(nil, mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])).to be_nil + end + + it 'returns empty string when input is empty string' do + expect(described_class.mask('', mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])).to eq('') + end + + it 'returns array when input is array' do + expect(described_class.mask([], mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])).to eq([]) + end + + it 'returns expected with valid JSON' do + parsed = { + card_number: '1234567890123456', + expiration_date: '12/23', + cvv: '***', + name: 'John Doe', + address: 'Jakarta', + external_id: '12398123123', + information: { + email: 'bill@john.com', + account_number: '1092830182309123' + }, + items: [ + { + quantity: 89_821_823, + amount: 15_000, + email: 'john@bill.com', + more_info: { + email: 'hello@gmail.com', + booking_id: '1234567890', + page: 1, + limit: 2 + } + } + ] + } + + masked = { + 'card_number' => '123*************', + 'expiration_date' => '*****', + 'cvv' => '*****', + 'name' => 'Joh*****', + 'address' => 'Jakarta', + 'external_id' => '12398123123', + 'information' => { + 'email' => 'bil**********', + 'account_number' => '*****' + }, + 'items' => [ + { + 'quantity' => 89_821_823, + 'amount' => '*****', + 'email' => 'joh**********', + 'more_info' => { + 'email' => 'hel************', + 'booking_id' => '1234567890', + 'page' => 1, + 'limit' => 2 + } + } + ] + } + + output = described_class.mask(parsed.to_json, mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number]) + + expect(output).to eq(masked) + end + + it 'returns expected with invalid JSON' do + data = 'this is invalid json' + output = described_class.mask(data, mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number]) + + expect(output).to eq(data) + end + end +end From 7308711ec9032890d0aa56abbc74b51c417b763c Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Wed, 7 May 2025 21:00:33 +0700 Subject: [PATCH 02/10] KRED-2061 update json masker --- lib/xendit_api/json_masker.rb | 24 ++++++------- .../{url_master.rb => url_masker.rb} | 20 +++++------ spec/xendit_api/url_masker_spec.rb | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+), 22 deletions(-) rename lib/xendit_api/{url_master.rb => url_masker.rb} (89%) create mode 100644 spec/xendit_api/url_masker_spec.rb diff --git a/lib/xendit_api/json_masker.rb b/lib/xendit_api/json_masker.rb index 846eaa6..0b1d17d 100644 --- a/lib/xendit_api/json_masker.rb +++ b/lib/xendit_api/json_masker.rb @@ -3,37 +3,37 @@ class JsonMasker def self.mask(json, options = {}) return json unless json.is_a?(String) return json if json.empty? - + output = JSON.parse(json) - JsonMasker.new(output, options).to_hash + XenditApi::JsonMasker.new(output, options).to_hash rescue JSON::ParserError json end - + def initialize(data, options = {}) @data = data @options = options @mask_params = options[:mask_params] || [] @full_hide_params = options[:full_hide_params] || [] end - + def to_hash return @data if @mask_params.empty? && @full_hide_params.empty? - + filter(@data) end - + private - + # rubocop:disable Style/CaseLikeIf def filter(output) output.each do |key, value| output[key] = if value.is_a?(Hash) - JsonMasker.new(value, @options).to_hash + XenditApi::JsonMasker.new(value, @options).to_hash elsif value.is_a?(Array) value.map do |item| if item.is_a?(Hash) - JsonMasker.new(item, @options).to_hash + XenditApi::JsonMasker.new(item, @options).to_hash else item end @@ -44,14 +44,14 @@ def filter(output) end end # rubocop:enable Style/CaseLikeIf - + def mask_value(key, value) return '*****' if @full_hide_params.include?(key) return value if @mask_params.include?(key) == false - + value = value.to_s return '*****' if value.length <= 5 - + unmasked = value[0..2] masked = value[3..-1].gsub(/./, '*') "#{unmasked}#{masked}" diff --git a/lib/xendit_api/url_master.rb b/lib/xendit_api/url_masker.rb similarity index 89% rename from lib/xendit_api/url_master.rb rename to lib/xendit_api/url_masker.rb index 9263679..650836b 100644 --- a/lib/xendit_api/url_master.rb +++ b/lib/xendit_api/url_masker.rb @@ -2,30 +2,30 @@ module XenditApi class UrlMasker def self.mask(url, options = {}) return url unless url.is_a?(String) - return url if url.blank? - + return url if url.empty? + url = URI.parse(url) - UrlMasker.new(url, options).to_s + XenditApi::UrlMasker.new(url, options).to_s rescue URI::Error url end - + def initialize(url, options = {}) @url = url @mask_params = options[:mask_params] || [] @full_hide_params = options[:full_hide_params] || [] end - + def to_s filter(@url) end - + private - + def filter(url) query_params = URI.decode_www_form(url.query || '').to_h - return url.to_s if query_params.blank? - + return url.to_s if query_params.empty? + query_params.each do |key, value| if @full_hide_params.include?(key) query_params[key] = '*****' @@ -35,7 +35,7 @@ def filter(url) query_params[key] = '*****' next end - + unmasked = value[0..2] masked = value[3..-1].gsub(/./, '*') query_params[key] = "#{unmasked}#{masked}" diff --git a/spec/xendit_api/url_masker_spec.rb b/spec/xendit_api/url_masker_spec.rb new file mode 100644 index 0000000..3337a8e --- /dev/null +++ b/spec/xendit_api/url_masker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' +require 'xendit_api/url_masker' + +RSpec.describe XenditApi::UrlMasker do + describe '.mask' do + it 'returns expected with valid URL' do + url = 'https://example.com?token=1234567890123456&cvv=123&other_param=value&account_number=123456789&bank=bca' + expected = 'https://example.com?token=*****&cvv=*****&other_param=value&account_number=123******&bank=*****' + options = { + full_hide_params: %w[token cvv], + mask_params: %w[account_number bank] + } + expect(described_class.mask(url, options)).to eq(expected) + end + + it 'returns expected with empty URL' do + url = '' + expected = '' + options = { + full_hide_params: %w[token cvv], + mask_params: %w[account_number name] + } + expect(described_class.mask(url, options)).to eq(expected) + end + + it 'returns expected when there is no query string' do + url = 'https://example.com' + expected = 'https://example.com' + options = { + full_hide_params: %w[token cvv], + mask_params: %w[account_number name] + } + expect(described_class.mask(url, options)).to eq(expected) + end + end +end From a22de538f0883ee963c60ecf43541fa0254d78e9 Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Wed, 7 May 2025 21:40:49 +0700 Subject: [PATCH 03/10] KRED-2061 handle array --- lib/xendit_api/client.rb | 10 ++-- lib/xendit_api/json_masker.rb | 17 +++++- .../middleware/faraday_log_formatter.rb | 59 +++++++++++-------- spec/xendit_api/json_masker_spec.rb | 36 +++++++++++ 4 files changed, 89 insertions(+), 33 deletions(-) diff --git a/lib/xendit_api/client.rb b/lib/xendit_api/client.rb index cd3cdde..52b2dba 100644 --- a/lib/xendit_api/client.rb +++ b/lib/xendit_api/client.rb @@ -30,12 +30,10 @@ def initialize(authorization = nil, options = {}) logger = find_logger(options[:logger]) if logger - connection.response :logger, logger, { headers: false, bodies: true, errors: true } do |_log| - faraday.response :logger, { - full_hide_params: options[:filtered_logs] || [], - mask_params: options[:mask_params] || [] - }, formatter: FaradayLogFormatter - end + connection.response :logger, logger, + full_hide_params: options[:filtered_logs] || [], + mask_params: options[:mask_params] || [], + formatter: XenditApi::Middleware::FaradayLogFormatter end connection.use XenditApi::Middleware::HandleResponseException, logger connection.adapter Faraday.default_adapter diff --git a/lib/xendit_api/json_masker.rb b/lib/xendit_api/json_masker.rb index 0b1d17d..cbfa36b 100644 --- a/lib/xendit_api/json_masker.rb +++ b/lib/xendit_api/json_masker.rb @@ -20,13 +20,28 @@ def initialize(data, options = {}) def to_hash return @data if @mask_params.empty? && @full_hide_params.empty? - filter(@data) + case @data + when Array + @data.map do |item| + if item.is_a?(Hash) + XenditApi::JsonMasker.new(item, @options).to_hash + else + item + end + end + when Hash + filter(@data) + else + @data + end end private # rubocop:disable Style/CaseLikeIf def filter(output) + return output unless output.is_a?(Hash) + output.each do |key, value| output[key] = if value.is_a?(Hash) XenditApi::JsonMasker.new(value, @options).to_hash diff --git a/lib/xendit_api/middleware/faraday_log_formatter.rb b/lib/xendit_api/middleware/faraday_log_formatter.rb index 1b7b11f..38eb2e1 100644 --- a/lib/xendit_api/middleware/faraday_log_formatter.rb +++ b/lib/xendit_api/middleware/faraday_log_formatter.rb @@ -1,35 +1,42 @@ require 'faraday/logging/formatter' +require 'xendit_api/json_masker' +require 'xendit_api/url_masker' -class FaradayLogFormatter < Faraday::Logging::Formatter - MAX_LOG_SIZE = 10_000 +module XenditApi + module Middleware + class FaradayLogFormatter < Faraday::Logging::Formatter + MAX_LOG_SIZE = 10_000 - def initialize(env = {}) - @log_opts = env[:logger] || {} - super(logger: env[:logger], options: env[:options]) - end + def initialize(env = {}) + @logger = env[:logger] + @options = env[:options] + super(logger: env[:logger], options: env[:options]) + end - def request(env) - masked_url = UrlMasker.mask(env[:url].to_s, @log_opts) - Rails.logger.info "#{env[:method].upcase} #{masked_url}" - return if env[:request_body].blank? - return if env[:request_body].size > MAX_LOG_SIZE + def request(env) + masked_url = XenditApi::UrlMasker.mask(env[:url].to_s, @options) + @logger.info "#{env[:method].upcase} #{masked_url}" + return if env[:request_body].to_s.empty? + return if env[:request_body].to_s.size > MAX_LOG_SIZE - message = { - body: JsonMasker.mask(env[:request_body], @log_opts) - } - Rails.logger.info({ request: message }.to_json) - end + message = { + body: XenditApi::JsonMasker.mask(env[:request_body], @options) + } + @logger.info({ request: message }.to_json) + end - def response(env) - return if env[:response_body].blank? - return if env[:request_body].to_s.size > MAX_LOG_SIZE + def response(env) + return if env[:response_body].to_s.empty? + return if env[:request_body].to_s.size > MAX_LOG_SIZE - message = { - status: env[:status], - body: JsonMasker.mask(env[:response_body], @log_opts) - } - Rails.logger.info({ response: message }.to_json) - end + message = { + status: env[:status], + body: XenditApi::JsonMasker.mask(env[:response_body], @options) + } + @logger.info({ response: message }.to_json) + end - def exception(exc); end + def exception(exc); end + end + end end diff --git a/spec/xendit_api/json_masker_spec.rb b/spec/xendit_api/json_masker_spec.rb index 812c0f8..6b11186 100644 --- a/spec/xendit_api/json_masker_spec.rb +++ b/spec/xendit_api/json_masker_spec.rb @@ -15,6 +15,42 @@ expect(described_class.mask([], mask_params: %w[card_number expiration_date cvv name email], full_hide_params: %w[amount account_number])).to eq([]) end + it 'returns expected when data is an array' do + parsed = [ + { + card_number: '123456789012', + cvv: '123', + address: 'Jakarta', + email: 'hello@email.com' + }, + { + card_number: '123456789012', + cvv: '123', + address: 'Jakarta', + email: 'hello@email.com' + } + ] + + masked = [ + { + 'card_number' => '*****', + 'cvv' => '*****', + 'address' => 'Jakarta', + 'email' => 'hel************' + }, + { + 'card_number' => '*****', + 'cvv' => '*****', + 'address' => 'Jakarta', + 'email' => 'hel************' + } + ] + + output = described_class.mask(parsed.to_json, mask_params: %w[email], full_hide_params: %w[card_number cvv]) + + expect(output).to eq(masked) + end + it 'returns expected with valid JSON' do parsed = { card_number: '1234567890123456', From faa4538781dd9f202e2acd54d9dbc3bdd3c0b186 Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Wed, 7 May 2025 21:45:26 +0700 Subject: [PATCH 04/10] KRED-2061 refactor --- lib/xendit_api/json_masker.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/xendit_api/json_masker.rb b/lib/xendit_api/json_masker.rb index cbfa36b..ee70410 100644 --- a/lib/xendit_api/json_masker.rb +++ b/lib/xendit_api/json_masker.rb @@ -5,7 +5,7 @@ def self.mask(json, options = {}) return json if json.empty? output = JSON.parse(json) - XenditApi::JsonMasker.new(output, options).to_hash + XenditApi::JsonMasker.new(output, options).to_masked rescue JSON::ParserError json end @@ -17,7 +17,7 @@ def initialize(data, options = {}) @full_hide_params = options[:full_hide_params] || [] end - def to_hash + def to_masked return @data if @mask_params.empty? && @full_hide_params.empty? case @data @@ -36,12 +36,14 @@ def to_hash end end + def to_hash + filter(@data) + end + private - # rubocop:disable Style/CaseLikeIf + # rubocop:disable Style/CaseLikeIf, Metrics/PerceivedComplexity def filter(output) - return output unless output.is_a?(Hash) - output.each do |key, value| output[key] = if value.is_a?(Hash) XenditApi::JsonMasker.new(value, @options).to_hash @@ -58,7 +60,7 @@ def filter(output) end end end - # rubocop:enable Style/CaseLikeIf + # rubocop:enable Style/CaseLikeIf, Metrics/PerceivedComplexity def mask_value(key, value) return '*****' if @full_hide_params.include?(key) From cc58566bd4267c25a7d39bba848f6f7fc003ce5b Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Wed, 7 May 2025 21:49:52 +0700 Subject: [PATCH 05/10] KRED-2061 fixes lint --- lib/xendit_api/json_masker.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/xendit_api/json_masker.rb b/lib/xendit_api/json_masker.rb index ee70410..039ae0a 100644 --- a/lib/xendit_api/json_masker.rb +++ b/lib/xendit_api/json_masker.rb @@ -42,7 +42,7 @@ def to_hash private - # rubocop:disable Style/CaseLikeIf, Metrics/PerceivedComplexity + # rubocop:disable Style/CaseLikeIf def filter(output) output.each do |key, value| output[key] = if value.is_a?(Hash) @@ -60,7 +60,7 @@ def filter(output) end end end - # rubocop:enable Style/CaseLikeIf, Metrics/PerceivedComplexity + # rubocop:enable Style/CaseLikeIf def mask_value(key, value) return '*****' if @full_hide_params.include?(key) From b580b5bf88bbcd7112060af63286f39dc5aa895a Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Thu, 8 May 2025 10:24:28 +0700 Subject: [PATCH 06/10] KRED-2061 update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b54f927..56a8ff4 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ end client = XenditApi::Client.new('secret_key') # when you need to filter logs due to PII or security -client = XenditAPi::Client.new('secret_key', filtered_logs: [:card_cvv, :expected_amount]) +client = XenditAPi::Client.new('secret_key', filtered_logs: [:card_cvv, :expected_amount], mask_logs: [:email, :full_name]) ``` When you need to filter logs, also make sure you already inject the logger object first, because we don't provide any default logger object. If you writing in Rails, you could use `Rails.logger`. From eed4dc7f6dfa968ac7ba385fa0ea97b0f196d729 Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Thu, 8 May 2025 10:27:12 +0700 Subject: [PATCH 07/10] KRED-2061 supports symbol and string --- lib/xendit_api/json_masker.rb | 6 +++-- spec/xendit_api/json_masker_spec.rb | 36 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/xendit_api/json_masker.rb b/lib/xendit_api/json_masker.rb index 039ae0a..65f1133 100644 --- a/lib/xendit_api/json_masker.rb +++ b/lib/xendit_api/json_masker.rb @@ -63,8 +63,10 @@ def filter(output) # rubocop:enable Style/CaseLikeIf def mask_value(key, value) - return '*****' if @full_hide_params.include?(key) - return value if @mask_params.include?(key) == false + full_hide_params_to_s = @full_hide_params.map(&:to_s) + return '*****' if full_hide_params_to_s.include?(key.to_s) + mask_params_to_s = @mask_params.map(&:to_s) + return value if mask_params_to_s.include?(key.to_s) == false value = value.to_s return '*****' if value.length <= 5 diff --git a/spec/xendit_api/json_masker_spec.rb b/spec/xendit_api/json_masker_spec.rb index 6b11186..0b2032c 100644 --- a/spec/xendit_api/json_masker_spec.rb +++ b/spec/xendit_api/json_masker_spec.rb @@ -51,6 +51,42 @@ expect(output).to eq(masked) end + it 'returns expected when data is an array and attribute symbol' do + parsed = [ + { + card_number: '123456789012', + cvv: '123', + address: 'Jakarta', + email: 'hello@email.com' + }, + { + card_number: '123456789012', + cvv: '123', + address: 'Jakarta', + email: 'hello@email.com' + } + ] + + masked = [ + { + 'card_number' => '*****', + 'cvv' => '*****', + 'address' => 'Jakarta', + 'email' => 'hel************' + }, + { + 'card_number' => '*****', + 'cvv' => '*****', + 'address' => 'Jakarta', + 'email' => 'hel************' + } + ] + + output = described_class.mask(parsed.to_json, mask_params: %i[email], full_hide_params: %i[card_number cvv]) + + expect(output).to eq(masked) + end + it 'returns expected with valid JSON' do parsed = { card_number: '1234567890123456', From 12cdc425a98f5e32e479f199e1f5454b26300897 Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Thu, 8 May 2025 10:28:56 +0700 Subject: [PATCH 08/10] KRED-2061 improve spec --- lib/xendit_api/url_masker.rb | 6 ++++-- spec/xendit_api/url_masker_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/xendit_api/url_masker.rb b/lib/xendit_api/url_masker.rb index 650836b..bce54a4 100644 --- a/lib/xendit_api/url_masker.rb +++ b/lib/xendit_api/url_masker.rb @@ -27,9 +27,11 @@ def filter(url) return url.to_s if query_params.empty? query_params.each do |key, value| - if @full_hide_params.include?(key) + full_hide_params_to_s = @full_hide_params.map(&:to_s) + mask_params_to_s = @mask_params.map(&:to_s) + if full_hide_params_to_s.include?(key) query_params[key] = '*****' - elsif @mask_params.include?(key) + elsif mask_params_to_s.include?(key) value = value.to_s if value.length <= 5 query_params[key] = '*****' diff --git a/spec/xendit_api/url_masker_spec.rb b/spec/xendit_api/url_masker_spec.rb index 3337a8e..0030a87 100644 --- a/spec/xendit_api/url_masker_spec.rb +++ b/spec/xendit_api/url_masker_spec.rb @@ -13,6 +13,16 @@ expect(described_class.mask(url, options)).to eq(expected) end + it 'returns expected with valid URL with symbol params' do + url = 'https://example.com?token=1234567890123456&cvv=123&other_param=value&account_number=123456789&bank=bca' + expected = 'https://example.com?token=*****&cvv=*****&other_param=value&account_number=123******&bank=*****' + options = { + full_hide_params: %i[token cvv], + mask_params: %i[account_number bank] + } + expect(described_class.mask(url, options)).to eq(expected) + end + it 'returns expected with empty URL' do url = '' expected = '' From 74003b20ac7b742637ad70dbe7c048e6211a59d8 Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Thu, 8 May 2025 10:29:36 +0700 Subject: [PATCH 09/10] KRED-2061 update guide in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56a8ff4..9479ea7 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ end client = XenditApi::Client.new('secret_key') # when you need to filter logs due to PII or security -client = XenditAPi::Client.new('secret_key', filtered_logs: [:card_cvv, :expected_amount], mask_logs: [:email, :full_name]) +client = XenditAPi::Client.new('secret_key', filtered_logs: [:card_cvv, :expected_amount], mask_params: [:email, :full_name]) ``` When you need to filter logs, also make sure you already inject the logger object first, because we don't provide any default logger object. If you writing in Rails, you could use `Rails.logger`. From de73fc40503ddd20128b1be3f9c7910fe9f60879 Mon Sep 17 00:00:00 2001 From: Philip Lambok Date: Thu, 8 May 2025 10:31:15 +0700 Subject: [PATCH 10/10] KRED-2061 fix cops --- lib/xendit_api/json_masker.rb | 1 + lib/xendit_api/url_masker.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/xendit_api/json_masker.rb b/lib/xendit_api/json_masker.rb index 65f1133..424f4ab 100644 --- a/lib/xendit_api/json_masker.rb +++ b/lib/xendit_api/json_masker.rb @@ -65,6 +65,7 @@ def filter(output) def mask_value(key, value) full_hide_params_to_s = @full_hide_params.map(&:to_s) return '*****' if full_hide_params_to_s.include?(key.to_s) + mask_params_to_s = @mask_params.map(&:to_s) return value if mask_params_to_s.include?(key.to_s) == false diff --git a/lib/xendit_api/url_masker.rb b/lib/xendit_api/url_masker.rb index bce54a4..d88f115 100644 --- a/lib/xendit_api/url_masker.rb +++ b/lib/xendit_api/url_masker.rb @@ -22,6 +22,7 @@ def to_s private + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def filter(url) query_params = URI.decode_www_form(url.query || '').to_h return url.to_s if query_params.empty? @@ -49,5 +50,6 @@ def filter(url) masked_url.query = masked_query masked_url.to_s end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end end