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
611
lib/tarakan_web/live/finding_live/show.html.heex
Normal file
611
lib/tarakan_web/live/finding_live/show.html.heex
Normal file
|
|
@ -0,0 +1,611 @@
|
|||
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
|
||||
<Layouts.page>
|
||||
<.breadcrumbs id="finding-breadcrumb">
|
||||
<:crumb navigate={~p"/"}>registry</:crumb>
|
||||
<:crumb navigate={TarakanWeb.RepositoryPaths.repository_security_path(@repository)}>
|
||||
{@repository.owner}/{@repository.name}
|
||||
</:crumb>
|
||||
<:crumb>finding</:crumb>
|
||||
</.breadcrumbs>
|
||||
|
||||
<TarakanWeb.BountyComponents.wanted_banner bounties={@open_bounties} />
|
||||
|
||||
<%!-- Status + trust chips --%>
|
||||
<% canonical = @finding.canonical_finding %>
|
||||
<% cwe_url = canonical && TarakanWeb.FindingPresentation.cwe_url(canonical.cwe_id) %>
|
||||
<% cve_url = canonical && TarakanWeb.FindingPresentation.cve_url(canonical.cve_id) %>
|
||||
<%!-- Chips are reserved for the two things that rank this finding against
|
||||
every other one: how bad it is, and whether anyone has confirmed it.
|
||||
Everything else - trust signals, references, run counts - is evidence,
|
||||
and evidence reads better as a line than as a wall of badges. --%>
|
||||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||
<.notch_badge id="finding-severity" class="bg-signal text-ground">
|
||||
{@finding.severity}
|
||||
</.notch_badge>
|
||||
<.notch_badge
|
||||
:if={@finding.canonical_finding && @finding.canonical_finding.status == "verified"}
|
||||
id="finding-verified-badge"
|
||||
class="bg-ink text-ground"
|
||||
>
|
||||
Verified
|
||||
</.notch_badge>
|
||||
<.notch_badge
|
||||
:if={
|
||||
@finding.canonical_finding && @finding.canonical_finding.status not in [nil, "verified"]
|
||||
}
|
||||
class="text-ink-muted"
|
||||
>
|
||||
{@finding.canonical_finding.status}
|
||||
</.notch_badge>
|
||||
<.notch_badge
|
||||
:if={canonical && canonical.reproductions_count > 0}
|
||||
id="finding-reproduced-badge"
|
||||
class="bg-ink text-ground"
|
||||
>
|
||||
Reproduced
|
||||
</.notch_badge>
|
||||
<.notch_badge id="finding-disclosure-badge" class="bg-ink text-ground">
|
||||
{TarakanWeb.FindingPresentation.disclosure_badge(@scan.visibility, @scan.provenance)}
|
||||
</.notch_badge>
|
||||
</div>
|
||||
|
||||
<%!-- The submitter's severity claim and a rubric-scored second opinion are
|
||||
different statements. Showing only one of them hides a disagreement
|
||||
that is itself information about the submitter. --%>
|
||||
<p
|
||||
:if={
|
||||
canonical && canonical.calibrated_severity &&
|
||||
canonical.calibrated_severity != @finding.severity
|
||||
}
|
||||
id="finding-severity-calibration"
|
||||
class="mt-2 font-mono text-[11px] text-ink-faint"
|
||||
>
|
||||
submitter said <span class="text-ink-muted">{@finding.severity}</span>
|
||||
· rescored <span class="text-ink">{canonical.calibrated_severity}</span>
|
||||
<span :if={canonical.severity_rubric}>({canonical.severity_rubric})</span>
|
||||
</p>
|
||||
|
||||
<p
|
||||
id="finding-evidence-line"
|
||||
class="mt-2 flex flex-wrap items-center gap-x-1.5 font-mono text-[11px] text-ink-faint"
|
||||
>
|
||||
<span class="text-ink-muted" title={@scan.review_status}>
|
||||
{review_kind_label(@scan.review_kind)}
|
||||
</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{TarakanWeb.FindingPresentation.status_blurb(@scan.review_status)}</span>
|
||||
<span :if={@finding.canonical_finding} id="finding-canonical-memory">
|
||||
<span aria-hidden="true">·</span>
|
||||
detected in {@finding.canonical_finding.detections_count}
|
||||
{if @finding.canonical_finding.detections_count == 1, do: "run", else: "runs"}
|
||||
</span>
|
||||
<span :if={@finding_trust.agent_reproduced} id="finding-trust-agent">
|
||||
<span aria-hidden="true">·</span> agent reproduced
|
||||
</span>
|
||||
<span
|
||||
:if={@finding_trust.agent_disputed}
|
||||
id="finding-trust-agent-disputed"
|
||||
class="text-signal"
|
||||
>
|
||||
<span aria-hidden="true">·</span> agent disputed
|
||||
</span>
|
||||
<span :if={@finding_trust.human_checked} id="finding-trust-human">
|
||||
<span aria-hidden="true">·</span> human checked
|
||||
</span>
|
||||
<span :if={canonical && canonical.reproductions_count > 0} id="finding-reproductions">
|
||||
<span aria-hidden="true">·</span> reproduced {canonical.reproductions_count}
|
||||
{if canonical.reproductions_count == 1, do: "time", else: "times"}
|
||||
</span>
|
||||
<%!-- Surviving attacks is a stronger claim than collecting agreements, so
|
||||
it is reported as attempts survived rather than as a confirmation. --%>
|
||||
<span :if={canonical && canonical.refutations_count > 0} id="finding-refutations">
|
||||
<span aria-hidden="true">·</span>
|
||||
survived {canonical.survived_refutations_count} of {canonical.refutations_count}
|
||||
{if canonical.refutations_count == 1, do: "attack", else: "attacks"}
|
||||
</span>
|
||||
<span :if={cwe_url || cve_url} aria-hidden="true">·</span>
|
||||
<.link
|
||||
:if={cwe_url}
|
||||
id="finding-cwe-badge"
|
||||
href={cwe_url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-ink-muted underline decoration-rule underline-offset-2 transition hover:text-signal"
|
||||
>
|
||||
{canonical.cwe_id}
|
||||
</.link>
|
||||
<.link
|
||||
:if={cve_url}
|
||||
id="finding-cve-badge"
|
||||
href={cve_url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-ink-muted underline decoration-rule underline-offset-2 transition hover:text-signal"
|
||||
>
|
||||
{canonical.cve_id}
|
||||
</.link>
|
||||
</p>
|
||||
|
||||
<%!-- Title + vote --%>
|
||||
<div class="mt-4 flex items-start gap-3">
|
||||
<.vote_control
|
||||
subject_type="canonical_finding"
|
||||
subject_id={@finding.canonical_finding.id}
|
||||
summary={@finding_votes}
|
||||
can_vote={@can_vote}
|
||||
class="mt-1 shrink-0"
|
||||
/>
|
||||
<h1
|
||||
id="finding-title"
|
||||
class="min-w-0 break-words font-display text-2xl font-medium leading-tight tracking-[0.02em] text-ink sm:text-3xl"
|
||||
>
|
||||
{@finding.title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex min-w-0 flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<.link
|
||||
id="finding-source-link"
|
||||
navigate={~p"/findings/#{@finding.public_id}/code"}
|
||||
class="inline-flex max-w-full min-w-0 items-center gap-1.5 border border-strong bg-panel px-3 py-2 font-mono text-xs text-ink transition hover:border-signal hover:text-signal"
|
||||
aria-label={"Open #{@finding.file_path} at the finding's pinned commit"}
|
||||
>
|
||||
<span class="min-w-0 break-all">
|
||||
{@finding.file_path}{finding_lines(@finding)}
|
||||
</span>
|
||||
<.icon name="hero-arrow-right" class="size-3.5 shrink-0" />
|
||||
</.link>
|
||||
<span class="font-mono text-[11px] text-ink-faint" title={@scan.commit_sha}>
|
||||
@ {String.slice(@scan.commit_sha || "", 0, 7)}
|
||||
</span>
|
||||
<button
|
||||
id="finding-copy-link"
|
||||
type="button"
|
||||
phx-hook="CopyLink"
|
||||
data-copy-text={@public_url}
|
||||
data-copied-label="Copied"
|
||||
class="inline-flex items-center gap-1.5 border border-strong px-3 py-2 font-mono text-[11px] uppercase tracking-[0.12em] text-ink-muted transition hover:border-signal hover:text-signal"
|
||||
>
|
||||
<.icon name="hero-link" class="size-3.5" />
|
||||
<span data-copy-label>Copy link</span>
|
||||
</button>
|
||||
</div>
|
||||
<p id="finding-cite" class="mt-2 max-w-3xl break-all font-mono text-[10px] text-ink-faint">
|
||||
{@public_url}
|
||||
</p>
|
||||
|
||||
<%!-- What / why (main content) --%>
|
||||
<% structured = TarakanWeb.FindingPresentation.structure_description(@finding.description) %>
|
||||
<section id="finding-description" class="mt-8 max-w-3xl">
|
||||
<h2 class="font-display text-sm uppercase tracking-[0.12em] text-ink-faint">
|
||||
What & why
|
||||
</h2>
|
||||
<div class="mt-3 space-y-4">
|
||||
<p
|
||||
:if={structured.lead != ""}
|
||||
class="whitespace-pre-line border-l-2 border-signal pl-4 text-sm leading-7 text-ink"
|
||||
phx-no-format
|
||||
>{structured.lead}</p>
|
||||
<div
|
||||
:for={{label, body} <- structured.sections}
|
||||
class="bg-panel px-4 py-3"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-ink">
|
||||
{label}
|
||||
</h3>
|
||||
<p class="mt-1.5 whitespace-pre-line text-sm leading-6 text-ink-muted" phx-no-format>{body}</p>
|
||||
</div>
|
||||
<p
|
||||
:if={structured.lead == "" and structured.sections == []}
|
||||
class="whitespace-pre-line border-l-2 border-signal pl-4 text-sm leading-7 text-ink-muted"
|
||||
phx-no-format
|
||||
>{@finding.description}</p>
|
||||
<div
|
||||
:if={canonical.reproduction_steps}
|
||||
id="finding-reproduction"
|
||||
class="bg-panel px-4 py-3"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-ink">
|
||||
Reproduction
|
||||
</h3>
|
||||
<pre
|
||||
class="mt-1.5 overflow-x-auto whitespace-pre-wrap font-mono text-xs leading-6 text-ink-muted"
|
||||
phx-no-format
|
||||
>{canonical.reproduction_steps}</pre>
|
||||
</div>
|
||||
<div
|
||||
:if={canonical.affected_versions}
|
||||
id="finding-affected-versions"
|
||||
class="bg-panel px-4 py-3"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-ink">
|
||||
Affected versions
|
||||
</h3>
|
||||
<p class="mt-1.5 font-mono text-xs leading-6 text-ink-muted">
|
||||
{canonical.affected_versions}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- At a glance meta --%>
|
||||
<section id="finding-review-record" class="mt-8">
|
||||
<h2 class="font-display text-sm uppercase tracking-[0.12em] text-ink-faint">
|
||||
At a glance
|
||||
</h2>
|
||||
<div class="mt-3 grid gap-px border-2 border-strong bg-rule sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="bg-ground px-4 py-3">
|
||||
<p class="text-xs font-medium text-ink-faint">Commit</p>
|
||||
<p class="mt-1 font-mono text-sm tabular-nums text-ink" title={@scan.commit_sha}>
|
||||
{String.slice(@scan.commit_sha || "", 0, 12)}
|
||||
</p>
|
||||
<p :if={@scan.commit_committed_at} class="mt-0.5 font-mono text-[10px] text-ink-faint">
|
||||
{record_time(@scan.commit_committed_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-ground px-4 py-3">
|
||||
<p class="text-xs font-medium text-ink-faint">How made</p>
|
||||
<p id="finding-provenance" class="mt-1 text-sm text-ink">
|
||||
{TarakanWeb.FindingPresentation.how_made_label(@scan.provenance)}
|
||||
<span class="text-ink-faint"> (claim)</span>
|
||||
</p>
|
||||
<p :if={@scan.model} class="mt-0.5 font-mono text-[10px] text-ink-faint">
|
||||
{@scan.model}
|
||||
<span :if={@scan.prompt_version}> · {@scan.prompt_version}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-ground px-4 py-3">
|
||||
<p class="text-xs font-medium text-ink-faint">
|
||||
Submitted
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-ink">
|
||||
<.handle_link handle={@scan.submitted_by.handle} />
|
||||
</p>
|
||||
<p class="mt-0.5 font-mono text-[10px] text-ink-faint">
|
||||
{record_time(@scan.inserted_at)}
|
||||
</p>
|
||||
</div>
|
||||
<%!--
|
||||
One first finder per issue, permanently. Later reports corroborate
|
||||
it but never displace the credit.
|
||||
--%>
|
||||
<div :if={@first_finder} id="finding-first-finder" class="bg-ground px-4 py-3">
|
||||
<p class="text-xs font-medium text-ink-faint">First found by</p>
|
||||
<p class="mt-1 text-sm text-ink">
|
||||
<.handle_link handle={@first_finder.handle} />
|
||||
</p>
|
||||
<p class="mt-0.5 font-mono text-[10px] text-ink-faint">
|
||||
{record_time(@first_finder.reported_at)} · {String.slice(
|
||||
@first_finder.commit_sha || "",
|
||||
0,
|
||||
7
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-ground px-4 py-3">
|
||||
<p class="text-xs font-medium text-ink-faint">Checks</p>
|
||||
<p id="finding-verdict-counts" class="mt-1 font-mono text-sm text-ink">
|
||||
{@finding.canonical_finding.confirmations_count} confirmed · {@finding.canonical_finding.disputes_count} disputed
|
||||
</p>
|
||||
<p class="mt-0.5 font-mono text-[10px] text-ink-faint">
|
||||
status {@finding.canonical_finding.status}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@review_stake || @scan.reviewed_at || @scan.notes}
|
||||
class="mt-3 space-y-2 text-xs text-ink-muted"
|
||||
>
|
||||
<p :if={@review_stake} id="finding-stake" class="font-mono">
|
||||
<span class="text-ink-faint">Stake</span>
|
||||
<span class={[
|
||||
"ml-2 tabular-nums",
|
||||
@review_stake.status == :slashed && "text-signal",
|
||||
@review_stake.status == :returned && "text-quote",
|
||||
@review_stake.status == :at_risk && "text-ink"
|
||||
]}>
|
||||
{@review_stake.amount} · {stake_label(@review_stake.status)}
|
||||
</span>
|
||||
</p>
|
||||
<p :if={@scan.reviewed_at} class="font-mono text-ink-faint">
|
||||
Disclosed {record_time(@scan.reviewed_at)}
|
||||
</p>
|
||||
<div :if={@scan.notes} class="max-w-3xl border border-rule bg-panel px-3 py-2">
|
||||
<p class="text-xs font-semibold text-ink-muted">
|
||||
Report summary
|
||||
</p>
|
||||
<% notes = TarakanWeb.FindingPresentation.humanize_notes(@scan.notes) %>
|
||||
<div :if={notes && notes.kind == :summary} class="mt-1">
|
||||
<p class="text-ink">
|
||||
{notes.count} {if notes.count == 1, do: "finding", else: "findings"} in this report.
|
||||
</p>
|
||||
<ol :if={notes.tops != []} class="mt-1 list-decimal space-y-0.5 pl-5 text-ink-muted">
|
||||
<li :for={top <- notes.tops}>{top}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p
|
||||
:if={notes && notes.kind == :plain}
|
||||
class="mt-1 whitespace-pre-line text-ink-muted"
|
||||
phx-no-format
|
||||
>{notes.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Lifecycle timeline --%>
|
||||
<% events = TarakanWeb.FindingPresentation.timeline_events(canonical, @finding_checks) %>
|
||||
<section id="finding-timeline" class="mt-8 max-w-3xl">
|
||||
<h2 class="font-display text-sm uppercase tracking-[0.12em] text-ink-faint">
|
||||
Lifecycle
|
||||
</h2>
|
||||
<ol class="mt-3 border-l-2 border-rule">
|
||||
<li
|
||||
:for={event <- events}
|
||||
id={"finding-timeline-#{event.id}"}
|
||||
class="relative pb-4 pl-4 last:pb-0"
|
||||
>
|
||||
<span class="absolute top-1.5 -left-[5px] size-2 bg-ink-faint"></span>
|
||||
<p class="text-sm leading-5 text-ink">
|
||||
{event.title}
|
||||
<span
|
||||
:if={event.detail}
|
||||
class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint"
|
||||
>
|
||||
· {event.detail}
|
||||
</span>
|
||||
</p>
|
||||
<p class="mt-0.5 font-mono text-[10px] text-ink-faint">
|
||||
{record_time(event.at)}
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<.form
|
||||
:if={@can_record_vendor_notification}
|
||||
for={@vendor_form}
|
||||
id="finding-vendor-form"
|
||||
phx-submit="record_vendor_notification"
|
||||
class="mt-4 flex flex-wrap items-end gap-2 border border-strong bg-panel px-4 py-3"
|
||||
>
|
||||
<.input
|
||||
field={@vendor_form[:notified_on]}
|
||||
type="date"
|
||||
label="Vendor notified on (empty clears)"
|
||||
class="border border-strong bg-ground px-3 py-2 font-mono text-xs text-ink placeholder:text-ink-faint focus:border-signal focus:outline-none"
|
||||
/>
|
||||
<.button size="sm">Record</.button>
|
||||
</.form>
|
||||
</section>
|
||||
|
||||
<%!-- Independent checks --%>
|
||||
<section id="finding-verification" class="mt-10">
|
||||
<div class="flex flex-wrap items-end justify-between gap-3 border-b-2 border-strong pb-3">
|
||||
<h2 class="font-display text-lg uppercase tracking-[0.02em] text-ink">
|
||||
Checks
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<%!-- Hidden duplicate for tests that query counts only inside verification; primary counts live in glance grid --%>
|
||||
<div class="sr-only" aria-hidden="true">
|
||||
{@finding.canonical_finding.confirmations_count} confirmed · {@finding.canonical_finding.disputes_count} disputed
|
||||
</div>
|
||||
|
||||
<p
|
||||
:if={@finding_checks == []}
|
||||
id="finding-no-checks"
|
||||
class="py-6 text-center text-sm text-ink-faint"
|
||||
>
|
||||
No checks at this commit yet.
|
||||
</p>
|
||||
|
||||
<div
|
||||
:if={@finding_checks != []}
|
||||
id="finding-checks"
|
||||
class="divide-y divide-rule border-y border-rule"
|
||||
>
|
||||
<article :for={check <- @finding_checks} id={"finding-check-#{check.id}"} class="py-4">
|
||||
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<.handle_link handle={check.account.handle} class="font-mono text-xs font-semibold" />
|
||||
<span class={[
|
||||
"font-mono text-[10px] uppercase tracking-[0.12em]",
|
||||
check.verdict == "confirmed" && "text-quote",
|
||||
check.verdict == "disputed" && "text-signal",
|
||||
check.verdict == "fixed" && "text-quote"
|
||||
]}>
|
||||
{check.verdict}
|
||||
</span>
|
||||
<span class="font-mono text-[10px] text-ink-faint">
|
||||
· {provenance_label(check.provenance)} · {record_time(check.inserted_at)}
|
||||
</span>
|
||||
<span class={[
|
||||
"ml-auto font-mono text-[10px] uppercase tracking-[0.12em]",
|
||||
check.counts_toward_quorum && "text-ink-muted",
|
||||
!check.counts_toward_quorum && "text-ink-faint"
|
||||
]}>
|
||||
{if check.counts_toward_quorum,
|
||||
do: "counts toward quorum",
|
||||
else: "corroboration only"}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 whitespace-pre-line text-sm leading-6 text-ink-muted" phx-no-format>{check.notes}</p>
|
||||
<details :if={check.evidence} class="mt-2">
|
||||
<summary class="cursor-pointer font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint hover:text-ink">
|
||||
Evidence
|
||||
</summary>
|
||||
<pre class="mt-2 overflow-x-auto border border-rule bg-panel px-3 py-2 font-mono text-[11px] leading-5 text-ink-muted">{check.evidence}</pre>
|
||||
</details>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<%!-- Queue an agent instead of doing it yourself. Each of these asks for
|
||||
a different kind of evidence than "somebody agreed": an attack that
|
||||
failed, a reproduction that ran, a score against a rubric. --%>
|
||||
<div
|
||||
:if={@can_open_finding_job}
|
||||
id="finding-agent-jobs"
|
||||
class="mt-5 flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<span class="mr-1 font-mono text-[11px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
Open a job
|
||||
</span>
|
||||
<.button
|
||||
id="finding-job-refute"
|
||||
type="button"
|
||||
size="sm"
|
||||
phx-click="open_finding_job"
|
||||
phx-value-kind="refute_finding"
|
||||
>
|
||||
Refute
|
||||
</.button>
|
||||
<.button
|
||||
id="finding-job-reproduce"
|
||||
type="button"
|
||||
size="sm"
|
||||
phx-click="open_finding_job"
|
||||
phx-value-kind="reproduce_finding"
|
||||
>
|
||||
Reproduce
|
||||
</.button>
|
||||
<.button
|
||||
id="finding-job-calibrate"
|
||||
type="button"
|
||||
size="sm"
|
||||
phx-click="open_finding_job"
|
||||
phx-value-kind="calibrate_severity"
|
||||
>
|
||||
Re-score
|
||||
</.button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={@can_check}
|
||||
id="finding-verdict"
|
||||
class="mt-5 border border-strong bg-panel px-4 py-4"
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-ink">
|
||||
Record a check
|
||||
</h3>
|
||||
<form
|
||||
id="finding-verdict-form"
|
||||
phx-submit="record_finding_verdict"
|
||||
class="mt-2 flex flex-wrap gap-2"
|
||||
>
|
||||
<input
|
||||
id="finding-verdict-notes"
|
||||
type="text"
|
||||
name="notes"
|
||||
required
|
||||
minlength="20"
|
||||
maxlength="2000"
|
||||
placeholder="Evidence notes (required, 20+ chars)"
|
||||
autocomplete="off"
|
||||
class="min-w-0 flex-1 basis-48 border border-strong bg-ground px-3 py-2 font-mono text-xs text-ink placeholder:text-ink-faint focus:border-signal focus:outline-none"
|
||||
/>
|
||||
<.button name="verdict" value="confirmed" size="sm">Confirm</.button>
|
||||
<.button name="verdict" value="disputed" variant="danger" size="sm">
|
||||
Dispute
|
||||
</.button>
|
||||
<.button name="verdict" value="fixed" size="sm">Fixed</.button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<.link
|
||||
:if={is_nil(@current_scope) or is_nil(@current_scope.account)}
|
||||
id="finding-verify-login"
|
||||
navigate={~p"/accounts/log-in"}
|
||||
class="mt-4 block font-mono text-xs text-signal hover:underline"
|
||||
>
|
||||
Sign in to confirm or dispute this finding.
|
||||
</.link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Discussion --%>
|
||||
<section id="finding-discussion" class="mt-10">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2 border-b-2 border-strong pb-3">
|
||||
<h2 class="font-display text-lg uppercase tracking-[0.02em] text-ink">
|
||||
Discussion
|
||||
</h2>
|
||||
<span id="finding-comment-count" class="font-mono text-[10px] tabular-nums text-ink-faint">
|
||||
{@comment_count} {if @comment_count == 1, do: "comment", else: "comments"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<form
|
||||
:if={@current_scope && @current_scope.account}
|
||||
id="finding-comment-form"
|
||||
phx-submit="post_comment"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<textarea
|
||||
id="finding-comment-body"
|
||||
name="body"
|
||||
rows="3"
|
||||
placeholder="Add evidence, a counter-argument, or context…"
|
||||
class="w-full border border-strong bg-transparent px-3 py-2 font-mono text-xs text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-phosphor"
|
||||
></textarea>
|
||||
<div>
|
||||
<.button variant="primary" size="sm">Post comment</.button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<.link
|
||||
:if={is_nil(@current_scope) or is_nil(@current_scope.account)}
|
||||
id="finding-comment-login"
|
||||
navigate={~p"/accounts/log-in"}
|
||||
class="block font-mono text-xs text-signal hover:underline"
|
||||
>
|
||||
Sign in to join the discussion.
|
||||
</.link>
|
||||
|
||||
<p
|
||||
:if={@comments == []}
|
||||
id="finding-no-comments"
|
||||
class="mt-6 text-center font-mono text-xs text-ink-faint"
|
||||
>
|
||||
No comments yet.
|
||||
</p>
|
||||
|
||||
<div :if={@comments != []} class="mt-5 space-y-4">
|
||||
<.comment_thread
|
||||
:for={comment <- @comments}
|
||||
comment={comment}
|
||||
reply_to={@reply_to}
|
||||
can_reply={!is_nil(@current_scope) and !is_nil(@current_scope.account)}
|
||||
can_moderate={@can_moderate_comments}
|
||||
can_vote={@can_vote}
|
||||
votes={@comment_votes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Technical dump last --%>
|
||||
<details
|
||||
:if={@scan.raw_document}
|
||||
id="finding-raw-report"
|
||||
class="mt-10 border-2 border-strong"
|
||||
>
|
||||
<summary class="cursor-pointer bg-panel px-4 py-3 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint transition hover:text-ink">
|
||||
Technical · raw report JSON
|
||||
<span class="ml-2 normal-case tracking-normal text-ink-faint/80">
|
||||
full report, not only this finding
|
||||
</span>
|
||||
</summary>
|
||||
<pre class="max-h-[24rem] overflow-auto border-t border-rule px-4 py-4 font-mono text-[11px] leading-5 text-ink-muted">{raw_report(@scan)}</pre>
|
||||
</details>
|
||||
|
||||
<p class="mt-8 font-mono text-xs">
|
||||
<.link
|
||||
id="finding-record-link"
|
||||
navigate={TarakanWeb.RepositoryPaths.repository_security_path(@repository)}
|
||||
class="text-signal transition hover:underline"
|
||||
>
|
||||
← All findings for {@repository.owner}/{@repository.name}
|
||||
</.link>
|
||||
</p>
|
||||
</Layouts.page>
|
||||
</Layouts.app>
|
||||
Loading…
Add table
Add a link
Reference in a new issue