mirror of
https://github.com/hanzoai/trust.git
synced 2026-10-30 14:54:58 +00:00
207 lines
6.1 KiB
TypeScript
207 lines
6.1 KiB
TypeScript
/**
|
|
* The trust centre document, and the one request that gets it.
|
|
*
|
|
* ONE public GET, no auth, made in the browser — this site is a static export,
|
|
* so nothing here runs at build time and no figure is baked into the HTML. Every
|
|
* number on the page is the one the API answered with, this second.
|
|
*/
|
|
|
|
export const SOURCE = 'https://api.hanzo.ai/v1/trust/published/hanzo';
|
|
|
|
/** How completely a control is carried out. */
|
|
export type Status = 'automated' | 'partial' | 'absent';
|
|
|
|
/** The eight groups a control belongs to. A closed vocabulary: the page renders
|
|
* one filter per group, and an unrecognised ninth would filter to nothing. */
|
|
export const CATEGORIES = [
|
|
'infrastructure',
|
|
'data',
|
|
'access',
|
|
'network',
|
|
'endpoint',
|
|
'corporate',
|
|
'product',
|
|
'incident',
|
|
] as const;
|
|
|
|
export type Category = (typeof CATEGORIES)[number];
|
|
|
|
export type Profile = {
|
|
name: string;
|
|
tagline?: string;
|
|
summary?: string;
|
|
contact?: string;
|
|
/** Where a reader asks for what is not published. */
|
|
access?: string;
|
|
published: boolean;
|
|
updated: number;
|
|
};
|
|
|
|
export type Inventory = {
|
|
total: number;
|
|
automated: number;
|
|
partial: number;
|
|
absent: number;
|
|
unverified: number;
|
|
statement: string;
|
|
};
|
|
|
|
export type Coverage = {
|
|
framework: string;
|
|
name: string;
|
|
publisher: string;
|
|
edition: string;
|
|
/** The singular and plural noun for a clause of THIS framework. */
|
|
unit: string;
|
|
units: string;
|
|
total: number;
|
|
automated: number;
|
|
partial: number;
|
|
none: number;
|
|
statement: string;
|
|
note?: string;
|
|
};
|
|
|
|
/** Where the mechanism lives. */
|
|
export type Site = { repo: string; path: string; symbol?: string; line?: number };
|
|
|
|
/**
|
|
* What was done to check the control, and what that reading rests on.
|
|
*
|
|
* `at` is WHERE it was checked — the same shape as `enforced`, not a timestamp —
|
|
* and `actions` names the audit actions that carry the evidence, not a count of
|
|
* them. Both are lists.
|
|
*/
|
|
export type Check = { method: string; at?: Site[]; actions?: string[]; detail?: string };
|
|
|
|
/** A control answering a clause, and how well. */
|
|
export type Mapping = { clause: string; strength: string };
|
|
|
|
export type Control = {
|
|
id: string;
|
|
title?: string;
|
|
claim: string;
|
|
mechanism: string;
|
|
status: Status;
|
|
note?: string;
|
|
category?: Category;
|
|
enforced: Site[];
|
|
verified: Check[];
|
|
maps: Mapping[];
|
|
};
|
|
|
|
export type Doc = {
|
|
id: string;
|
|
title: string;
|
|
kind: string;
|
|
label: string;
|
|
attested: boolean;
|
|
tier: 'public' | 'gated';
|
|
updated: number;
|
|
note?: string;
|
|
href?: string;
|
|
released: boolean;
|
|
};
|
|
|
|
/**
|
|
* A party this organization sends data to, or buys from.
|
|
*
|
|
* `role` is the disclosure and `data` is what it stands for: a processor
|
|
* receives or can reach customer data and says what; a vendor is a party we buy
|
|
* from that no customer data reaches, and says that in words rather than by
|
|
* leaving the field out. Both are required by the API, so neither is optional
|
|
* here — a row that reaches this page has answered.
|
|
*/
|
|
export type Role = 'processor' | 'vendor';
|
|
|
|
/** Where the relationship is declared, or the host a browser reaches it at. */
|
|
export type Trace = { repo?: string; path?: string; symbol?: string; origin?: string; note?: string };
|
|
|
|
export type Subprocessor = {
|
|
id: string;
|
|
name: string;
|
|
role: Role;
|
|
purpose: string;
|
|
data: string;
|
|
location?: string;
|
|
url?: string;
|
|
terms?: string;
|
|
mark?: string;
|
|
evidence: Trace[];
|
|
updated?: number;
|
|
};
|
|
|
|
export type Policy = { id: string; title: string; summary?: string; href?: string; updated: number };
|
|
|
|
export type Question = { id: string; question: string; answer: string; tags?: string[]; updated: number };
|
|
|
|
export type Update = { id: string; at: number; title: string; body?: string; updated: number };
|
|
|
|
export type Center = {
|
|
version: string;
|
|
generated: number;
|
|
org: string;
|
|
profile: Profile;
|
|
inventory: Inventory;
|
|
coverage: Coverage[];
|
|
frameworks: { framework: string; name: string; publisher: string; edition: string; unit: string; units: string; total: number }[];
|
|
controls: Control[];
|
|
documents: Doc[];
|
|
subprocessors: Subprocessor[];
|
|
policies: Policy[];
|
|
faq: Question[];
|
|
updates: Update[];
|
|
risk: { items: { label: string; value: string }[] };
|
|
};
|
|
|
|
const list = <T>(v: T[] | undefined): T[] => (Array.isArray(v) ? v : []);
|
|
|
|
/**
|
|
* Read the document.
|
|
*
|
|
* Every failure the network can produce becomes a sentence a reader can act on,
|
|
* because the alternative — a blank panel — is indistinguishable from an
|
|
* organization that publishes nothing. A short document is not an error: a
|
|
* missing list reads as an empty one and the section says so in words.
|
|
*/
|
|
export async function read(signal?: AbortSignal): Promise<Center> {
|
|
const res = await fetch(SOURCE, { signal, headers: { accept: 'application/json' } });
|
|
if (!res.ok) throw new Error(`${SOURCE} answered ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`);
|
|
|
|
const body = await res.text();
|
|
let doc: Partial<Center>;
|
|
try {
|
|
doc = JSON.parse(body) as Partial<Center>;
|
|
} catch {
|
|
throw new Error(`${SOURCE} answered ${res.status}, but the body is not JSON`);
|
|
}
|
|
if (!doc || !doc.profile) throw new Error(`${SOURCE} answered a document with no profile`);
|
|
|
|
return {
|
|
...(doc as Center),
|
|
coverage: list(doc.coverage),
|
|
frameworks: list(doc.frameworks),
|
|
controls: list(doc.controls),
|
|
documents: list(doc.documents),
|
|
subprocessors: list(doc.subprocessors),
|
|
policies: list(doc.policies),
|
|
faq: list(doc.faq),
|
|
updates: list(doc.updates),
|
|
risk: { items: list(doc.risk?.items) },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* One date format for the whole page, in UTC.
|
|
*
|
|
* A trust centre is read by two people comparing notes across a timezone, and a
|
|
* document dated the 3rd for one of them and the 4th for the other is a document
|
|
* they cannot talk about.
|
|
*/
|
|
const DAY = new Intl.DateTimeFormat('en-US', { timeZone: 'UTC', year: 'numeric', month: 'short', day: 'numeric' });
|
|
|
|
export const day = (ms: number): string => (Number.isFinite(ms) && ms > 0 ? DAY.format(new Date(ms)) : '—');
|
|
|
|
/** The singular or plural noun for `n` of a framework's clauses. */
|
|
export const unit = (n: number, c: Pick<Coverage, 'unit' | 'units'>): string => (n === 1 ? c.unit : c.units);
|