111 lines
4.5 KiB
TypeScript
111 lines
4.5 KiB
TypeScript
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`;
|
|
}
|