fix: humanize raw error strings in integration sync modules

Replace binary passthrough in humanize_sync_error/1 across all 6 sync
modules (Gaiia, VISP, NetBox, Preseem, Splynx, Sonar) so that raw
Erlang/SSL error messages never surface in the UI. SSL/CA cert errors
get a clean message; all other raw strings are logged and replaced with
a generic user-facing message.
This commit is contained in:
Graham McIntire 2026-03-12 13:53:28 -05:00
parent ecc69b4e96
commit ad4fde9415
No known key found for this signature in database
8 changed files with 78 additions and 8 deletions

View file

@ -214,7 +214,14 @@ defmodule Towerops.Gaiia.Sync do
"Connection error — unable to establish a secure connection to Gaiia"
end
defp humanize_sync_error(reason) when is_binary(reason), do: reason
defp humanize_sync_error(reason) when is_binary(reason) do
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
"SSL certificate verification failed — unable to establish a secure connection"
else
Logger.warning("Sync failed with raw error: #{reason}")
"An unexpected error occurred during sync"
end
end
defp humanize_sync_error({:graphql_errors, errors}) when is_list(errors) do
messages = Enum.map_join(errors, "; ", &(&1["message"] || inspect(&1)))

View file

@ -240,7 +240,14 @@ defmodule Towerops.NetBox.Sync do
"Connection error — unable to establish a secure connection to NetBox"
end
defp humanize_sync_error(reason) when is_binary(reason), do: reason
defp humanize_sync_error(reason) when is_binary(reason) do
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
"SSL certificate verification failed — unable to establish a secure connection"
else
Logger.warning("Sync failed with raw error: #{reason}")
"An unexpected error occurred during sync"
end
end
defp humanize_sync_error({:unexpected_status, status}), do: "NetBox returned unexpected HTTP #{status}"

View file

@ -174,6 +174,14 @@ defmodule Towerops.Preseem.Sync do
defp humanize_sync_error(:timeout), do: "Connection timed out — Preseem may be temporarily unavailable"
defp humanize_sync_error(reason) when is_binary(reason), do: reason
defp humanize_sync_error(reason) when is_binary(reason) do
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
"SSL certificate verification failed — unable to establish a secure connection"
else
Logger.warning("Sync failed with raw error: #{reason}")
"An unexpected error occurred during sync"
end
end
defp humanize_sync_error(_reason), do: "An unexpected error occurred during sync"
end

View file

@ -74,7 +74,14 @@ defmodule Towerops.Sonar.Sync do
"GraphQL errors: #{messages}"
end
defp humanize_sync_error(reason) when is_binary(reason), do: reason
defp humanize_sync_error(reason) when is_binary(reason) do
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
"SSL certificate verification failed — unable to establish a secure connection"
else
Logger.warning("Sync failed with raw error: #{reason}")
"An unexpected error occurred during sync"
end
end
defp humanize_sync_error(reason) do
Logger.warning("Sonar sync failed with unexpected error: #{inspect(reason)}")

View file

@ -67,7 +67,15 @@ defmodule Towerops.Splynx.Sync do
defp humanize_sync_error(:forbidden), do: "Access denied — your API credentials may not have sufficient permissions"
defp humanize_sync_error({:rate_limited, retry_after}), do: "Rate limited by Splynx — retry after #{retry_after}s"
defp humanize_sync_error({:unexpected_status, status}), do: "Splynx returned unexpected HTTP #{status}"
defp humanize_sync_error(reason) when is_binary(reason), do: reason
defp humanize_sync_error(reason) when is_binary(reason) do
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
"SSL certificate verification failed — unable to establish a secure connection"
else
Logger.warning("Sync failed with raw error: #{reason}")
"An unexpected error occurred during sync"
end
end
defp humanize_sync_error(reason) do
Logger.warning("Splynx sync failed with unexpected error: #{inspect(reason)}")

View file

@ -70,7 +70,15 @@ defmodule Towerops.Visp.Sync do
defp humanize_sync_error(:forbidden), do: "Access denied — your API key may not have sufficient permissions"
defp humanize_sync_error({:rate_limited, retry_after}), do: "Rate limited by VISP — retry after #{retry_after}s"
defp humanize_sync_error({:unexpected_status, status}), do: "VISP returned unexpected HTTP #{status}"
defp humanize_sync_error(reason) when is_binary(reason), do: reason
defp humanize_sync_error(reason) when is_binary(reason) do
if String.contains?(reason, ["CA trust store", "cacert", "certificate"]) do
"SSL certificate verification failed — unable to establish a secure connection"
else
Logger.warning("Sync failed with raw error: #{reason}")
"An unexpected error occurred during sync"
end
end
defp humanize_sync_error(reason) do
Logger.warning("VISP sync failed with unexpected error: #{inspect(reason)}")

View file

@ -100,6 +100,32 @@ defmodule Towerops.Gaiia.SyncTest do
assert updated.last_sync_status == "failed"
end
test "humanizes SSL/CA certificate errors in sync message", %{integration: integration} do
Req.Test.stub(Client, fn _conn ->
raise "default CA trust store not available; please add `:castore` to your project's dependencies"
end)
assert {:error, _} = Sync.sync_organization(integration)
updated = Repo.reload!(integration)
assert updated.last_sync_status == "failed"
assert updated.last_sync_message ==
"SSL certificate verification failed — unable to establish a secure connection"
end
test "does not leak raw error strings in sync message", %{integration: integration} do
Req.Test.stub(Client, fn _conn ->
raise "some internal server stacktrace details"
end)
assert {:error, _} = Sync.sync_organization(integration)
updated = Repo.reload!(integration)
assert updated.last_sync_status == "failed"
assert updated.last_sync_message == "An unexpected error occurred during sync"
end
test "handles API that returns null when after variable is null", %{
org: _org,
integration: integration

View file

@ -283,8 +283,7 @@ defmodule Towerops.Visp.SyncTest do
updated_integration = Integrations.get_integration!(org.id, "visp")
assert updated_integration.last_sync_status == "failed"
# Error message should contain the exception details
assert updated_integration.last_sync_message =~ "Connection refused"
assert updated_integration.last_sync_message == "An unexpected error occurred during sync"
end
end
end