defmodule TarakanWeb.BadgeController do @moduledoc """ Embeddable SVG badges for a repository's security posture. Served without authentication so a README on any host can render one, and cached briefly so an embedded badge cannot be used to hammer the database. The SVG is self-contained (no external fonts or images) because most hosts proxy and sanitize badge images. """ use TarakanWeb, :controller alias Tarakan.Repositories alias Tarakan.SecurityPosture @cache_seconds 900 def show(conn, %{"host" => host, "owner" => owner, "name" => name}) do render_badge(conn, Repositories.get_repository(host, owner, name)) end def show(conn, %{"owner" => owner, "name" => name}) do render_badge(conn, Repositories.get_repository_by_slug(owner, name)) end defp render_badge(conn, repository) do badge = case repository do %{listing_status: "listed"} = repository -> SecurityPosture.cached_badge(repository) _unlisted_or_missing -> %{label: "security", message: "not listed", color: "#5a6a72"} end conn |> put_resp_content_type("image/svg+xml") |> put_resp_header("cache-control", "public, max-age=#{@cache_seconds}") |> send_resp(200, svg(badge)) end # Flat two-part badge. Widths are estimated from character count because the # SVG carries no font metrics; 6.6px per character matches the 11px # sans-serif stack closely enough for the shape to stay tight. defp svg(%{label: label, message: message, color: color}) do label_width = text_width(label) message_width = text_width(message) total = label_width + message_width """ #{escape(label)}: #{escape(message)} #{escape(label)} #{escape(message)} """ end defp text_width(text), do: round(String.length(text) * 6.6) + 12 defp escape(value) do value |> to_string() |> String.replace("&", "&") |> String.replace("<", "<") |> String.replace(">", ">") |> String.replace("\"", """) end end