Skip to content

How it is put together

Component Job
Frontend A single page served by the application; renders every value as text through the document interface with no markup sink, holds the session token in memory rather than browser storage, and runs under a content policy that forbids inline script and style (D-036). The look is tokens and a sidebar shell (D-075); the detail opens beside the list, every list has a skeleton and an empty state, and a real browser proves the page by use (D-077)
Routes The trust boundary; authentication checked on every request, every response shaped by a declared model
Import parsing Parses an imported identity export file, bounded on every axis, in memory, append-only
Derivation engine Computes each identity's state and enrichment from the observation history at read time
Governance records The human layer: owners, flags, attestations, written with attribution and an audit row in one transaction
Report builder Produces the self-contained risk report and the escaped CSV and JSON exports
Audit trail Records every governance action, written with the action in one transaction
PostgreSQL Holds observations, governance records, and the audit trail; access controlled, with encryption at rest supplied by the deployment layer (D-020)
The tool's own cloud credential, not yet present Version one holds none. The live pull phases add a read-only role in the target AWS account, and from that day it is the identity that must be governed best
flowchart LR
    O[Operator browser] -- session token --> R[Routes]
    R --> I[Import parsing]
    I -- observations, append only --> P[(PostgreSQL)]
    R --> D[Derivation engine]
    P -- history --> D
    D -- derived inventory --> R
    R --> G[Governance records]
    G -- action plus audit, one transaction --> P
    R --> X[Report builder]
    D --> X

An import records observations and touches nothing else. A view derives the inventory from the history and stores nothing. A governance action is the only ordinary write besides ingestion, and it commits with its audit row as one unit. The report builder consumes the same derived inventory the view does, so a report can never disagree with the screen.

The data model shape

The model is provider-neutral: no table carries a word only one cloud uses, and the provider's own vocabulary ends at the parser (D-071).

scope_nodes --< scope_nodes (the tree, global at the top)
scope_nodes --< imports --< identity_observations >-- identities
imports --< credentials >-- identities
imports --< grants >-- identities, role_definitions, scope_nodes
imports --< memberships >-- identities (groups are identities)
imports --< observed_relationships >-- identities
identities --< governance_records
identities --< authorizations >-- scope_nodes
users --< role_bindings >-- scope_nodes
campaigns --< campaign_items
alerts --< alert_deliveries
audit_events
  • A scope node is one place in a provider's hierarchy: an organization, an account, a tenant, a subscription, a cluster. Each names its partition explicitly, commercial or government, so a government estate is labeled on every record rather than inferred. One synthetic node named global sits above all of them.
  • An identity is one principal at one scope node, keyed by the provider's immutable identifier, never the name or ARN, which are display attributes (D-016). A recreated principal is a new identity. Groups are identities of kind group: governable sources of privilege, never actors (D-019).
  • An import is one file or pull: one scope node, one source kind, one capture time taken from the file's own content (D-008), unique on that triple, so a re-import is rejected rather than double-counted.
  • A credential is one credential as one import saw it. Two access keys are two rows and a provider with five is five rows, so nothing in the model assumes a cloud that offers exactly two.
  • A grant is an identity holding a role definition at a scope, by a path and in a mode. The path records how the privilege arrives, hop by hop, through a membership or a trust; the mode records whether it is held now or can be obtained, which is what PIM-style eligibility is. A role definition is versioned by the hash of its contents, so a changed built-in role is a new row and a review can show what the role allowed on the day of the decision.
  • A role binding is authority: a user holding a role at a scope node, covering that node and everything beneath it (D-072). Users carry no role column. A binding is revoked, never deleted, so the record of who could act when survives.
  • An authorization is what a person said an identity may hold: one grant path, an owner, the authorizer taken from the session, a justification, and a window that ends (D-073). Nothing is edited. A renewal writes a new row that supersedes the old one and a revocation writes one too, so the chain from the first authorization to the last is the history. Expiry is the clock compared to a column, never a job that might not run. Which fields an authorization must carry is the administrator's choice, shipped strict, and every change to that choice is audited (D-070). Authorizations arrive through the form on an identity's page or through a file. A file keeps its own shape: the import carries a mapping that names which of their columns holds each field, or a constant for a field their file does not have, so nobody is asked to transform their spreadsheet before they get anything back (D-074). A mapping is written and superseded rather than edited, and every import names the mapping that read it, so a mapping later found wrong leaves every row it produced findable. Nothing is guessed: a missing required column refuses the file, a missing optional one is named in the result, unmapped columns are counted, and a date is read by a format the mapping declares, because 03/04/2026 is two different days in two countries. A dry run shows how the file was understood and writes nothing. Neither door asks anyone to retype what the system can already see. The observed grants come back in the authorized record's own shape, as a prefill on an identity's page and as an export in the import's columns, with the owner and the justification left empty because they are the two things the observed side cannot know. What already carries an authorization is marked, so the page asks where the answer is still owed. It prefills and never writes: turning what is into what should be without a person in the middle would leave the delta comparing the observed record against a copy of itself (D-024).
  • The delta is the product, and it is stored nowhere. It is the difference between the two records, computed every time somebody asks, in nine classes: held but not authorized, reached through an unauthorized relationship, expired and still held, can be obtained and is not authorized, the role changed after it was authorized, a custom definition changed after it was authorized, a custom definition nobody authorized, authorized but not held, and owner disagreement. The two that read the route rather than the hold arrived with 1.6, because comparing what an identity holds against what was authorized cannot see access that arrives by assuming a role, or the door it arrives through. The two about a definition arrived with 1.7, because a custom policy is a thing somebody wrote and should own, and when it changes after it was agreed the finding names the actions that arrived rather than only saying it moved. Every finding carries when each side was last heard from, because a finding from a month-old import is true about a month-old world, and a stale side makes a difference look like agreement (threat 15).
  • An alert is a record that people were told, and each delivery to each recipient is its own row with its result, so "nobody told me" is answerable either way. Alerts fire on an authorization written or revoked, an authorization entering its expiry window, and a revocation recommended by a review, which is the work item the tool produces because it never acts. Delivery sits behind one narrow interface; this release records and does not send, so the failure paths and the recipient bound are built and tested before any mail server is involved. A delivery that fails is recorded as failed and never breaks the action that raised it.
  • An integration token opens a read-only surface under /api/v1/ for a system rather than a person: identities paged from a cursor, the delta, and a change feed that walks the audit record from a cursor and returns the next one, so a consumer follows decisions as they happen rather than polling a full dump. A token is a second kind of credential, stored as a hash the way sessions are, shown once at minting and never again, revocable, and rate limited per token. Session routes refuse tokens and token routes refuse sessions. This surface is demonstration-grade: enough to show the shape, and not yet hardened for anyone to rely on, which is an open question the plan carries on purpose.
  • GitHub is the second native provider (D-076): one document assembled from the REST API's own objects becomes the same neutral rows, with the organization and its repositories as scope nodes, teams as groups, outside collaborators as guests, app installations and deploy keys as identities of their own, and the fixed permission levels as provider-managed definitions whose contents say what the level can do in the terms the privilege reading already speaks. An organization owner, a repository admin, and an app that may write members read as administrator equivalent through the same finding as an AWS administrator policy.
  • Kubernetes is the third native provider (D-079): the cluster's own dump becomes the same rows, with the cluster and its namespaces as scope nodes, service accounts as services, users and outside groups as identities the authenticator asserts, the cluster's own two groups with their members written, and every role's rules read as capabilities, so a role that can bind or escalate reads as changing access and a rule for every verb on every resource reads as administering. The rules ride as actions, so a changed custom role names what it gained.
  • Google Cloud is the fourth native provider (D-080): a document of gcloud's own answers becomes the same rows, with the project as the scope node, service accounts keyed by the identifier the provider never reuses and their user-managed keys as credentials with ages, every member form a policy can write read (a group, a domain, the two public forms, a deleted principal a binding still names, a federated principal), and a role read from its permission list when the export carries one, from a table of the fixed roles otherwise, and from its name as the last resort.
  • Azure and Entra are the fifth native provider (D-081): one document of Graph's objects and the command line's output becomes the same rows, with the tenant, its subscriptions, and their resource groups as scope nodes, members with a password whose second factor state the registration report answers, guests from another tenant, service principals with their secrets and certificates as credentials that expire, groups passing their members up, directory roles held standing by members and in the eligible mode by privileged identity management, and Azure roles read from their actions or their names. An eligibility is what an identity can obtain and is never counted as privilege held.
  • Active Directory is the seventh native provider, with two doors (D-083, D-084): a document of the directory cmdlets' objects or the SharpHound collector's zip becomes the same rows, with the domain as the scope node keyed by its identifier and every organizational unit beneath it, a password per user active while the account is enabled, a Kerberos key beside it for an account with a service principal name, computers as workloads with their machine accounts, groups flattened through every level of nesting with the holding group named, the built-in groups that hold the domain read from a table of what each may do, a member from another domain as an external identity, a trust as a relationship, and, from the collector alone, a control right on the domain or on a privileged group or one of its members as access the principal can obtain.
  • Okta is the sixth native provider (D-082): one document of the management API's objects becomes the same rows, with the organization as the scope node, a password only for users whose credentials Okta holds, the enrolled factors as the second factor state, groups carrying their roles and applications to their members with the group's name kept, administrator roles read from a table of their types and custom roles from their permissions, and every application assignment a grant somebody can be asked to authorize.
  • The observed side reads any provider's table through the same mapping mechanism the authorized side uses, with its own field set and a shipped template. An organization with a spreadsheet of on-premises accounts, or a vendor's export with no schema, gets the same neutral rows the AWS parsers produce and the delta on them the same day; a provider earns a native parser later if it earns one at all. A definition read this way carries no document, so the capability reading says nothing about it, and that limit is stated beside the source. The import routes also read a file's shape before parsing it and refuse one named as one source and shaped as another, naming both.
  • A governance record is the human layer: an owner, a purpose, a flag, or an attestation, on an identity or a group (D-019), attributed and audited, stored rather than derived because it IS the human input.
  • A review campaign scopes a set of identities and groups to a set of reviewers with a due date (D-021); its items hold each disposition, including insufficient evidence, and the campaign closes into an evidence export.
  • Everything shown about an identity's state, current, stale, unused, unowned, over-privileged, is derived from the observed rows plus governance records at read time. No status column exists anywhere.

The route surface

The complete surface, stated so it can be counted. A test asserts this block against the application's actual route table, so this list and the API cannot silently disagree; the health routes and the page shell are public, and every other route answers to the role matrix.

GET /
GET /health
GET /health/database
POST /auth/login
GET /auth/me
POST /auth/logout
GET /admin/users
POST /admin/users
POST /admin/users/{username}/sessions/revoke
POST /admin/users/{username}/bindings
POST /admin/users/{username}/bindings/{binding_id}/revoke
GET /admin/scopes
POST /admin/scopes
GET /admin/tokens
POST /admin/tokens
POST /admin/tokens/{token_id}/revoke
GET /admin/settings
PUT /admin/settings
POST /imports/credential-report
POST /imports/authorization-details
POST /imports/github-organization
POST /imports/kubernetes-rbac
POST /imports/google-cloud
POST /imports/azure-tenant
POST /imports/okta-org
POST /imports/active-directory
POST /imports/sharphound
POST /imports/observed/dry-run
POST /imports/observed
GET /imports
GET /identities
GET /identities/{identity_id}
GET /groups
POST /identities/{identity_id}/governance
POST /groups/{group_id}/governance
POST /identities/{identity_id}/attest
POST /groups/{group_id}/attest
DELETE /governance/{record_id}
GET /identities/{identity_id}/authorizations
POST /identities/{identity_id}/authorizations
POST /authorizations/{authorization_id}/revoke
GET /relationships
POST /relationships/authorize
POST /relationships/{authorization_id}/revoke
GET /role-definitions
POST /role-definitions/authorize
POST /role-definitions/{authorization_id}/revoke
GET /alerts
GET /delta
GET /identities/{identity_id}/delta
GET /identities/{identity_id}/observed-grants
GET /export/observed-grants.csv
GET /mappings
POST /mappings
POST /authorizations/import/dry-run
POST /authorizations/import
POST /campaigns
GET /campaigns
GET /campaigns/rollup
GET /campaigns/{campaign_id}
POST /campaigns/{campaign_id}/items/{item_id}/disposition
POST /campaigns/{campaign_id}/close
GET /export.csv
GET /export.json
GET /report.html
GET /campaigns/{campaign_id}/evidence
GET /campaigns/{campaign_id}/evidence.csv
GET /api/v1/identities
GET /api/v1/delta
GET /api/v1/changes

GET /identities is paged, because the sample estates' seventy-seven rows say nothing about an account with thousands: it takes q (a name substring), type, and tier as filters, applied on the server rather than in the browser, plus sort and direction over a named set of columns, applied to the whole matched set before the page is cut, plus limit (default 100, at most 500) and offset. The response carries the page of rows, the count the filters matched, and account-wide dashboard tiles that no filter changes, so the payload is bounded at any inventory size while state stays derived at read (D-006).

Repository map

The package is split by part, and each part owns its own tables, its own routes, and nothing else: core holds who may act and where, observe holds what was seen, authorize holds what people allowed, decide holds the reviews and the alerts.

Path Role
manifest_identity/main.py Application assembly: routes, security headers, the static shell
manifest_identity/models.py One import surface over every part's tables
manifest_identity/core/roles.py The role matrix, single source: who may call what
manifest_identity/core/scope.py The scope tree and the one authority question, asked nowhere else
manifest_identity/core/deps.py Authentication, the matrix check, the scope check, and the write budget
manifest_identity/core/models.py Users, sessions, scope nodes, role bindings, settings, the audit chain
manifest_identity/core/audit.py The audit spine: the record commits with the action
manifest_identity/core/verify_chain.py The offline verifier: recompute the chain, compare to an anchor
manifest_identity/observe/providers/ The nine parsers, AWS, GitHub, Kubernetes, Google Cloud, Azure, Okta, and Active Directory through its cmdlets and through SharpHound: bounded, in memory, distrusting their own preconditions
manifest_identity/observe/importer.py AWS records become neutral rows; the vocabulary ends here
manifest_identity/observe/estate.py What every native importer does the same way, written once: the root node, the identities at it, one observation per import
manifest_identity/observe/github_importer.py GitHub records become the same neutral rows: teams as groups, permission levels as capability documents
manifest_identity/observe/kubernetes_importer.py A cluster's dump becomes the same rows: rules read as capabilities, bindings as grants at the namespace or the cluster
manifest_identity/observe/google_cloud_importer.py A project export becomes the same rows: user-managed keys as credentials, every member form a policy writes, roles read from permissions, a table, or a name
manifest_identity/observe/azure_importer.py A tenant export becomes the same rows: members with passwords and guests without, secrets and certificates as credentials, directory roles held or eligible, assignments at the subscription or resource group
manifest_identity/observe/okta_importer.py An organization export becomes the same rows: passwords only where Okta holds them, roles reaching members through their groups, every application assignment a grant
manifest_identity/observe/active_directory_importer.py A domain, from either door, becomes the same rows: the built-in groups as definitions reaching members through every nesting, a control right as access to obtain, a trust as a relationship
manifest_identity/observe/models.py Imports, identities, credentials, grants, role definitions, relationships
manifest_identity/observe/derive.py State from history at read time; the freshest value per field
manifest_identity/observe/principals.py Who a trust policy names, one principal per row, allow statements only
manifest_identity/observe/paths.py How access reaches an identity: direct, through a group, by assuming a role
manifest_identity/observe/findings.py Credential findings, each explaining itself with its OWASP anchor
manifest_identity/observe/policy_analysis.py What a policy document grants, read by capability, and what one version has that another did not
manifest_identity/observe/privilege.py The privilege picture with source attribution; shadow admin detection
manifest_identity/observe/assessment.py The one computation the page, the campaigns, and the exports all read
manifest_identity/authorize/authorizations.py The authorization write path: attributed, append-only, bounded
manifest_identity/authorize/from_observed.py The observed side in the authorized side's shape, prefill and export
manifest_identity/authorize/relationships.py Authorizing the door: the trust itself, appended and superseded
manifest_identity/authorize/role_definitions.py Authorizing a custom definition as written, bound to the hash that was agreed
manifest_identity/compare/delta.py The difference between the two records, nine classes, stored nowhere
manifest_identity/authorize/csv_import.py The file door: rows become the same request the form builds
manifest_identity/observe/mapping.py The bounded table reader and the mapping every door shares
manifest_identity/observe/generic_import.py The observed side's file door: any provider's table becomes the neutral rows
manifest_identity/api/deps.py Authenticating an integration token, a second credential with its own door and budget
manifest_identity/api/routes_read.py The read-only surface: identities, the delta, and the change feed from a cursor
manifest_identity/authorize/governance.py The human layer: typed owners, purposes, flags, attestations
manifest_identity/core/options.py Administrator settings, secure by default, audited on every change
manifest_identity/decide/campaigns.py Recommendations with reasons, and the delta since last certification
manifest_identity/decide/alerts.py The alert record, the delivery interface, and the one deliverer that records rather than sends
manifest_identity/decide/reports.py The ranked report and the escaped exports
manifest_identity/sample_data.py The deterministic sample account generator
frontend/ One page, no build step; every value rendered as text
migrations/ The schema from the first table
sample-data/ The generated demo estates and one sample table per provider, committed and checked
recipes/ Each provider's own export turned into the table door's file; the jq ones run in the suite
tests/ The attack checklist; the matrix walked row by row
scripts/ The gates: docs-truth, digest parity, the mutation check
diagrams/ Working sketches under the drawing doctrine
requirements*.in / *.txt Chosen packages, and the hash-pinned trees that install
Dockerfile / docker-compose.yml Digest-pinned base, non-root user, the composed stack
.github/workflows/ The pipeline: tests, types, scanners, the container jobs, and the software bill of materials each run delivers
.pre-commit-config.yaml Secret scan, writing rules, lint, types, the repository's own scanner rules, the page lint, and the truth gates at commit time; CodeQL before the push
.semgrep/ The repository's own scanner rules, each one a lesson a scanner taught after a push (D-086)
scripts/scan.sh The pipeline's CodeQL queries run locally before the push, bundles pinned by checksum
scripts/audit.sh Every pinned tree audited against known vulnerabilities, with no exceptions
scripts/compile_scan.py Compiles the scanner tree and overrides the one pin Semgrep declares too low, hashes from the index
eslint.config.mjs / package.json The page's one lint rule and its pinned tools
.env.example Documents required configuration without containing it