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
687
lib/tarakan_web/live/repository_live/show.html.heex
Normal file
687
lib/tarakan_web/live/repository_live/show.html.heex
Normal file
|
|
@ -0,0 +1,687 @@
|
|||
<Layouts.app flash={@flash} current_scope={@current_scope} current_path={assigns[:current_path]}>
|
||||
<Layouts.page>
|
||||
<.repository_header repository={@repository} active_tab={:security} />
|
||||
|
||||
<TarakanWeb.BountyComponents.wanted_banner bounties={@open_bounties} />
|
||||
|
||||
<div
|
||||
:if={@can_manage_disclosure}
|
||||
id="managed-disclosure-controls"
|
||||
class="mt-4 flex flex-wrap items-center gap-3 border border-rule bg-panel px-4 py-3"
|
||||
>
|
||||
<p class="text-xs text-ink-muted">
|
||||
Managed disclosure is
|
||||
<span class="font-semibold text-ink">
|
||||
{if @repository.managed_disclosure, do: "on", else: "off"}
|
||||
</span>
|
||||
for this repository. Staff triage findings with the vendor; the public record is unchanged.
|
||||
</p>
|
||||
<button
|
||||
id="toggle-managed-disclosure"
|
||||
type="button"
|
||||
phx-click="toggle_managed_disclosure"
|
||||
class="ml-auto border border-strong px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] text-ink transition hover:border-quote hover:text-quote"
|
||||
>
|
||||
{if @repository.managed_disclosure, do: "Disable", else: "Enable"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%!-- At a glance --%>
|
||||
<section
|
||||
id="security-summary"
|
||||
class="mt-6 grid border-2 border-strong sm:grid-cols-2 lg:grid-cols-4"
|
||||
>
|
||||
<div class="border-b border-rule px-4 py-4 lg:border-b-0 sm:border-r">
|
||||
<p class="text-xs font-medium text-ink-faint">
|
||||
Open findings
|
||||
</p>
|
||||
<p id="open-findings-count" class="mt-1 font-display text-3xl tabular-nums text-ink">
|
||||
{@repository.open_findings_count}
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-b border-rule px-4 py-4 lg:border-b-0 lg:border-r">
|
||||
<p class="text-xs font-medium text-ink-faint">Verified</p>
|
||||
<p id="verified-findings-count" class="mt-1 font-display text-3xl tabular-nums text-ink">
|
||||
{@repository.verified_findings_count}
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-b border-rule px-4 py-4 sm:border-r lg:border-b-0">
|
||||
<p class="text-xs font-medium text-ink-faint">Reports</p>
|
||||
<p id="scan-count" class="mt-1 font-display text-3xl tabular-nums text-ink">
|
||||
{@repository.scan_count}
|
||||
</p>
|
||||
</div>
|
||||
<div class="px-4 py-4">
|
||||
<p class="text-xs font-medium text-ink-faint">Median fix</p>
|
||||
<p id="median-time-to-fix" class="mt-1 font-display text-3xl tabular-nums text-ink">
|
||||
{Tarakan.SecurityPosture.format_days(@posture.median_days_to_fix)}
|
||||
</p>
|
||||
<p
|
||||
:if={@posture.oldest_open_days}
|
||||
class="mt-1 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint"
|
||||
>
|
||||
oldest open {Tarakan.SecurityPosture.format_days(@posture.oldest_open_days)}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Fixed, then back. Only rendered when it has actually happened. --%>
|
||||
<section
|
||||
:if={@regressions != []}
|
||||
id="finding-regressions"
|
||||
class="mt-6 border-2 border-signal"
|
||||
>
|
||||
<h2 class="border-b border-rule px-4 py-3 font-display text-sm uppercase tracking-[0.12em] text-signal">
|
||||
Regressed - {@posture.regressions_count} fixed finding(s) came back
|
||||
</h2>
|
||||
<ul class="divide-y divide-rule">
|
||||
<li
|
||||
:for={regression <- @regressions}
|
||||
id={"regression-#{regression.id}"}
|
||||
class="flex flex-wrap items-baseline gap-x-3 gap-y-1 px-4 py-3"
|
||||
>
|
||||
<.link
|
||||
navigate={~p"/findings/#{regression.canonical_finding.public_id}"}
|
||||
class="min-w-0 flex-1 truncate text-sm text-ink hover:text-signal hover:underline"
|
||||
>
|
||||
{regression.canonical_finding.title}
|
||||
</.link>
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
fixed at {String.slice(regression.fixed_commit_sha || "", 0, 7)} · back at {String.slice(
|
||||
regression.detected_commit_sha,
|
||||
0,
|
||||
7
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<%!-- Findings first: what matters --%>
|
||||
<section id="canonical-findings" 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">
|
||||
Findings
|
||||
</h2>
|
||||
<span class="font-mono text-[11px] text-ink-faint">
|
||||
{@repository.open_findings_count} unique open
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form
|
||||
:if={@checkable_findings_count > 0}
|
||||
id="bulk-check-findings-form"
|
||||
phx-submit="bulk_check_findings"
|
||||
class="mt-4 border border-strong bg-panel px-4 py-4"
|
||||
>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-semibold text-ink">
|
||||
Check all ({@checkable_findings_count})
|
||||
</p>
|
||||
<input
|
||||
id="bulk-check-notes"
|
||||
type="text"
|
||||
name="notes"
|
||||
required
|
||||
minlength="20"
|
||||
maxlength="2000"
|
||||
placeholder="Independent evidence notes (required, 20+ chars)"
|
||||
autocomplete="off"
|
||||
class="mt-3 w-full 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"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex shrink-0 flex-wrap gap-2">
|
||||
<.button name="verdict" value="confirmed" size="sm">Confirm all</.button>
|
||||
<.button name="verdict" value="disputed" variant="danger" size="sm">
|
||||
Dispute all
|
||||
</.button>
|
||||
<.button name="verdict" value="fixed" size="sm">Fixed all</.button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
:if={@canonical_findings == []}
|
||||
id="findings-empty"
|
||||
class="border-b border-rule px-1 py-10 text-center"
|
||||
>
|
||||
<p class="text-sm font-medium text-ink">No findings yet</p>
|
||||
<p class="mt-1 text-xs leading-5 text-ink-muted">
|
||||
<code class="font-mono text-ink">tarakan worker --agent kimi</code> or open a Job below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div :if={@canonical_findings != []} class="divide-y divide-rule border-b border-rule">
|
||||
<article
|
||||
:for={finding <- @canonical_findings}
|
||||
id={"canonical-#{finding.public_id}"}
|
||||
class="py-5"
|
||||
>
|
||||
<div class="flex flex-wrap items-start gap-3">
|
||||
<.vote_control
|
||||
subject_type="canonical_finding"
|
||||
subject_id={finding.id}
|
||||
summary={finding.vote_summary}
|
||||
can_vote={@can_vote}
|
||||
class="mt-0.5 shrink-0"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-[10px] font-semibold uppercase tracking-[0.12em] text-signal">
|
||||
{finding.severity}
|
||||
</span>
|
||||
<.notch_badge class={[
|
||||
finding.status == "verified" && "bg-ink text-ground",
|
||||
finding.status == "fixed" && "bg-quote text-ground",
|
||||
finding.status in ["open", "disputed"] && "text-ink-muted"
|
||||
]}>
|
||||
{finding.status}
|
||||
</.notch_badge>
|
||||
<.notch_badge
|
||||
:if={finding.trust && finding.trust.agent_reproduced}
|
||||
class="text-ink-muted"
|
||||
>
|
||||
Agent reproduced
|
||||
</.notch_badge>
|
||||
<.notch_badge
|
||||
:if={finding.trust && finding.trust.human_checked}
|
||||
class="text-ink-muted"
|
||||
>
|
||||
Human checked
|
||||
</.notch_badge>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-1.5 text-base font-semibold leading-snug text-ink">
|
||||
<.link
|
||||
navigate={~p"/findings/#{finding.occurrence_public_id}"}
|
||||
class="transition hover:text-signal"
|
||||
>
|
||||
{finding.title}
|
||||
</.link>
|
||||
</h3>
|
||||
|
||||
<p class="mt-1 font-mono text-[11px] text-ink-faint">
|
||||
<span class="text-ink-muted">{finding.file_path}{finding_lines(finding)}</span>
|
||||
<span class="mx-1.5" aria-hidden="true">·</span>
|
||||
last at {short_sha(finding.last_seen_commit_sha)}
|
||||
</p>
|
||||
|
||||
<p class="mt-2 max-w-3xl text-sm leading-6 text-ink-muted">
|
||||
{TarakanWeb.FindingPresentation.description_excerpt(finding.description, 220)}
|
||||
</p>
|
||||
|
||||
<p class="mt-2 font-mono text-[10px] text-ink-faint">
|
||||
detected in {finding.detections_count} {if finding.detections_count == 1,
|
||||
do: "run",
|
||||
else: "runs"} · {finding.distinct_submitters_count} {if finding.distinct_submitters_count ==
|
||||
1,
|
||||
do: "submitter",
|
||||
else: "submitters"} · {finding.confirmations_count} confirmed · {finding.disputes_count} disputed
|
||||
</p>
|
||||
|
||||
<details :if={finding.can_check} class="mt-3">
|
||||
<summary class="cursor-pointer font-mono text-[11px] text-signal transition hover:underline">
|
||||
Override this finding only
|
||||
</summary>
|
||||
<form
|
||||
id={"canonical-#{finding.public_id}-check-form"}
|
||||
phx-submit="record_finding_verdict"
|
||||
class="mt-2 flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<input type="hidden" name="finding_id" value={finding.public_id} />
|
||||
<input type="hidden" name="commit_sha" value={finding.last_seen_commit_sha} />
|
||||
<input
|
||||
type="text"
|
||||
name="notes"
|
||||
required
|
||||
minlength="20"
|
||||
maxlength="2000"
|
||||
placeholder="Notes for this finding only"
|
||||
autocomplete="off"
|
||||
class="min-w-0 flex-1 basis-64 border border-rule bg-transparent px-3 py-1.5 font-mono text-xs text-ink placeholder:text-ink-faint focus:outline-none focus:ring-1 focus:ring-phosphor"
|
||||
/>
|
||||
<.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>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Jobs: action --%>
|
||||
<section id="review-work" 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">
|
||||
Jobs
|
||||
</h2>
|
||||
<.link
|
||||
navigate={~p"/jobs"}
|
||||
class="font-mono text-[10px] uppercase tracking-[0.12em] text-signal hover:underline"
|
||||
>
|
||||
All open jobs →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 border border-strong bg-panel px-4 py-4">
|
||||
<%= if @current_scope && @current_scope.account do %>
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<.button
|
||||
id="quick-open-job-button"
|
||||
type="button"
|
||||
variant="primary"
|
||||
phx-click="quick_open_job"
|
||||
class="w-full phx-click-loading:opacity-60 sm:w-auto"
|
||||
>
|
||||
{if @can_auto_open_job,
|
||||
do: "Open security report job",
|
||||
else: "Propose security report job"}
|
||||
</.button>
|
||||
<button
|
||||
id="propose-task-toggle"
|
||||
type="button"
|
||||
phx-click="toggle_task_form"
|
||||
class="self-start py-1 font-mono text-xs text-signal transition hover:underline sm:self-auto"
|
||||
>
|
||||
{if @show_task_form, do: "Cancel", else: "Customize…"}
|
||||
</button>
|
||||
</div>
|
||||
<p :if={!@can_auto_open_job} class="mt-2 max-w-xl text-xs leading-5 text-ink-muted">
|
||||
A steward publishes proposals before they go live.
|
||||
</p>
|
||||
|
||||
<.form
|
||||
:if={@show_task_form}
|
||||
for={@task_form}
|
||||
id="review-task-form"
|
||||
phx-change="validate_task"
|
||||
phx-submit="create_task"
|
||||
class="mt-5 border-t border-rule pt-5"
|
||||
>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label
|
||||
for="task-branch-select"
|
||||
class="mb-1.5 block text-sm font-semibold text-ink"
|
||||
>
|
||||
Branch
|
||||
</label>
|
||||
<select
|
||||
id="task-branch-select"
|
||||
name="branch"
|
||||
phx-change="select_branch"
|
||||
class="w-full border border-strong bg-ground px-3 py-2 font-mono text-sm text-ink focus:border-signal focus:outline-none"
|
||||
>
|
||||
<option
|
||||
:for={branch <- @branch_options}
|
||||
value={branch}
|
||||
selected={branch == @selected_branch}
|
||||
>
|
||||
{branch}{if branch == @repository.default_branch, do: " (default)", else: ""}
|
||||
</option>
|
||||
<option :if={@branch_options == []} value="" selected>
|
||||
{if @repository.default_branch,
|
||||
do: @repository.default_branch,
|
||||
else: "No branches listed"}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<.input
|
||||
field={@task_form[:commit_sha]}
|
||||
type="text"
|
||||
label="Commit SHA"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<button
|
||||
id="use-default-commit-button"
|
||||
type="button"
|
||||
phx-click="use_default_commit"
|
||||
class="mt-1.5 font-mono text-[11px] text-signal transition hover:underline"
|
||||
>
|
||||
Refresh tip of {@selected_branch || @repository.default_branch || "branch"}
|
||||
</button>
|
||||
</div>
|
||||
<.input
|
||||
field={@task_form[:kind]}
|
||||
type="select"
|
||||
label="Job type"
|
||||
options={@task_kind_options}
|
||||
/>
|
||||
<%!-- diff_review reviews base..head, so it needs the other end of
|
||||
the range. Everything else reviews the snapshot at commit_sha. --%>
|
||||
<div :if={@task_form[:kind].value == "diff_review"}>
|
||||
<.input
|
||||
field={@task_form[:base_commit_sha]}
|
||||
type="text"
|
||||
label="Base commit (reviews base..commit)"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div :if={@task_form[:kind].value == "verify_findings"}>
|
||||
<.input
|
||||
field={@task_form[:target_review_id]}
|
||||
type="select"
|
||||
label="Report to check"
|
||||
options={[{"Choose a report", ""} | @task_target_options]}
|
||||
/>
|
||||
<p
|
||||
:if={@task_target_options == []}
|
||||
class="mt-1.5 text-xs leading-5 text-ink-faint"
|
||||
>
|
||||
No reports with findings to check yet.
|
||||
</p>
|
||||
</div>
|
||||
<.input
|
||||
field={@task_form[:capability]}
|
||||
type="select"
|
||||
label="Who can do it"
|
||||
options={@capability_options}
|
||||
/>
|
||||
<.input
|
||||
field={@task_form[:title]}
|
||||
type="text"
|
||||
label="Title (optional)"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<.input
|
||||
field={@task_form[:description]}
|
||||
type="textarea"
|
||||
label="What to do (optional)"
|
||||
placeholder="Leave blank for an auto brief, or add focus."
|
||||
/>
|
||||
</div>
|
||||
<.button
|
||||
id="create-review-task-button"
|
||||
type="submit"
|
||||
variant="primary"
|
||||
class="mt-4 phx-submit-loading:opacity-60"
|
||||
>
|
||||
{if @can_auto_open_job, do: "Open job", else: "Propose job"}
|
||||
</.button>
|
||||
</.form>
|
||||
<% else %>
|
||||
<div id="review-task-auth-gate">
|
||||
<p class="text-sm text-ink-muted">Sign in to open or claim jobs on this repo.</p>
|
||||
<.link
|
||||
navigate={~p"/accounts/log-in"}
|
||||
class="mt-2 inline-block font-mono text-xs text-signal hover:underline"
|
||||
>
|
||||
Sign in →
|
||||
</.link>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="review-tasks"
|
||||
phx-update="stream"
|
||||
class="mt-4 divide-y divide-rule border-t border-rule"
|
||||
>
|
||||
<div
|
||||
id="review-tasks-empty"
|
||||
class="hidden py-8 text-center text-sm text-ink-faint only:block"
|
||||
>
|
||||
No jobs yet. Open one above or run <code class="font-mono text-ink">tarakan worker --agent kimi</code>.
|
||||
</div>
|
||||
<article
|
||||
:for={{id, task} <- @streams.tasks}
|
||||
id={id}
|
||||
class="grid gap-3 px-1 py-4 transition-colors hover:bg-panel sm:grid-cols-[1fr_auto] sm:items-center sm:px-2"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2 font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint">
|
||||
<span class="text-ink-muted">{review_kind_label(task.kind)}</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>{provenance_label(task.capability)} required</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span title={task.commit_sha}>{short_sha(task.commit_sha)}</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span class="text-ink">{short_task_status(task)}</span>
|
||||
</div>
|
||||
<h3 class="mt-1.5 text-sm font-semibold text-ink">{task.title}</h3>
|
||||
<p class="mt-1 line-clamp-2 text-xs leading-5 text-ink-muted">{task.description}</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 flex-wrap items-center gap-2">
|
||||
<.button
|
||||
id={"review-task-#{task.id}-link"}
|
||||
navigate={~p"/jobs/#{task.id}"}
|
||||
size="sm"
|
||||
>
|
||||
Open
|
||||
</.button>
|
||||
<.button
|
||||
:if={can_cancel_job?(task, @current_scope)}
|
||||
id={"review-task-#{task.id}-cancel"}
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
phx-click="cancel_task"
|
||||
phx-value-id={task.id}
|
||||
data-confirm="Cancel this job? It will leave the open queue."
|
||||
>
|
||||
Cancel
|
||||
</.button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<%!-- Raw reports last: provenance --%>
|
||||
<section id="security-record" class="mt-10">
|
||||
<div class="border-b-2 border-strong pb-3">
|
||||
<h2 class="font-display text-lg uppercase tracking-[0.02em] text-ink">
|
||||
Reports
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="scans" phx-update="stream" class="border-b border-rule">
|
||||
<div
|
||||
id="scans-empty"
|
||||
class="hidden px-1 py-10 text-center text-sm text-ink-faint only:block"
|
||||
>
|
||||
No reports yet.
|
||||
</div>
|
||||
|
||||
<article :for={{id, scan} <- @streams.scans} id={id} class="border-b border-rule py-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<.handle_link
|
||||
handle={scan.submitted_by.handle}
|
||||
class="text-sm font-semibold text-ink"
|
||||
/>
|
||||
<span
|
||||
id={"scan-#{scan.id}-provenance-attestation"}
|
||||
class="font-mono text-[11px] text-ink-muted"
|
||||
title="Submitter claim, not independently verified"
|
||||
>
|
||||
{TarakanWeb.FindingPresentation.how_made_label(scan.provenance)}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-1 font-mono text-[11px] text-ink-faint">
|
||||
{review_kind_label(scan.review_kind)}
|
||||
<span class="mx-1" aria-hidden="true">·</span>
|
||||
<span title={scan.review_status}>
|
||||
{TarakanWeb.FindingPresentation.status_blurb(scan.review_status)}
|
||||
</span>
|
||||
<span class="mx-1" aria-hidden="true">·</span>
|
||||
<span title={scan.commit_sha}>{short_sha(scan.commit_sha)}</span>
|
||||
<span class="mx-1" aria-hidden="true">·</span>
|
||||
{scan_time(scan.inserted_at)}
|
||||
</p>
|
||||
</div>
|
||||
<.notch_badge :if={scan.findings_count > 0} class="shrink-0 text-signal">
|
||||
{scan.findings_count}
|
||||
{if scan.findings_count == 1, do: "finding", else: "findings"}
|
||||
</.notch_badge>
|
||||
<.notch_badge :if={scan.findings_count == 0} class="shrink-0 text-ink-muted">
|
||||
{empty_review_label(scan)}
|
||||
</.notch_badge>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={scan.details_visible && (scan.model || scan.prompt_version)}
|
||||
class="mt-2 font-mono text-[11px] text-ink-faint"
|
||||
>
|
||||
<span :if={scan.model}>via {scan.model}</span>
|
||||
<span :if={scan.model && scan.prompt_version}> · </span>
|
||||
<span :if={scan.prompt_version}>{scan.prompt_version}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:if={scan.details_visible && scan.notes}
|
||||
class="mt-3 max-w-3xl text-sm leading-6 text-ink-muted"
|
||||
>
|
||||
<% notes = TarakanWeb.FindingPresentation.humanize_notes(scan.notes) %>
|
||||
<p :if={notes && notes.kind == :summary} class="text-ink-muted">
|
||||
{notes.count} findings
|
||||
<span :if={notes.tops != []}>
|
||||
· top: {Enum.join(Enum.take(notes.tops, 2), "; ")}
|
||||
</span>
|
||||
</p>
|
||||
<p :if={notes && notes.kind == :plain}>{notes.text}</p>
|
||||
</div>
|
||||
|
||||
<details
|
||||
:if={scan.details_visible && scan.findings_count > 0}
|
||||
class="mt-3"
|
||||
open={scan.findings_count <= 3}
|
||||
>
|
||||
<summary class="cursor-pointer font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint transition hover:text-ink">
|
||||
Findings in this report ({scan.findings_count})
|
||||
</summary>
|
||||
<ul class="mt-3 space-y-3 border-l-2 border-rule pl-4">
|
||||
<li :for={finding <- scan.findings}>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-mono text-[10px] uppercase tracking-[0.12em] text-signal">
|
||||
{finding.severity}
|
||||
</span>
|
||||
<.notch_badge
|
||||
:if={
|
||||
finding.canonical_finding && finding.canonical_finding.detections_count > 1
|
||||
}
|
||||
class="text-ink-muted"
|
||||
>
|
||||
seen in {finding.canonical_finding.detections_count} runs
|
||||
</.notch_badge>
|
||||
<.link
|
||||
id={"finding-source-#{finding.id}"}
|
||||
navigate={~p"/findings/#{finding.public_id}/code"}
|
||||
class="font-mono text-[11px] text-ink-faint transition hover:text-ink"
|
||||
aria-label={"Open #{finding.file_path} at the finding's pinned commit"}
|
||||
>
|
||||
{finding.file_path}{finding_lines(finding)}
|
||||
</.link>
|
||||
</div>
|
||||
<h4 class="mt-0.5 text-sm font-semibold text-ink">
|
||||
<.link
|
||||
id={"finding-page-#{finding.id}"}
|
||||
navigate={~p"/findings/#{finding.public_id}"}
|
||||
class="transition hover:text-signal"
|
||||
>
|
||||
{finding.title}
|
||||
</.link>
|
||||
</h4>
|
||||
<p class="mt-1 max-w-3xl text-sm leading-6 text-ink-muted">
|
||||
{TarakanWeb.FindingPresentation.description_excerpt(finding.description, 180)}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
<details
|
||||
:if={scan.details_visible && scan.raw_document}
|
||||
class="mt-2"
|
||||
>
|
||||
<summary class="cursor-pointer font-mono text-[10px] uppercase tracking-[0.12em] text-ink-faint transition hover:text-ink">
|
||||
Raw JSON
|
||||
</summary>
|
||||
<pre
|
||||
id={"scan-#{scan.id}-raw-report"}
|
||||
class="mt-2 max-h-48 overflow-auto border border-rule bg-panel px-3 py-2 font-mono text-[11px] leading-5 text-ink-muted"
|
||||
>{raw_report(scan)}</pre>
|
||||
</details>
|
||||
|
||||
<p
|
||||
:if={!scan.details_visible}
|
||||
class="mt-3 text-xs leading-5 text-ink-faint"
|
||||
>
|
||||
Detailed evidence is restricted.
|
||||
</p>
|
||||
|
||||
<details
|
||||
:if={can_moderate?(scan, @current_scope)}
|
||||
class="mt-4 border-l-2 border-rule pl-4"
|
||||
>
|
||||
<summary class="cursor-pointer text-sm font-semibold text-ink transition hover:text-signal">
|
||||
Moderate this report
|
||||
</summary>
|
||||
<.form
|
||||
for={@moderation_form}
|
||||
id={"scan-#{scan.id}-moderation-form"}
|
||||
phx-submit="moderate_scan"
|
||||
class="mt-3"
|
||||
>
|
||||
<input type="hidden" name="scan_id" value={scan.id} />
|
||||
<.input
|
||||
id={"scan-#{scan.id}-moderation-notes"}
|
||||
field={@moderation_form[:moderation_notes]}
|
||||
type="textarea"
|
||||
label="What did you check?"
|
||||
placeholder="What was independently verified (20+ characters)."
|
||||
/>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<.button
|
||||
:if={scan.review_status != "accepted"}
|
||||
name="decision"
|
||||
value="accept"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
Accept
|
||||
</.button>
|
||||
<.button name="decision" value="contest" size="sm">Contest</.button>
|
||||
<.button name="decision" value="reject" variant="danger" size="sm">
|
||||
Reject
|
||||
</.button>
|
||||
<.button
|
||||
:if={scan.visibility != "public"}
|
||||
name="decision"
|
||||
value="publish_full"
|
||||
size="sm"
|
||||
>
|
||||
Restore full evidence
|
||||
</.button>
|
||||
<.button
|
||||
:if={scan.visibility == "public"}
|
||||
name="decision"
|
||||
value="publish_summary"
|
||||
size="sm"
|
||||
>
|
||||
Redact to summary
|
||||
</.button>
|
||||
<.button
|
||||
:if={scan.visibility != "restricted"}
|
||||
name="decision"
|
||||
value="restrict"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
Restrict
|
||||
</.button>
|
||||
</div>
|
||||
</.form>
|
||||
</details>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</Layouts.page>
|
||||
</Layouts.app>
|
||||
Loading…
Add table
Add a link
Reference in a new issue