Current state with latest updates
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user