mirror of
https://github.com/hanzoai/trust.git
synced 2026-10-30 14:54:58 +00:00
1797 lines
115 KiB
JavaScript
1797 lines
115 KiB
JavaScript
// @hanzo/trust — goja bundle (GENERATED by goja/build.mjs; edit src/*.ts + controls.json).
|
|
// Self-contained, ESM-free; runs in dop251/goja inside hanzoai/cloud (HIP-0106).
|
|
"use strict";
|
|
(() => {
|
|
var __defProp = Object.defineProperty;
|
|
var __defProps = Object.defineProperties;
|
|
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true,
|
|
writable: true, value }) : obj[key] = value;
|
|
var __spreadValues = (a, b) => {
|
|
for (var prop in b || (b = {}))
|
|
if (__hasOwnProp.call(b, prop))
|
|
__defNormalProp(a, prop, b[prop]);
|
|
if (__getOwnPropSymbols)
|
|
for (var prop of __getOwnPropSymbols(b)) {
|
|
if (__propIsEnum.call(b, prop))
|
|
__defNormalProp(a, prop, b[prop]);
|
|
}
|
|
return a;
|
|
};
|
|
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
|
|
// <define:__CONTROLS__>
|
|
var define_CONTROLS_default = [{ id: "iam.issuer", category: "access", claim: "Hanzo IAM issues ev\
|
|
ery credential, and signing in is an OIDC redirect rather than a form any application holds.", mechanism: "\
|
|
The authorization endpoint validates the request and returns a code; a single mint path turns that c\
|
|
ode into tokens. Everything else in the estate consumes tokens rather than issuing them.", status: "\
|
|
partial", note: "Partial for scope, not for quality. The redirect flow and the single mint path are \
|
|
both there to read. Establishing that NOTHING else in the estate issues a credential means auditing \
|
|
every service for a second path, and that sweep has not been done \u2014 so the narrower word is the hone\
|
|
st one.", enforced: [{ repo: "hanzoai/iam", path: "internal/oidc/authorize.go", symbol: "Authorize" },
|
|
{ repo: "hanzoai/iam", path: "internal/oidc/mint.go", symbol: "mint" }], verified: [{ method: "rea\
|
|
d", at: [{ repo: "hanzoai/iam", path: "internal/oidc/authorize.go", symbol: "Authorize" }, { repo: "\
|
|
hanzoai/iam", path: "internal/oidc/mint.go", symbol: "mint" }], detail: "The redirect flow and the s\
|
|
ingle mint choke point were read in the source. Rows under issue-user-token, mint-user-keys, revoke-\
|
|
user-keys, token-exchange evidence this. Every credential the platform issues, rotates or revokes le\
|
|
aves a row under a reserved action." }, { method: "audit", actions: ["issue-user-token", "mint-user-\
|
|
keys", "revoke-user-keys", "token-exchange"] }], maps: [{ clause: "soc2:CC6.1", strength: "partial" },
|
|
{ clause: "soc2:CC6.2", strength: "partial" }, { clause: "soc2:CC6.3", strength: "partial" }, { clause: "\
|
|
iso27001:A.5.15", strength: "partial" }, { clause: "iso27001:A.5.16", strength: "partial" }, { clause: "\
|
|
iso27001:A.5.17", strength: "partial" }, { clause: "nist80053:IA", strength: "partial" }, { clause: "\
|
|
nist80053:AC", strength: "partial" }] }, { id: "iam.pkce.s256", category: "access", claim: "A client\
|
|
with no registered secret must present a challenge, and only the hashed method is accepted \u2014 offeri\
|
|
ng the plain method is refused rather than quietly downgraded.", mechanism: "A public client arrivin\
|
|
g with no challenge is refused at the authorization endpoint. An explicitly supplied method other th\
|
|
an the hashed one is refused there, at the mint path, and at the token endpoint; the verifier compar\
|
|
es against a single constant. Discovery advertises the hashed method alone.", status: "automated", enforced: [
|
|
{ repo: "hanzoai/iam", path: "internal/oidc/authorize.go", symbol: "Authorize" }, { repo: "hanzoai\
|
|
/iam", path: "internal/oidc/pkce.go", symbol: "VerifyChallenge" }, { repo: "hanzoai/iam", path: "int\
|
|
ernal/oidc/oidc.go", symbol: "Discovery" }], verified: [{ method: "read", at: [{ repo: "hanzoai/iam",
|
|
path: "internal/oidc/authorize.go", symbol: "Authorize" }, { repo: "hanzoai/iam", path: "internal/\
|
|
oidc/pkce.go", symbol: "VerifyChallenge" }, { repo: "hanzoai/iam", path: "internal/oidc/oidc.go", symbol: "\
|
|
Discovery" }], detail: "Three refusal sites were read: the authorization endpoint, the mint path and\
|
|
the verifier. Two boundaries are worth knowing \u2014 an OMITTED method defaults to the hashed one rathe\
|
|
r than being refused, so the method parameter is optional while the challenge is not; and the device\
|
|
-code grant has no challenge branch at all." }], maps: [{ clause: "soc2:CC6.1", strength: "full" }, {
|
|
clause: "soc2:CC6.6", strength: "full" }, { clause: "iso27001:A.8.5", strength: "full" }, { clause: "\
|
|
iso27001:A.8.24", strength: "full" }, { clause: "nist80053:IA", strength: "full" }, { clause: "nist8\
|
|
0053:SC", strength: "full" }] }, { id: "iam.password.argon2id", category: "access", claim: "New pass\
|
|
words are hashed with a memory-hard function at 64 MiB and two passes with a random salt, the cost p\
|
|
arameters ride inside the stored digest, and an unrecognised scheme fails closed.", mechanism: "One \
|
|
credential package owns hashing. It writes a digest carrying memory, passes and parallelism, so rais\
|
|
ing the cost later does not lock anyone out. Verification reads the scheme off the row and refuses a\
|
|
nything outside the supported set; the terminal path returns false rather than falling through.", status: "\
|
|
automated", enforced: [{ repo: "hanzoai/iam", path: "internal/cred/cred.go", symbol: "CreateHash" },
|
|
{ repo: "hanzoai/iam", path: "internal/users/users.go", symbol: "CheckPassword" }], verified: [{ method: "\
|
|
read", at: [{ repo: "hanzoai/iam", path: "internal/cred/cred.go", symbol: "CreateHash" }, { repo: "h\
|
|
anzoai/iam", path: "internal/users/users.go", symbol: "CheckPassword" }], detail: "The parameter blo\
|
|
ck reads 64 MiB memory, 2 iterations, 16-byte salt, 32-byte key, and the digest is written in the st\
|
|
andard parameterised form. An older scheme remains an accepted VERIFY path so legacy rows still auth\
|
|
enticate; every new or updated digest uses the memory-hard one. The doc comment on the user record d\
|
|
escribes this backwards \u2014 the prose is stale, the code is not." }], maps: [{ clause: "soc2:CC6.\
|
|
1", strength: "full" }, { clause: "iso27001:A.5.17", strength: "full" }, { clause: "iso27001:A.8.24",
|
|
strength: "full" }, { clause: "nist80053:IA", strength: "full" }, { clause: "nist80053:SC", strength: "\
|
|
full" }] }, { id: "iam.token.algorithms", category: "access", claim: "Bearer tokens verify against a\
|
|
closed list of signing algorithms. Symmetric signing and the none algorithm are absent from it, so \
|
|
no forged header can select a path where the verifying key is also the signing key.", mechanism: "On\
|
|
e accepted-algorithms list is handed to the parser as its valid-methods option on the single verific\
|
|
ation path. The post-quantum signature method is registered against a real implementation, not a pla\
|
|
ceholder, and is in the default build.", status: "automated", enforced: [{ repo: "hanzoai/iam", path: "\
|
|
internal/oidc/verify.go", symbol: "acceptedAlgs" }, { repo: "hanzoai/iam", path: "internal/oidc/mlds\
|
|
a.go", symbol: "init" }, { repo: "hanzoai/iam", path: "internal/authz/authz_http_test.go" }], verified: [
|
|
{ method: "read", at: [{ repo: "hanzoai/iam", path: "internal/oidc/verify.go", symbol: "acceptedAl\
|
|
gs" }, { repo: "hanzoai/iam", path: "internal/oidc/mldsa.go", symbol: "init" }, { repo: "hanzoai/iam",
|
|
path: "internal/authz/authz_http_test.go" }], detail: "A test at internal/authz/authz_http_test.go\
|
|
asserts this. A test forges a symmetrically-signed token and asserts it is refused at parse. The li\
|
|
st carries two RSA variants, three NIST curves and one lattice scheme; the remaining RSA variants an\
|
|
d the probabilistic-padding series are not accepted. Tokens from a federated provider are checked ag\
|
|
ainst a SEPARATE closed list with slightly different members." }], maps: [{ clause: "soc2:CC6.1", strength: "\
|
|
full" }, { clause: "soc2:CC6.6", strength: "full" }, { clause: "iso27001:A.8.5", strength: "full" },
|
|
{ clause: "iso27001:A.8.24", strength: "full" }, { clause: "nist80053:IA", strength: "full" }, { clause: "\
|
|
nist80053:SC", strength: "full" }] }, { id: "iam.refresh.rotation", category: "access", claim: "Refr\
|
|
esh tokens are single-use. Spending one mints a successor in the same family; presenting a spent one\
|
|
deletes every token in that family, the working successor included.", mechanism: "Redemption marks \
|
|
the row consumed before the successor is issued, and the successor inherits the family id set when t\
|
|
he grant was established. A request carrying an already-consumed token revokes the family and answer\
|
|
s with a grant error.", status: "automated", enforced: [{ repo: "hanzoai/iam", path: "internal/oidc/\
|
|
refresh.go", symbol: "Refresh" }], verified: [{ method: "read", at: [{ repo: "hanzoai/iam", path: "i\
|
|
nternal/oidc/refresh.go", symbol: "Refresh" }], detail: "The consumed flag is set before the success\
|
|
or is minted, and the family cascade DELETES rows rather than only flagging them. One caveat: the ca\
|
|
scade is best-effort \u2014 it swallows the list error and ignores each delete error while still refusing\
|
|
the caller \u2014 so a partial revocation fails quietly and only the caller's refusal is guaranteed." }],
|
|
maps: [{ clause: "soc2:CC6.1", strength: "full" }, { clause: "soc2:CC7.2", strength: "full" }, { clause: "\
|
|
iso27001:A.5.17", strength: "full" }, { clause: "iso27001:A.8.16", strength: "full" }, { clause: "ni\
|
|
st80053:IA", strength: "full" }, { clause: "nist80053:SI", strength: "full" }] }, { id: "iam.mfa.ses\
|
|
sions", category: "access", claim: "Adding a second factor, removing one, or changing which is prefe\
|
|
rred drops every other session and clears the remember-this-device window.", mechanism: "All three m\
|
|
utations run through one save path, which revokes every session but the calling one and blanks the r\
|
|
emember deadline and its digest. A session id pruned from the row is treated as revoked on its next \
|
|
resolve.", status: "automated", enforced: [{ repo: "hanzoai/iam", path: "internal/mfa/mfa.go", symbol: "\
|
|
save" }, { repo: "hanzoai/iam", path: "internal/sessions/resolve.go", symbol: "RevokeOthers" }], verified: [
|
|
{ method: "read", at: [{ repo: "hanzoai/iam", path: "internal/mfa/mfa.go", symbol: "save" }, { repo: "\
|
|
hanzoai/iam", path: "internal/sessions/resolve.go", symbol: "RevokeOthers" }], detail: "One shared s\
|
|
ave path revokes other sessions and clears the remember window. Worth knowing: this revokes BROWSER \
|
|
SESSIONS. Access and refresh tokens already outstanding for that user are not revoked here, so a tok\
|
|
en issued before the change keeps working until it expires or its family is revoked by another route\
|
|
." }], maps: [{ clause: "soc2:CC6.1", strength: "full" }, { clause: "soc2:CC6.3", strength: "full" },
|
|
{ clause: "iso27001:A.5.17", strength: "full" }, { clause: "iso27001:A.8.5", strength: "full" }, {
|
|
clause: "nist80053:IA", strength: "full" }, { clause: "nist80053:AC", strength: "full" }] }, { id: "\
|
|
iam.scim.users", category: "access", claim: "A directory provisions users over the standard protocol\
|
|
\u2014 create, update, replace, deactivate, delete, with filter and patch. Someone removed from your dir\
|
|
ectory loses their account by that act.", mechanism: "A provisioning surface routes the full user li\
|
|
fecycle and publishes its service-provider configuration, schemas and resource types for a client to\
|
|
discover.", status: "automated", enforced: [{ repo: "hanzoai/iam", path: "internal/scim/scim.go", symbol: "\
|
|
Route" }, { repo: "hanzoai/iam", path: "internal/scim/schemas.go", symbol: "ServiceProviderConfig" }],
|
|
verified: [{ method: "read", at: [{ repo: "hanzoai/iam", path: "internal/scim/scim.go", symbol: "R\
|
|
oute" }, { repo: "hanzoai/iam", path: "internal/scim/schemas.go", symbol: "ServiceProviderConfig" }],
|
|
detail: "The user lifecycle routes are all present. The published capabilities declare bulk, sort \
|
|
and etag as false, so a client is told what is missing rather than discovering it at runtime." }], maps: [
|
|
{ clause: "soc2:CC6.2", strength: "full" }, { clause: "soc2:CC6.3", strength: "full" }, { clause: "\
|
|
iso27001:A.5.16", strength: "full" }, { clause: "iso27001:A.5.18", strength: "full" }, { clause: "is\
|
|
o27001:A.6.5", strength: "full" }, { clause: "nist80053:AC", strength: "full" }, { clause: "nist8005\
|
|
3:PS", strength: "full" }] }, { id: "iam.scim.groups", category: "access", claim: "Group membership \
|
|
arrives from a customer directory and drives role assignment.", mechanism: "None. There is no group \
|
|
route, no handler, and no group resource type \u2014 the published resource-type set has exactly one memb\
|
|
er.", status: "absent", note: "Absent, not stubbed. Role assignment cannot be driven from a customer\
|
|
directory today: users provision, group membership does not. A request for the group collection ans\
|
|
wers a plain 404 rather than a protocol error document, because the published-resource path covers o\
|
|
nly schema and resource-type lookups.", enforced: [{ repo: "hanzoai/iam", path: "internal/scim/schem\
|
|
as.go", symbol: "resourceTypes" }], verified: [{ method: "read", at: [{ repo: "hanzoai/iam", path: "\
|
|
internal/scim/schemas.go", symbol: "resourceTypes" }], detail: "The resource-type list has one membe\
|
|
r, User. No group handler exists under any spelling." }], maps: [{ clause: "soc2:CC6.3", strength: "\
|
|
partial" }, { clause: "iso27001:A.5.18", strength: "partial" }, { clause: "nist80053:AC", strength: "\
|
|
partial" }] }, { id: "iam.passkey.signin", category: "access", claim: "A registered passkey can be u\
|
|
sed to sign in.", mechanism: "None in the running build. Passkey credentials can be registered and m\
|
|
anaged, and there is no assertion ceremony to challenge one \u2014 no begin-login route of any spelling e\
|
|
xists.", status: "absent", note: "The capability flag returns false and the login descriptor is mask\
|
|
ed, so no screen offers passkey sign-in. That is the only honest thing a login screen can do about a\
|
|
method the server cannot perform. Second factors that do work: an authenticator app, SMS and email,\
|
|
with recovery codes.", enforced: [{ repo: "hanzoai/iam", path: "pkg/schema/passkey.go", symbol: "Pa\
|
|
sskeySignin" }, { repo: "hanzoai/iam", path: "internal/webauthn/webauthn.go", symbol: "Route" }, { repo: "\
|
|
hanzoai/iam", path: "internal/oidc/frontdoor_passkey_test.go" }], verified: [{ method: "read", at: [
|
|
{ repo: "hanzoai/iam", path: "pkg/schema/passkey.go", symbol: "PasskeySignin" }, { repo: "hanzoai/\
|
|
iam", path: "internal/webauthn/webauthn.go", symbol: "Route" }, { repo: "hanzoai/iam", path: "intern\
|
|
al/oidc/frontdoor_passkey_test.go" }], detail: "A test at internal/oidc/frontdoor_passkey_test.go as\
|
|
serts this. A test names the four candidate ceremony paths and asserts all four answer 404. The cred\
|
|
ential surface offers registration and management only." }], maps: [{ clause: "soc2:CC6.1", strength: "\
|
|
partial" }, { clause: "iso27001:A.8.5", strength: "partial" }, { clause: "nist80053:IA", strength: "\
|
|
partial" }] }, { id: "authz.path.grant", category: "access", claim: "A resource has a path, and a gr\
|
|
ant covers a path and everything below it, so org-wide access, one workspace, and a narrowed agent c\
|
|
redential all fall out of one check.", mechanism: "One authorization function tests the requested pa\
|
|
th against the paths a caller holds, admitting a path and its descendants.", status: "partial", note: "\
|
|
The containment model is real and it is one function. What it is built on is a byte-by-byte prefix c\
|
|
omparison, which does not do what the model needs \u2014 that is recorded separately as authz.path.segmen\
|
|
t, and it is why this is partial rather than automated.", enforced: [{ repo: "hanzoai/iam", path: "i\
|
|
nternal/authz/authz.go", symbol: "pathAuthorized" }], verified: [{ method: "read", at: [{ repo: "han\
|
|
zoai/iam", path: "internal/authz/authz.go", symbol: "pathAuthorized" }], detail: "One function answe\
|
|
rs the containment question for every caller shape." }], maps: [{ clause: "soc2:CC6.1", strength: "p\
|
|
artial" }, { clause: "soc2:CC6.3", strength: "partial" }, { clause: "iso27001:A.5.15", strength: "pa\
|
|
rtial" }, { clause: "iso27001:A.5.18", strength: "partial" }, { clause: "iso27001:A.8.3", strength: "\
|
|
partial" }, { clause: "nist80053:AC", strength: "partial" }] }, { id: "authz.path.segment", category: "\
|
|
access", claim: "A grant on one path does not cover a longer path that merely starts with the same c\
|
|
haracters, because the comparison advances a segment at a time.", mechanism: "None. The comparison i\
|
|
s a byte prefix. The collisions someone already hit are handled by a separate exact-match table rath\
|
|
er than by making the comparison segment-wise.", status: "absent", note: "Recorded absent because th\
|
|
e mechanism named in the claim does not exist \u2014 not because path authorization is missing. The comme\
|
|
nt above the function records hitting this exact hazard, where one action name is a character prefix\
|
|
of another, and working around it with an exact-match map. That workaround covers the collisions so\
|
|
meone thought of; a segment walk would make the whole class impossible. A segment-wise implementatio\
|
|
n exists elsewhere in the estate but is not on this path.", enforced: [{ repo: "hanzoai/iam", path: "\
|
|
internal/authz/authz.go", symbol: "pathAuthorized" }], verified: [{ method: "read", at: [{ repo: "ha\
|
|
nzoai/iam", path: "internal/authz/authz.go", symbol: "pathAuthorized" }], detail: "The function body\
|
|
is a byte prefix test, and the comment above it documents the workaround." }], maps: [{ clause: "so\
|
|
c2:CC6.1", strength: "partial" }, { clause: "soc2:CC6.3", strength: "partial" }, { clause: "iso27001\
|
|
:A.5.15", strength: "partial" }, { clause: "iso27001:A.8.3", strength: "partial" }, { clause: "nist8\
|
|
0053:AC", strength: "partial" }] }, { id: "edge.identity.headers", category: "network", claim: "Head\
|
|
ers naming an organization, a user, an email or a role are stripped at the edge before a handler rea\
|
|
ds one, and identity is written back only from a verified token.", mechanism: "The request middlewar\
|
|
e deletes every authority header and every sub-scope header by name, then re-mints them from validat\
|
|
ed claims. A second layer deletes any header in two reserved families by prefix, so a new name canno\
|
|
t arrive unhandled.", status: "automated", enforced: [{ repo: "hanzoai/cloud", path: "middleware_ide\
|
|
ntity.go", symbol: "authorityHeaders" }, { repo: "hanzoai/base", path: "tools/claims/claims.go", symbol: "\
|
|
StripIdentityHeaders" }], verified: [{ method: "read", at: [{ repo: "hanzoai/cloud", path: "middlewa\
|
|
re_identity.go", symbol: "authorityHeaders" }, { repo: "hanzoai/base", path: "tools/claims/claims.go",
|
|
symbol: "StripIdentityHeaders" }], detail: "The deletes run before anything reads a header, and th\
|
|
e re-mint draws only from validated claims. The organization a caller asks to act in is honoured onl\
|
|
y where the signed membership already admits it; asking for one they are not in falls back to their \
|
|
own." }], maps: [{ clause: "soc2:CC6.1", strength: "full" }, { clause: "soc2:CC6.6", strength: "full" },
|
|
{ clause: "iso27001:A.8.3", strength: "full" }, { clause: "iso27001:A.8.20", strength: "full" }, {
|
|
clause: "nist80053:AC", strength: "full" }, { clause: "nist80053:SC", strength: "full" }] }, { id: "\
|
|
base.tenant.file", category: "data", claim: "An organization's data is its own database file, so a q\
|
|
uery cannot reach across organizations \u2014 there is no second file open to reach into.", mechanism: "\
|
|
One function is the only way a subsystem opens a per-organization database, and it builds the path f\
|
|
rom the organization slug. A subsystem cannot name a file outside its tenant because it never builds\
|
|
the path itself.", status: "automated", enforced: [{ repo: "hanzoai/cloud", path: "orgdb.go", symbol: "\
|
|
OpenOrgDB" }], verified: [{ method: "read", at: [{ repo: "hanzoai/cloud", path: "orgdb.go", symbol: "\
|
|
OpenOrgDB" }], detail: "The path is composed from the organization slug and the subsystem name; ever\
|
|
y caller goes through the one opener." }], maps: [{ clause: "soc2:CC6.1", strength: "full" }, { clause: "\
|
|
soc2:C1.1", strength: "full" }, { clause: "iso27001:A.8.3", strength: "full" }, { clause: "iso27001:\
|
|
A.8.12", strength: "full" }, { clause: "nist80053:AC", strength: "full" }, { clause: "nist80053:SC",
|
|
strength: "full" }] }, { id: "base.tenant.key", category: "data", claim: "Each database is opened \
|
|
under a key derived for that organization alone from one master, so a leaked key is worth one tenant\
|
|
instead of the estate.", mechanism: "A key is derived from one master through a standard derivation\
|
|
function, bound to the namespace that owns it. The derivation is a pure function of its inputs, so \
|
|
a file reopens after a restart with nothing kept beside it, and a master of the wrong length is an e\
|
|
rror rather than a quiet fall back to no key.", status: "partial", note: "Two different derivations \
|
|
answer to this claim and only one is per-organization. The data plane derives per organization and p\
|
|
er user, as the claim describes. The cloud file layer derives per FILE, from a random file id \u2014 so t\
|
|
wo files of the SAME organization have unrelated keys, and the key is not a function of the organiza\
|
|
tion. Per-file derivation is not weaker against a stolen file, it is stronger; it just does not supp\
|
|
ort a sentence that says the key belongs to the organization.", enforced: [{ repo: "hanzoai/base", path: "\
|
|
plugins/org/org_db.go", symbol: "orgDEK" }, { repo: "hanzoai/cloud", path: "cek/cek.go", symbol: "De\
|
|
rive" }], verified: [{ method: "read", at: [{ repo: "hanzoai/base", path: "plugins/org/org_db.go", symbol: "\
|
|
orgDEK" }, { repo: "hanzoai/cloud", path: "cek/cek.go", symbol: "Derive" }], detail: "Both derivatio\
|
|
ns were read side by side. They use the same primitive and bind different things into it." }], maps: [
|
|
{ clause: "soc2:CC6.1", strength: "partial" }, { clause: "soc2:C1.1", strength: "partial" }, { clause: "\
|
|
iso27001:A.8.24", strength: "partial" }, { clause: "iso27001:A.8.12", strength: "partial" }, { clause: "\
|
|
nist80053:SC", strength: "partial" }] }, { id: "audit.request", category: "infrastructure", claim: "\
|
|
One row records who acted, from where, the method and path, the action, the status the server return\
|
|
ed, and the time.", mechanism: "Request middleware writes one record per request from the validated \
|
|
principal and the response, into a table with a monotonic sequence. Credential-shaped path segments \
|
|
and the user agent are scrubbed before the row is written.", status: "automated", enforced: [{ repo: "\
|
|
hanzoai/cloud", path: "audit_middleware.go", symbol: "Audit" }, { repo: "hanzoai/cloud", path: "audi\
|
|
t/store.go", symbol: "Append" }, { repo: "hanzoai/cloud", path: "audit/record.go" }], verified: [{ method: "\
|
|
read", at: [{ repo: "hanzoai/cloud", path: "audit_middleware.go", symbol: "Audit" }, { repo: "hanzoa\
|
|
i/cloud", path: "audit/store.go", symbol: "Append" }, { repo: "hanzoai/cloud", path: "audit/record.g\
|
|
o" }], detail: "There are TWO trails and they are not the same shape. This one is per request. The i\
|
|
dentity service keeps a second record type carrying organization, user, address, method, request pat\
|
|
h, action, payload, response and status \u2014 but it is written for accountable actions rather than for \
|
|
every request, and its address field is deliberately left empty because behind the ingress the peer \
|
|
address is the ingress pod, which identifies nothing while still being personal data." }], maps: [{ clause: "\
|
|
soc2:CC7.2", strength: "full" }, { clause: "soc2:CC4.1", strength: "full" }, { clause: "iso27001:A.8\
|
|
.15", strength: "full" }, { clause: "iso27001:A.8.16", strength: "full" }, { clause: "nist80053:AU",
|
|
strength: "full" }] }, { id: "audit.redaction", category: "data", claim: "What the trail records i\
|
|
s scrubbed of secrets before it is written.", mechanism: "Payloads emitted with a record are walked \
|
|
recursively against a denylist and matching values are replaced with a marker. The request path is s\
|
|
crubbed of credential-shaped segments and the user agent is scrubbed, before the row is written.", status: "\
|
|
partial", note: "A Hanzo page describes this as the request body recorded with passwords masked. No \
|
|
request body is captured anywhere on this path: the middleware builds a row from method, path, statu\
|
|
s and principal only, and the identity service's two writers set the payload field explicitly. Redac\
|
|
tion is real; the body capture it is described as protecting does not happen. Nothing is leaking \u2014 t\
|
|
he sentence is wrong, and that is the defect this entry records.", enforced: [{ repo: "hanzoai/cloud",
|
|
path: "audit/redact.go", symbol: "Redact" }, { repo: "hanzoai/cloud", path: "audit_middleware.go",
|
|
symbol: "scrubPath" }], verified: [{ method: "read", at: [{ repo: "hanzoai/cloud", path: "audit/re\
|
|
dact.go", symbol: "Redact" }, { repo: "hanzoai/cloud", path: "audit_middleware.go", symbol: "scrubPa\
|
|
th" }], detail: "The denylist covers password, secret, token, client secret and private key among ot\
|
|
hers, and the walk is recursive. The middleware never reads a request body, so there is no captured \
|
|
body to mask." }], maps: [{ clause: "soc2:CC6.1", strength: "partial" }, { clause: "iso27001:A.8.11",
|
|
strength: "partial" }, { clause: "iso27001:A.8.12", strength: "partial" }, { clause: "nist80053:AU",
|
|
strength: "partial" }, { clause: "nist80053:SC", strength: "partial" }] }, { id: "audit.reserved",
|
|
category: "corporate", claim: "Rows recording the platform's accountable actions are reserved: the\
|
|
API refuses to create, correct or delete one, so evidence cannot be forged with an invented grant o\
|
|
r quietly trimmed of a real one.", mechanism: "One predicate names the reserved actions, and the aud\
|
|
it surface calls it on every write verb \u2014 on create, on delete, and on update twice, for the stored \
|
|
row's action and for the incoming one, so a row cannot be forged by relabelling one you own.", status: "\
|
|
automated", enforced: [{ repo: "hanzoai/iam", path: "pkg/schema/audit_log.go", symbol: "PlatformWrit\
|
|
ten" }, { repo: "hanzoai/iam", path: "internal/auditlogs/auditlogs.go", symbol: "refusePlatformActio\
|
|
n" }, { repo: "hanzoai/cloud", path: "apps/admin/audit/audit.go", symbol: "Route" }, { repo: "hanzoa\
|
|
i/iam", path: "internal/auditlogs/reserved_test.go" }], verified: [{ method: "read", at: [{ repo: "h\
|
|
anzoai/iam", path: "pkg/schema/audit_log.go", symbol: "PlatformWritten" }, { repo: "hanzoai/iam", path: "\
|
|
internal/auditlogs/auditlogs.go", symbol: "refusePlatformAction" }, { repo: "hanzoai/cloud", path: "\
|
|
apps/admin/audit/audit.go", symbol: "Route" }, { repo: "hanzoai/iam", path: "internal/auditlogs/rese\
|
|
rved_test.go" }], detail: "A test at internal/auditlogs/reserved_test.go asserts this. The test seed\
|
|
s a reserved row the way the platform writes one, then attempts to create, update, relabel and delet\
|
|
e it through the API, and asserts all four are refused. Remove the gate and the test goes red. The c\
|
|
loud trail is stronger still: it publishes no write route at all, only a list and a chain check. Row\
|
|
s under consent-training, issue-user-token, mint-user-keys, revoke-user-keys, token-exchange evidenc\
|
|
e this. The five reserved actions. A row carrying one of these is evidence precisely because the sub\
|
|
ject of the evidence cannot write it." }, { method: "audit", actions: ["consent-training", "issue-us\
|
|
er-token", "mint-user-keys", "revoke-user-keys", "token-exchange"] }], maps: [{ clause: "soc2:CC7.2",
|
|
strength: "full" }, { clause: "soc2:CC4.1", strength: "full" }, { clause: "iso27001:A.5.28", strength: "\
|
|
full" }, { clause: "iso27001:A.8.15", strength: "full" }, { clause: "nist80053:AU", strength: "full" }] },
|
|
{ id: "audit.index", category: "infrastructure", claim: "Organization, user, action and time each \
|
|
carry an index, so a reviewer's question is a lookup rather than a walk through the whole trail.", mechanism: "\
|
|
The cloud audit table declares real indexes on organization with sequence, action with sequence, res\
|
|
ult with sequence, and timestamp.", status: "partial", note: "Partial on two counts, both measured r\
|
|
ather than reasoned. First, the cloud trail indexes organization, action and time but NOT user \u2014 so \
|
|
of the four questions named, 'everything one person did' is the one that still walks the table. Seco\
|
|
nd, the identity service's record declares index tags on organization, user, action and time and the\
|
|
y are INERT: the tag parser reads only two directives and index is not one of them, and that backend\
|
|
stores every entity as a JSON payload in a single shared table with indexes on kind, parent and del\
|
|
eted alone. The declaration exists; on this backend it materializes nothing.", enforced: [{ repo: "h\
|
|
anzoai/cloud", path: "audit/store.go", symbol: "schema" }, { repo: "hanzoai/orm", path: "model.go", symbol: "\
|
|
parseStructTags" }], verified: [{ method: "read", at: [{ repo: "hanzoai/cloud", path: "audit/store.g\
|
|
o", symbol: "schema" }, { repo: "hanzoai/orm", path: "model.go", symbol: "parseStructTags" }], detail: "\
|
|
The index statements in the cloud store were read directly. The inert-tag finding was measured, not \
|
|
inferred: an entity was written through the record layer to a real database file and the resulting s\
|
|
chema read back, which showed one shared table with a JSON payload column and three indexes, none of\
|
|
them on the declared fields." }], maps: [{ clause: "soc2:CC7.2", strength: "partial" }, { clause: "\
|
|
iso27001:A.8.15", strength: "partial" }, { clause: "iso27001:A.8.16", strength: "partial" }, { clause: "\
|
|
nist80053:AU", strength: "partial" }] }, { id: "audit.chain", category: "infrastructure", claim: "Ea\
|
|
ch record carries the previous record's hash and its own, so a row removed or altered after the fact\
|
|
is detectable rather than silent.", mechanism: "Records are appended over a monotonic sequence, eac\
|
|
h carrying the prior hash, and an endpoint walks the chain and reports where it breaks. The store is\
|
|
sues no update and no delete.", status: "automated", enforced: [{ repo: "hanzoai/cloud", path: "audi\
|
|
t/record.go", symbol: "Hash" }, { repo: "hanzoai/cloud", path: "apps/admin/audit/audit.go", symbol: "\
|
|
Verify" }, { repo: "hanzoai/cloud", path: "audit/store.go" }], verified: [{ method: "read", at: [{ repo: "\
|
|
hanzoai/cloud", path: "audit/record.go", symbol: "Hash" }, { repo: "hanzoai/cloud", path: "apps/admi\
|
|
n/audit/audit.go", symbol: "Verify" }, { repo: "hanzoai/cloud", path: "audit/store.go" }], detail: "\
|
|
Every record carries a previous hash and its own, the sequence is the primary key, and the store pac\
|
|
kage issues no update and no delete statement anywhere." }], maps: [{ clause: "soc2:CC7.2", strength: "\
|
|
full" }, { clause: "soc2:PI1.5", strength: "full" }, { clause: "iso27001:A.5.28", strength: "full" },
|
|
{ clause: "iso27001:A.8.15", strength: "full" }, { clause: "nist80053:AU", strength: "full" }, { clause: "\
|
|
nist80053:SI", strength: "full" }] }, { id: "privacy.consent", category: "data", claim: "A member's \
|
|
training-consent answer is recorded in the trail as a row the organization cannot author, alter or r\
|
|
emove.", mechanism: "The consent endpoint writes a row carrying the before and after state under a r\
|
|
eserved action, inside the same transaction as the change itself, so the record and the change land \
|
|
together or not at all.", status: "automated", enforced: [{ repo: "hanzoai/iam", path: "internal/oid\
|
|
c/consent.go", symbol: "auditConsent" }, { repo: "hanzoai/iam", path: "pkg/schema/audit_log.go", symbol: "\
|
|
ActionConsentTraining" }], verified: [{ method: "read", at: [{ repo: "hanzoai/iam", path: "internal/\
|
|
oidc/consent.go", symbol: "auditConsent" }, { repo: "hanzoai/iam", path: "pkg/schema/audit_log.go", symbol: "\
|
|
ActionConsentTraining" }], detail: "Rows under consent-training evidence this. One row per transitio\
|
|
n, carrying the from and to state. The action is reserved, which is what makes the row evidence rath\
|
|
er than an assertion." }, { method: "audit", actions: ["consent-training"] }], maps: [{ clause: "soc\
|
|
2:P2.1", strength: "full" }, { clause: "soc2:P6.2", strength: "full" }, { clause: "iso27001:A.5.34",
|
|
strength: "full" }, { clause: "nist80053:PT", strength: "full" }, { clause: "nist80053:AU", strength: "\
|
|
full" }] }, { id: "kms.signing.handle", category: "data", claim: "The signing interface takes a key \
|
|
id and some bytes and returns a signature. The private key is made inside an external module and sta\
|
|
ys there, so there is no moment when it exists in our process to be logged, leaked or written into a\
|
|
crash dump.", mechanism: "None found. Signing keys are held as strings on the identity service's ow\
|
|
n records and parsed in the running binary to build a signer. The cloud key service is embedded in t\
|
|
he binary, with a master key read from the environment.", status: "absent", note: "Recorded absent a\
|
|
fter searching the four repositories that serve this platform for any external key-service client. T\
|
|
here is none, and the key service exposes no signing function at all. Private keys are held and used\
|
|
in-process today. Threshold signing is wired but fails closed unless a backend is configured, so it\
|
|
is not a path in service. A Hanzo page states this control in the present tense and names four exte\
|
|
rnal modules; on the evidence available it is a roadmap item. This is the most consequential gap in \
|
|
this inventory.", enforced: [{ repo: "hanzoai/iam", path: "pkg/schema/cert.go", symbol: "PrivateKey" },
|
|
{ repo: "hanzoai/cloud", path: "clients/kms/kms.go", symbol: "New" }, { repo: "hanzoai/iam", path: "\
|
|
internal/oidc/jwt.go" }], verified: [{ method: "read", at: [{ repo: "hanzoai/iam", path: "pkg/schema\
|
|
/cert.go", symbol: "PrivateKey" }, { repo: "hanzoai/cloud", path: "clients/kms/kms.go", symbol: "New" },
|
|
{ repo: "hanzoai/iam", path: "internal/oidc/jwt.go" }], detail: "The private key is a string field\
|
|
on a record, and the signer is built by parsing it in the running binary. The one hardware-module m\
|
|
ention in the tree is a note explaining why a dependency was NOT taken." }], maps: [{ clause: "soc2:\
|
|
CC6.1", strength: "partial" }, { clause: "soc2:CC6.7", strength: "partial" }, { clause: "iso27001:A.\
|
|
8.24", strength: "partial" }, { clause: "nist80053:SC", strength: "partial" }] }, { id: "crypto.vali\
|
|
dation", category: "data", claim: "The cryptographic modules this platform runs hold an independent \
|
|
validation.", mechanism: "None. The code implements published standards, the lattice-based key-encap\
|
|
sulation and signature schemes among them. Implementing a standard is not the same as holding a vali\
|
|
dation, and a validation belongs to the vendor of a module rather than to the software that calls it\
|
|
.", status: "absent", note: "Carried so the gap is counted rather than omitted. There is nothing to \
|
|
read here \u2014 no certificate exists to point at \u2014 so this entry rests on a reading of what is absent r\
|
|
ather than on anything that could fail. Where a deployment needs validated modules, that is a questi\
|
|
on about which module signs, and it is answered by kms.signing.handle, which is itself absent.", enforced: [
|
|
{ repo: "hanzoai/iam", path: "internal/oidc/mldsa.go", symbol: "init" }], verified: [{ method: "re\
|
|
ad", at: [{ repo: "hanzoai/iam", path: "internal/oidc/mldsa.go", symbol: "init" }], detail: "The sig\
|
|
nature scheme is implemented in software inside our own binary. No external validated module is reac\
|
|
hed." }], maps: [{ clause: "iso27001:A.8.24", strength: "partial" }, { clause: "nist80053:SC", strength: "\
|
|
partial" }] }, { id: "ci.claim.copy", category: "product", claim: "No page we publish can say we hol\
|
|
d a credential an outside party would have to grant.", mechanism: "A gate reads every tracked page a\
|
|
nd refuses a line that names a published standard beside a word asserting we hold something under it\
|
|
, and refuses a hedge about a future assessment on the same line. It runs before anything is built, \
|
|
so a page that says it never reaches a reader.", status: "partial", note: "Partial for scope, not fo\
|
|
r quality. The gate itself refuses on the spot and the run stops. What is unestablished is its reach\
|
|
: it runs wherever a repository calls the shared pipeline, and the sweep that would say which reposi\
|
|
tories ought to and do not has not been done, so the honest word is the narrower one.", enforced: [{
|
|
repo: "hanzoai/ci", path: "bin/certclaims" }, { repo: "hanzoai/ci", path: ".hanzo/workflows/build.\
|
|
yml", note: "the step that runs the gates, ahead of every build and publish" }], verified: [{ method: "\
|
|
test", at: [{ repo: "hanzoai/ci", path: "bin/certclaims" }, { repo: "hanzoai/ci", path: ".hanzo/work\
|
|
flows/build.yml", note: "the step that runs the gates, ahead of every build and publish" }], detail: "\
|
|
The gate exits non-zero on a hit and the step runs under `set -e`, so a page making the claim fails \
|
|
the run rather than annotating it. bin/certclaims_test.sh drives it both ways." }], maps: [{ clause: "\
|
|
soc2:CC2.3", strength: "partial" }, { clause: "soc2:CC5.3", strength: "partial" }, { clause: "iso270\
|
|
01:A.5.36", strength: "partial" }, { clause: "nist80053:CM", strength: "partial" }] }, { id: "ci.mer\
|
|
ge.unresolved", category: "product", claim: "A merge nobody finished cannot ship.", mechanism: "A ga\
|
|
te refuses a tracked file still carrying the three-part marker git writes when it cannot merge. Thos\
|
|
e markers are ordinary content afterwards \u2014 the tree is clean, the diff is empty, and both sides of \
|
|
the merge ship \u2014 and in the config and data formats we publish they are silently valid.", status: "\
|
|
partial", note: "Partial for scope, not for quality. The gate itself refuses on the spot and the run\
|
|
stops. What is unestablished is its reach: it runs wherever a repository calls the shared pipeline,\
|
|
and the sweep that would say which repositories ought to and do not has not been done, so the hones\
|
|
t word is the narrower one.", enforced: [{ repo: "hanzoai/ci", path: "bin/conflictmarkers" }, { repo: "\
|
|
hanzoai/ci", path: ".hanzo/workflows/build.yml", note: "the step that runs the gates, ahead of every\
|
|
build and publish" }], verified: [{ method: "test", at: [{ repo: "hanzoai/ci", path: "bin/conflictm\
|
|
arkers" }, { repo: "hanzoai/ci", path: ".hanzo/workflows/build.yml", note: "the step that runs the g\
|
|
ates, ahead of every build and publish" }], detail: "Exits non-zero naming each file and line. It re\
|
|
quires the whole three-part shape and strips fenced regions in prose files, so a documented example \
|
|
of the format is not read as an unfinished merge." }], maps: [{ clause: "soc2:CC8.1", strength: "par\
|
|
tial" }, { clause: "iso27001:A.8.28", strength: "partial" }, { clause: "iso27001:A.8.32", strength: "\
|
|
partial" }, { clause: "nist80053:CM", strength: "partial" }, { clause: "nist80053:SI", strength: "pa\
|
|
rtial" }] }, { id: "ci.arg.public", category: "product", claim: "A value baked into a published imag\
|
|
e has to have said it was public first.", mechanism: "A build argument ends up inside the image, whe\
|
|
re anyone who can pull it can read it back. So the gate reads the names a repository declares and re\
|
|
fuses any that has not declared itself public by its own prefix. The name carries the assertion beca\
|
|
use at that moment the value does not exist yet, and guessing secrecy from the shape of a string is \
|
|
wrong in both directions.", status: "partial", note: "Partial for scope, not for quality. The gate i\
|
|
tself refuses on the spot and the run stops. What is unestablished is its reach: it runs wherever a \
|
|
repository calls the shared pipeline, and the sweep that would say which repositories ought to and d\
|
|
o not has not been done, so the honest word is the narrower one.", enforced: [{ repo: "hanzoai/ci", path: "\
|
|
bin/publishable" }, { repo: "hanzoai/ci", path: ".hanzo/workflows/build.yml", note: "the step that r\
|
|
uns the gates, ahead of every build and publish" }], verified: [{ method: "test", at: [{ repo: "hanz\
|
|
oai/ci", path: "bin/publishable" }, { repo: "hanzoai/ci", path: ".hanzo/workflows/build.yml", note: "\
|
|
the step that runs the gates, ahead of every build and publish" }], detail: "Exits non-zero on an un\
|
|
declared name and the image is never built. bin/publishable_test.sh drives the accepted and refused \
|
|
sets." }], maps: [{ clause: "soc2:CC6.1", strength: "partial" }, { clause: "soc2:CC6.7", strength: "\
|
|
partial" }, { clause: "iso27001:A.8.12", strength: "partial" }, { clause: "iso27001:A.8.25", strength: "\
|
|
partial" }, { clause: "nist80053:SA", strength: "partial" }, { clause: "nist80053:SC", strength: "pa\
|
|
rtial" }] }, { id: "surface.party", category: "product", claim: "A third party our pages reach is a \
|
|
third party this trust centre names.", mechanism: "A gate loads each surface as a browser would, tak\
|
|
es the hosts the markup names as subresources, reads the enforced content policy, and treats a host \
|
|
as reached only when the markup names it and the policy would permit it. A reached host that no part\
|
|
y in the subprocessor list declares fails the run, and so does any advertising or product-analytics \
|
|
host, which is matched over the whole document because the ordinary way one arrives is a script that\
|
|
builds another script at run time.", status: "partial", note: "Partial for scope. The rule is exact\
|
|
and it fails the run, but it measures the surfaces it is given, and those are named where it is inv\
|
|
oked rather than derived from the set of hosts the estate declares. It also sees only what the first\
|
|
response contains: a party a page reaches later, from code the browser runs, is outside what markup\
|
|
and a policy can show.", enforced: [{ repo: "hanzoai/trust", path: "bin/surface.mjs", symbol: "main" },
|
|
{ repo: "hanzoai/trust", path: "subprocessors.json", note: "the declared parties, and the origins \
|
|
each one is reached at" }], verified: [{ method: "test", at: [{ repo: "hanzoai/trust", path: "bin/su\
|
|
rface.mjs", symbol: "main" }, { repo: "hanzoai/trust", path: "hanzo.yml", note: "the surfaces it is \
|
|
run against" }], detail: "Exits non-zero naming the surface and the host. Run over the public surfac\
|
|
es it reports, per surface, whether a content policy is enforced at all and which hosts that policy \
|
|
refuses." }], maps: [{ clause: "soc2:CC3.4", strength: "partial" }, { clause: "soc2:CC9.2", strength: "\
|
|
partial" }, { clause: "iso27001:A.5.19", strength: "partial" }, { clause: "iso27001:A.5.22", strength: "\
|
|
partial" }, { clause: "nist80053:SR", strength: "partial" }] }, { id: "status.probe", category: "inc\
|
|
ident", claim: "Whether the platform is answering is measured continuously and published without a c\
|
|
redential.", mechanism: "A prober loops over each declared endpoint in its own goroutine, evaluates \
|
|
the conditions written for it, and keeps the transitions between healthy and unhealthy as events. Th\
|
|
e read side answers with no credential at all, so the record a customer reads is the record we read.",
|
|
status: "partial", note: "Partial for coverage. The prober watches the endpoints named in its conf\
|
|
iguration and the estate declares considerably more hosts than that, so a host nobody added is a hos\
|
|
t nothing watches. Uptime is also kept at hourly resolution for about two days and daily for thirty,\
|
|
so a question about a longer period has no answer here.", enforced: [{ repo: "hanzoai/status", path: "\
|
|
watchdog/watchdog.go", symbol: "Monitor" }, { repo: "hanzoai/status", path: "api/endpoint_status.go",
|
|
symbol: "EndpointStatuses" }, { repo: "hanzoai/status", path: "api/raw.go", symbol: "UptimeRaw" }],
|
|
verified: [{ method: "read", at: [{ repo: "hanzoai/status", path: "watchdog/watchdog.go", symbol: "\
|
|
Monitor" }, { repo: "hanzoai/status", path: "api/endpoint_status.go", symbol: "EndpointStatuses" }],
|
|
detail: "The prober loop and the two read paths were read in the source, and the public read was e\
|
|
xercised against the running service. Nothing here can fail on the control's behalf, so it is a read\
|
|
ing and counts as one." }], maps: [{ clause: "soc2:CC2.3", strength: "partial" }, { clause: "soc2:CC\
|
|
7.2", strength: "partial" }, { clause: "iso27001:A.8.16", strength: "partial" }, { clause: "nist8005\
|
|
3:SI", strength: "partial" }] }, { id: "incident.notice", category: "incident", claim: "A failing pr\
|
|
obe reaches a person, and a reader is told what happened.", mechanism: "The prober's alerting path f\
|
|
ires on a run of failures and resolves on recovery, addressed to the operations channel. What a visi\
|
|
tor reads is a separate thing: a notice carries a type and a message and is published by editing the\
|
|
service's own configuration.", status: "partial", note: "The half that wakes somebody is automatic;\
|
|
the half a customer reads is not. A notice is a hand-written entry, so nothing derives one from a f\
|
|
ailing probe and nothing requires that one be written. The public record can therefore be silent thr\
|
|
ough an event the alerting path reported, and only a person closes that gap.", enforced: [{ repo: "h\
|
|
anzoai/status", path: "watchdog/alerting.go" }, { repo: "hanzoai/status", path: "config/announcement\
|
|
/announcement.go" }], verified: [{ method: "read", at: [{ repo: "hanzoai/status", path: "watchdog/al\
|
|
erting.go" }, { repo: "hanzoai/status", path: "config/announcement/announcement.go" }], detail: "Bot\
|
|
h paths were read. The notice type is a closed set and the list is served on the public configuratio\
|
|
n read, so a published notice is reachable without a credential once it exists." }], maps: [{ clause: "\
|
|
soc2:CC7.3", strength: "partial" }, { clause: "soc2:CC7.4", strength: "partial" }, { clause: "iso270\
|
|
01:A.5.24", strength: "partial" }, { clause: "iso27001:A.5.26", strength: "partial" }, { clause: "is\
|
|
o27001:A.6.8", strength: "partial" }, { clause: "nist80053:IR", strength: "partial" }] }, { id: "end\
|
|
point.posture", category: "endpoint", claim: "The state of the machine a person signs in from is tak\
|
|
en into account.", mechanism: "None. Signing in proves a password and then a second factor, and both\
|
|
are facts about the person rather than about the device in front of them. Nothing in the flow reads\
|
|
a device identity, a disk-encryption state, a patch level or a management enrolment, and there is n\
|
|
o register of machines against which such a signal could be resolved.", status: "absent", note: "Car\
|
|
ried so the group is answered rather than left blank. The place named below is where the signal woul\
|
|
d be read \u2014 the point after the password is accepted and before a session exists \u2014 and reading it is\
|
|
how you can see that nothing consults one. This is the whole of what we assert about the devices pe\
|
|
ople work from.", enforced: [{ repo: "hanzoai/iam", path: "internal/oidc/mfa_gate.go", symbol: "Gate",
|
|
note: "the second-factor gate \u2014 the point in the flow a device signal would be read at" }], verified: [
|
|
{ method: "read", at: [{ repo: "hanzoai/iam", path: "internal/oidc/mfa_gate.go", symbol: "Gate" }],
|
|
detail: "The gate was read end to end. It resolves the factors a user and their organization allow\
|
|
and proves one of them; no branch of it reads anything about the machine the request came from." }],
|
|
maps: [{ clause: "soc2:CC6.1", strength: "partial" }, { clause: "iso27001:A.6.7", strength: "parti\
|
|
al" }, { clause: "iso27001:A.8.1", strength: "partial" }, { clause: "nist80053:AC", strength: "parti\
|
|
al" }] }, { id: "edge.transport", category: "network", claim: "A browser is told to reach every one \
|
|
of our hosts over TLS and nothing else.", mechanism: "One shared piece of edge configuration sets th\
|
|
e transport-security header for a year with subdomains included, and carries the frame, sniffing, re\
|
|
ferrer and permissions headers with it. It is attached per route rather than at the entry point, so \
|
|
a route carries it when whoever added the route said so.", status: "partial", note: "Measured rather\
|
|
than assumed, and the measurement is why this is not automated. Fewer than half the declared routes\
|
|
name the shared configuration, so most hosts assert nothing; and on the wire the header is absent f\
|
|
rom public hosts whose route does name it, so naming it is not sufficient either. Setting it once at\
|
|
the entry point would make the default right and make a route that opts out visible, which is the o\
|
|
pposite of the arrangement today.", enforced: [{ repo: "hanzoai/universe", path: "infra/k8s/ingress/\
|
|
routes.yaml", note: "the shared header configuration, and the routes that name it" }], verified: [{ method: "\
|
|
read", at: [{ repo: "hanzoai/universe", path: "infra/k8s/ingress/routes.yaml" }], detail: "The routi\
|
|
ng table was parsed and the routes naming the shared header configuration were counted against the w\
|
|
hole set, and the header was then read off the wire from several public hosts. Nothing asserts this \
|
|
on our behalf between one reading and the next, so it is a reading." }], maps: [{ clause: "soc2:CC6.\
|
|
6", strength: "partial" }, { clause: "soc2:CC6.7", strength: "partial" }, { clause: "iso27001:A.8.20",
|
|
strength: "partial" }, { clause: "iso27001:A.8.21", strength: "partial" }, { clause: "iso27001:A.8\
|
|
.24", strength: "partial" }, { clause: "nist80053:SC", strength: "partial" }] }, { id: "dataroom.tie\
|
|
r", category: "data", claim: "An artifact somebody outside this organization signed can never be pub\
|
|
lished to everyone.", mechanism: "The storage engine refuses it. The table holding a published artif\
|
|
act carries a constraint saying an item whose signer is external must stay gated, and the tier colum\
|
|
n defaults to gated \u2014 so a kind nobody has thought of yet arrives private and somebody has to open i\
|
|
t deliberately.", status: "automated", enforced: [{ repo: "hanzoai/cloud", path: "apps/dataroom/sche\
|
|
ma.go", note: "the artifact table and the three constraints on it" }], verified: [{ method: "test", at: [
|
|
{ repo: "hanzoai/cloud", path: "apps/dataroom/trust_test.go", symbol: "TestAuditorSignedCannotBePu\
|
|
blic" }, { repo: "hanzoai/cloud", path: "apps/dataroom/trust_test.go", symbol: "TestTierDefaultsToGa\
|
|
ted" }, { repo: "hanzoai/cloud", path: "apps/dataroom/trust_test.go", symbol: "TestPublicCenterWithh\
|
|
oldsTheGatedTier" }], detail: "Three cases drive it: the write is refused, the default holds, and th\
|
|
e public reading of a centre carries no gated item. Because the rule lives in the schema, a path thr\
|
|
ough the code written later cannot get round it \u2014 which is what these assert, rather than the behavi\
|
|
our of any one handler." }], maps: [{ clause: "soc2:C1.1", strength: "full" }, { clause: "soc2:CC6.1",
|
|
strength: "partial" }, { clause: "iso27001:A.5.12", strength: "partial" }, { clause: "iso27001:A.8\
|
|
.3", strength: "full" }, { clause: "nist80053:AC", strength: "partial" }] }, { id: "dataroom.grant",
|
|
category: "access", claim: "A gated artifact opens for the party that asked for it and for nobody \
|
|
else.", mechanism: "A request to read the gated tier is a row with a state. Granting it produces a l\
|
|
ink bound to the party that asked, so the queue and the live grants are one list, and an administrat\
|
|
or of another organization reaches none of it.", status: "automated", enforced: [{ repo: "hanzoai/cl\
|
|
oud", path: "apps/dataroom/trust.go" }, { repo: "hanzoai/cloud", path: "apps/dataroom/schema.go", note: "\
|
|
the request table and the states it may hold" }], verified: [{ method: "test", at: [{ repo: "hanzoai\
|
|
/cloud", path: "apps/dataroom/trust_test.go", symbol: "TestGrantOpensOnlyForThePartyThatAsked" }, { repo: "\
|
|
hanzoai/cloud", path: "apps/dataroom/trust_test.go", symbol: "TestAForeignOrgAdminReachesNothing" },
|
|
{ repo: "hanzoai/cloud", path: "apps/dataroom/trust_test.go", symbol: "TestAskIsRecordedOrRefused" }],
|
|
detail: "Driven from both sides: the party that asked gets in, a second party holding the same add\
|
|
ress does not, and an administrator of another organization reaches nothing at all." }], maps: [{ clause: "\
|
|
soc2:CC6.1", strength: "full" }, { clause: "soc2:CC6.3", strength: "full" }, { clause: "soc2:C1.1", strength: "\
|
|
partial" }, { clause: "iso27001:A.5.15", strength: "full" }, { clause: "iso27001:A.8.3", strength: "\
|
|
full" }, { clause: "nist80053:AC", strength: "partial" }] }, { id: "dataroom.trail", category: "data",
|
|
claim: "Reading a released document leaves a record of who read which page.", mechanism: "A viewin\
|
|
g session records a page-view at a time against the link it was opened with, so what a reviewer actu\
|
|
ally opened is a list rather than a recollection.", status: "partial", note: "Partial because of wha\
|
|
t the record can and cannot prove. The session and the pages are ours and are written on our side, b\
|
|
ut the address naming the viewer is taken as stated and is not proven \u2014 so the trail establishes tha\
|
|
t a link was used and which pages were reached, and does not establish who was holding it.", enforced: [
|
|
{ repo: "hanzoai/cloud", path: "apps/dataroom/dataroom.go", note: "the page-view record on an open\
|
|
viewing session" }], verified: [{ method: "read", at: [{ repo: "hanzoai/cloud", path: "apps/dataroo\
|
|
m/dataroom.go" }], detail: "The route and the record it writes were read, along with the note record\
|
|
ing that the viewer address is taken as stated. Nothing here fails on the control's behalf." }], maps: [
|
|
{ clause: "soc2:CC6.8", strength: "partial" }, { clause: "soc2:P6.2", strength: "partial" }, { clause: "\
|
|
iso27001:A.8.15", strength: "partial" }, { clause: "nist80053:AU", strength: "partial" }] }];
|
|
|
|
// <define:__FRAMEWORKS__>
|
|
var define_FRAMEWORKS_default = { $comment: "Framework clause catalogs. Reference data: the publis\
|
|
hed clause lists, verbatim in id and title, with no Hanzo judgement in them. They are the DENOMINATO\
|
|
R of every coverage number \u2014 a control in controls.json may only map to an id that appears here, whi\
|
|
ch is what makes a typo a build failure instead of a silently uncounted clause. Every field here des\
|
|
cribes a published clause, so coverage against that list is the only thing this file can say.", soc2: {
|
|
name: "SOC 2 Trust Services Criteria", edition: "2017, with the 2022 revised points of focus", publisher: "\
|
|
AICPA", unit: "criterion", units: "criteria", note: "All five trust services categories. Security (t\
|
|
he common criteria) applies to every engagement; the other four are included only when in scope for \
|
|
a given report.", clauses: [{ id: "CC1.1", group: "Control Environment", title: "Demonstrates a comm\
|
|
itment to integrity and ethical values" }, { id: "CC1.2", group: "Control Environment", title: "Boar\
|
|
d of directors demonstrates independence and exercises oversight" }, { id: "CC1.3", group: "Control \
|
|
Environment", title: "Establishes structures, reporting lines, authorities and responsibilities" }, {
|
|
id: "CC1.4", group: "Control Environment", title: "Demonstrates a commitment to attract, develop a\
|
|
nd retain competent individuals" }, { id: "CC1.5", group: "Control Environment", title: "Holds indiv\
|
|
iduals accountable for their internal control responsibilities" }, { id: "CC2.1", group: "Communicat\
|
|
ion and Information", title: "Obtains or generates relevant, quality information to support internal\
|
|
control" }, { id: "CC2.2", group: "Communication and Information", title: "Internally communicates \
|
|
information necessary to support internal control" }, { id: "CC2.3", group: "Communication and Infor\
|
|
mation", title: "Communicates with external parties regarding matters affecting internal control" },
|
|
{ id: "CC3.1", group: "Risk Assessment", title: "Specifies objectives with sufficient clarity to i\
|
|
dentify and assess risk" }, { id: "CC3.2", group: "Risk Assessment", title: "Identifies and analyzes\
|
|
risk to the achievement of its objectives" }, { id: "CC3.3", group: "Risk Assessment", title: "Cons\
|
|
iders the potential for fraud in assessing risk" }, { id: "CC3.4", group: "Risk Assessment", title: "\
|
|
Identifies and assesses changes that could significantly impact internal control" }, { id: "CC4.1", group: "\
|
|
Monitoring Activities", title: "Selects, develops and performs ongoing and separate evaluations of c\
|
|
ontrols" }, { id: "CC4.2", group: "Monitoring Activities", title: "Evaluates and communicates intern\
|
|
al control deficiencies in a timely manner" }, { id: "CC5.1", group: "Control Activities", title: "S\
|
|
elects and develops control activities that mitigate risk to acceptable levels" }, { id: "CC5.2", group: "\
|
|
Control Activities", title: "Selects and develops general control activities over technology" }, { id: "\
|
|
CC5.3", group: "Control Activities", title: "Deploys control activities through policies and procedu\
|
|
res" }, { id: "CC6.1", group: "Logical and Physical Access", title: "Implements logical access secur\
|
|
ity software, infrastructure and architectures over protected information assets" }, { id: "CC6.2", group: "\
|
|
Logical and Physical Access", title: "Registers and authorizes new internal and external users befor\
|
|
e issuing credentials" }, { id: "CC6.3", group: "Logical and Physical Access", title: "Authorizes, m\
|
|
odifies or removes access based on roles, responsibilities and least privilege" }, { id: "CC6.4", group: "\
|
|
Logical and Physical Access", title: "Restricts physical access to facilities and protected informat\
|
|
ion assets" }, { id: "CC6.5", group: "Logical and Physical Access", title: "Discontinues logical and\
|
|
physical protections over assets on disposal" }, { id: "CC6.6", group: "Logical and Physical Access",
|
|
title: "Implements logical access security measures against threats from outside the system bounda\
|
|
ries" }, { id: "CC6.7", group: "Logical and Physical Access", title: "Restricts the transmission, mo\
|
|
vement and removal of information" }, { id: "CC6.8", group: "Logical and Physical Access", title: "I\
|
|
mplements controls to prevent or detect and act upon unauthorized or malicious software" }, { id: "C\
|
|
C7.1", group: "System Operations", title: "Uses detection and monitoring procedures to identify conf\
|
|
iguration changes and vulnerabilities" }, { id: "CC7.2", group: "System Operations", title: "Monitor\
|
|
s system components for anomalies indicative of malicious acts, natural disasters and errors" }, { id: "\
|
|
CC7.3", group: "System Operations", title: "Evaluates security events to determine whether they coul\
|
|
d or have resulted in a failure" }, { id: "CC7.4", group: "System Operations", title: "Responds to i\
|
|
dentified security incidents" }, { id: "CC7.5", group: "System Operations", title: "Identifies, deve\
|
|
lops and implements activities to recover from identified security incidents" }, { id: "CC8.1", group: "\
|
|
Change Management", title: "Authorizes, designs, develops, tests, approves and implements changes to\
|
|
infrastructure, data, software and procedures" }, { id: "CC9.1", group: "Risk Mitigation", title: "\
|
|
Identifies, selects and develops risk mitigation activities for business disruptions" }, { id: "CC9.\
|
|
2", group: "Risk Mitigation", title: "Assesses and manages risks associated with vendors and busines\
|
|
s partners" }, { id: "A1.1", group: "Availability", title: "Maintains, monitors and evaluates curren\
|
|
t processing capacity and use of system components" }, { id: "A1.2", group: "Availability", title: "\
|
|
Authorizes, designs, implements and maintains environmental protections, backup and recovery infrast\
|
|
ructure" }, { id: "A1.3", group: "Availability", title: "Tests recovery plan procedures supporting s\
|
|
ystem recovery" }, { id: "C1.1", group: "Confidentiality", title: "Identifies and maintains confiden\
|
|
tial information to meet confidentiality objectives" }, { id: "C1.2", group: "Confidentiality", title: "\
|
|
Disposes of confidential information to meet confidentiality objectives" }, { id: "PI1.1", group: "P\
|
|
rocessing Integrity", title: "Obtains, generates, uses and communicates relevant quality information\
|
|
about processing objectives" }, { id: "PI1.2", group: "Processing Integrity", title: "Implements po\
|
|
licies and procedures over system inputs" }, { id: "PI1.3", group: "Processing Integrity", title: "I\
|
|
mplements policies and procedures over system processing" }, { id: "PI1.4", group: "Processing Integ\
|
|
rity", title: "Implements policies and procedures to make output available in accordance with specif\
|
|
ications" }, { id: "PI1.5", group: "Processing Integrity", title: "Implements policies and procedure\
|
|
s to store inputs, items in processing and outputs completely and accurately" }, { id: "P1.1", group: "\
|
|
Privacy \u2014 Notice", title: "Provides notice about its privacy practices to data subjects" }, { id: "\
|
|
P2.1", group: "Privacy \u2014 Choice and Consent", title: "Communicates choices available regarding \
|
|
personal information and obtains consent" }, { id: "P3.1", group: "Privacy \u2014 Collection", title: "\
|
|
Collects personal information consistent with its objectives" }, { id: "P3.2", group: "Privacy \u2014 Col\
|
|
lection", title: "Obtains explicit consent for sensitive personal information" }, { id: "P4.1", group: "\
|
|
Privacy \u2014 Use, Retention and Disposal", title: "Limits the use of personal information to its s\
|
|
tated purposes" }, { id: "P4.2", group: "Privacy \u2014 Use, Retention and Disposal", title: "Retain\
|
|
s personal information consistent with its objectives" }, { id: "P4.3", group: "Privacy \u2014 Use, Reten\
|
|
tion and Disposal", title: "Securely disposes of personal information" }, { id: "P5.1", group: "Priv\
|
|
acy \u2014 Access", title: "Grants identified and authenticated data subjects access to their person\
|
|
al information" }, { id: "P5.2", group: "Privacy \u2014 Access", title: "Corrects, amends or appends\
|
|
personal information on request" }, { id: "P6.1", group: "Privacy \u2014 Disclosure and Notification",
|
|
title: "Discloses personal information only with the consent of the data subject" }, { id: "P6.2",
|
|
group: "Privacy \u2014 Disclosure and Notification", title: "Creates and retains a record of autho\
|
|
rized disclosures" }, { id: "P6.3", group: "Privacy \u2014 Disclosure and Notification", title: "Cre\
|
|
ates and retains a record of detected or reported unauthorized disclosures" }, { id: "P6.4", group: "\
|
|
Privacy \u2014 Disclosure and Notification", title: "Obtains privacy commitments from third parties \
|
|
to whom information is disclosed" }, { id: "P6.5", group: "Privacy \u2014 Disclosure and Notification",
|
|
title: "Obtains commitments from third parties to notify of unauthorized disclosures" }, { id: "P6\
|
|
.6", group: "Privacy \u2014 Disclosure and Notification", title: "Provides notification of breaches \
|
|
and incidents to affected data subjects, regulators and others" }, { id: "P6.7", group: "Privacy \u2014 D\
|
|
isclosure and Notification", title: "Provides data subjects with an accounting of personal informati\
|
|
on held and disclosures made" }, { id: "P7.1", group: "Privacy \u2014 Quality", title: "Collects and\
|
|
maintains accurate, complete and relevant personal information" }, { id: "P8.1", group: "Privacy \u2014 \
|
|
Monitoring and Enforcement", title: "Implements a process for receiving, addressing and resolving pr\
|
|
ivacy complaints" }] }, iso27001: { name: "ISO/IEC 27001 Annex A", edition: "2022", publisher: "ISO/\
|
|
IEC", unit: "control", units: "controls", note: "All 93 Annex A controls across the four themes. Ann\
|
|
ex A is a reference set; a real Statement of Applicability scopes it per organization. This catalog \
|
|
does not scope it \u2014 every control is counted, and the ones no engineering control can reach show as \
|
|
uncovered rather than as excluded.", clauses: [{ id: "A.5.1", group: "Organizational", title: "Polic\
|
|
ies for information security" }, { id: "A.5.2", group: "Organizational", title: "Information securit\
|
|
y roles and responsibilities" }, { id: "A.5.3", group: "Organizational", title: "Segregation of duti\
|
|
es" }, { id: "A.5.4", group: "Organizational", title: "Management responsibilities" }, { id: "A.5.5",
|
|
group: "Organizational", title: "Contact with authorities" }, { id: "A.5.6", group: "Organizationa\
|
|
l", title: "Contact with special interest groups" }, { id: "A.5.7", group: "Organizational", title: "\
|
|
Threat intelligence" }, { id: "A.5.8", group: "Organizational", title: "Information security in proj\
|
|
ect management" }, { id: "A.5.9", group: "Organizational", title: "Inventory of information and othe\
|
|
r associated assets" }, { id: "A.5.10", group: "Organizational", title: "Acceptable use of informati\
|
|
on and other associated assets" }, { id: "A.5.11", group: "Organizational", title: "Return of assets" },
|
|
{ id: "A.5.12", group: "Organizational", title: "Classification of information" }, { id: "A.5.13",
|
|
group: "Organizational", title: "Labelling of information" }, { id: "A.5.14", group: "Organization\
|
|
al", title: "Information transfer" }, { id: "A.5.15", group: "Organizational", title: "Access contro\
|
|
l" }, { id: "A.5.16", group: "Organizational", title: "Identity management" }, { id: "A.5.17", group: "\
|
|
Organizational", title: "Authentication information" }, { id: "A.5.18", group: "Organizational", title: "\
|
|
Access rights" }, { id: "A.5.19", group: "Organizational", title: "Information security in supplier \
|
|
relationships" }, { id: "A.5.20", group: "Organizational", title: "Addressing information security w\
|
|
ithin supplier agreements" }, { id: "A.5.21", group: "Organizational", title: "Managing information \
|
|
security in the ICT supply chain" }, { id: "A.5.22", group: "Organizational", title: "Monitoring, re\
|
|
view and change management of supplier services" }, { id: "A.5.23", group: "Organizational", title: "\
|
|
Information security for use of cloud services" }, { id: "A.5.24", group: "Organizational", title: "\
|
|
Information security incident management planning and preparation" }, { id: "A.5.25", group: "Organi\
|
|
zational", title: "Assessment and decision on information security events" }, { id: "A.5.26", group: "\
|
|
Organizational", title: "Response to information security incidents" }, { id: "A.5.27", group: "Orga\
|
|
nizational", title: "Learning from information security incidents" }, { id: "A.5.28", group: "Organi\
|
|
zational", title: "Collection of evidence" }, { id: "A.5.29", group: "Organizational", title: "Infor\
|
|
mation security during disruption" }, { id: "A.5.30", group: "Organizational", title: "ICT readiness\
|
|
for business continuity" }, { id: "A.5.31", group: "Organizational", title: "Legal, statutory, regu\
|
|
latory and contractual requirements" }, { id: "A.5.32", group: "Organizational", title: "Intellectua\
|
|
l property rights" }, { id: "A.5.33", group: "Organizational", title: "Protection of records" }, { id: "\
|
|
A.5.34", group: "Organizational", title: "Privacy and protection of personally identifiable informat\
|
|
ion" }, { id: "A.5.35", group: "Organizational", title: "Independent review of information security" },
|
|
{ id: "A.5.36", group: "Organizational", title: "Compliance with policies, rules and standards for\
|
|
information security" }, { id: "A.5.37", group: "Organizational", title: "Documented operating proc\
|
|
edures" }, { id: "A.6.1", group: "People", title: "Screening" }, { id: "A.6.2", group: "People", title: "\
|
|
Terms and conditions of employment" }, { id: "A.6.3", group: "People", title: "Information security \
|
|
awareness, education and training" }, { id: "A.6.4", group: "People", title: "Disciplinary process" },
|
|
{ id: "A.6.5", group: "People", title: "Responsibilities after termination or change of employment" },
|
|
{ id: "A.6.6", group: "People", title: "Confidentiality or non-disclosure agreements" }, { id: "A.\
|
|
6.7", group: "People", title: "Remote working" }, { id: "A.6.8", group: "People", title: "Informatio\
|
|
n security event reporting" }, { id: "A.7.1", group: "Physical", title: "Physical security perimeter\
|
|
s" }, { id: "A.7.2", group: "Physical", title: "Physical entry" }, { id: "A.7.3", group: "Physical",
|
|
title: "Securing offices, rooms and facilities" }, { id: "A.7.4", group: "Physical", title: "Physi\
|
|
cal security monitoring" }, { id: "A.7.5", group: "Physical", title: "Protecting against physical an\
|
|
d environmental threats" }, { id: "A.7.6", group: "Physical", title: "Working in secure areas" }, { id: "\
|
|
A.7.7", group: "Physical", title: "Clear desk and clear screen" }, { id: "A.7.8", group: "Physical",
|
|
title: "Equipment siting and protection" }, { id: "A.7.9", group: "Physical", title: "Security of \
|
|
assets off-premises" }, { id: "A.7.10", group: "Physical", title: "Storage media" }, { id: "A.7.11",
|
|
group: "Physical", title: "Supporting utilities" }, { id: "A.7.12", group: "Physical", title: "Cab\
|
|
ling security" }, { id: "A.7.13", group: "Physical", title: "Equipment maintenance" }, { id: "A.7.14",
|
|
group: "Physical", title: "Secure disposal or re-use of equipment" }, { id: "A.8.1", group: "Techn\
|
|
ological", title: "User endpoint devices" }, { id: "A.8.2", group: "Technological", title: "Privileg\
|
|
ed access rights" }, { id: "A.8.3", group: "Technological", title: "Information access restriction" },
|
|
{ id: "A.8.4", group: "Technological", title: "Access to source code" }, { id: "A.8.5", group: "Te\
|
|
chnological", title: "Secure authentication" }, { id: "A.8.6", group: "Technological", title: "Capac\
|
|
ity management" }, { id: "A.8.7", group: "Technological", title: "Protection against malware" }, { id: "\
|
|
A.8.8", group: "Technological", title: "Management of technical vulnerabilities" }, { id: "A.8.9", group: "\
|
|
Technological", title: "Configuration management" }, { id: "A.8.10", group: "Technological", title: "\
|
|
Information deletion" }, { id: "A.8.11", group: "Technological", title: "Data masking" }, { id: "A.8\
|
|
.12", group: "Technological", title: "Data leakage prevention" }, { id: "A.8.13", group: "Technologi\
|
|
cal", title: "Information backup" }, { id: "A.8.14", group: "Technological", title: "Redundancy of i\
|
|
nformation processing facilities" }, { id: "A.8.15", group: "Technological", title: "Logging" }, { id: "\
|
|
A.8.16", group: "Technological", title: "Monitoring activities" }, { id: "A.8.17", group: "Technolog\
|
|
ical", title: "Clock synchronization" }, { id: "A.8.18", group: "Technological", title: "Use of priv\
|
|
ileged utility programs" }, { id: "A.8.19", group: "Technological", title: "Installation of software\
|
|
on operational systems" }, { id: "A.8.20", group: "Technological", title: "Networks security" }, { id: "\
|
|
A.8.21", group: "Technological", title: "Security of network services" }, { id: "A.8.22", group: "Te\
|
|
chnological", title: "Segregation of networks" }, { id: "A.8.23", group: "Technological", title: "We\
|
|
b filtering" }, { id: "A.8.24", group: "Technological", title: "Use of cryptography" }, { id: "A.8.2\
|
|
5", group: "Technological", title: "Secure development life cycle" }, { id: "A.8.26", group: "Techno\
|
|
logical", title: "Application security requirements" }, { id: "A.8.27", group: "Technological", title: "\
|
|
Secure system architecture and engineering principles" }, { id: "A.8.28", group: "Technological", title: "\
|
|
Secure coding" }, { id: "A.8.29", group: "Technological", title: "Security testing in development an\
|
|
d acceptance" }, { id: "A.8.30", group: "Technological", title: "Outsourced development" }, { id: "A\
|
|
.8.31", group: "Technological", title: "Separation of development, test and production environments" },
|
|
{ id: "A.8.32", group: "Technological", title: "Change management" }, { id: "A.8.33", group: "Tech\
|
|
nological", title: "Test information" }, { id: "A.8.34", group: "Technological", title: "Protection \
|
|
of information systems during audit testing" }] }, nist80053: { name: "NIST SP 800-53 control famili\
|
|
es", edition: "Revision 5", publisher: "NIST", unit: "family", units: "families", note: "Mapped at F\
|
|
AMILY granularity, not to individual controls. Rev 5 has over a thousand controls and enhancements; \
|
|
claiming a per-control mapping we have not performed would be the exact dishonesty this system exist\
|
|
s to prevent. A family counts as automated when at least one verified automated control lands inside\
|
|
it \u2014 that is a weaker statement than covering the family, and it is stated that way everywhere it i\
|
|
s reported.", clauses: [{ id: "AC", group: "800-53 Rev 5", title: "Access Control" }, { id: "AT", group: "\
|
|
800-53 Rev 5", title: "Awareness and Training" }, { id: "AU", group: "800-53 Rev 5", title: "Audit a\
|
|
nd Accountability" }, { id: "CA", group: "800-53 Rev 5", title: "Assessment, Authorization and Monit\
|
|
oring" }, { id: "CM", group: "800-53 Rev 5", title: "Configuration Management" }, { id: "CP", group: "\
|
|
800-53 Rev 5", title: "Contingency Planning" }, { id: "IA", group: "800-53 Rev 5", title: "Identific\
|
|
ation and Authentication" }, { id: "IR", group: "800-53 Rev 5", title: "Incident Response" }, { id: "\
|
|
MA", group: "800-53 Rev 5", title: "Maintenance" }, { id: "MP", group: "800-53 Rev 5", title: "Media\
|
|
Protection" }, { id: "PE", group: "800-53 Rev 5", title: "Physical and Environmental Protection" },
|
|
{ id: "PL", group: "800-53 Rev 5", title: "Planning" }, { id: "PM", group: "800-53 Rev 5", title: "\
|
|
Program Management" }, { id: "PS", group: "800-53 Rev 5", title: "Personnel Security" }, { id: "PT",
|
|
group: "800-53 Rev 5", title: "Personally Identifiable Information Processing and Transparency" },
|
|
{ id: "RA", group: "800-53 Rev 5", title: "Risk Assessment" }, { id: "SA", group: "800-53 Rev 5", title: "\
|
|
System and Services Acquisition" }, { id: "SC", group: "800-53 Rev 5", title: "System and Communicat\
|
|
ions Protection" }, { id: "SI", group: "800-53 Rev 5", title: "System and Information Integrity" }, {
|
|
id: "SR", group: "800-53 Rev 5", title: "Supply Chain Risk Management" }] } };
|
|
|
|
// <define:__SUBPROCESSORS__>
|
|
var define_SUBPROCESSORS_default = [{ id: "digitalocean", name: "DigitalOcean", role: "processor",
|
|
mark: "digitalocean", purpose: "The infrastructure everything runs on: the Kubernetes clusters, th\
|
|
e block volumes each database lives on, and the load balancers that terminate traffic. Separately, t\
|
|
he inference endpoint that serves embeddings, images, video and reranking.", data: "Everything. Ever\
|
|
y request reaches a process on their compute, and every byte we store at rest sits on their volumes,\
|
|
so there is no customer record that is not on their hardware. The inference endpoint additionally r\
|
|
eceives the text or image sent to it.", location: "United States (sfo3), on the operator's global fo\
|
|
otprint", url: "https://www.digitalocean.com", terms: "https://www.digitalocean.com/legal/data-proce\
|
|
ssing-agreement", evidence: [{ repo: "hanzoai/universe", path: "infra/terraform/main.tf", note: "the\
|
|
provider, the network, the firewall and the nodes are declared here" }, { repo: "hanzoai/universe",
|
|
path: "infra/k8s/ingress/service.yaml", note: "the load balancer that terminates public traffic" },
|
|
{ repo: "hanzoai/universe", path: "charts/app/values/hanzo/cloud.yaml", note: "the inference endpo\
|
|
int and the key that reaches it" }] }, { id: "cloudflare", name: "Cloudflare", role: "processor", mark: "\
|
|
cloudflare", purpose: "Authoritative DNS for every zone, and the reverse proxy in front of the hosts\
|
|
marked proxied \u2014 which means they terminate TLS for those and see the request in the clear. A worke\
|
|
r sits in the analytics path, the sign-in pages are served from their hosting, and aggregate mail re\
|
|
ports are addressed to them.", data: "For a proxied host: the whole request. The visitor's address, \
|
|
the URL, the headers, the body, and the response. Their page-timing script is served to hanzo.ai and\
|
|
reports that visitor's page views back to them.", location: "United States, served from the operato\
|
|
r's global edge", url: "https://www.cloudflare.com", terms: "https://www.cloudflare.com/cloudflare-c\
|
|
ustomer-dpa/", evidence: [{ repo: "hanzoai/universe", path: "infra/cf-zones/hanzo-ai.yaml", note: "t\
|
|
he zone, the records marked proxied, and the address aggregate mail reports go to" }, { repo: "hanzo\
|
|
ai/universe", path: "workers/analytics-proxy/worker.js", note: "a worker in the request path of ever\
|
|
y analytics beacon" }, { repo: "hanzoai/universe", path: "charts/app/values/hanzo/tabs.yaml", note: "\
|
|
records that the page-timing script is inserted at the edge rather than by us, so it appears in no p\
|
|
age we build" }, { origin: "static.cloudflareinsights.com", note: "the page-timing script. Inserted \
|
|
into every hanzo.ai host; only the surfaces whose enforced policy omits it refuse to run it" }] }, {
|
|
id: "openrouter", name: "OpenRouter", role: "processor", mark: "openrouter", purpose: "Model infer\
|
|
ence. The route most text and vision requests take.", data: "The prompt as sent, including whatever \
|
|
a customer put in it, and the completion returned. They route onward to the operator that runs the m\
|
|
odel, so a prompt reaching them may reach that operator too.", location: "United States", url: "http\
|
|
s://openrouter.ai", terms: "https://openrouter.ai/terms", evidence: [{ repo: "hanzoai/universe", path: "\
|
|
charts/app/values/hanzo/cloud.yaml", note: "the endpoint and the key" }, { repo: "hanzoai/universe",
|
|
path: "charts/app/values/enso/enso.yaml", note: "the same endpoint declared for the router" }] }, {
|
|
id: "fireworks", name: "Fireworks AI", role: "processor", purpose: "The embedder behind the knowle\
|
|
dge store \u2014 it turns a document into the vector the store is searched by.", data: "The text of \
|
|
every document ingested into a knowledge store, and the text of every query run against one. This is\
|
|
the widest document flow on this list and it is worth reading twice: a file a customer uploads for \
|
|
retrieval is sent here in full.", location: "United States", url: "https://fireworks.ai", terms: "ht\
|
|
tps://fireworks.ai/terms-of-service", evidence: [{ repo: "hanzoai/universe", path: "charts/app/value\
|
|
s/hanzo/cloud.yaml", note: "the embedding endpoint, the model and the key reference" }] }, { id: "op\
|
|
enai", name: "OpenAI", role: "processor", purpose: "Model inference, on a key we hold.", data: "The \
|
|
prompt sent to a model they serve, and the completion returned.", location: "United States", url: "h\
|
|
ttps://openai.com", terms: "https://openai.com/policies/data-processing-addendum", evidence: [{ repo: "\
|
|
hanzoai/universe", path: "charts/app/values/hanzo/cloud.yaml", note: "the key, held in the shared mo\
|
|
del-key secret" }] }, { id: "anthropic", name: "Anthropic", role: "processor", mark: "anthropic", purpose: "\
|
|
Model inference, on a key we hold.", data: "The prompt sent to a model they serve, and the completio\
|
|
n returned.", location: "United States", url: "https://www.anthropic.com", terms: "https://www.anthr\
|
|
opic.com/legal/commercial-terms", evidence: [{ repo: "hanzoai/universe", path: "charts/app/values/ha\
|
|
nzo/cloud.yaml", note: "the key, held in the shared model-key secret" }] }, { id: "deepseek", name: "\
|
|
DeepSeek", role: "processor", mark: "deepseek", purpose: "Model inference, on a key we hold.", data: "\
|
|
The prompt sent to a model they serve, and the completion returned.", location: "China", url: "https\
|
|
://www.deepseek.com", terms: "https://platform.deepseek.com/downloads/DeepSeek%20Open%20Platform%20T\
|
|
erms%20of%20Service.html", evidence: [{ repo: "hanzoai/universe", path: "cloud/src/providers.rs", note: "\
|
|
the endpoint" }] }, { id: "twilio", name: "Twilio", role: "processor", purpose: "Message delivery \u2014 \
|
|
text, voice and chat through the messaging interface, and mail through the mail interface. One party\
|
|
, two products.", data: "The recipient's number or address, and the body of the message sent to them\
|
|
.", location: "United States", url: "https://www.twilio.com", terms: "https://www.twilio.com/en-us/l\
|
|
egal/data-protection-addendum", evidence: [{ repo: "hanzoai/cloud", path: "apps/notify/twilio.go", symbol: "\
|
|
register", note: "one party registered as two providers, and the credentials each reads" }, { repo: "\
|
|
hanzoai/universe", path: "charts/app/values/hanzo/iam.yaml", note: "records that the credentials are\
|
|
held per organization in the key store rather than as a deployment secret" }] }, { id: "google", name: "\
|
|
Google", role: "processor", mark: "google", purpose: "Two unrelated things. Signing in with a Google\
|
|
account, which is a customer's own choice at the sign-in page. And the mail exchange for our own do\
|
|
mains, which is where a message a customer sends us arrives.", data: "For sign-in: the account ident\
|
|
ifier and the profile fields that flow back with it. For mail: any message a customer addresses to u\
|
|
s, in full.", location: "United States", url: "https://workspace.google.com", terms: "https://cloud.\
|
|
google.com/terms/data-processing-addendum", evidence: [{ repo: "hanzoai/universe", path: "infra/k8s/\
|
|
kms-canonical-crs/hanzo/google-oauth-kms-sync.yaml", note: "the sign-in credentials" }, { repo: "han\
|
|
zoai/universe", path: "infra/cf-zones/hanzo-ai.yaml", note: "the mail exchange records for the zone" }] },
|
|
{ id: "github", name: "GitHub", role: "processor", mark: "github", purpose: "Signing in with a Git\
|
|
Hub account, and reading the repository a customer asks us to build. The public mirror of our own so\
|
|
urce is a different relationship and no customer data is in it.", data: "For sign-in: the account id\
|
|
entifier and the profile fields that flow back. For a build: the contents of the repository a custom\
|
|
er connected, read with a token scoped to what they installed us on.", location: "United States", url: "\
|
|
https://github.com", terms: "https://github.com/customer-terms/github-data-protection-agreement", evidence: [
|
|
{ repo: "hanzoai/universe", path: "infra/k8s/kms-canonical-crs/hanzo/github-oauth-kms-sync.yaml", note: "\
|
|
the sign-in credentials" }, { repo: "hanzoai/universe", path: "infra/k8s/hanzo-build/ghcr-renew.yaml",
|
|
note: "the installation token minted to read a connected repository" }] }, { id: "square", name: "\
|
|
Square", role: "processor", mark: "square", purpose: "Taking payment. The card is entered into a fra\
|
|
me they serve, so the number itself goes from the browser to them and never through us.", data: "The\
|
|
buyer's identity and the amount. The card number reaches them directly and does not reach us at any\
|
|
point.", location: "United States", url: "https://squareup.com", terms: "https://squareup.com/legal\
|
|
/general/dpa", evidence: [{ repo: "hanzoai/universe", path: "charts/app/values/hanzo/pay.yaml", note: "\
|
|
the origins the checkout page is permitted to reach" }, { repo: "hanzoai/cloud", path: "apps/integra\
|
|
tions/payments.go", note: "the endpoint" }] }, { id: "slack", name: "Slack", role: "processor", purpose: "\
|
|
Where operational alerts are posted, and the app a customer can install into their own workspace.", data: "\
|
|
The text of an alert, which names a service and a condition. For an installed app, whatever the cust\
|
|
omer's own workspace sends it.", location: "United States", url: "https://slack.com", terms: "https:\
|
|
//slack.com/trust/data-management/data-processing-addendum", evidence: [{ repo: "hanzoai/universe", path: "\
|
|
infra/k8s/kms-canonical-crs/hanzo/cloud-slack-kms-sync.yaml", note: "the app credentials and the sig\
|
|
ning secret" }, { repo: "hanzoai/universe", path: "charts/app/values/hanzo/cloud.yaml", note: "the c\
|
|
hannel alerts are addressed to" }] }, { id: "amazon", name: "Amazon Web Services", role: "vendor", purpose: "\
|
|
Named in configuration and not used. Their command-line tool is the client we run against our OWN ob\
|
|
ject store, and the region string beside it is a value that client requires rather than a place anyt\
|
|
hing is stored. The managed-model endpoint is written down and left switched off.", data: "None. Eve\
|
|
ry bucket those commands address resolves inside the cluster, and the model endpoint is commented ou\
|
|
t rather than configured.", location: "Not applicable", url: "https://aws.amazon.com", evidence: [{ repo: "\
|
|
hanzoai/universe", path: "infra/k8s/git/create-bucket-job.yaml", note: "the tool, run against the in\
|
|
-cluster store" }, { repo: "hanzoai/universe", path: "charts/app/values/enso/enso.yaml", note: "the \
|
|
managed-model endpoint, commented out and marked as waiting on credentials" }] }, { id: "googleads",
|
|
name: "Google Ads", role: "vendor", mark: "googleads", purpose: "Advertising we buy. Buying an adv\
|
|
ertisement is a purchase from a platform, not a route data travels.", data: "None. No script of thei\
|
|
rs is served by any Hanzo surface, no identifier is shared with them, and nothing about a visitor to\
|
|
our sites reaches them. `bin/surface` is what keeps that true rather than remembered.", location: "\
|
|
Not applicable", url: "https://ads.google.com", evidence: [{ repo: "hanzoai/trust", path: "bin/surfa\
|
|
ce.mjs", note: "refuses the build if a measurement tag of theirs is ever reached from a Hanzo surfac\
|
|
e" }] }, { id: "meta", name: "Meta", role: "vendor", mark: "meta", purpose: "Advertising we buy. Sam\
|
|
e relationship as the platform above and the same boundary.", data: "None. No script of theirs is se\
|
|
rved by any Hanzo surface, and no visitor event is sent to them.", location: "Not applicable", url: "\
|
|
https://www.facebook.com/business", evidence: [{ repo: "hanzoai/trust", path: "bin/surface.mjs", note: "\
|
|
refuses the build if their tag is ever reached from a Hanzo surface" }] }];
|
|
|
|
// check.mjs
|
|
var ID = /^[a-z0-9]+(\.[a-z0-9-]+)+$/;
|
|
var STATUS = ["automated", "partial", "absent"];
|
|
var STRENGTH = ["full", "partial"];
|
|
var METHOD = ["read", "test", "audit"];
|
|
var CLAIMS = [
|
|
/\bcertifi(ed|cation)\b/i,
|
|
/\baccredit(ed|ation)\b/i,
|
|
/\battest(ed|ation)\b/i,
|
|
/\bcompliant\b/i,
|
|
/\bcompliance with\b/i,
|
|
/\baudited by\b/i,
|
|
/\bfips[\s-]?(140[\s-]?[23][\s-]?)?(validated|certified|compliant|approved)\b/i
|
|
];
|
|
var FRAMEWORK_WORDS = [
|
|
/\bsoc[\s-]?[12]\b/i,
|
|
/\biso[\s-]?270\d\d\b/i,
|
|
/\b800[\s-]?53\b/i,
|
|
/\bfedramp\b/i,
|
|
/\bpci[\s-]?dss\b/i,
|
|
/\bhitrust\b/i,
|
|
/\bhipaa\b/i,
|
|
/\bcsa[\s-]?star\b/i
|
|
];
|
|
var isStr = (v) => typeof v === "string" && v.trim() !== "";
|
|
var isArr = (v) => Array.isArray(v) && v.length > 0;
|
|
function prose(control2) {
|
|
const out = [];
|
|
const walk = (v) => {
|
|
if (typeof v === "string") out.push(v);
|
|
else if (Array.isArray(v)) v.forEach(walk);
|
|
else if (v && typeof v === "object") {
|
|
for (const k of Object.keys(v)) {
|
|
if (k === "maps" || k === "id") continue;
|
|
walk(v[k]);
|
|
}
|
|
}
|
|
};
|
|
walk(control2);
|
|
return out.join("\n");
|
|
}
|
|
function checkPlace(place, where, problems) {
|
|
if (!place || typeof place !== "object") {
|
|
problems.push(`${where}: not an object`);
|
|
return;
|
|
}
|
|
if (!isStr(place.repo)) problems.push(`${where}: repo is required`);
|
|
if (!isStr(place.path)) problems.push(`${where}: path is required`);
|
|
else if (/^\//.test(place.path) || /\s/.test(place.path) || /(^|\/)\.\.?(\/|$)/.test(place.path)) {
|
|
problems.push(`${where}: path must be repo-relative with no spaces (got "${place.path}")`);
|
|
}
|
|
if (place.line !== void 0 && (!Number.isInteger(place.line) || place.line < 1)) {
|
|
problems.push(`${where}: line must be a positive whole number when present`);
|
|
}
|
|
}
|
|
function checkFrameworks(frameworks2) {
|
|
const problems = [];
|
|
const index = /* @__PURE__ */ new Set();
|
|
if (!frameworks2 || typeof frameworks2 !== "object" || Array.isArray(frameworks2)) {
|
|
return [["frameworks.json: expected an object keyed by framework id"], index];
|
|
}
|
|
for (const id of Object.keys(frameworks2)) {
|
|
if (id.charAt(0) === "$") continue;
|
|
const f = frameworks2[id];
|
|
const at = `frameworks.json[${id}]`;
|
|
if (!/^[a-z0-9]+$/.test(id)) problems.push(`${at}: id must be lowercase alphanumeric`);
|
|
if (!f || typeof f !== "object") {
|
|
problems.push(`${at}: not an object`);
|
|
continue;
|
|
}
|
|
for (const k of ["name", "publisher", "edition"]) {
|
|
if (!isStr(f[k])) problems.push(`${at}: ${k} is required`);
|
|
}
|
|
if (!isStr(f.unit)) problems.push(`${at}: unit is required \u2014 a count with no unit is not a fac\
|
|
t`);
|
|
if (!isStr(f.units)) problems.push(`${at}: units (the plural) is required`);
|
|
if (!isArr(f.clauses)) {
|
|
problems.push(`${at}: clauses must be a non-empty array`);
|
|
continue;
|
|
}
|
|
const seen = /* @__PURE__ */ new Set();
|
|
for (const c of f.clauses) {
|
|
if (!c || typeof c !== "object" || !isStr(c.id)) {
|
|
problems.push(`${at}: a clause has no id`);
|
|
continue;
|
|
}
|
|
if (seen.has(c.id)) problems.push(`${at}: duplicate clause ${c.id}`);
|
|
seen.add(c.id);
|
|
if (!isStr(c.title)) problems.push(`${at}.${c.id}: title is required`);
|
|
if (c.group !== void 0 && !isStr(c.group)) {
|
|
problems.push(`${at}.${c.id}: group must be a non-empty string when present`);
|
|
}
|
|
index.add(`${id}:${c.id}`);
|
|
}
|
|
}
|
|
return [problems, index];
|
|
}
|
|
function checkControls(input, clauseIndex) {
|
|
const problems = [];
|
|
const controls2 = Array.isArray(input) ? input : input && input.controls;
|
|
if (!Array.isArray(controls2)) {
|
|
return ["controls.json: expected a list of controls, or an envelope holding one"];
|
|
}
|
|
const seen = /* @__PURE__ */ new Set();
|
|
for (let i = 0; i < controls2.length; i++) {
|
|
const c = controls2[i];
|
|
const at = `controls.json[${c && c.id || i}]`;
|
|
if (!c || typeof c !== "object") {
|
|
problems.push(`${at}: not an object`);
|
|
continue;
|
|
}
|
|
if (!isStr(c.id)) problems.push(`${at}: id is required`);
|
|
else if (!ID.test(c.id)) problems.push(`${at}: id must be dotted lowercase, e.g. iam.pkce.s256`);
|
|
else if (seen.has(c.id)) problems.push(`${at}: duplicate id`);
|
|
if (isStr(c.id)) seen.add(c.id);
|
|
for (const k of ["claim", "mechanism"]) {
|
|
if (!isStr(c[k])) problems.push(`${at}: ${k} is required`);
|
|
}
|
|
if (c.title !== void 0 && !isStr(c.title)) {
|
|
problems.push(`${at}: title must be a non-empty string when present`);
|
|
}
|
|
if (STATUS.indexOf(c.status) < 0) {
|
|
problems.push(`${at}: status must be one of ${STATUS.join(", ")}`);
|
|
}
|
|
if (c.status !== "automated" && !isStr(c.note)) {
|
|
problems.push(`${at}: status "${c.status}" requires a note saying what is missing`);
|
|
}
|
|
if (!isArr(c.enforced)) problems.push(`${at}: enforced must name at least one place`);
|
|
else c.enforced.forEach((p, n) => checkPlace(p, `${at}.enforced[${n}]`, problems));
|
|
if (!isArr(c.verified)) problems.push(`${at}: verified must name at least one check`);
|
|
else
|
|
c.verified.forEach((v, n) => {
|
|
const vat = `${at}.verified[${n}]`;
|
|
if (!v || typeof v !== "object" || METHOD.indexOf(v.method) < 0) {
|
|
problems.push(`${vat}: method must be one of ${METHOD.join(", ")}`);
|
|
return;
|
|
}
|
|
if (v.method === "audit") {
|
|
if (!isArr(v.actions) || !v.actions.every(isStr)) {
|
|
problems.push(`${vat}: an audit check must name the actions that evidence it`);
|
|
}
|
|
} else if (!isArr(v.at)) {
|
|
problems.push(`${vat}: a ${v.method} check must name where it is`);
|
|
} else {
|
|
v.at.forEach((p, m) => checkPlace(p, `${vat}.at[${m}]`, problems));
|
|
}
|
|
});
|
|
if (!isArr(c.maps)) problems.push(`${at}: maps must name at least one clause`);
|
|
else {
|
|
const refs = /* @__PURE__ */ new Set();
|
|
for (const m of c.maps) {
|
|
if (!m || typeof m !== "object" || !isStr(m.clause)) {
|
|
problems.push(`${at}: a mapping has no clause`);
|
|
continue;
|
|
}
|
|
if (STRENGTH.indexOf(m.strength) < 0) {
|
|
problems.push(`${at} -> ${m.clause}: strength must be one of ${STRENGTH.join(", ")}`);
|
|
}
|
|
if (!clauseIndex.has(m.clause)) {
|
|
problems.push(`${at} -> ${m.clause}: no framework declares that clause`);
|
|
}
|
|
if (refs.has(m.clause)) problems.push(`${at}: duplicate mapping to ${m.clause}`);
|
|
refs.add(m.clause);
|
|
}
|
|
}
|
|
const text = prose(c);
|
|
for (const re of CLAIMS) {
|
|
const hit = re.exec(text);
|
|
if (hit) problems.push(`${at}: prose claims "${hit[0]}" \u2014 this repo does not claim status`);
|
|
}
|
|
for (const re of FRAMEWORK_WORDS) {
|
|
const hit = re.exec(text);
|
|
if (hit) {
|
|
problems.push(
|
|
`${at}: prose names "${hit[0]}" \u2014 a framework belongs in maps, where it carries a number`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return problems;
|
|
}
|
|
var ROLES = ["processor", "vendor"];
|
|
var SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
function checkEvidence(e, where, problems) {
|
|
if (!e || typeof e !== "object") {
|
|
problems.push(`${where}: not an object`);
|
|
return;
|
|
}
|
|
const origin = typeof e.origin === "string" ? e.origin.trim() : "";
|
|
if (origin) {
|
|
if (!/^[a-z0-9]([-a-z0-9.]*[a-z0-9])?$/.test(origin) || origin.indexOf(".") < 0) {
|
|
problems.push(`${where}: origin must be a bare hostname (got "${e.origin}")`);
|
|
}
|
|
if (e.repo !== void 0 || e.path !== void 0) {
|
|
problems.push(`${where}: an origin and a repository path are two kinds of evidence, not one \
|
|
entry`);
|
|
}
|
|
return;
|
|
}
|
|
checkPlace(e, where, problems);
|
|
}
|
|
function checkSubprocessors(input) {
|
|
const problems = [];
|
|
const rows = Array.isArray(input) ? input : input && input.subprocessors;
|
|
if (!Array.isArray(rows)) {
|
|
return ["subprocessors.json: expected a list of parties, or an envelope holding one"];
|
|
}
|
|
const seen = /* @__PURE__ */ new Set();
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const s = rows[i];
|
|
const at = `subprocessors.json[${s && s.id || i}]`;
|
|
if (!s || typeof s !== "object") {
|
|
problems.push(`${at}: not an object`);
|
|
continue;
|
|
}
|
|
if (!isStr(s.id)) problems.push(`${at}: id is required`);
|
|
else if (!SLUG.test(s.id)) problems.push(`${at}: id must be a lowercase slug, e.g. digitalocea\
|
|
n`);
|
|
else if (seen.has(s.id)) problems.push(`${at}: duplicate id`);
|
|
if (isStr(s.id)) seen.add(s.id);
|
|
if (!isStr(s.name)) problems.push(`${at}: name is required`);
|
|
if (!isStr(s.purpose)) problems.push(`${at}: purpose is required \u2014 why this party is here`);
|
|
if (!isStr(s.data)) {
|
|
problems.push(`${at}: data is required \u2014 what customer data reaches this party, or that none\
|
|
does and why`);
|
|
}
|
|
if (s.mark !== void 0 && (!isStr(s.mark) || !SLUG.test(s.mark))) {
|
|
problems.push(`${at}: mark must be a lowercase slug naming a canonical mark`);
|
|
}
|
|
if (s.url !== void 0 && !isStr(s.url)) problems.push(`${at}: url must be a non-empty string wh\
|
|
en present`);
|
|
if (ROLES.indexOf(s.role) < 0) {
|
|
problems.push(`${at}: role must be one of ${ROLES.join(", ")}`);
|
|
} else if (s.role === "processor") {
|
|
if (!isStr(s.location)) {
|
|
problems.push(`${at}: a processor must state where it is \u2014 the data goes there`);
|
|
}
|
|
if (!isStr(s.terms)) {
|
|
problems.push(`${at}: a processor must name the terms it processes under`);
|
|
}
|
|
}
|
|
if (!isArr(s.evidence)) {
|
|
problems.push(`${at}: evidence must name at least one place or origin`);
|
|
} else {
|
|
s.evidence.forEach((e, n) => checkEvidence(e, `${at}.evidence[${n}]`, problems));
|
|
}
|
|
const text = prose(s);
|
|
for (const re of CLAIMS) {
|
|
const hit = re.exec(text);
|
|
if (hit) problems.push(`${at}: prose claims "${hit[0]}" \u2014 this repo does not restate another\
|
|
party's status`);
|
|
}
|
|
for (const re of FRAMEWORK_WORDS) {
|
|
const hit = re.exec(text);
|
|
if (hit) problems.push(`${at}: prose names "${hit[0]}" \u2014 a framework belongs in maps, where \
|
|
it carries a number`);
|
|
}
|
|
}
|
|
return problems;
|
|
}
|
|
|
|
// src/inventory.ts
|
|
var CONTROLS = define_CONTROLS_default;
|
|
var FRAMEWORKS = define_FRAMEWORKS_default;
|
|
var SUBPROCESSORS = define_SUBPROCESSORS_default;
|
|
var VERSION = "0.2.4";
|
|
function ids() {
|
|
const out = [];
|
|
for (const k in FRAMEWORKS) {
|
|
if (!Object.prototype.hasOwnProperty.call(FRAMEWORKS, k)) continue;
|
|
if (k.charAt(0) === "$") continue;
|
|
out.push(k);
|
|
}
|
|
return out.sort();
|
|
}
|
|
function framework(id) {
|
|
if (!id || id.charAt(0) === "$") return null;
|
|
return Object.prototype.hasOwnProperty.call(FRAMEWORKS, id) ? FRAMEWORKS[id] : null;
|
|
}
|
|
function control(id) {
|
|
for (let i = 0; i < CONTROLS.length; i++) {
|
|
if (CONTROLS[i].id === id) return CONTROLS[i];
|
|
}
|
|
return null;
|
|
}
|
|
function verified(c) {
|
|
for (let i = 0; i < c.verified.length; i++) {
|
|
const m = c.verified[i].method;
|
|
if (m === "test" || m === "audit") return true;
|
|
}
|
|
return false;
|
|
}
|
|
function evidence(c) {
|
|
for (let i = 0; i < c.verified.length; i++) {
|
|
const v = c.verified[i];
|
|
if (v.method === "audit" && v.actions && v.actions.length) return v.actions;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// src/db.ts
|
|
function db() {
|
|
const d = globalThis.__db;
|
|
return d && typeof d.query === "function" ? d : null;
|
|
}
|
|
function own() {
|
|
const f = globalThis.__own;
|
|
return typeof f === "function" ? f() : false;
|
|
}
|
|
function newId() {
|
|
const f = globalThis.__newId;
|
|
if (typeof f !== "function") throw new Error("trust: host injected no __newId");
|
|
return f();
|
|
}
|
|
var KINDS = [
|
|
"profile",
|
|
"control",
|
|
"document",
|
|
"subprocessor",
|
|
"policy",
|
|
"faq",
|
|
"update",
|
|
"risk"
|
|
];
|
|
function isKind(s) {
|
|
for (let i = 0; i < KINDS.length; i++) if (KINDS[i] === s) return true;
|
|
return false;
|
|
}
|
|
var SINGLE = { profile: true, risk: true };
|
|
function parse(r) {
|
|
let data = {};
|
|
try {
|
|
const v = JSON.parse(r.data);
|
|
if (v && typeof v === "object" && !Array.isArray(v)) data = v;
|
|
} catch (e) {
|
|
data = {};
|
|
}
|
|
return { id: r.id, ord: Number(r.ord) || 0, updated: Number(r.updated) || 0, data };
|
|
}
|
|
function list(kind) {
|
|
const d = db();
|
|
if (!d) return [];
|
|
const rows = d.query(
|
|
"SELECT kind, id, ord, data, updated FROM record WHERE kind = ? ORDER BY ord ASC, id ASC",
|
|
[kind]
|
|
);
|
|
const out = [];
|
|
for (let i = 0; i < rows.length; i++) out.push(parse(rows[i]));
|
|
return out;
|
|
}
|
|
function get(kind, id) {
|
|
const d = db();
|
|
if (!d) return null;
|
|
const rows = d.query(
|
|
"SELECT kind, id, ord, data, updated FROM record WHERE kind = ? AND id = ?",
|
|
[kind, id]
|
|
);
|
|
return rows.length ? parse(rows[0]) : null;
|
|
}
|
|
function put(kind, id, ord, data, at) {
|
|
const d = db();
|
|
if (!d) throw new Error("trust: no store");
|
|
d.exec(
|
|
"INSERT INTO record (kind, id, ord, data, updated) VALUES (?, ?, ?, ?, ?) ON CONFLICT(kind, id\
|
|
) DO UPDATE SET ord = excluded.ord, data = excluded.data, updated = excluded.updated",
|
|
[kind, id, ord, JSON.stringify(data), at]
|
|
);
|
|
}
|
|
function drop(kind, id) {
|
|
const d = db();
|
|
if (!d) throw new Error("trust: no store");
|
|
return d.exec("DELETE FROM record WHERE kind = ? AND id = ?", [kind, id]).changes;
|
|
}
|
|
|
|
// src/center.ts
|
|
var CATEGORIES = [
|
|
"infrastructure",
|
|
"data",
|
|
"access",
|
|
"network",
|
|
"endpoint",
|
|
"corporate",
|
|
"product",
|
|
"incident"
|
|
];
|
|
var KINDS2 = {
|
|
soc2: { label: "SOC 2 report", attested: true },
|
|
iso: { label: "ISO/IEC 27001 certificate", attested: true },
|
|
pentest: { label: "Penetration test report", attested: true },
|
|
letter: { label: "Auditor letter", attested: true },
|
|
caiq: { label: "CAIQ self-assessment", attested: false },
|
|
sig: { label: "SIG self-assessment", attested: false },
|
|
vsa: { label: "VSA self-assessment", attested: false },
|
|
questionnaire: { label: "Security questionnaire", attested: false },
|
|
policy: { label: "Policy", attested: false },
|
|
other: { label: "Document", attested: false }
|
|
};
|
|
var TIERS = ["public", "gated"];
|
|
var str = (v) => typeof v === "string" ? v.trim() : "";
|
|
var has = (v) => str(v) !== "";
|
|
function controls() {
|
|
const base = own() ? CONTROLS : [];
|
|
const seen = {};
|
|
for (let i = 0; i < base.length; i++) seen[base[i].id] = true;
|
|
const out = base.slice();
|
|
const rows = list("control");
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const c = Object.assign({}, rows[i].data, { id: rows[i].id });
|
|
if (seen[c.id]) continue;
|
|
out.push(c);
|
|
}
|
|
return out;
|
|
}
|
|
function parties() {
|
|
const base = own() ? SUBPROCESSORS : [];
|
|
const seen = {};
|
|
for (let i = 0; i < base.length; i++) seen[base[i].id] = true;
|
|
const out = base.slice();
|
|
const rows = list("subprocessor");
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const s = Object.assign({}, rows[i].data, { id: rows[i].id });
|
|
if (seen[s.id]) continue;
|
|
out.push(s);
|
|
}
|
|
return out;
|
|
}
|
|
var COMPILED = {
|
|
control: CONTROLS,
|
|
subprocessor: SUBPROCESSORS
|
|
};
|
|
function governed(kind, id) {
|
|
if (!own() || !id) return false;
|
|
const rows = Object.prototype.hasOwnProperty.call(COMPILED, kind) ? COMPILED[kind] : null;
|
|
if (!rows) return false;
|
|
for (let i = 0; i < rows.length; i++) if (rows[i].id === id) return true;
|
|
return false;
|
|
}
|
|
function validate(kind, id, data) {
|
|
switch (kind) {
|
|
case "control": {
|
|
const c = Object.assign({}, data, { id });
|
|
const [, index] = checkFrameworks(FRAMEWORKS);
|
|
const problems = checkControls([c], index);
|
|
const cat = str(data.category);
|
|
if (cat) {
|
|
let ok2 = false;
|
|
for (let i = 0; i < CATEGORIES.length; i++) if (CATEGORIES[i] === cat) ok2 = true;
|
|
if (!ok2) problems.push("category must be one of " + CATEGORIES.join(", "));
|
|
}
|
|
return problems;
|
|
}
|
|
case "document": {
|
|
const p = [];
|
|
if (!has(data.title)) p.push("title is required");
|
|
const k = str(data.kind) || "other";
|
|
if (!Object.prototype.hasOwnProperty.call(KINDS2, k)) {
|
|
p.push("kind must be one of " + Object.keys(KINDS2).join(", "));
|
|
return p;
|
|
}
|
|
const tier = str(data.tier) || "gated";
|
|
if (TIERS.indexOf(tier) < 0) p.push("tier must be public or gated");
|
|
if (KINDS2[k].attested && tier === "public") {
|
|
p.push(
|
|
"a " + KINDS2[k].label + " is attested by someone outside this organization, so it is re\
|
|
leased through a grant rather than published"
|
|
);
|
|
}
|
|
return p;
|
|
}
|
|
// The SAME module the build gate runs, so an authored party is held to the
|
|
// rule a committed one is: it must take a role from the closed pair, say
|
|
// what customer data reaches it, and — if it is a processor — say where it
|
|
// is and under which terms. A name and a sentence is not a disclosure.
|
|
case "subprocessor":
|
|
return checkSubprocessors([Object.assign({}, data, { id })]);
|
|
case "policy":
|
|
return has(data.title) ? [] : ["title is required"];
|
|
case "faq": {
|
|
const p = [];
|
|
if (!has(data.question)) p.push("question is required");
|
|
if (!has(data.answer)) p.push("answer is required");
|
|
return p;
|
|
}
|
|
case "update": {
|
|
const p = [];
|
|
if (!has(data.title)) p.push("title is required");
|
|
if (!has(data.at)) p.push("at is required \u2014 an update with no date cannot be read in order");
|
|
return p;
|
|
}
|
|
case "profile":
|
|
return has(data.name) ? [] : ["name is required"];
|
|
case "risk":
|
|
return Array.isArray(data.items) ? [] : ["items must be a list of {label, value}"];
|
|
}
|
|
return ["unknown section " + kind];
|
|
}
|
|
function write(kind, id, body, at, ord) {
|
|
if (!isKind(kind)) return { ok: false, status: 404, message: "no trust section " + kind };
|
|
const k = kind;
|
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
return { ok: false, status: 400, message: "body must be an object" };
|
|
}
|
|
const data = Object.assign({}, body);
|
|
let key = SINGLE[k] ? "" : str(id) || newId();
|
|
delete data.id;
|
|
if (governed(k, key)) {
|
|
return {
|
|
ok: false,
|
|
status: 409,
|
|
message: k + " " + key + " is part of this deployment's own inventory, which is governed in \
|
|
git and cannot be authored through the API"
|
|
};
|
|
}
|
|
const errors = validate(k, key, data);
|
|
if (errors.length) {
|
|
return { ok: false, status: 400, message: "the " + k + " is malformed", errors };
|
|
}
|
|
if (k === "document" && !has(data.tier)) data.tier = "gated";
|
|
put(k, key, ord, data, at);
|
|
return { ok: true, wrote: { kind: k, id: key, updated: at } };
|
|
}
|
|
function remove(kind, id) {
|
|
if (!isKind(kind)) return { ok: false, status: 404, message: "no trust section " + kind };
|
|
const k = kind;
|
|
if (governed(k, id)) {
|
|
return {
|
|
ok: false,
|
|
status: 409,
|
|
message: k + " " + id + " is part of this deployment's own inventory and is removed by a com\
|
|
mit, not by a request"
|
|
};
|
|
}
|
|
const n = drop(k, SINGLE[k] ? "" : id);
|
|
if (!n) return { ok: false, status: 404, message: "no " + k + " " + id };
|
|
return { ok: true, status: 200, message: "" };
|
|
}
|
|
function shaped(rows) {
|
|
const out = [];
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const r = Object.assign({}, rows[i].data);
|
|
r.id = rows[i].id;
|
|
r.updated = rows[i].updated;
|
|
out.push(r);
|
|
}
|
|
return out;
|
|
}
|
|
function section(kind) {
|
|
return shaped(list(kind));
|
|
}
|
|
function single(kind) {
|
|
const r = get(kind, "");
|
|
if (!r) return null;
|
|
const out = Object.assign({}, r.data);
|
|
out.updated = r.updated;
|
|
return out;
|
|
}
|
|
function document(d, grant) {
|
|
const k = str(d.kind) || "other";
|
|
const meta = Object.prototype.hasOwnProperty.call(KINDS2, k) ? KINDS2[k] : KINDS2.other;
|
|
const tier = str(d.tier) || "gated";
|
|
const open = tier === "public" || grant;
|
|
const out = {
|
|
id: d.id,
|
|
title: d.title,
|
|
kind: k,
|
|
label: meta.label,
|
|
attested: meta.attested,
|
|
tier,
|
|
updated: d.updated
|
|
};
|
|
if (has(d.note)) out.note = d.note;
|
|
if (open && has(d.href)) out.href = d.href;
|
|
out.released = open;
|
|
return out;
|
|
}
|
|
function documents(grant) {
|
|
const rows = section("document");
|
|
const out = [];
|
|
for (let i = 0; i < rows.length; i++) out.push(document(rows[i], grant));
|
|
return out;
|
|
}
|
|
|
|
// src/coverage.ts
|
|
var RANK = { none: 0, partial: 1, automated: 2 };
|
|
function worth(c, m) {
|
|
if (c.status === "absent") return "none";
|
|
if (c.status === "partial") return "partial";
|
|
if (m.strength !== "full") return "partial";
|
|
return verified(c) ? "automated" : "partial";
|
|
}
|
|
function inventory(cs = CONTROLS) {
|
|
let automated = 0;
|
|
let partial = 0;
|
|
let absent = 0;
|
|
let unverified = 0;
|
|
for (let i = 0; i < cs.length; i++) {
|
|
const c = cs[i];
|
|
if (c.status === "automated") automated++;
|
|
else if (c.status === "partial") partial++;
|
|
else absent++;
|
|
if (!verified(c)) unverified++;
|
|
}
|
|
return {
|
|
total: cs.length,
|
|
automated,
|
|
partial,
|
|
absent,
|
|
unverified,
|
|
statement: automated + " of " + cs.length + " controls automated, " + partial + " partial, " +
|
|
absent + " absent" + (unverified ? "; " + unverified + " rest on a reading rather than a test" :
|
|
"")
|
|
};
|
|
}
|
|
function coverClause(clause, id, cs) {
|
|
let level = "none";
|
|
const hits = [];
|
|
const ref = id + ":" + clause.id;
|
|
for (let i = 0; i < cs.length; i++) {
|
|
const c = cs[i];
|
|
for (let j = 0; j < c.maps.length; j++) {
|
|
const m = c.maps[j];
|
|
if (m.clause !== ref) continue;
|
|
const got = worth(c, m);
|
|
if (got === "none") continue;
|
|
if (RANK[got] > RANK[level]) level = got;
|
|
hits.push({ id: c.id, rank: RANK[got] });
|
|
}
|
|
}
|
|
hits.sort((a, b) => b.rank - a.rank || (a.id < b.id ? -1 : 1));
|
|
const out = {
|
|
id: clause.id,
|
|
title: clause.title,
|
|
level,
|
|
controls: hits.map((h) => h.id)
|
|
};
|
|
if (clause.group) out.group = clause.group;
|
|
return out;
|
|
}
|
|
function summarize(id, f, cs) {
|
|
const clauses = [];
|
|
const tally = { total: f.clauses.length, automated: 0, partial: 0, none: 0 };
|
|
for (let i = 0; i < f.clauses.length; i++) {
|
|
const cc = coverClause(f.clauses[i], id, cs);
|
|
clauses.push(cc);
|
|
tally[cc.level]++;
|
|
}
|
|
const detail = {
|
|
framework: id,
|
|
name: f.name,
|
|
publisher: f.publisher,
|
|
edition: f.edition,
|
|
unit: f.unit,
|
|
units: f.units,
|
|
total: tally.total,
|
|
automated: tally.automated,
|
|
partial: tally.partial,
|
|
none: tally.none,
|
|
statement: tally.automated + " of " + tally.total + " " + f.units + " have an automated contro\
|
|
l, " + tally.partial + " partial, " + tally.none + " none",
|
|
clauses
|
|
};
|
|
if (f.note) detail.note = f.note;
|
|
return detail;
|
|
}
|
|
function framework2(id, cs = CONTROLS) {
|
|
const f = framework(id);
|
|
return f ? summarize(id, f, cs) : null;
|
|
}
|
|
function summary(cs = CONTROLS) {
|
|
const list2 = ids();
|
|
const out = [];
|
|
for (let i = 0; i < list2.length; i++) {
|
|
const d = summarize(list2[i], framework(list2[i]), cs);
|
|
const row = {
|
|
framework: d.framework,
|
|
name: d.name,
|
|
publisher: d.publisher,
|
|
edition: d.edition,
|
|
unit: d.unit,
|
|
units: d.units,
|
|
total: d.total,
|
|
automated: d.automated,
|
|
partial: d.partial,
|
|
none: d.none,
|
|
statement: d.statement
|
|
};
|
|
if (d.note) row.note = d.note;
|
|
out.push(row);
|
|
}
|
|
return { controls: inventory(cs), frameworks: out };
|
|
}
|
|
|
|
// src/host.ts
|
|
function now() {
|
|
return globalThis.__now();
|
|
}
|
|
function audit(q) {
|
|
const fn = globalThis.__audit;
|
|
if (typeof fn !== "function") return null;
|
|
return fn(q);
|
|
}
|
|
|
|
// src/reply.ts
|
|
var ok = (body) => ({ status: 200, body });
|
|
var badReq = (message) => ({
|
|
status: 400,
|
|
body: { success: false, message }
|
|
});
|
|
var notFound = (message, extra) => ({
|
|
status: 404,
|
|
body: extra ? __spreadValues({ success: false, message }, extra) : { success: false, message }
|
|
});
|
|
var readOnly = (message) => ({
|
|
status: 405,
|
|
body: { success: false, message, allow: "GET" }
|
|
});
|
|
var unmounted = (message) => ({
|
|
status: 501,
|
|
body: { success: false, message }
|
|
});
|
|
|
|
// src/evidence.ts
|
|
var LIMIT = 100;
|
|
var MAX = 1e3;
|
|
var DATE = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2}))?$/;
|
|
function bound(v, field, errs) {
|
|
const s = v || "";
|
|
if (s && !DATE.test(s)) errs.push(field + " must be an RFC 3339 date");
|
|
return s;
|
|
}
|
|
function query(ctx) {
|
|
const id = ctx.params.control || "";
|
|
if (!id) return badReq("control is required");
|
|
const c = control(id);
|
|
if (!c) return notFound("no control " + id + " in the inventory");
|
|
const errs = [];
|
|
const from = bound(ctx.params.from, "from", errs);
|
|
const to = bound(ctx.params.to, "to", errs);
|
|
let limit = LIMIT;
|
|
const raw = ctx.params.limit;
|
|
if (raw !== void 0 && raw !== "") {
|
|
const n = Number(raw);
|
|
if (!isFinite(n) || Math.floor(n) !== n || n < 1) {
|
|
errs.push("limit must be a positive whole number");
|
|
} else {
|
|
limit = n > MAX ? MAX : n;
|
|
}
|
|
}
|
|
if (from && to && from >= to) errs.push("from must be before to");
|
|
if (errs.length) return badReq(errs.join("; "));
|
|
const actions = evidence(c);
|
|
const head = {
|
|
control: c.id,
|
|
claim: c.claim,
|
|
status: c.status,
|
|
verified: verified(c),
|
|
actions,
|
|
from,
|
|
to,
|
|
limit
|
|
};
|
|
if (actions.length === 0) {
|
|
return ok({
|
|
control: head.control,
|
|
claim: head.claim,
|
|
status: head.status,
|
|
verified: head.verified,
|
|
actions,
|
|
queried: false,
|
|
total: 0,
|
|
note: "no audit action evidences this control; it is verified by reading the source at the f\
|
|
iles the inventory names, not from the trail",
|
|
enforced: c.enforced
|
|
});
|
|
}
|
|
const trail = audit({ actions, from, to, limit });
|
|
if (!trail) {
|
|
return unmounted(
|
|
"this deployment mounted trust without __audit, so the trail cannot be read"
|
|
);
|
|
}
|
|
return ok({
|
|
control: head.control,
|
|
claim: head.claim,
|
|
status: head.status,
|
|
verified: head.verified,
|
|
actions,
|
|
from,
|
|
to,
|
|
limit,
|
|
queried: true,
|
|
total: trail.total,
|
|
count: trail.rows.length,
|
|
truncated: trail.total > trail.rows.length,
|
|
rows: trail.rows
|
|
});
|
|
}
|
|
|
|
// src/index.ts
|
|
function frameworks() {
|
|
return ids().map((id) => {
|
|
const f = framework(id);
|
|
return {
|
|
framework: id,
|
|
name: f ? f.name : "",
|
|
publisher: f ? f.publisher : "",
|
|
edition: f ? f.edition : "",
|
|
unit: f ? f.unit : "",
|
|
units: f ? f.units : "",
|
|
total: f ? f.clauses.length : 0
|
|
};
|
|
});
|
|
}
|
|
function find(id) {
|
|
const cs = controls();
|
|
for (let i = 0; i < cs.length; i++) if (cs[i].id === id) return cs[i];
|
|
return control(id);
|
|
}
|
|
function center(org, grant) {
|
|
const cs = controls();
|
|
const s = summary(cs);
|
|
const profile = single("profile") || {};
|
|
return {
|
|
version: VERSION,
|
|
generated: now(),
|
|
org,
|
|
profile,
|
|
controls: cs,
|
|
coverage: s.frameworks,
|
|
inventory: s.controls,
|
|
frameworks: frameworks(),
|
|
documents: documents(grant),
|
|
subprocessors: parties(),
|
|
policies: section("policy"),
|
|
faq: section("faq"),
|
|
updates: section("update"),
|
|
risk: single("risk") || { items: [] }
|
|
};
|
|
}
|
|
var routes = {
|
|
health: () => ok({ service: "trust", status: "ok", version: VERSION }),
|
|
center: (ctx) => ok(center(ctx.org, true)),
|
|
// The public door. It answers only for an organization that has said its
|
|
// centre is public: an unpublished one is NOT FOUND rather than empty, because
|
|
// an empty centre and a centre nobody meant to show read the same and are not
|
|
// the same. No credential reaches here, so nothing gated does either.
|
|
published: (ctx) => {
|
|
const p = single("profile");
|
|
if (!p || p.published !== true) {
|
|
return notFound("no published trust centre for " + ctx.org);
|
|
}
|
|
return ok(center(ctx.org, false));
|
|
},
|
|
"profile.get": () => ok(single("profile") || {}),
|
|
"controls.list": () => {
|
|
const cs = controls();
|
|
return ok(__spreadProps(__spreadValues({ version: VERSION }, summary(cs).controls), { controls: cs }));
|
|
},
|
|
"controls.get": (ctx) => {
|
|
const c = find(ctx.params.id || "");
|
|
return c ? ok(c) : notFound("no control " + (ctx.params.id || "") + " in this trust centre");
|
|
},
|
|
"frameworks.list": () => ok({ frameworks: frameworks() }),
|
|
"coverage.list": () => {
|
|
const cs = controls();
|
|
return ok(__spreadValues({ version: VERSION, generated: now() }, summary(cs)));
|
|
},
|
|
"coverage.get": (ctx) => {
|
|
const id = ctx.params.framework || "";
|
|
const d = framework2(id, controls());
|
|
if (!d) return notFound("no framework " + id + " is mapped", { frameworks: ids() });
|
|
return ok(__spreadValues({ version: VERSION, generated: now() }, d));
|
|
},
|
|
"documents.list": () => ok({ documents: documents(true) }),
|
|
"subprocessors.list": () => ok({ subprocessors: parties() }),
|
|
"policies.list": () => ok({ policies: section("policy") }),
|
|
"faq.list": () => ok({ faq: section("faq") }),
|
|
"updates.list": () => ok({ updates: section("update") }),
|
|
"risk.get": () => ok(single("risk") || { items: [] }),
|
|
"evidence.get": query,
|
|
// ONE write route for every section. What may be in a record is decided by
|
|
// center.validate, which runs the SAME module the build gate runs, so a
|
|
// control authored here is held to the rule a committed one is held to.
|
|
"section.put": (ctx, req) => {
|
|
const kind = ctx.params.kind || "";
|
|
const at = now();
|
|
const env = req.body || {};
|
|
const ord = typeof env.ord === "number" ? env.ord : 0;
|
|
const r = write(kind, ctx.params.id || "", env.data, at, ord);
|
|
if (!r.ok) {
|
|
const body = { success: false, message: r.message };
|
|
if (r.errors) body.errors = r.errors;
|
|
return { status: r.status, body };
|
|
}
|
|
return ok(r.wrote);
|
|
},
|
|
"section.delete": (ctx) => {
|
|
const r = remove(ctx.params.kind || "", ctx.params.id || "");
|
|
if (!r.ok) return { status: r.status, body: { success: false, message: r.message } };
|
|
return ok({ kind: ctx.params.kind, id: ctx.params.id, deleted: true });
|
|
}
|
|
};
|
|
var WRITES = {
|
|
"section.put": "PUT",
|
|
"section.delete": "DELETE"
|
|
};
|
|
var READ_ONLY = "the trust inventory is read here: this deployment's own controls are build-time d\
|
|
ata governed in git, and the audit trail is written by the platform.";
|
|
globalThis.handle = (req) => {
|
|
req = req || {};
|
|
const route = req.route || "";
|
|
const fn = routes[route];
|
|
if (!fn) return notFound("no trust route " + route);
|
|
const method = (req.method || "GET").toUpperCase();
|
|
const wants = WRITES[route];
|
|
if (wants) {
|
|
if (method !== wants) return readOnly(READ_ONLY);
|
|
} else if (method !== "GET" && method !== "HEAD") {
|
|
return readOnly(READ_ONLY);
|
|
}
|
|
const org = req.orgId || "";
|
|
if (route !== "health" && !org) {
|
|
return { status: 401, body: { success: false, message: "missing tenant" } };
|
|
}
|
|
const params = {};
|
|
const bags = [req.params, req.query];
|
|
for (let i = 0; i < bags.length; i++) {
|
|
const bag = bags[i];
|
|
if (!bag) continue;
|
|
for (const k in bag) {
|
|
if (Object.prototype.hasOwnProperty.call(bag, k)) params[k] = bag[k];
|
|
}
|
|
}
|
|
try {
|
|
return fn({ org, params }, req);
|
|
} catch (err) {
|
|
return {
|
|
status: 500,
|
|
body: { success: false, message: String(err && err.message || err) }
|
|
};
|
|
}
|
|
};
|
|
globalThis.trust = {
|
|
version: VERSION,
|
|
controls: () => controls(),
|
|
frameworks: () => ids(),
|
|
summary,
|
|
framework: framework2
|
|
};
|
|
})();
|