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""" <.breadcrumbs id="infestation-breadcrumb"> <:crumb navigate={~p"/"}>registry <:crumb navigate={~p"/infestations"}>infestations <:crumb>pattern

{@infestation.title}

{@infestation.severity}

Repos

{@infestation.repo_count}

Open

{@infestation.open_count}

Verified

{@infestation.verified_count}

Fixed

{@infestation.fixed_count}

<%!-- The registry's payoff: one repository fixed this, so every other repository carrying the pattern can start from that fix. --%>

Fixed on <.link navigate={RepositoryPaths.repository_path(@fix_template.source_repository)} class="hover:underline" > {@fix_template.source_repository.owner}/{@fix_template.source_repository.name}

carried to {length(@propagations)} repo(s)

{String.slice(@fix_template.summary, 0, 600)}

  • <.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 :if={propagation.review_task} navigate={~p"/jobs/#{propagation.review_task.id}"} class="shrink-0 font-mono text-[11px] text-signal hover:underline" > Job → {propagation.status}

Affected repositories

<.infestation_graph id="infestation-graph" infestation={@infestation} repos={repo_rows(@graph_repos)} pattern_key={@pattern_key} />

0} id="infestation-graph-more" class="mt-3 font-mono text-[11px] text-ink-faint" > +{@graph_hidden} more repositories

Instances

No instances.

  • <.link :if={instance[:host]} navigate={RepositoryPaths.repository_path(instance)} class="text-ink-muted hover:text-signal hover:underline" > {instance.owner}/{instance.name} {instance.owner}/{instance.name} · {instance.status} · {String.slice(instance.commit_sha || "", 0, 7)}

    {instance.file_path}

    <.link :if={instance.occurrence_public_id} navigate={~p"/findings/#{instance.occurrence_public_id}"} class="font-mono text-xs text-signal hover:underline" > Open →
""" 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