tarakan/lib/tarakan_web/controllers/feed_controller.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

139 lines
3.8 KiB
Elixir

defmodule TarakanWeb.FeedController do
@moduledoc """
Atom 1.0 feeds over the public record.
Both feeds reuse the anonymous disclosure rules: findings come from
`Scans.list_recent_disclosed_findings/1` (listed repositories, fully public
reviews, terminal trust states), infestations from `Infestations.list_infestations/1`
(public canonical findings on listed repositories only).
"""
use TarakanWeb, :controller
alias Tarakan.Infestations
alias Tarakan.Scans
@feed_limit 50
@cache_control "public, max-age=300"
def findings(conn, _params) do
base = TarakanWeb.Endpoint.url()
entries =
[limit: @feed_limit]
|> Scans.list_recent_disclosed_findings()
|> Enum.map(fn %{canonical: canonical, repository: repository} = row ->
url = base <> ~p"/findings/#{row.occurrence_public_id}"
%{
id: url,
link: url,
title: canonical.title,
updated: canonical.updated_at,
summary:
"#{String.capitalize(canonical.severity)} in " <>
"#{repository.owner}/#{repository.name} (#{canonical.file_path}): " <>
truncate(canonical.description, 240)
}
end)
send_feed(conn, "Tarakan findings", "#{base}/feeds/findings.xml", entries)
end
def infestations(conn, _params) do
base = TarakanWeb.Endpoint.url()
entries =
[days: 30, limit: @feed_limit]
|> Infestations.list_infestations()
|> Enum.map(fn pattern ->
url = base <> ~p"/infestations/#{pattern.pattern_key}"
%{
id: url,
link: url,
title: pattern.title,
updated: pattern.last_seen_at || pattern.first_seen_at || DateTime.utc_now(),
summary:
"Seen in #{pattern.repo_count} repositories over 30 days: " <>
"#{pattern.open_count} open, #{pattern.verified_count} verified, " <>
"#{pattern.fixed_count} fixed, #{pattern.disputed_count} disputed."
}
end)
send_feed(conn, "Tarakan infestations", "#{base}/feeds/infestations.xml", entries)
end
defp send_feed(conn, title, self_url, entries) do
base = TarakanWeb.Endpoint.url()
updated =
entries |> Enum.map(& &1.updated) |> Enum.max(DateTime, fn -> DateTime.utc_now() end)
body = [
~s(<?xml version="1.0" encoding="UTF-8"?>),
~s(<feed xmlns="http://www.w3.org/2005/Atom">),
"<title>",
xml_escape(title),
"</title>",
"<id>",
xml_escape(self_url),
"</id>",
~s(<link rel="self" href="),
xml_escape(self_url),
~s("/>),
~s(<link rel="alternate" href="),
xml_escape(base),
~s("/>),
"<updated>",
rfc3339(updated),
"</updated>",
Enum.map(entries, &entry_xml/1),
"</feed>"
]
conn
|> put_resp_content_type("application/atom+xml")
|> put_resp_header("cache-control", @cache_control)
|> send_resp(200, body)
end
defp entry_xml(entry) do
[
"<entry>",
"<id>",
xml_escape(entry.id),
"</id>",
"<title>",
xml_escape(entry.title),
"</title>",
"<updated>",
rfc3339(entry.updated),
"</updated>",
~s(<link href="),
xml_escape(entry.link),
~s("/>),
"<summary>",
xml_escape(entry.summary),
"</summary>",
"</entry>"
]
end
defp rfc3339(%DateTime{} = datetime) do
datetime |> DateTime.truncate(:second) |> DateTime.to_iso8601()
end
defp truncate(text, max) when byte_size(text) <= max, do: text
defp truncate(text, max), do: String.slice(text, 0, max - 1) <> ""
defp xml_escape(nil), do: ""
defp xml_escape(text) do
text
|> String.replace("&", "&amp;")
|> String.replace("<", "&lt;")
|> String.replace(">", "&gt;")
|> String.replace("\"", "&quot;")
|> String.replace("'", "&apos;")
end
end