OpenVuln / src /components /ReportBody.tsx
zRzRzRzRzRzRzR
feat: restore minimal Docker Space
2eb87e3
Raw
History Blame Contribute Delete
9.96 kB
/**
* Structured HTML render of a disclosed VH/VulnForge report
* (parsed report object preferred; raw yaml fallback).
*/
import { useMemo } from "react";
import { load as parseYaml } from "js-yaml";
type Anchor = { file_path?: string; line?: number | string; function?: string };
type FlowStep = { step?: number | string; location?: string; description?: string };
type ReportDoc = {
metadata?: {
title?: string;
vuln_type?: string;
cwe?: string;
cvss_vector?: string;
cvss_score?: number | string;
ev_priority?: string;
ev_score?: number | string;
ev_rationale?: string;
poc_status?: string;
exp_status?: string;
affected_versions?: string;
anchors?: Anchor[];
};
description?: {
background?: string;
detailed_description?: string;
summary?: string;
attack_payload_description?: string;
attack_description?: string;
impact?: string;
combined_impact?: string;
remediation?: string;
fix_suggestion?: string;
};
code?: {
dataflow?: FlowStep[];
data_flow?: FlowStep[];
fix_patch?: string;
patch?: string;
};
references?: unknown;
};
function asRecord(v: unknown): Record<string, unknown> {
if (v && typeof v === "object" && !Array.isArray(v)) return v as Record<string, unknown>;
return {};
}
function str(v: unknown): string {
if (v == null) return "";
if (typeof v === "string") return v.trim();
if (typeof v === "number" || typeof v === "boolean") return String(v);
return "";
}
function Section({ label, children }: { label: string; children: React.ReactNode }) {
if (!children) return null;
return (
<section>
<p className="flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-wider text-ink-tertiary">
<span className="h-3 w-[3px] rounded-full bg-accent-600" aria-hidden />
{label}
</p>
<div className="mt-1.5">{children}</div>
</section>
);
}
function Prose({ text }: { text: string }) {
if (!text) return null;
return (
<p className="whitespace-pre-wrap break-words text-sm leading-relaxed text-ink-secondary">
{text}
</p>
);
}
function normalizeDoc(input: unknown): ReportDoc | null {
if (!input || typeof input !== "object") return null;
const r = input as Record<string, unknown>;
return {
metadata: asRecord(r.metadata) as ReportDoc["metadata"],
description: asRecord(r.description) as ReportDoc["description"],
code: asRecord(r.code) as ReportDoc["code"],
references: r.references,
};
}
export type StructuredReport = {
metadata: Record<string, unknown>;
description: Record<string, unknown>;
code: Record<string, unknown>;
references: unknown;
};
/** Prefer structured `report`; raw yaml only as fallback. */
export function ReportBody({
report,
yaml,
}: {
report?: StructuredReport | null;
yaml?: string | null;
}) {
const doc = useMemo<ReportDoc | null>(() => {
if (report) return normalizeDoc(report);
if (yaml?.trim()) {
try {
return normalizeDoc(parseYaml(yaml));
} catch {
return null;
}
}
return null;
}, [report, yaml]);
if (!doc) {
if (yaml?.trim()) {
return (
<pre className="mt-4 max-h-[32rem] overflow-auto whitespace-pre-wrap break-words rounded-md border border-line bg-surface px-4 py-3 font-mono text-[12px] leading-relaxed text-ink">
{yaml}
</pre>
);
}
return (
<p className="mt-4 text-sm text-ink-tertiary">
Structured report is not available for this disclosure.
</p>
);
}
const meta = doc.metadata ?? {};
const fullTitle = typeof meta === "object" && "title" in meta ? (meta as { title?: string }).title : undefined;
const desc = doc.description ?? {};
const anchors = (meta.anchors ?? []).filter((a) => a && (a.file_path || a.function));
const dataflow = (doc.code?.dataflow ?? doc.code?.data_flow ?? []).filter(Boolean);
const refList = Array.isArray(doc.references)
? doc.references.filter((r) => typeof r === "string" && (r as string).trim())
: [];
return (
<div className="mt-4 space-y-5">
{fullTitle && (
<h4 className="font-display text-[15px] font-semibold leading-snug text-ink">{fullTitle}</h4>
)}
<dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-[13px] sm:grid-cols-4">
{meta.vuln_type && (
<div>
<dt className="text-ink-tertiary">Type</dt>
<dd className="mt-0.5 font-medium text-ink">{meta.vuln_type}</dd>
</div>
)}
{meta.cvss_score != null && (
<div>
<dt className="text-ink-tertiary">CVSS</dt>
<dd className="mt-0.5 font-medium text-ink">{meta.cvss_score}</dd>
</div>
)}
{meta.ev_priority && (
<div>
<dt className="text-ink-tertiary">EV priority</dt>
<dd className="mt-0.5 font-medium text-ink">{meta.ev_priority}</dd>
</div>
)}
{meta.poc_status && (
<div>
<dt className="text-ink-tertiary">PoC status</dt>
<dd className="mt-0.5 font-medium text-ink">{meta.poc_status}</dd>
</div>
)}
{meta.exp_status && (
<div>
<dt className="text-ink-tertiary">EXP status</dt>
<dd className="mt-0.5 font-medium text-ink">{meta.exp_status}</dd>
</div>
)}
{meta.affected_versions && (
<div className="col-span-2 sm:col-span-2">
<dt className="text-ink-tertiary">Affected</dt>
<dd className="mt-0.5 font-medium text-ink">{meta.affected_versions}</dd>
</div>
)}
{meta.cvss_vector && (
<div className="col-span-2 sm:col-span-4">
<dt className="text-ink-tertiary">Vector</dt>
<dd className="mt-0.5 font-mono text-[12px] text-ink-secondary">{meta.cvss_vector}</dd>
</div>
)}
</dl>
{str(meta.ev_rationale) && (
<Section label="Exploitability">
<Prose text={str(meta.ev_rationale)} />
</Section>
)}
{anchors.length > 0 && (
<Section label="Anchors">
<ul className="space-y-1.5">
{anchors.map((a, i) => (
<li key={i} className="flex flex-wrap items-baseline gap-x-2.5 text-[13px]">
<span className="shrink-0 font-mono font-medium text-ink">
{a.file_path}
{a.line != null && `:${a.line}`}
</span>
{a.function && <span className="text-ink-secondary">{a.function}</span>}
</li>
))}
</ul>
</Section>
)}
{str(desc.background) && (
<Section label="Background">
<Prose text={str(desc.background)} />
</Section>
)}
{(str(desc.detailed_description) || str(desc.summary)) && (
<Section label="Detailed analysis">
<Prose text={str(desc.detailed_description) || str(desc.summary)} />
</Section>
)}
{(str(desc.attack_payload_description) || str(desc.attack_description)) && (
<Section label="Attack">
{str(desc.attack_description) && <Prose text={str(desc.attack_description)} />}
{str(desc.attack_payload_description) && (
<Prose text={str(desc.attack_payload_description)} />
)}
</Section>
)}
{(str(desc.impact) || str(desc.combined_impact)) && (
<Section label="Impact">
<Prose text={str(desc.impact) || str(desc.combined_impact)} />
</Section>
)}
{(str(desc.remediation) || str(desc.fix_suggestion)) && (
<Section label="Remediation">
<Prose text={str(desc.remediation) || str(desc.fix_suggestion)} />
</Section>
)}
{dataflow.length > 0 && (
<Section label="Dataflow">
<ol className="space-y-2.5">
{dataflow.map((st, i) => (
<li key={i} className="flex gap-3 text-[13px]">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-surface-sunken font-mono text-[11px] text-ink-secondary">
{st.step ?? i + 1}
</span>
<div className="min-w-0">
{st.location && (
<p className="font-mono text-[12px] font-medium text-ink">{st.location}</p>
)}
{st.description && (
<p className="mt-0.5 whitespace-pre-wrap break-words leading-relaxed text-ink-secondary">
{st.description}
</p>
)}
</div>
</li>
))}
</ol>
</Section>
)}
{(str(doc.code?.fix_patch) || str(doc.code?.patch)) && (
<Section label="Fix patch">
<pre className="overflow-x-auto whitespace-pre-wrap break-words rounded-md border border-line bg-surface-sunken px-4 py-3 font-mono text-[12px] leading-relaxed text-ink">
{str(doc.code?.fix_patch) || str(doc.code?.patch)}
</pre>
</Section>
)}
{refList.length > 0 && (
<Section label="References">
<ul className="space-y-1 text-[13px]">
{(refList as string[]).map((r, i) =>
/^https?:\/\//.test(r) ? (
<li key={i}>
<a
href={r}
target="_blank"
rel="noreferrer"
className="break-all font-mono text-[12px] text-accent-600 hover:underline"
>
{r}
</a>
</li>
) : (
<li key={i} className="text-ink-secondary">
{r}
</li>
),
)}
</ul>
</Section>
)}
</div>
);
}