mirror of
https://github.com/hanzoai/trust.git
synced 2026-10-30 14:54:58 +00:00
1989 lines
70 KiB
JSON
1989 lines
70 KiB
JSON
{
|
|
"$comment": "The control inventory. Each entry names a mechanism, where it is enforced, how we know it holds, and which published clauses it maps to. `status` is what the control DOES (automated | partial | absent); whether anything can FAIL on its behalf is derived from `verified` — a reading is not a test. Several entries record that a mechanism is weaker than a Hanzo page says it is; that is the point of the file. Nothing in this schema can express a certification, and the build refuses prose that tries.",
|
|
"controls": [
|
|
{
|
|
"id": "iam.issuer",
|
|
"category": "access",
|
|
"claim": "Hanzo IAM issues every 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 code 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 — so the narrower word is the honest one.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/oidc/authorize.go",
|
|
"symbol": "Authorize"
|
|
},
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/oidc/mint.go",
|
|
"symbol": "mint"
|
|
}
|
|
],
|
|
"verified": [
|
|
{
|
|
"method": "read",
|
|
"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 single 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 leaves 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 — offering the plain method is refused rather than quietly downgraded.",
|
|
"mechanism": "A public client arriving with no challenge is refused at the authorization endpoint. An explicitly supplied method other than the hashed one is refused there, at the mint path, and at the token endpoint; the verifier compares 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": "internal/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 — an OMITTED method defaults to the hashed one rather 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": "nist80053:SC",
|
|
"strength": "full"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "iam.password.argon2id",
|
|
"category": "access",
|
|
"claim": "New passwords are hashed with a memory-hard function at 64 MiB and two passes with a random salt, the cost parameters 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 raising the cost later does not lock anyone out. Verification reads the scheme off the row and refuses anything 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": "hanzoai/iam",
|
|
"path": "internal/users/users.go",
|
|
"symbol": "CheckPassword"
|
|
}
|
|
],
|
|
"detail": "The parameter block reads 64 MiB memory, 2 iterations, 16-byte salt, 32-byte key, and the digest is written in the standard parameterised form. An older scheme remains an accepted VERIFY path so legacy rows still authenticate; every new or updated digest uses the memory-hard one. The doc comment on the user record describes this backwards — 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": "One accepted-algorithms list is handed to the parser as its valid-methods option on the single verification path. The post-quantum signature method is registered against a real implementation, not a placeholder, 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/mldsa.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": "acceptedAlgs"
|
|
},
|
|
{
|
|
"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 list carries two RSA variants, three NIST curves and one lattice scheme; the remaining RSA variants and the probabilistic-padding series are not accepted. Tokens from a federated provider are checked against 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": "Refresh 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 the grant was established. A request carrying an already-consumed token revokes the family and answers 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": "internal/oidc/refresh.go",
|
|
"symbol": "Refresh"
|
|
}
|
|
],
|
|
"detail": "The consumed flag is set before the successor is minted, and the family cascade DELETES rows rather than only flagging them. One caveat: the cascade is best-effort — it swallows the list error and ignores each delete error while still refusing the caller — 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": "nist80053:IA",
|
|
"strength": "full"
|
|
},
|
|
{
|
|
"clause": "nist80053:SI",
|
|
"strength": "full"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "iam.mfa.sessions",
|
|
"category": "access",
|
|
"claim": "Adding a second factor, removing one, or changing which is preferred drops every other session and clears the remember-this-device window.",
|
|
"mechanism": "All three mutations run through one save path, which revokes every session but the calling one and blanks the remember 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 save 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 token 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 — create, update, replace, deactivate, delete, with filter and patch. Someone removed from your directory loses their account by that act.",
|
|
"mechanism": "A provisioning surface routes the full user lifecycle 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": "Route"
|
|
},
|
|
{
|
|
"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": "iso27001:A.6.5",
|
|
"strength": "full"
|
|
},
|
|
{
|
|
"clause": "nist80053:AC",
|
|
"strength": "full"
|
|
},
|
|
{
|
|
"clause": "nist80053: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 — the published resource-type set has exactly one member.",
|
|
"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 answers a plain 404 rather than a protocol error document, because the published-resource path covers only schema and resource-type lookups.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/scim/schemas.go",
|
|
"symbol": "resourceTypes"
|
|
}
|
|
],
|
|
"verified": [
|
|
{
|
|
"method": "read",
|
|
"at": [
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/scim/schemas.go",
|
|
"symbol": "resourceTypes"
|
|
}
|
|
],
|
|
"detail": "The resource-type list has one member, 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 used to sign in.",
|
|
"mechanism": "None in the running build. Passkey credentials can be registered and managed, and there is no assertion ceremony to challenge one — no begin-login route of any spelling exists.",
|
|
"status": "absent",
|
|
"note": "The capability flag returns false and the login descriptor is masked, 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": "PasskeySignin"
|
|
},
|
|
{
|
|
"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": "internal/oidc/frontdoor_passkey_test.go"
|
|
}
|
|
],
|
|
"detail": "A test at internal/oidc/frontdoor_passkey_test.go asserts this. A test names the four candidate ceremony paths and asserts all four answer 404. The credential 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 grant covers a path and everything below it, so org-wide access, one workspace, and a narrowed agent credential all fall out of one check.",
|
|
"mechanism": "One authorization function tests the requested path 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 comparison, which does not do what the model needs — that is recorded separately as authz.path.segment, and it is why this is partial rather than automated.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/authz/authz.go",
|
|
"symbol": "pathAuthorized"
|
|
}
|
|
],
|
|
"verified": [
|
|
{
|
|
"method": "read",
|
|
"at": [
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/authz/authz.go",
|
|
"symbol": "pathAuthorized"
|
|
}
|
|
],
|
|
"detail": "One function answers the containment question for every caller shape."
|
|
}
|
|
],
|
|
"maps": [
|
|
{
|
|
"clause": "soc2:CC6.1",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "soc2:CC6.3",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "iso27001:A.5.15",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"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 characters, because the comparison advances a segment at a time.",
|
|
"mechanism": "None. The comparison is a byte prefix. The collisions someone already hit are handled by a separate exact-match table rather than by making the comparison segment-wise.",
|
|
"status": "absent",
|
|
"note": "Recorded absent because the mechanism named in the claim does not exist — not because path authorization is missing. The comment 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 someone thought of; a segment walk would make the whole class impossible. A segment-wise implementation 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": "hanzoai/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": "soc2: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": "nist80053:AC",
|
|
"strength": "partial"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "edge.identity.headers",
|
|
"category": "network",
|
|
"claim": "Headers naming an organization, a user, an email or a role are stripped at the edge before a handler reads one, and identity is written back only from a verified token.",
|
|
"mechanism": "The request middleware deletes every authority header and every sub-scope header by name, then re-mints them from validated claims. A second layer deletes any header in two reserved families by prefix, so a new name cannot arrive unhandled.",
|
|
"status": "automated",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "middleware_identity.go",
|
|
"symbol": "authorityHeaders"
|
|
},
|
|
{
|
|
"repo": "hanzoai/base",
|
|
"path": "tools/claims/claims.go",
|
|
"symbol": "StripIdentityHeaders"
|
|
}
|
|
],
|
|
"verified": [
|
|
{
|
|
"method": "read",
|
|
"at": [
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "middleware_identity.go",
|
|
"symbol": "authorityHeaders"
|
|
},
|
|
{
|
|
"repo": "hanzoai/base",
|
|
"path": "tools/claims/claims.go",
|
|
"symbol": "StripIdentityHeaders"
|
|
}
|
|
],
|
|
"detail": "The deletes run before anything reads a header, and the re-mint draws only from validated claims. The organization a caller asks to act in is honoured only 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 query cannot reach across organizations — 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 from 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; every 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 error 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 per user, as the claim describes. The cloud file layer derives per FILE, from a random file id — so two files of the SAME organization have unrelated keys, and the key is not a function of the organization. Per-file derivation is not weaker against a stolen file, it is stronger; it just does not support 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": "Derive"
|
|
}
|
|
],
|
|
"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 derivations 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 returned, 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": "audit/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": "hanzoai/cloud",
|
|
"path": "audit/store.go",
|
|
"symbol": "Append"
|
|
},
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "audit/record.go"
|
|
}
|
|
],
|
|
"detail": "There are TWO trails and they are not the same shape. This one is per request. The identity service keeps a second record type carrying organization, user, address, method, request path, action, payload, response and status — 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 is 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 scrubbed 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, status and principal only, and the identity service's two writers set the payload field explicitly. Redaction is real; the body capture it is described as protecting does not happen. Nothing is leaking — the 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/redact.go",
|
|
"symbol": "Redact"
|
|
},
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "audit_middleware.go",
|
|
"symbol": "scrubPath"
|
|
}
|
|
],
|
|
"detail": "The denylist covers password, secret, token, client secret and private key among others, 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 or quietly trimmed of a real one.",
|
|
"mechanism": "One predicate names the reserved actions, and the audit surface calls it on every write verb — 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": "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/reserved_test.go"
|
|
}
|
|
],
|
|
"verified": [
|
|
{
|
|
"method": "read",
|
|
"at": [
|
|
{
|
|
"repo": "hanzoai/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/reserved_test.go"
|
|
}
|
|
],
|
|
"detail": "A test at internal/auditlogs/reserved_test.go asserts this. The test seeds a reserved row the way the platform writes one, then attempts to create, update, relabel and delete it through the API, and asserts all four are refused. Remove the gate and the test goes red. The cloud trail is stronger still: it publishes no write route at all, only a list and a chain check. Rows under consent-training, issue-user-token, mint-user-keys, revoke-user-keys, token-exchange evidence this. The five reserved actions. A row carrying one of these is evidence precisely because the subject of the evidence cannot write it."
|
|
},
|
|
{
|
|
"method": "audit",
|
|
"actions": [
|
|
"consent-training",
|
|
"issue-user-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, result with sequence, and timestamp.",
|
|
"status": "partial",
|
|
"note": "Partial on two counts, both measured rather than reasoned. First, the cloud trail indexes organization, action and time but NOT user — so of the four questions named, 'everything one person did' is the one that still walks the table. Second, the identity service's record declares index tags on organization, user, action and time and they 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 deleted alone. The declaration exists; on this backend it materializes nothing.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/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.go",
|
|
"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 schema 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": "Each 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, each carrying the prior hash, and an endpoint walks the chain and reports where it breaks. The store issues no update and no delete.",
|
|
"status": "automated",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "audit/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/admin/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 package 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 remove.",
|
|
"mechanism": "The consent endpoint writes a row carrying the before and after state under a reserved 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/oidc/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 transition, carrying the from and to state. The action is reserved, which is what makes the row evidence rather than an assertion."
|
|
},
|
|
{
|
|
"method": "audit",
|
|
"actions": [
|
|
"consent-training"
|
|
]
|
|
}
|
|
],
|
|
"maps": [
|
|
{
|
|
"clause": "soc2: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 stays 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 own records and parsed in the running binary to build a signer. The cloud key service is embedded in the binary, with a master key read from the environment.",
|
|
"status": "absent",
|
|
"note": "Recorded absent after searching the four repositories that serve this platform for any external key-service client. There 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 external 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 mention 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.validation",
|
|
"category": "data",
|
|
"claim": "The cryptographic modules this platform runs hold an independent validation.",
|
|
"mechanism": "None. The code implements published standards, the lattice-based key-encapsulation and signature schemes among them. Implementing a standard is not the same as holding a validation, 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 — no certificate exists to point at — so this entry rests on a reading of what is absent rather than on anything that could fail. Where a deployment needs validated modules, that is a question 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": "read",
|
|
"at": [
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/oidc/mldsa.go",
|
|
"symbol": "init"
|
|
}
|
|
],
|
|
"detail": "The signature scheme is implemented in software inside our own binary. No external validated module is reached."
|
|
}
|
|
],
|
|
"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 hold a credential an outside party would have to grant.",
|
|
"mechanism": "A gate reads every tracked page and 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 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 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/workflows/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": "iso27001:A.5.36",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "nist80053:CM",
|
|
"strength": "partial"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "ci.merge.unresolved",
|
|
"category": "product",
|
|
"claim": "A merge nobody finished cannot ship.",
|
|
"mechanism": "A gate refuses a tracked file still carrying the three-part marker git writes when it cannot merge. Those markers are ordinary content afterwards — the tree is clean, the diff is empty, and both sides of the merge ship — 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 honest 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/conflictmarkers"
|
|
},
|
|
{
|
|
"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 naming each file and line. It requires 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": "partial"
|
|
},
|
|
{
|
|
"clause": "iso27001:A.8.28",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "iso27001:A.8.32",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "nist80053:CM",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "nist80053:SI",
|
|
"strength": "partial"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "ci.arg.public",
|
|
"category": "product",
|
|
"claim": "A value baked into a published image has to have said it was public first.",
|
|
"mechanism": "A build argument ends up inside the image, where anyone who can pull it can read it back. So the gate reads the names a repository declares and refuses any that has not declared itself public by its own prefix. The name carries the assertion because 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 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 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 runs the gates, ahead of every build and publish"
|
|
}
|
|
],
|
|
"verified": [
|
|
{
|
|
"method": "test",
|
|
"at": [
|
|
{
|
|
"repo": "hanzoai/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 undeclared 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": "partial"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"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, takes 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 party 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 invoked 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/surface.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 surfaces 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": "incident",
|
|
"claim": "Whether the platform is answering is measured continuously and published without a credential.",
|
|
"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. The 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 configuration and the estate declares considerably more hosts than that, so a host nobody added is a host 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 exercised against the running service. Nothing here can fail on the control's behalf, so it is a reading and counts as one."
|
|
}
|
|
],
|
|
"maps": [
|
|
{
|
|
"clause": "soc2:CC2.3",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "soc2:CC7.2",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "iso27001:A.8.16",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "nist80053:SI",
|
|
"strength": "partial"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "incident.notice",
|
|
"category": "incident",
|
|
"claim": "A failing probe reaches a person, and a reader is told what happened.",
|
|
"mechanism": "The prober's alerting path fires on a run of failures and resolves on recovery, addressed to the operations channel. What a visitor 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 failing probe and nothing requires that one be written. The public record can therefore be silent through an event the alerting path reported, and only a person closes that gap.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/status",
|
|
"path": "watchdog/alerting.go"
|
|
},
|
|
{
|
|
"repo": "hanzoai/status",
|
|
"path": "config/announcement/announcement.go"
|
|
}
|
|
],
|
|
"verified": [
|
|
{
|
|
"method": "read",
|
|
"at": [
|
|
{
|
|
"repo": "hanzoai/status",
|
|
"path": "watchdog/alerting.go"
|
|
},
|
|
{
|
|
"repo": "hanzoai/status",
|
|
"path": "config/announcement/announcement.go"
|
|
}
|
|
],
|
|
"detail": "Both paths were read. The notice type is a closed set and the list is served on the public configuration 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": "iso27001:A.5.24",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "iso27001:A.5.26",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "iso27001:A.6.8",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "nist80053:IR",
|
|
"strength": "partial"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"id": "endpoint.posture",
|
|
"category": "endpoint",
|
|
"claim": "The state of the machine a person signs in from is taken 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 no register of machines against which such a signal could be resolved.",
|
|
"status": "absent",
|
|
"note": "Carried so the group is answered rather than left blank. The place named below is where the signal would be read — the point after the password is accepted and before a session exists — and reading it is how you can see that nothing consults one. This is the whole of what we assert about the devices people work from.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/iam",
|
|
"path": "internal/oidc/mfa_gate.go",
|
|
"symbol": "Gate",
|
|
"note": "the second-factor gate — 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": "partial"
|
|
},
|
|
{
|
|
"clause": "iso27001:A.8.1",
|
|
"strength": "partial"
|
|
},
|
|
{
|
|
"clause": "nist80053:AC",
|
|
"strength": "partial"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"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 the transport-security header for a year with subdomains included, and carries the frame, sniffing, referrer 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 from 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 opposite 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 routing table was parsed and the routes naming the shared header configuration were counted against the whole 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.tier",
|
|
"category": "data",
|
|
"claim": "An artifact somebody outside this organization signed can never be published to everyone.",
|
|
"mechanism": "The storage engine refuses it. The table holding a published artifact carries a constraint saying an item whose signer is external must stay gated, and the tier column defaults to gated — so a kind nobody has thought of yet arrives private and somebody has to open it deliberately.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "apps/dataroom/schema.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": "TestAuditorSignedCannotBePublic"
|
|
},
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "apps/dataroom/trust_test.go",
|
|
"symbol": "TestTierDefaultsToGated"
|
|
},
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"path": "apps/dataroom/trust_test.go",
|
|
"symbol": "TestPublicCenterWithholdsTheGatedTier"
|
|
}
|
|
],
|
|
"detail": "Three cases drive it: the write is refused, the default holds, and the public reading of a centre carries no gated item. Because the rule lives in the schema, a path through the code written later cannot get round it — which is what these assert, rather than the behaviour of any one handler."
|
|
}
|
|
],
|
|
"status": "automated",
|
|
"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 link bound to the party that asked, so the queue and the live grants are one list, and an administrator of another organization reaches none of it.",
|
|
"enforced": [
|
|
{
|
|
"repo": "hanzoai/cloud",
|
|
"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 address does not, and an administrator of another organization reaches nothing at all."
|
|
}
|
|
],
|
|
"status": "automated",
|
|
"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 viewing session records a page-view at a time against the link it was opened with, so what a reviewer actually opened is a list rather than a recollection.",
|
|
"status": "partial",
|
|
"note": "Partial because of what the record can and cannot prove. The session and the pages are ours and are written on our side, but the address naming the viewer is taken as stated and is not proven — so the trail establishes that 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/dataroom/dataroom.go"
|
|
}
|
|
],
|
|
"detail": "The route and the record it writes were read, along with the note recording 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"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|