Files
SkinbaseNova/resources/js/Pages/Admin/System/SecurityReportIndex.jsx
T

217 lines
10 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react'
import { Head, router, usePage } from '@inertiajs/react'
import AdminLayout from '../../../Layouts/AdminLayout'
function formatTimestamp(value) {
if (!value) {
return 'Not finished'
}
return new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(value))
}
function SummaryCard({ label, value, tone }) {
const tones = {
red: 'border-rose-400/15 bg-rose-500/10 text-rose-100',
orange: 'border-orange-400/15 bg-orange-500/10 text-orange-100',
yellow: 'border-amber-400/15 bg-amber-500/10 text-amber-100',
blue: 'border-sky-400/15 bg-sky-500/10 text-sky-100',
slate: 'border-white/[0.07] bg-white/[0.02] text-white',
}
return (
<div className={`rounded-2xl border p-5 ${tones[tone] ?? tones.slate}`}>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</p>
<p className="mt-3 text-3xl font-semibold tracking-tight">{value}</p>
</div>
)
}
function RiskBadge({ label }) {
const tone = label === 'Critical'
? 'border-rose-400/20 bg-rose-500/10 text-rose-200'
: label === 'High'
? 'border-orange-400/20 bg-orange-500/10 text-orange-200'
: label === 'Medium'
? 'border-amber-400/20 bg-amber-500/10 text-amber-200'
: label === 'Low'
? 'border-sky-400/20 bg-sky-500/10 text-sky-200'
: 'border-emerald-400/20 bg-emerald-500/10 text-emerald-200'
return <span className={`inline-flex rounded-full border px-2.5 py-1 text-xs font-semibold uppercase tracking-[0.16em] ${tone}`}>{label}</span>
}
export default function SecurityReportIndex({ latest, reports, canRunScan }) {
const flash = usePage().props.flash ?? {}
const items = reports?.data ?? []
const runScan = () => {
router.post('/moderation/system/security-report/run')
}
return (
<AdminLayout title="Security Report" subtitle="Private Composer and npm vulnerability reporting for administrators.">
<Head title="Admin · Security Report" />
<div className="space-y-6">
{flash.success ? (
<div className="rounded-2xl border border-emerald-400/20 bg-emerald-500/10 px-4 py-3 text-sm text-emerald-100">
{flash.success}
</div>
) : null}
{latest ? (
<>
<div className="grid gap-4 md:grid-cols-5">
<SummaryCard label="Critical" value={latest.total_critical} tone="red" />
<SummaryCard label="High" value={latest.total_high} tone="orange" />
<SummaryCard label="Medium" value={latest.total_medium} tone="yellow" />
<SummaryCard label="Low" value={latest.total_low} tone="blue" />
<SummaryCard label="Unknown" value={latest.total_unknown} tone="slate" />
</div>
<div className="rounded-[28px] border border-white/[0.07] bg-white/[0.02] p-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div>
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Latest scan</p>
<div className="mt-3 flex items-center gap-3">
<RiskBadge label={latest.risk_label} />
<span className="text-sm text-slate-400">{latest.status}</span>
</div>
</div>
{canRunScan ? (
<button
type="button"
onClick={runScan}
className="rounded-2xl bg-rose-500/80 px-5 py-3 text-sm font-semibold text-white transition hover:bg-rose-500"
>
Run Scan
</button>
) : null}
</div>
<dl className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Started</dt>
<dd className="mt-2 text-sm text-white">{formatTimestamp(latest.started_at)}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Finished</dt>
<dd className="mt-2 text-sm text-white">{formatTimestamp(latest.finished_at)}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Triggered by</dt>
<dd className="mt-2 text-sm text-white">{latest.triggered_by || 'Unknown'}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Composer outdated</dt>
<dd className="mt-2 text-sm text-white">{latest.composer_outdated_count}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">npm outdated</dt>
<dd className="mt-2 text-sm text-white">{latest.npm_outdated_count}</dd>
</div>
<div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">Requested by</dt>
<dd className="mt-2 text-sm text-white">{latest.user ? (latest.user.username ? `@${latest.user.username}` : latest.user.name) : 'System'}</dd>
</div>
</dl>
<div className="mt-6">
<a href={latest.show_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]">
View Details
</a>
</div>
</div>
</>
) : (
<div className="rounded-[28px] border border-amber-400/20 bg-amber-500/10 p-6 text-amber-100">
<p className="text-sm font-semibold">No security report has been generated yet.</p>
<p className="mt-2 text-sm text-amber-50/80">Run <code className="rounded bg-black/20 px-1.5 py-0.5">php artisan security:scan --notify</code> or use the manual scan button after the queue worker is running.</p>
{canRunScan ? (
<button
type="button"
onClick={runScan}
className="mt-4 rounded-2xl bg-rose-500/80 px-5 py-3 text-sm font-semibold text-white transition hover:bg-rose-500"
>
Run Scan
</button>
) : null}
</div>
)}
<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">Scan History</h2>
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[980px] text-sm">
<thead>
<tr className="border-b border-white/[0.07] text-left text-xs font-semibold uppercase tracking-[0.18em] text-slate-500">
<th className="px-5 py-4">ID</th>
<th className="px-5 py-4">Finished</th>
<th className="px-5 py-4">Status</th>
<th className="px-5 py-4">Risk</th>
<th className="px-5 py-4">Critical</th>
<th className="px-5 py-4">High</th>
<th className="px-5 py-4">Medium</th>
<th className="px-5 py-4">Low</th>
<th className="px-5 py-4">Outdated</th>
<th className="px-5 py-4"></th>
</tr>
</thead>
<tbody className="divide-y divide-white/[0.05]">
{items.length === 0 ? (
<tr>
<td colSpan={10} className="px-5 py-12 text-center text-slate-500">No reports recorded yet.</td>
</tr>
) : items.map((report) => (
<tr key={report.id} className="transition hover:bg-white/[0.025]">
<td className="px-5 py-4 text-white">#{report.id}</td>
<td className="px-5 py-4 text-slate-300">{formatTimestamp(report.finished_at)}</td>
<td className="px-5 py-4 text-slate-300">{report.status}</td>
<td className="px-5 py-4"><RiskBadge label={report.risk_label} /></td>
<td className="px-5 py-4 text-slate-300">{report.total_critical}</td>
<td className="px-5 py-4 text-slate-300">{report.total_high}</td>
<td className="px-5 py-4 text-slate-300">{report.total_medium}</td>
<td className="px-5 py-4 text-slate-300">{report.total_low}</td>
<td className="px-5 py-4 text-slate-300">Composer: {report.composer_outdated_count}, npm: {report.npm_outdated_count}</td>
<td className="px-5 py-4">
<a href={report.show_url} className="text-sky-200 transition hover:text-sky-100">Details</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
{reports?.last_page > 1 ? (
<div className="flex items-center justify-between border-t border-white/[0.06] px-5 py-4">
<p className="text-xs text-slate-500">Showing {reports.from}–{reports.to} of {reports.total} reports</p>
<div className="flex gap-1">
{reports.links.map((link, index) => (
link.url ? (
<button
key={`${link.label}-${index}`}
type="button"
onClick={() => router.get(link.url, {}, { preserveScroll: true, preserveState: true })}
className={`rounded-lg px-3 py-1.5 text-xs transition ${link.active ? 'bg-rose-500/20 font-semibold text-rose-300' : 'text-slate-500 hover:bg-white/[0.06] hover:text-white'}`}
dangerouslySetInnerHTML={{ __html: link.label }}
/>
) : (
<span key={`${link.label}-${index}`} className="rounded-lg px-3 py-1.5 text-xs text-slate-700" dangerouslySetInnerHTML={{ __html: link.label }} />
)
))}
</div>
</div>
) : null}
</div>
</div>
</AdminLayout>
)
}