How it works
One page for the person who has to reason about the whole thing: what runs where, as whom, reading what, writing where.
One page for the person who has to reason about the whole thing: what runs where, as whom, reading what, writing
where. The spec (posture-spec.md) says what the product is; this says how it is put together. Dated statements are
decisions; (verify) marks what has not been proven live yet.
The parts
| Part | Where it lives | What it is |
|---|---|---|
Module Tenantkeep.Posture |
this repo, PowerShell Gallery and signed zips | Collectors, check engine, catalog, report renderers, licence verifier. Read-only against the tenant, always. |
| Check catalog and baselines | checks/, baselines/ |
Data, not code: 204 checks in 13 domains, baselines that select and weight them, framework crosswalks. |
| Report | templates/, rendered by the module |
One self-contained HTML file per run (inline CSS and JS, no CDN), plus Markdown and optional PDF. |
| Windows AD collector | collectors/windows/Get-ADBaseline.ps1 |
Standalone read-only script for a domain-joined machine; its files join the evidence folder. |
| Customer-side setup | deploy/setup/, deploy/automation/Grant-ManagedIdentityGraphRole.ps1 |
The only tenant writes in the repo: app registration, consents, role grants. Run once by the customer's admin, never by the module. |
| Automation deployment | deploy/automation/ (Bicep, deploy.ps1), deploy/schedules/ (runbooks) |
Option B: an Automation account in the customer's subscription with a managed identity, a nightly Conditional Access cycle and a monthly full posture cycle. |
| Licensing | Tenantkeep.Posture/Private/Test-PostureLicense.ps1, deploy/licensing/, private repo Kainor-LLC/tenantkeep-licensing |
A signed licence file per tenant, verified offline; issued by a Stripe-driven function in the Kainor tenant that signs through Key Vault. |
| Websites | repos Kainor-LLC/kainor.com, Kainor-LLC/tenantkeep.app |
Company site and the product family site (guide, generated catalog page). |
A run, end to end
- Sign in. Locally, the admin signs in to Graph, Exchange, Purview, SharePoint, Teams and Azure (macOS needs separate processes for some; docs/collection.md). In Automation, the account's managed identity signs in to everything except Purview (see Identities).
- Collect.
Invoke-PostureCollectionwrites one evidence folder per run: JSON and CSV files named after the audit plan (04-role-assignments-active.csv,exo-04-antiphish.json, ...), acollection.jsonmanifest with the tenant id, account, scopes, API versions and errors, and the Conditional Access snapshot. Every Graph call pins its API version. Nothing leaves the customer's environment. - Profile.
New-PostureProfilediscovers what evidence can tell (licences, platforms, identity model, BYOD, Copilot) and asks only what it cannot (third-party tools, thresholds, break-glass accounts, frameworks, audience). The result isprofile.jsonin the client folder, besidebrand.json,exceptions.json,tenantkeep.licandfindings.csv. - Evaluate. The engine reads the catalog, applies the baseline, asks one fact provider per domain for facts and
affected objects from the evidence, evaluates each check's pass expression against thresholds, applies exceptions
(accepted risk counts as a pass), third-party coverage and licence applicability.
checks.jsonrecords every result with its reason, evidence files and affected list. - Report.
New-PostureReportbuilds a model (score, headline numbers, domain scorecards, top risks, admins, Copilot readiness, Conditional Access, recommendations, trend against a previous run, crosswalk, appendix) and renders it.Export-PostureFindingswrites the register, merging owner, status, effort and dependency by check id from the previous register. - Keep. Locally the run folder stays under
~/tenantkeep/evidence/. In Automation it is uploaded to theposture-runscontainer, the register is written back to theclientcontainer, old runs are pruned.
Identities and permissions
- Local runs use the signed-in admin's delegated permissions. Global Reader covers most reads; Exchange needs
View-Only Organization Management, SharePoint the SharePoint Administrator role with the Tenantkeep SharePoint app
(delegated, created by
New-PostureSharePointApp.ps1), Purview a compliance read role, Azure Reader and Security Reader. - Automation uses the account's system-assigned managed identity.
Grant-ManagedIdentityGraphRole.ps1 -IncludePosturegives it the Graph application roles every collector needs, Exchange.ManageAsApp, SharePoint Sites.FullControl.All (verify the least that works), the Teams tenant admin API role and the Global Reader directory role; the deployer adds Reader and Security Reader on the subscriptions. The identity also has Storage Blob Data Contributor on the deployment's storage account and nothing else. - Purview in Automation (decided and built 2026-10-15, first live run pending): Microsoft's module has no managed identity sign-in for Purview, only a certificate-backed app registration. The customer's setup script creates that app and a certificate in a Key Vault in the customer's own subscription; the managed identity reads the certificate at run time and signs in with it. The certificate never appears in code, configuration, the runbook or this repository, and the customer can revoke it at any time. This is the one exception to "delegated or managed identity only", and it is the customer's credential in the customer's vault.
- No secrets anywhere else. No app secrets in code or config, no callbacks, no telemetry.
Licensing and integrity
- A licence is a small JSON file: tenant id, tier (
caorfull), customer, issued and expiry dates, key id, and an ECDSA P-256 signature over the canonical fields. The module ships the public keys; the private key is in Kainor's Key Vault (kv-tkp-lic-9195) and never leaves it. Verification is offline and binds the licence to the tenant id the evidence recorded. The Conditional Access domain and the three CA cmdlets are free; everything else needsfull. docs/licensing.md has the detail and the honest limits. - Issuing is automatic: a Stripe payment link with a Tenant ID field, a webhook to an Azure Function in the Kainor tenant, a signature through Key Vault, an email from licensing@kainor.com, a row in the register. Renewals re-issue.
- Releases carry a signed
integrity.json(every file's hash). At import the module checks itself and prints the build state, release, development or modified, in every report header. A label, not a lock.
Storage layout in Automation
<storage account>/ca-snapshots/<stamp>-ca/... nightly Conditional Access runs
<storage account>/posture-runs/<stamp>/... monthly posture runs: evidence, checks.json, report.html, report.md, findings.csv
<storage account>/client/ profile.json, brand.json, exceptions.json, tenantkeep.lic, findings.csv, hybrid/ad-*
<storage account>/modules/ module zips imported into the runtime environment
The report site (Option B opt-in, decided 2026-10-15, being built)
The report is a file, so until now "save an exception" meant editing exceptions.json by hand. The report site makes
the browser the place of record: a small web app in the customer's subscription, signed in with the customer's own
Entra accounts, that serves the latest report and accepts saves. Exceptions, the register (owner, status, effort,
dependency) and the tenant configuration (thresholds, third-party tools, baseline, break-glass accounts, frameworks)
are written to the client container, which is exactly what the next scheduled run reads. Recording an exception
re-scores the page immediately; the next run makes it permanent. Design details are in docs/report-site.md once built.
What is still manual
- The Windows AD collector runs on a domain-joined machine and its files are copied into the evidence folder (local)
or the
client/hybrid/prefix (Automation). A Hybrid Runbook Worker is the v1.1 answer. - Purview in Automation until the customer runs
New-PosturePurviewApp.ps1once. - Role grants and app registrations: always the customer's admin, by design.
Repositories
| Repo | Visibility | Holds |
|---|---|---|
| Kainor-LLC/tenantkeep-posture | source-available (currently private on GitHub; the licence assumes readers) | the module, catalog, deployment, docs |
| Kainor-LLC/tenantkeep-licensing | private | the issuing function |
| Kainor-LLC/tenantkeep.app, Kainor-LLC/kainor.com | public | the websites, served by Cloudflare Pages (GitHub Pages does not allow purchase links) |
| Kainor-LLC/LAPSlock (public), Kainor-LLC/tenantkeep-oncall (private) | the other products |