178 lines
8.0 KiB
React
178 lines
8.0 KiB
React
import React from 'react'
|
|
import { Head } from '@inertiajs/react'
|
|
import AdminLayout from '../../../Layouts/AdminLayout'
|
|
|
|
function formatTimestamp(value) {
|
|
if (!value) {
|
|
return 'Not recorded'
|
|
}
|
|
|
|
return new Intl.DateTimeFormat('en-GB', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'medium',
|
|
}).format(new Date(value))
|
|
}
|
|
|
|
function JsonPanel({ title, value }) {
|
|
return (
|
|
<div className="rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-5">
|
|
<h2 className="text-sm font-bold uppercase tracking-[0.18em] text-slate-400">{title}</h2>
|
|
<pre className="nova-scrollbar mt-4 max-h-[28rem] overflow-auto rounded-2xl bg-slate-950/80 p-4 text-[11px] leading-5 text-slate-200">
|
|
{JSON.stringify(value ?? {}, null, 2)}
|
|
</pre>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function FindingTable({ title, rows, columns }) {
|
|
return (
|
|
<div className="overflow-hidden rounded-[28px] border border-white/[0.07] bg-white/[0.02]">
|
|
<div className="border-b border-white/[0.06] px-5 py-4">
|
|
<h2 className="text-sm font-bold uppercase tracking-[0.18em] text-slate-400">{title}</h2>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full min-w-[900px] text-sm">
|
|
<thead>
|
|
<tr className="border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
|
|
{columns.map((column) => (
|
|
<th key={column.key} className="px-5 py-4">{column.label}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/[0.05]">
|
|
{rows.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={columns.length} className="px-5 py-12 text-center text-slate-500">No entries in this section.</td>
|
|
</tr>
|
|
) : rows.map((row, index) => (
|
|
<tr key={`${row.package}-${index}`} className="align-top transition hover:bg-white/[0.025]">
|
|
{columns.map((column) => (
|
|
<td key={column.key} className="px-5 py-4 text-slate-300 break-words">
|
|
{row[column.key] || '—'}
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function SecurityReportShow({ report }) {
|
|
return (
|
|
<AdminLayout title={`Security Report #${report.id}`} subtitle="Private dependency audit details for administrators only.">
|
|
<Head title={`Admin · Security Report #${report.id}`} />
|
|
|
|
<div className="space-y-6">
|
|
<div className="rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-6">
|
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Status</p>
|
|
<p className="mt-2 text-sm text-white">{report.status}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Risk</p>
|
|
<p className="mt-2 text-sm text-white">{report.risk_label}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Started</p>
|
|
<p className="mt-2 text-sm text-white">{formatTimestamp(report.started_at)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Finished</p>
|
|
<p className="mt-2 text-sm text-white">{formatTimestamp(report.finished_at)}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Triggered by</p>
|
|
<p className="mt-2 text-sm text-white">{report.triggered_by || 'Unknown'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Requested by</p>
|
|
<p className="mt-2 text-sm text-white">{report.user ? (report.user.username ? `@${report.user.username}` : report.user.name) : 'System'}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Composer outdated</p>
|
|
<p className="mt-2 text-sm text-white">{report.composer_outdated_count}</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">npm outdated</p>
|
|
<p className="mt-2 text-sm text-white">{report.npm_outdated_count}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{report.error_message ? (
|
|
<div className="mt-6 rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm text-rose-100">
|
|
{report.error_message}
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="mt-6">
|
|
<a href={report.index_url} className="inline-flex rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-medium text-white transition hover:bg-white/[0.08]">
|
|
Back to Reports
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 md:grid-cols-5">
|
|
<div className="rounded-2xl border border-rose-400/15 bg-rose-500/10 p-5 text-rose-100">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-rose-200/70">Critical</p>
|
|
<p className="mt-3 text-3xl font-semibold">{report.total_critical}</p>
|
|
</div>
|
|
<div className="rounded-2xl border border-orange-400/15 bg-orange-500/10 p-5 text-orange-100">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-orange-200/70">High</p>
|
|
<p className="mt-3 text-3xl font-semibold">{report.total_high}</p>
|
|
</div>
|
|
<div className="rounded-2xl border border-amber-400/15 bg-amber-500/10 p-5 text-amber-100">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-amber-200/70">Medium</p>
|
|
<p className="mt-3 text-3xl font-semibold">{report.total_medium}</p>
|
|
</div>
|
|
<div className="rounded-2xl border border-sky-400/15 bg-sky-500/10 p-5 text-sky-100">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-sky-200/70">Low</p>
|
|
<p className="mt-3 text-3xl font-semibold">{report.total_low}</p>
|
|
</div>
|
|
<div className="rounded-2xl border border-white/[0.07] bg-white/[0.02] p-5 text-white">
|
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Unknown</p>
|
|
<p className="mt-3 text-3xl font-semibold">{report.total_unknown}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<FindingTable
|
|
title="Composer Advisories"
|
|
rows={report.composer_advisories ?? []}
|
|
columns={[
|
|
{ key: 'package', label: 'Package' },
|
|
{ key: 'severity', label: 'Severity' },
|
|
{ key: 'title', label: 'Title' },
|
|
{ key: 'cve', label: 'CVE / Ref' },
|
|
{ key: 'affected_versions', label: 'Affected versions' },
|
|
{ key: 'link', label: 'Source URL' },
|
|
]}
|
|
/>
|
|
|
|
<FindingTable
|
|
title="npm Vulnerabilities"
|
|
rows={report.npm_vulnerabilities ?? []}
|
|
columns={[
|
|
{ key: 'package', label: 'Package' },
|
|
{ key: 'severity', label: 'Severity' },
|
|
{ key: 'title', label: 'Title' },
|
|
{ key: 'cve', label: 'CVE' },
|
|
{ key: 'range', label: 'Affected range' },
|
|
{ key: 'fix_available', label: 'Fix available' },
|
|
{ key: 'url', label: 'Source URL' },
|
|
]}
|
|
/>
|
|
|
|
<div className="grid gap-6 xl:grid-cols-2">
|
|
<JsonPanel title="Composer Audit Raw JSON" value={report.composer_audit} />
|
|
<JsonPanel title="npm Audit Raw JSON" value={report.npm_audit} />
|
|
<JsonPanel title="Composer Outdated Raw JSON" value={report.composer_outdated} />
|
|
<JsonPanel title="npm Outdated Raw JSON" value={report.npm_outdated} />
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|