Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
commit
af6077b9c3
455 changed files with 78366 additions and 0 deletions
343
lib/tarakan_web/live/infestation_live/show.ex
Normal file
343
lib/tarakan_web/live/infestation_live/show.ex
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
defmodule TarakanWeb.InfestationLive.Show do
|
||||
@moduledoc "One cross-repo infestation pattern: affected repos and instances."
|
||||
use TarakanWeb, :live_view
|
||||
|
||||
alias Tarakan.Fixes
|
||||
alias Tarakan.Infestations
|
||||
alias Tarakan.Policy
|
||||
alias TarakanWeb.RepositoryPaths
|
||||
|
||||
@graph_limit 64
|
||||
@ledger_page 50
|
||||
|
||||
@impl true
|
||||
def mount(%{"pattern_key" => pattern_key}, _session, socket) do
|
||||
infestation = Infestations.get_infestation(pattern_key)
|
||||
|
||||
if is_nil(infestation) or infestation.repo_count < 1 do
|
||||
raise Ecto.NoResultsError, queryable: Tarakan.Scans.CanonicalFinding
|
||||
end
|
||||
|
||||
graph = Infestations.list_pattern_repos_page(pattern_key, limit: @graph_limit)
|
||||
ledger = Infestations.list_instances_page(pattern_key, limit: @ledger_page)
|
||||
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, infestation.title)
|
||||
|> assign(
|
||||
:meta_description,
|
||||
"#{infestation.title}. Seen in #{infestation.repo_count} listed repositories on Tarakan."
|
||||
)
|
||||
|> assign(:canonical_path, ~p"/infestations/#{pattern_key}")
|
||||
|> assign(:pattern_key, pattern_key)
|
||||
|> assign(:infestation, infestation)
|
||||
|> assign(:open_bounties, Tarakan.Market.open_bounties_for_target(:infestation, pattern_key))
|
||||
|> assign(:graph_repos, graph.entries)
|
||||
|> assign(:graph_hidden, max(infestation.repo_count - length(graph.entries), 0))
|
||||
|> stream(:ledger_instances, ledger.entries, reset: true)
|
||||
|> assign(:ledger_cursor, ledger.next_cursor)
|
||||
|> assign(:ledger_empty?, ledger.entries == [])
|
||||
|> assign(
|
||||
:can_moderate,
|
||||
Policy.allowed?(socket.assigns.current_scope, :moderate)
|
||||
)
|
||||
|> load_fix_template(pattern_key)}
|
||||
end
|
||||
|
||||
# A pattern has a fix template only once one of its instances has actually
|
||||
# been fixed, so the whole section is absent until then.
|
||||
defp load_fix_template(socket, pattern_key) do
|
||||
case Fixes.latest_template(pattern_key) do
|
||||
nil ->
|
||||
socket
|
||||
|> assign(:fix_template, nil)
|
||||
|> assign(:propagations, [])
|
||||
|> assign(:propagation_targets, 0)
|
||||
|
||||
template ->
|
||||
socket
|
||||
|> assign(:fix_template, template)
|
||||
|> assign(:propagations, Fixes.list_propagations(template))
|
||||
|> assign(:propagation_targets, length(Fixes.propagation_targets(template)))
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("propagate_fix", _params, socket) do
|
||||
template = socket.assigns.fix_template
|
||||
|
||||
cond do
|
||||
not socket.assigns.can_moderate ->
|
||||
{:noreply, put_flash(socket, :error, "Not authorized.")}
|
||||
|
||||
is_nil(template) ->
|
||||
{:noreply, put_flash(socket, :error, "No captured fix to propagate.")}
|
||||
|
||||
true ->
|
||||
case Fixes.propagate(socket.assigns.current_scope, template) do
|
||||
{:ok, %{opened: opened, failed: failed}} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Opened #{opened} fix job(s). Failed #{failed}.")
|
||||
|> load_fix_template(socket.assigns.pattern_key)}
|
||||
|
||||
{:error, :unauthorized} ->
|
||||
{:noreply, put_flash(socket, :error, "Not authorized.")}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:noreply, put_flash(socket, :error, "Could not open fix jobs.")}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("load_more_instances", _params, socket) do
|
||||
case socket.assigns.ledger_cursor do
|
||||
nil ->
|
||||
{:noreply, socket}
|
||||
|
||||
cursor ->
|
||||
page =
|
||||
Infestations.list_instances_page(socket.assigns.pattern_key,
|
||||
limit: @ledger_page,
|
||||
cursor: cursor
|
||||
)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> stream(:ledger_instances, page.entries)
|
||||
|> assign(:ledger_cursor, page.next_cursor)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
|
||||
<Layouts.page width={:wide}>
|
||||
<.breadcrumbs id="infestation-breadcrumb">
|
||||
<:crumb navigate={~p"/"}>registry</:crumb>
|
||||
<:crumb navigate={~p"/infestations"}>infestations</:crumb>
|
||||
<:crumb>pattern</:crumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<TarakanWeb.BountyComponents.wanted_banner bounties={@open_bounties} />
|
||||
|
||||
<div class="mt-4 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div class="min-w-0 max-w-3xl">
|
||||
<h1 class="font-display text-2xl font-medium leading-tight tracking-[0.02em] text-ink sm:text-4xl">
|
||||
{@infestation.title}
|
||||
</h1>
|
||||
<p
|
||||
:if={@infestation.severity}
|
||||
class="mt-2 font-mono text-xs text-signal"
|
||||
>
|
||||
{@infestation.severity}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 grid gap-px border-2 border-strong bg-rule sm:grid-cols-4">
|
||||
<div class="bg-ground px-4 py-4">
|
||||
<p class="text-xs font-medium text-ink-faint">Repos</p>
|
||||
<p id="infestation-repo-count" class="mt-1 font-display text-3xl tabular-nums text-ink">
|
||||
{@infestation.repo_count}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-ground px-4 py-4">
|
||||
<p class="text-xs font-medium text-ink-faint">Open</p>
|
||||
<p class="mt-1 font-display text-3xl tabular-nums text-phosphor">
|
||||
{@infestation.open_count}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-ground px-4 py-4">
|
||||
<p class="text-xs font-medium text-ink-faint">Verified</p>
|
||||
<p class="mt-1 font-display text-3xl tabular-nums text-ink">
|
||||
{@infestation.verified_count}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-ground px-4 py-4">
|
||||
<p class="text-xs font-medium text-ink-faint">Fixed</p>
|
||||
<p class="mt-1 font-display text-3xl tabular-nums text-ink-muted">
|
||||
{@infestation.fixed_count}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!--
|
||||
The registry's payoff: one repository fixed this, so every other
|
||||
repository carrying the pattern can start from that fix.
|
||||
--%>
|
||||
<section
|
||||
:if={@fix_template}
|
||||
id="infestation-fix-template"
|
||||
class="mt-8 border-2 border-phosphor"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3 border-b border-rule px-4 py-3">
|
||||
<h2 class="min-w-0 flex-1 font-display text-sm uppercase tracking-[0.12em] text-phosphor">
|
||||
Fixed on
|
||||
<.link
|
||||
navigate={RepositoryPaths.repository_path(@fix_template.source_repository)}
|
||||
class="hover:underline"
|
||||
>
|
||||
{@fix_template.source_repository.owner}/{@fix_template.source_repository.name}
|
||||
</.link>
|
||||
</h2>
|
||||
<span
|
||||
:if={@propagations != []}
|
||||
class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint"
|
||||
>
|
||||
carried to {length(@propagations)} repo(s)
|
||||
</span>
|
||||
<button
|
||||
:if={@can_moderate and @propagation_targets > 0}
|
||||
id="infestation-propagate-fix"
|
||||
type="button"
|
||||
phx-click="propagate_fix"
|
||||
data-confirm={"Open fix jobs on #{@propagation_targets} repository(s) still carrying this pattern?"}
|
||||
class="shrink-0 bg-btn px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] text-btn-fg transition hover:opacity-90"
|
||||
>
|
||||
Propagate to {@propagation_targets}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="whitespace-pre-line px-4 py-3 text-sm leading-6 text-ink-muted" phx-no-format>{String.slice(@fix_template.summary, 0, 600)}</p>
|
||||
|
||||
<ul :if={@propagations != []} class="divide-y divide-rule border-t border-rule">
|
||||
<li
|
||||
:for={propagation <- @propagations}
|
||||
id={"propagation-#{propagation.id}"}
|
||||
class="flex flex-wrap items-baseline gap-x-3 gap-y-1 px-4 py-2.5"
|
||||
>
|
||||
<.link
|
||||
navigate={RepositoryPaths.repository_path(propagation.repository)}
|
||||
class="min-w-0 flex-1 truncate font-mono text-xs text-ink hover:text-signal hover:underline"
|
||||
>
|
||||
{propagation.repository.owner}/{propagation.repository.name}
|
||||
</.link>
|
||||
<.link
|
||||
:if={propagation.review_task}
|
||||
navigate={~p"/jobs/#{propagation.review_task.id}"}
|
||||
class="shrink-0 font-mono text-[11px] text-signal hover:underline"
|
||||
>
|
||||
Job →
|
||||
</.link>
|
||||
<span class={[
|
||||
"shrink-0 font-mono text-[10px] uppercase tracking-[0.12em]",
|
||||
propagation.status == "fixed" && "text-phosphor",
|
||||
propagation.status == "stale" && "text-ink-faint",
|
||||
propagation.status == "open" && "text-ink-muted"
|
||||
]}>
|
||||
{propagation.status}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="mb-3 font-display text-lg uppercase tracking-[0.02em] text-ink">
|
||||
Affected repositories
|
||||
</h2>
|
||||
<.infestation_graph
|
||||
id="infestation-graph"
|
||||
infestation={@infestation}
|
||||
repos={repo_rows(@graph_repos)}
|
||||
pattern_key={@pattern_key}
|
||||
/>
|
||||
<p
|
||||
:if={@graph_hidden > 0}
|
||||
id="infestation-graph-more"
|
||||
class="mt-3 font-mono text-[11px] text-ink-faint"
|
||||
>
|
||||
+{@graph_hidden} more repositories
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="infestation-instances" class="mt-10">
|
||||
<h2 class="mb-3 font-display text-lg uppercase tracking-[0.02em] text-ink">
|
||||
Instances
|
||||
</h2>
|
||||
|
||||
<p :if={@ledger_empty?} class="mt-4 font-mono text-xs text-ink-faint">No instances.</p>
|
||||
|
||||
<ul
|
||||
id="infestation-ledger"
|
||||
phx-update="stream"
|
||||
class="mt-4 divide-y divide-rule border-2 border-strong"
|
||||
>
|
||||
<li
|
||||
:for={{dom_id, instance} <- @streams.ledger_instances}
|
||||
id={dom_id}
|
||||
class="grid gap-2 px-4 py-4 sm:grid-cols-[auto_1fr_auto] sm:items-center sm:px-6"
|
||||
>
|
||||
<span class={[
|
||||
"size-2.5 shrink-0 rounded-full",
|
||||
instance.status == "open" && "bg-phosphor",
|
||||
instance.status == "verified" && "bg-ink",
|
||||
instance.status == "fixed" && "bg-ink-muted",
|
||||
instance.status not in ["open", "verified", "fixed"] && "bg-ink-faint"
|
||||
]}></span>
|
||||
<div class="min-w-0">
|
||||
<p class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
<.link
|
||||
:if={instance[:host]}
|
||||
navigate={RepositoryPaths.repository_path(instance)}
|
||||
class="text-ink-muted hover:text-signal hover:underline"
|
||||
>
|
||||
{instance.owner}/{instance.name}
|
||||
</.link>
|
||||
<span :if={is_nil(instance[:host])}>{instance.owner}/{instance.name}</span>
|
||||
<span class="mx-1">·</span>
|
||||
{instance.status}
|
||||
<span :if={instance[:commit_sha]} class="mx-1">·</span>
|
||||
<span :if={instance[:commit_sha]} title={instance.commit_sha}>
|
||||
{String.slice(instance.commit_sha || "", 0, 7)}
|
||||
</span>
|
||||
</p>
|
||||
<p class="mt-1 truncate font-mono text-xs text-ink-muted">
|
||||
{instance.file_path}
|
||||
</p>
|
||||
</div>
|
||||
<.link
|
||||
:if={instance.occurrence_public_id}
|
||||
navigate={~p"/findings/#{instance.occurrence_public_id}"}
|
||||
class="font-mono text-xs text-signal hover:underline"
|
||||
>
|
||||
Open →
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
:if={@ledger_cursor}
|
||||
id="infestation-load-more"
|
||||
type="button"
|
||||
phx-click="load_more_instances"
|
||||
class="mt-4 bg-panel px-4 py-2 font-mono text-[11px] uppercase tracking-[0.12em] text-ink transition hover:border-signal"
|
||||
>
|
||||
Load more
|
||||
</button>
|
||||
</section>
|
||||
</Layouts.page>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
# Repo matrix expects instance-shaped maps (status, owner, name, occurrence_public_id, id).
|
||||
# Rows are pattern_repos: one per repository, rolled up from the instances
|
||||
# listed below. instance_count/open_count are what make this table worth
|
||||
# showing separately - without them a repo hit twice looks like one hit once.
|
||||
defp repo_rows(repos) do
|
||||
Enum.map(repos, fn r ->
|
||||
%{
|
||||
id: r.repository_id,
|
||||
host: r.host,
|
||||
owner: r.owner,
|
||||
name: r.name,
|
||||
status: r.status,
|
||||
occurrence_public_id: r.occurrence_public_id,
|
||||
instance_count: r.instance_count,
|
||||
open_count: r.open_count
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue