Current state with latest updates

This commit is contained in:
2026-08-23 13:28:34 +02:00
parent 5af95f6533
commit f52879edbb
74 changed files with 5174 additions and 960 deletions
@@ -0,0 +1,216 @@
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>
)
}
@@ -0,0 +1,177 @@
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>
)
}