53 lines
2.9 KiB
TypeScript
53 lines
2.9 KiB
TypeScript
import type { InspectionResult } from "./types";
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
function finiteScore(value: unknown) {
|
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100;
|
|
}
|
|
|
|
function collectorStatus(value: unknown) {
|
|
return value === "ok" || value === "partial" || value === "failed";
|
|
}
|
|
|
|
function nonNegativeInteger(value: unknown): value is number {
|
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
}
|
|
|
|
function isFinding(value: unknown) {
|
|
if (!isRecord(value)) return false;
|
|
const category = value.category;
|
|
const severity = value.severity;
|
|
return typeof value.id === "string" && typeof value.title === "string" && typeof value.summary === "string"
|
|
&& (category === "dns" || category === "tls" || category === "http")
|
|
&& (severity === "critical" || severity === "warning" || severity === "info" || severity === "good")
|
|
&& typeof value.scoreImpact === "number" && Number.isFinite(value.scoreImpact)
|
|
&& Array.isArray(value.evidence) && value.evidence.every((pointer) => typeof pointer === "string")
|
|
&& (value.remediation === undefined || typeof value.remediation === "string");
|
|
}
|
|
|
|
/**
|
|
* A deliberately small client boundary: it validates the shape ReportView
|
|
* depends on without duplicating server-side schema logic or adding a runtime
|
|
* validation dependency to this compact project.
|
|
*/
|
|
export function isInspectionResult(value: unknown): value is InspectionResult {
|
|
if (!isRecord(value) || typeof value.host !== "string" || !value.host || typeof value.inspectedAt !== "string" || !Number.isFinite(Date.parse(value.inspectedAt))) return false;
|
|
if (!Array.isArray(value.addresses) || !value.addresses.every((address) => typeof address === "string")) return false;
|
|
const score = value.score;
|
|
if (!isRecord(score) || !finiteScore(score.overall) || !isRecord(score.categories)) return false;
|
|
const categories = score.categories;
|
|
if (!["dns", "tls", "http"].every((category) => finiteScore(categories[category]))) return false;
|
|
const completeness = value.completeness;
|
|
if (!isRecord(completeness) || !["complete", "partial", "failed"].includes(String(completeness.status))) return false;
|
|
const complete = completeness.complete;
|
|
const total = completeness.total;
|
|
if (!nonNegativeInteger(complete) || !nonNegativeInteger(total) || complete > total || !isRecord(completeness.collectors) || !Array.isArray(completeness.issues)) return false;
|
|
const collectors = completeness.collectors;
|
|
if (!["dns", "tls", "http"].every((category) => collectorStatus(collectors[category])) || !completeness.issues.every((issue) => typeof issue === "string")) return false;
|
|
if (!Array.isArray(value.findings) || !value.findings.every(isFinding)) return false;
|
|
return isRecord(value.dns) && isRecord(value.tls) && isRecord(value.http);
|
|
}
|