aprs.me/lib/aprsme/log_sanitizer.ex
Graham McIntire 35bd098b30
refactor: more pattern matching + drop unreachable get_field_value clause
- Aprsme.Packet.put_phg_fields: dispatch on the shape of the phg value
  (map / 4-char binary / anything else) via put_phg_fields_for/2.
- Aprsme.Packet.get_field_value: remove the catch-all nil clause. After
  the put_phg_fields refactor, dialyzer can prove every call site passes
  a map and flagged the fallback as unreachable — so it's gone, and
  a future mistyped caller now fails loudly instead of silently.
- LogSanitizer.sanitize_map: extract redact_field/2 for readability.
- DeviceIdentification.pattern_matches?: match_pattern/3 dispatched on
  String.contains?/2 keeps the rescue tight around the regex path.
- InfoLive.Show: get_received_at pattern-matches on key shape;
  format_distance splits locale × range via four format_*_distance
  helpers; calculate_course pipes through normalize_bearing/1.

Adds 15 tests exercising the InfoLive.Show helpers (imperial vs. metric
formatting, cardinal bearings, haversine symmetry, Decimal inputs).
Coverage 67.43 → 67.72%.
2026-04-23 14:09:53 -05:00

152 lines
3.5 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, &redact_field/2)
end
def sanitize_map(data), do: data
# Redact a sensitive field under either its atom key or its string form,
# whichever actually exists in the map. Missing = untouched.
defp redact_field(field, acc) do
string_key = to_string(field)
cond do
Map.has_key?(acc, field) -> Map.put(acc, field, "[REDACTED]")
Map.has_key?(acc, string_key) -> Map.put(acc, string_key, "[REDACTED]")
true -> acc
end
end
# Helper to redact a matched sensitive pattern
defp redact_match(match) do
case String.split(match, ["=", ":"]) do
[key, _value] -> "#{key}=[REDACTED]"
_ -> "[REDACTED]"
end
end
@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, &redact_match/1)
end)
end
def sanitize_string(data), do: data
@doc """
Sanitize any data structure recursively
"""
def sanitize(%{__struct__: _} = data) when is_exception(data) do
sanitize_string(Exception.message(data))
end
def sanitize(%{__struct__: _} = data) do
data
|> Map.from_struct()
|> sanitize()
end
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