aprs.me/lib/aprsme/log_sanitizer.ex

133 lines
3.1 KiB
Elixir

defmodule Aprsme.LogSanitizer do
@moduledoc """
Utility module for sanitizing log output to prevent sensitive data leakage
"""
@sensitive_fields [
:password,
:passcode,
:auth_token,
:api_key,
:secret,
:token,
:authorization,
:csrf_token,
:session_token,
:private_key,
:database_url,
:secret_key_base,
:signing_salt
]
@sensitive_patterns [
~r/password[=:]\s*\S+/i,
~r/passcode[=:]\s*\S+/i,
~r/token[=:]\s*\S+/i,
~r/key[=:]\s*\S+/i,
~r/secret[=:]\s*\S+/i,
~r/authorization[=:]\s*\S+/i
]
@doc """
Sanitize a map by removing or redacting sensitive fields
"""
def sanitize_map(data) when is_map(data) do
Enum.reduce(@sensitive_fields, data, fn field, acc ->
cond do
Map.has_key?(acc, field) -> Map.put(acc, field, "[REDACTED]")
Map.has_key?(acc, to_string(field)) -> Map.put(acc, to_string(field), "[REDACTED]")
true -> acc
end
end)
end
def sanitize_map(data), do: data
@doc """
Sanitize a string by replacing sensitive patterns
"""
def sanitize_string(data) when is_binary(data) do
Enum.reduce(@sensitive_patterns, data, fn pattern, acc ->
Regex.replace(pattern, acc, fn match ->
case String.split(match, ["=", ":"]) do
[key, _value] -> "#{key}=[REDACTED]"
_ -> "[REDACTED]"
end
end)
end)
end
def sanitize_string(data), do: data
@doc """
Sanitize any data structure recursively
"""
def sanitize(data) when is_map(data) do
data
|> sanitize_map()
|> Map.new(fn {k, v} -> {k, sanitize(v)} end)
end
def sanitize(data) when is_list(data) do
Enum.map(data, &sanitize/1)
end
def sanitize(data) when is_binary(data) do
sanitize_string(data)
end
def sanitize(data), do: data
@doc """
Sanitize packet data for logging
"""
def sanitize_packet_data(packet_data) when is_map(packet_data) do
# Remove potentially sensitive packet content but keep structure
packet_data
|> Map.drop([:raw_packet, :comment, :status, :data_extended])
|> sanitize_map()
end
def sanitize_packet_data(data), do: data
@doc """
Sanitize connection information for logging
"""
def sanitize_connection_info(conn_info) when is_map(conn_info) do
conn_info
|> Map.drop([:password, :passcode])
|> Map.put(:password, "[REDACTED]")
|> Map.put(:passcode, "[REDACTED]")
end
def sanitize_connection_info(data), do: data
@doc """
Sanitize error information for logging
"""
def sanitize_error_context(context) when is_map(context) do
context
|> sanitize_map()
|> Map.update(:stacktrace, nil, fn stacktrace ->
if is_list(stacktrace) do
# Limit stacktrace length
Enum.take(stacktrace, 5)
else
stacktrace
end
end)
end
def sanitize_error_context(data), do: data
@doc """
Create a sanitized logging keyword list
"""
def log_data(keyword_list) when is_list(keyword_list) do
Enum.map(keyword_list, fn {key, value} ->
{key, sanitize(value)}
end)
end
def log_data(data), do: sanitize(data)
end