filter snmp communities from log

This commit is contained in:
Graham McIntire 2026-01-28 14:51:47 -06:00
parent aed15dd6a1
commit 1f6151b5a8
6 changed files with 266 additions and 4 deletions

View file

@ -54,7 +54,20 @@ config :mime, :types, %{
} }
# Filter sensitive parameters from logs # Filter sensitive parameters from logs
config :phoenix, :filter_parameters, ["password", "snmp_community", "secret", "token", "api_key"] # These parameters will be replaced with "[FILTERED]" in Phoenix logs and error reports
config :phoenix, :filter_parameters, [
"password",
"snmp_community",
"community",
"secret",
"token",
"api_key",
# SNMPv3 credentials (for future use)
"auth_password",
"priv_password",
"auth_pass",
"priv_pass"
]
# Use Jason for JSON parsing in Phoenix # Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason config :phoenix, :json_library, Jason

View file

@ -1,6 +1,7 @@
defmodule Towerops.HoneybadgerFilter do defmodule Towerops.HoneybadgerFilter do
@moduledoc """ @moduledoc """
Custom Honeybadger filter to exclude noisy errors during deployments. Custom Honeybadger filter to exclude noisy errors during deployments
and filter sensitive SNMP credentials from error reports.
Filters out OTP supervisor/gen_server errors that occur during normal Filters out OTP supervisor/gen_server errors that occur during normal
node shutdown/restart. node shutdown/restart.
@ -15,6 +16,20 @@ defmodule Towerops.HoneybadgerFilter do
"Elixir.PollerSupervisor" "Elixir.PollerSupervisor"
] ]
# Sensitive keys that should be filtered from error reports
@sensitive_keys [
"password",
"snmp_community",
"community",
"secret",
"token",
"api_key",
"auth_password",
"priv_password",
"auth_pass",
"priv_pass"
]
@impl Honeybadger.Filter @impl Honeybadger.Filter
def filter_context(context) do def filter_context(context) do
cond do cond do
@ -27,10 +42,25 @@ defmodule Towerops.HoneybadgerFilter do
nil nil
true -> true ->
super(context) context
|> super()
|> filter_sensitive_data()
end end
end end
@impl Honeybadger.Filter
def filter_params(params) when is_map(params) do
Map.new(params, fn {key, value} ->
if should_filter?(key) do
{key, "[FILTERED]"}
else
{key, filter_params(value)}
end
end)
end
def filter_params(params), do: params
# Detect anonymous gen_server termination during shutdown # Detect anonymous gen_server termination during shutdown
defp otp_gen_server_shutdown?(context) do defp otp_gen_server_shutdown?(context) do
domain = get_in(context, [:domain]) domain = get_in(context, [:domain])
@ -38,4 +68,29 @@ defmodule Towerops.HoneybadgerFilter do
domain == ["otp"] and match?(["gen_server", "error_info", _], mfa) domain == ["otp"] and match?(["gen_server", "error_info", _], mfa)
end end
# Filter sensitive data from context
defp filter_sensitive_data(nil), do: nil
defp filter_sensitive_data(context) when is_map(context) do
context
|> Map.update(:cgi_data, %{}, &filter_params/1)
|> Map.update(:params, %{}, &filter_params/1)
|> Map.update(:session, %{}, &filter_params/1)
|> Map.update(:context, %{}, &filter_params/1)
end
defp filter_sensitive_data(context), do: context
# Check if a key should be filtered
defp should_filter?(key) when is_binary(key) do
key_lower = String.downcase(key)
Enum.any?(@sensitive_keys, &String.contains?(key_lower, &1))
end
defp should_filter?(key) when is_atom(key) do
should_filter?(Atom.to_string(key))
end
defp should_filter?(_), do: false
end end

View file

@ -65,6 +65,17 @@
required required
/> />
<%= unless @invitation do %>
<.input
name="user[organization_name]"
value=""
type="text"
label="Organization Name"
placeholder="My Company"
required
/>
<% end %>
<.button phx-disable-with="Creating account..." class="w-full" variant="primary"> <.button phx-disable-with="Creating account..." class="w-full" variant="primary">
{if @invitation, do: "Accept invitation and create account", else: "Create an account"} {if @invitation, do: "Accept invitation and create account", else: "Create an account"}
</.button> </.button>

View file

@ -0,0 +1,159 @@
defmodule Towerops.HoneybadgerFilterTest do
use ExUnit.Case, async: true
alias Towerops.HoneybadgerFilter
describe "filter_params/1" do
test "filters SNMP community strings" do
params = %{
"name" => "Device 1",
"snmp_community" => "public",
"ip_address" => "192.168.1.1"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["name"] == "Device 1"
assert filtered["snmp_community"] == "[FILTERED]"
assert filtered["ip_address"] == "192.168.1.1"
end
test "filters community strings with just 'community' key" do
params = %{"community" => "secret123", "device_id" => "abc"}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["community"] == "[FILTERED]"
assert filtered["device_id"] == "abc"
end
test "filters passwords" do
params = %{"email" => "user@example.com", "password" => "secret"}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["email"] == "user@example.com"
assert filtered["password"] == "[FILTERED]"
end
test "filters SNMPv3 credentials" do
params = %{
"snmp_version" => "3",
"auth_password" => "authpass",
"priv_password" => "privpass",
"username" => "admin"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["snmp_version"] == "3"
assert filtered["auth_password"] == "[FILTERED]"
assert filtered["priv_password"] == "[FILTERED]"
assert filtered["username"] == "admin"
end
test "filters API keys and tokens" do
params = %{
"api_key" => "key123",
"token" => "token456",
"secret" => "secret789"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["api_key"] == "[FILTERED]"
assert filtered["token"] == "[FILTERED]"
assert filtered["secret"] == "[FILTERED]"
end
test "filters nested params" do
params = %{
"device" => %{
"name" => "Router",
"snmp_community" => "private",
"ip_address" => "10.0.0.1"
}
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["device"]["name"] == "Router"
assert filtered["device"]["snmp_community"] == "[FILTERED]"
assert filtered["device"]["ip_address"] == "10.0.0.1"
end
test "filters atom keys" do
params = %{
name: "Device",
snmp_community: "public",
ip_address: "192.168.1.1"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered[:name] == "Device"
assert filtered[:snmp_community] == "[FILTERED]"
assert filtered[:ip_address] == "192.168.1.1"
end
test "handles case-insensitive filtering" do
params = %{
"SNMP_COMMUNITY" => "public",
"SnmpCommunity" => "private",
"API_KEY" => "secret"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered["SNMP_COMMUNITY"] == "[FILTERED]"
assert filtered["SnmpCommunity"] == "[FILTERED]"
assert filtered["API_KEY"] == "[FILTERED]"
end
test "does not filter non-sensitive data" do
params = %{
"name" => "Device",
"ip_address" => "192.168.1.1",
"description" => "Main router"
}
filtered = HoneybadgerFilter.filter_params(params)
assert filtered == params
end
end
describe "filter_context/1" do
test "filters sensitive data from context params" do
context = %{
params: %{
"device" => %{"snmp_community" => "public", "name" => "Router"}
}
}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered.params["device"]["name"] == "Router"
assert filtered.params["device"]["snmp_community"] == "[FILTERED]"
end
test "filters sensitive data from cgi_data" do
context = %{
cgi_data: %{"auth_token" => "Bearer token123", "user_agent" => "Mozilla/5.0"}
}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered.cgi_data["auth_token"] == "[FILTERED]"
assert filtered.cgi_data["user_agent"] == "Mozilla/5.0"
end
test "filters ignored registered names" do
context = %{registered_name: "memsup"}
filtered = HoneybadgerFilter.filter_context(context)
assert filtered == nil
end
end
end

View file

@ -57,5 +57,26 @@ defmodule ToweropsWeb.UserRegistrationControllerTest do
assert response =~ "Register" assert response =~ "Register"
assert response =~ "can&#39;t be blank" assert response =~ "can&#39;t be blank"
end end
test "creates organization with provided name", %{conn: conn} do
email = unique_user_email()
conn =
post(conn, ~p"/users/register", %{
"user" => %{
"email" => email,
"password" => valid_user_password(),
"organization_name" => "Acme Corp"
}
})
assert get_session(conn, :user_token)
assert redirected_to(conn) == ~p"/devices"
# Verify organization was created with the provided name
user = Towerops.Accounts.get_user_by_email(email)
[org | _] = Towerops.Organizations.list_user_organizations(user.id)
assert org.name == "Acme Corp"
end
end end
end end

View file

@ -16,7 +16,9 @@ defmodule ToweropsWeb.UserRegistrationHTMLTest do
render_to_string(ToweropsWeb.UserRegistrationHTML, "new", "html", render_to_string(ToweropsWeb.UserRegistrationHTML, "new", "html",
flash: %{}, flash: %{},
current_scope: nil, current_scope: nil,
form: form form: form,
invitation: nil,
invitation_token: nil
) )
assert html =~ "Register for an account" assert html =~ "Register for an account"
@ -24,6 +26,7 @@ defmodule ToweropsWeb.UserRegistrationHTMLTest do
assert html =~ "Log in" assert html =~ "Log in"
assert html =~ "Create an account" assert html =~ "Create an account"
assert html =~ "Password" assert html =~ "Password"
assert html =~ "Organization Name"
end end
end end
end end