Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
139 lines
3.8 KiB
Elixir
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("&", "&")
|
|
|> String.replace("<", "<")
|
|
|> String.replace(">", ">")
|
|
|> String.replace("\"", """)
|
|
|> String.replace("'", "'")
|
|
end
|
|
end
|