Initial curated CertScope source from fe2bbe3ca5c98bf64be26a09c6c58ee805a7f87e

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:30:09 +02:00
commit 7348faf6c0
65 files changed
+12509

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
import { analyzeHeaders, unavailableHeaderPosture, type HeaderControlStatus, type HeaderMap } from "@/lib/inspect/headers";
import { statusForHttp } from "@/lib/inspect/http-status";
import {
CopyIcon,
collectorLabel,
collectorMeta,
compact,
finiteNumber,
formatCertificateName,
formatMxRecords,
hasValue,
isRecord,
terminalHttpsResult,
triState,
} from "./report-utils";
type CopyHandler = (label: string, value: string) => void;
function EvidenceValue({ label, value, onCopy }: { label: string; value: unknown; onCopy: CopyHandler }) {
const text = label === "MX" ? formatMxRecords(value) : compact(value);
return <div className="evidence-value"><span>{text}</span><button type="button" aria-label={`Copy ${label} evidence`} onClick={() => onCopy(label, text)}><CopyIcon /></button></div>;
}
export function CollectorState({ value, label }: { value: Record<string, unknown>; label: string }) {
const meta = collectorMeta(value);
return <div className="collector-state"><div><strong>{label}</strong><span className={`status-${meta.status === "ok" ? "good" : meta.status === "partial" ? "warning" : "error"}`}>{collectorLabel(meta.status)}</span></div>{meta.errors.length ? <ul>{meta.errors.map((error, index) => <li key={index}>{isRecord(error) && hasValue(error.message) ? String(error.message) : String(error)}</li>)}</ul> : <p>No collector errors recorded.</p>}</div>;
}
export function DnsEvidence({ dns, highlightedEvidence, onCopy }: { dns: Record<string, unknown>; highlightedEvidence: string | null; onCopy: CopyHandler }) {
const rows: Array<[string, unknown]> = [["A", dns.a], ["AAAA", dns.aaaa], ["MX", dns.mx], ["NS", dns.ns], ["TXT", dns.txt], ["SPF", dns.spf], ["DMARC", dns.dmarc]];
const spfPosture = isRecord(dns.spfPosture) ? dns.spfPosture : null;
const dmarcPosture = isRecord(dns.dmarcPosture) ? dns.dmarcPosture : null;
return <div className="evidence-layout"><section id="dns-records" tabIndex={-1} className={`evidence-card ${highlightedEvidence === "dns-records" ? "evidence-highlight" : ""}`}><h3>DNS records</h3><div className="evidence-table">{rows.map(([label, value]) => <div className="evidence-row" key={label}><strong>{label}</strong><EvidenceValue label={label} value={value} onCopy={onCopy} /></div>)}</div></section><section id="dns-mail-posture" tabIndex={-1} className={`evidence-card ${highlightedEvidence === "dns-mail-posture" ? "evidence-highlight" : ""}`}><h3>Parsed mail posture</h3><dl className="posture-list"><div><dt>SPF</dt><dd>{compact(spfPosture?.status ?? dns.spf)}</dd></div><div><dt>SPF terminal all</dt><dd>{compact(spfPosture?.allQualifier ?? "Unavailable")}</dd></div><div><dt>DMARC</dt><dd>{compact(dmarcPosture?.status ?? dns.dmarc)}</dd></div><div><dt>Policy</dt><dd>{compact(dmarcPosture?.policy ?? "Unavailable")}</dd></div><div><dt>Records</dt><dd>{Array.isArray(dns.dmarc) ? dns.dmarc.length : 0} DMARC / {Array.isArray(dns.spf) ? dns.spf.length : 0} SPF</dd></div></dl></section><CollectorState value={dns} label="DNS collector" /></div>;
}
export function TlsEvidence({ tls, highlightedEvidence }: { tls: Record<string, unknown>; highlightedEvidence: string | null }) {
const attempts = Array.isArray(tls.attempts) ? tls.attempts : [];
const status = collectorMeta(tls).status;
return <div className="evidence-layout"><section id="tls-certificate" tabIndex={-1} className={`evidence-card ${highlightedEvidence === "tls-certificate" ? "evidence-highlight" : ""}`}><h3>Certificate</h3><dl className="posture-list"><div><dt>Protocol</dt><dd>{compact(tls.protocol)}</dd></div><div><dt>Endpoint</dt><dd>{compact(tls.address)}</dd></div><div><dt>Authorized</dt><dd className={tls.authorized === true ? "value-good" : tls.authorized === false ? "value-bad" : "value-muted"}>{triState(tls.authorized, status !== "failed")}</dd></div><div><dt>Hostname match</dt><dd className={tls.hostnameValid === true ? "value-good" : tls.hostnameValid === false ? "value-bad" : "value-muted"}>{triState(tls.hostnameValid, status !== "failed")}</dd></div><div><dt>Issuer</dt><dd>{formatCertificateName(tls.issuer)}</dd></div><div><dt>Subject</dt><dd>{formatCertificateName(tls.subject)}</dd></div><div><dt>SANs</dt><dd>{compact(tls.subjectaltname)}</dd></div><div><dt>Valid from</dt><dd>{compact(tls.validFrom)}</dd></div><div><dt>Valid to</dt><dd>{compact(tls.validTo)}</dd></div><div><dt>Days remaining</dt><dd className={finiteNumber(tls.daysRemaining) && tls.daysRemaining <= 30 ? "value-warning" : "value-muted"}>{finiteNumber(tls.daysRemaining) ? `${tls.daysRemaining} days` : "Unavailable"}</dd></div><div><dt>Fingerprint</dt><dd>{compact(tls.fingerprint256)}</dd></div></dl></section><section id="tls-attempts" tabIndex={-1} className={`evidence-card ${highlightedEvidence === "tls-attempts" ? "evidence-highlight" : ""}`}><h3>Connection attempts</h3>{attempts.length ? <ul className="attempt-list">{attempts.map((attempt, index) => <li key={index}><strong>{compact(isRecord(attempt) ? attempt.address : null)}</strong><span>{compact(isRecord(attempt) ? attempt.error : attempt)}</span></li>)}</ul> : <p className="empty-evidence">The first validated endpoint completed the TLS handshake.</p>}<CollectorState value={tls} label="TLS collector" /></section></div>;
}
function controlTone(status: HeaderControlStatus) {
return status === "strong" || status === "good" ? "good" : status === "unavailable" ? "muted" : "warning";
}
export function HttpEvidence({ http, highlightedEvidence }: { http: Record<string, unknown>; highlightedEvidence: string | null }) {
const terminal = terminalHttpsResult(http);
const headers = terminal && isRecord(terminal.headers) ? terminal.headers as HeaderMap : null;
const chain = Array.isArray(http.httpsChain) ? http.httpsChain.filter(isRecord) : [];
const plain = isRecord(http.http) ? http.http : null;
const plainStatus = statusForHttp(plain?.status);
const posture = headers ? analyzeHeaders(headers) : unavailableHeaderPosture();
const redirectHops = [plain, ...chain].filter((hop): hop is Record<string, unknown> => hop !== null && hasValue(hop.url));
const observedPlain = plain !== null && !("error" in plain);
const observedRedirectTarget = isRecord(http.httpRedirectTarget) && "validated" in http.httpRedirectTarget;
const labels: Record<string, string> = { hsts: "HSTS", csp: "CSP", nosniff: "Nosniff", "referrer-policy": "Referrer-Policy", "frame-protection": "Frame protection" };
const headerNames = ["strict-transport-security", "content-security-policy", "x-content-type-options", "referrer-policy", "x-frame-options"];
return <div className="evidence-layout http-evidence-layout"><section id="http-redirect-chain" tabIndex={-1} className={`evidence-card redirect-evidence ${highlightedEvidence === "http-redirect-chain" ? "evidence-highlight" : ""}`}><header className="evidence-card-heading"><div><p>Redirect path</p><h3>HTTP to HTTPS</h3></div><span className={`status-badge ${plainStatus.tone}`}>{plainStatus.label}</span></header>{redirectHops.length ? <ol className="redirect-flow">{redirectHops.map((hop, index) => <li key={index}><article className={`redirect-hop ${"error" in hop ? "error" : statusForHttp(hop.status).tone}`}><span className="redirect-step">{index + 1}</span><div><span className="redirect-protocol">{String(hop.url).startsWith("https:") ? "HTTPS" : "HTTP"}</span><strong>{compact(hop.url)}</strong>{hasValue(hop.address) && <small>Validated endpoint · {String(hop.address)}</small>}</div><b className={`status-chip ${"error" in hop ? "error" : statusForHttp(hop.status).tone}`}>{"error" in hop ? compact(hop.code ?? "Error") : `HTTP ${compact(hop.status)}`}</b></article>{index < redirectHops.length - 1 && <div className="redirect-connector" aria-hidden="true">↓</div>}</li>)}</ol> : <p className="empty-evidence">No HTTP(S) response was recorded.</p>}<dl className="posture-list redirect-facts"><div><dt>Validated target</dt><dd>{triState(isRecord(http.httpRedirectTarget) ? http.httpRedirectTarget.validated : undefined, observedRedirectTarget)}</dd></div><div><dt>HTTPS redirect</dt><dd>{triState(http.httpRedirectsToHttps, observedPlain)}</dd></div></dl>{http.httpsDowngrade === true && <p className="inline-alert error">HTTPS downgrade blocked before the HTTP target was requested.</p>}</section><section id="http-header-posture" tabIndex={-1} className={`evidence-card header-evidence ${highlightedEvidence === "http-header-posture" ? "evidence-highlight" : ""}`}><header className="evidence-card-heading"><div><p>Final HTTPS response</p><h3>Security header posture</h3></div><span className="status-badge">{posture.aggregate.label}</span></header><div className="header-matrix">{posture.controls.map((control) => <div key={control.id} title={control.reason}><span>{labels[control.id]}</span><strong className={`value-${controlTone(control.status)}`}>{control.status}</strong><small>{control.reason}</small></div>)}</div>{headers ? <details className="header-details"><summary>Show normalized header evidence</summary><ul className="header-list">{headerNames.map((name) => <li key={name}><span>{name}</span><code>{compact(headers[name])}</code></li>)}</ul></details> : <p className="empty-evidence">No terminal HTTPS response; controls are unavailable rather than missing.</p>}</section><CollectorState value={http} label="HTTP collector" /></div>;
}
+63
View File
@@ -0,0 +1,63 @@
"use client";
import { useEffect, useRef, useState } from "react";
function RefreshIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 1 0 2 5.3" fill="none" stroke="currentColor" strokeWidth="2"/><path d="M20 5v6h-6" fill="none" stroke="currentColor" strokeWidth="2"/></svg>;
}
function ShareIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 16V3m0 0L7 8m5-5 5 5M5 13v7h14v-7" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>;
}
type ReportActionsProps = {
loading: boolean;
onReinspect: () => void;
onInspectAnother: () => void;
onShare: () => void;
onExportJson: () => void;
onExportMarkdown: () => void;
};
export function ReportActions({ loading, onReinspect, onInspectAnother, onShare, onExportJson, onExportMarkdown }: ReportActionsProps) {
const [mobileOpen, setMobileOpen] = useState(false);
const [desktopExportOpen, setDesktopExportOpen] = useState(false);
const [mobileExportOpen, setMobileExportOpen] = useState(false);
const desktopRef = useRef<HTMLDivElement | null>(null);
const mobileRef = useRef<HTMLDivElement | null>(null);
const close = () => {
setMobileOpen(false);
setDesktopExportOpen(false);
setMobileExportOpen(false);
};
useEffect(() => {
if (!mobileOpen && !desktopExportOpen && !mobileExportOpen) return;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") close();
};
const onPointerDown = (event: PointerEvent) => {
if (![desktopRef.current, mobileRef.current].some((element) => element?.contains(event.target as Node))) close();
};
document.addEventListener("keydown", onKeyDown);
document.addEventListener("pointerdown", onPointerDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("pointerdown", onPointerDown);
};
}, [mobileOpen, desktopExportOpen, mobileExportOpen]);
const controls = (variant: "desktop" | "mobile") => {
const exportOpen = variant === "desktop" ? desktopExportOpen : mobileExportOpen;
const setExportOpen = variant === "desktop" ? setDesktopExportOpen : setMobileExportOpen;
const exportId = variant + "-export-options";
return <>
<button type="button" className="action-reinspect" onClick={() => { close(); onReinspect(); }} disabled={loading}><RefreshIcon />{loading ? "Inspecting…" : "Re-inspect"}</button>
<button type="button" className="inspect-another-btn" onClick={() => { close(); onInspectAnother(); }}>Inspect another host</button>
<button type="button" className="share-btn" onClick={() => { close(); onShare(); }}><ShareIcon />Share</button>
<div className="export-control"><button type="button" className="export-btn" aria-expanded={exportOpen} aria-controls={exportId} onClick={() => setExportOpen((open) => !open)}>Export <span aria-hidden="true">⌄</span></button>{exportOpen && <div id={exportId} className="export-popover" aria-label="Export report"><button type="button" onClick={() => { onExportJson(); close(); }}>JSON</button><button type="button" onClick={() => { onExportMarkdown(); close(); }}>Markdown</button></div>}</div>
</>;
};
return <><div ref={desktopRef} className="report-actions">{controls("desktop")}</div><div ref={mobileRef} className="mobile-action-menu"><button type="button" className="actions-toggle" aria-expanded={mobileOpen} aria-controls="mobile-report-actions" onClick={() => setMobileOpen((open) => !open)}>Actions <span aria-hidden="true">⌄</span></button>{mobileOpen && <div id="mobile-report-actions" className="actions-popover" aria-label="Report actions">{controls("mobile")}</div>}</div></>;
}
+110
View File
@@ -0,0 +1,110 @@
import type { CSSProperties } from "react";
import type { Finding } from "@/lib/inspect/types";
export type Tab = "overview" | "dns" | "tls" | "http" | "raw";
export type FindingFilter = "all" | Finding["severity"];
export const tabs: Tab[] = ["overview", "dns", "tls", "http", "raw"];
export const headerNames = [
"strict-transport-security",
"content-security-policy",
"x-content-type-options",
"referrer-policy",
"x-frame-options",
];
export function downloadText(filename: string, content: string, type: string) {
const blob = new Blob([content], { type });
const href = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = href;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(href), 0);
}
export function CopyIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="7" width="10" height="12" rx="2" fill="none" stroke="currentColor" strokeWidth="1.6"/><path d="M6 16H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" strokeWidth="1.6"/></svg>;
}
export function CloseIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>;
}
export function Gauge({ value, tone }: { value: number; tone: "green" | "amber" | "red" | "muted" }) {
const degrees = Math.max(0, Math.min(100, value)) * 3.6;
return <div className={`gauge ${tone}`} style={{ "--gauge": `${degrees}deg` } as CSSProperties}><div /></div>;
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
export function finiteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
export function hasValue(value: unknown) {
return value !== null && value !== undefined && value !== "";
}
export function compact(value: unknown) {
if (Array.isArray(value)) return value.length ? value.map((item) => item && typeof item === "object" ? JSON.stringify(item) : String(item)).join("\n") : "—";
if (value && typeof value === "object") return JSON.stringify(value);
return value == null || value === "" ? "—" : String(value);
}
export function formatMxRecords(value: unknown) {
if (!Array.isArray(value)) return compact(value);
return value.length ? value.map((record) => {
if (!isRecord(record) || typeof record.exchange !== "string" || !finiteNumber(record.priority)) return compact(record);
return `${record.priority} · ${record.exchange}`;
}).join("\n") : "—";
}
export function formatCertificateName(value: unknown) {
if (!isRecord(value)) return compact(value);
const labels: Record<string, string> = { O: "Organization", CN: "Common name", C: "Country" };
return Object.entries(value).map(([key, entry]) => `${labels[key] ?? key}: ${compact(entry)}`).join("\n") || "—";
}
export function severityLabel(severity: Finding["severity"]) {
return severity === "critical" ? "Critical" : severity === "warning" ? "Warning" : severity === "good" ? "Good" : "Info";
}
export function findingIcon(severity: Finding["severity"]) {
return severity === "good" ? "✓" : severity === "info" ? "i" : "!";
}
export function toneForScore(value: number) {
return value >= 90 ? "green" as const : value >= 75 ? "amber" as const : "red" as const;
}
export function terminalHttpsResult(http: Record<string, unknown>) {
const chain = Array.isArray(http.httpsChain) ? http.httpsChain.filter(isRecord) : [];
const last = chain.at(-1);
return last && !("error" in last) ? last : null;
}
export function collectorMeta(value: Record<string, unknown>) {
const meta = isRecord(value._meta) ? value._meta : {};
const status = meta.status === "ok" || meta.status === "partial" || meta.status === "failed" ? meta.status : "failed";
const errors = Array.isArray(meta.errors) ? meta.errors : [];
return { status, errors };
}
export function collectorLabel(status: string) {
return status === "ok" ? "Complete" : status === "partial" ? "Partial" : "Failed";
}
export function triState(value: unknown, observed: boolean) {
return value === true ? "Yes" : value === false && observed ? "No" : "Unavailable";
}
export function formatInspectedAt(value: string) {
const date = new Date(value);
if (!Number.isFinite(date.getTime())) return value;
return `${date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "medium" })} local`;
}