495 lines
23 KiB
Markdown
495 lines
23 KiB
Markdown
# Persona Engine
|
||
|
||
A modular, configuration-driven **PowerShell 7** identity-classification service for **Microsoft Entra ID**.
|
||
|
||
The engine enumerates Entra user accounts, evaluates each one against an ordered, JSON-defined rule set, deterministically assigns **exactly one persona**, and updates a single approved persona attribute — and only when the calculated value differs from the current value.
|
||
|
||
> **Project status: Stage A implementation complete — ready for tenant validation.**
|
||
>
|
||
> 109 of 121 tasks are done. Every remaining task needs something a developer workstation does not
|
||
> have: a tenant connection (T055, T056, T101–T103) or an Azure Automation account (T115–T121).
|
||
>
|
||
> **Nothing has ever been run against a real directory.** The next step is [Stage A2](#stage-a2--tenant-preview-read-only)
|
||
> — a delegated, read-only `-WhatIf` run. Follow the [checklist](#tenant-validation-checklist) in order.
|
||
|
||
---
|
||
|
||
## Quick start
|
||
|
||
```bash
|
||
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive
|
||
```
|
||
|
||
That validates the configuration through all four layers. No tenant, no credentials, no network. It
|
||
is the fastest way to see what the engine does. Pass `-TestDataPath <dir>` with a directory of
|
||
synthetic user/membership fixtures to also run the real rule engine against them and see what
|
||
personas it would assign.
|
||
|
||
---
|
||
|
||
## Deployment
|
||
|
||
### Prerequisites
|
||
|
||
| Requirement | Notes |
|
||
| --- | --- |
|
||
| PowerShell 7.2 or later | Developed on 7.6.5. `pwsh -v` to check. |
|
||
| `Microsoft.Graph.Authentication` | **Runtime only.** Not needed for the config editor's offline validation. |
|
||
| PSScriptAnalyzer | Only if you want to run the lint checks locally. |
|
||
| An Entra app registration | For the persona extension property and delegated scopes. |
|
||
|
||
|
||
```powershell
|
||
Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
|
||
Install-Module PSScriptAnalyzer -Scope CurrentUser
|
||
```
|
||
|
||
> Only `Microsoft.Graph.Authentication` is a runtime dependency (OTD-004). The engine calls Graph
|
||
> through `Invoke-MgGraphRequest` rather than resource-specific SDK modules, which keeps the import
|
||
> surface to one module and makes request bodies explicit values that tests can assert on — that is
|
||
> what makes the single-attribute guarantee provable.
|
||
|
||
### Step 1 — Get the code onto the target machine
|
||
|
||
```bash
|
||
git clone <REPO-URL> persona-engine
|
||
cd persona-engine
|
||
```
|
||
|
||
### Step 2 — Prove the machine can run it, before touching a tenant
|
||
|
||
```bash
|
||
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive
|
||
```
|
||
|
||
Exit code `0` expected. This needs no credentials and no network. If it does not pass, stop —
|
||
nothing downstream is trustworthy.
|
||
|
||
### Step 3 — Register the persona extension property
|
||
|
||
The persona is stored in a **directory (schema) extension property** on an app registration (OTD-001),
|
||
addressable in dynamic group rules as `user.extension_<appId>_<name>`.
|
||
|
||
```powershell
|
||
Connect-MgGraph -Scopes 'Application.ReadWrite.All'
|
||
|
||
$app = Get-MgApplication -Filter "displayName eq '<APP-REGISTRATION-NAME>'"
|
||
|
||
New-MgApplicationExtensionProperty -ApplicationId $app.Id -BodyParameter @{
|
||
name = '<APPROVED-PERSONA-ATTRIBUTE-NAME>'
|
||
dataType = 'String'
|
||
targetObjects = @('User')
|
||
}
|
||
```
|
||
|
||
Record the returned `name` — it is the full `extension_<32-hex-app-id>_<name>` string, and it is what
|
||
goes into `engine.targetAttribute`.
|
||
|
||
> `extensionAttribute1..15` were **rejected**: they cannot be written from the cloud on objects that
|
||
> are, or ever were, synchronized from on-premises, or on Exchange-originated objects. Custom
|
||
> security attributes were **rejected**: they are not exposed to the dynamic group engine, which
|
||
> defeats the purpose.
|
||
|
||
### Step 4 — Grant delegated scopes
|
||
|
||
Read-only, and enough for every preview run:
|
||
|
||
```powershell
|
||
Connect-MgGraph -Scopes 'User.Read.All','GroupMember.Read.All','RoleManagement.Read.Directory'
|
||
```
|
||
|
||
The engine requests only what the enabled rules need — a configuration with no role conditions never
|
||
asks for `RoleManagement.Read.Directory`.
|
||
|
||
`User.ReadWrite.All` is added only for Stage A3, and only after V-4 sign-off.
|
||
|
||
> **Never sign in with a standing privileged account.** A Global Administrator run invalidates V-3
|
||
> as evidence and removes every practical limit on what a defect could reach.
|
||
|
||
### Step 5 — Build the configuration
|
||
|
||
```bash
|
||
cp ./config/persona-engine.example.json ./config/persona-engine.json
|
||
```
|
||
|
||
Then replace every placeholder:
|
||
|
||
| Placeholder | Replace with |
|
||
| --- | --- |
|
||
| `extension_<EXTENSION-APP-ID>_<APPROVED-PERSONA-ATTRIBUTE-NAME>` | The full extension property name from step 3 |
|
||
| `<TEAM-NAME>`, `<CHANGE-REFERENCE>` | Ownership metadata |
|
||
| `<TIER0-ROLE-TEMPLATE-ID>` | Role **template** IDs (stable across tenants) |
|
||
| `00000000-0000-...` group IDs | Real group Object IDs |
|
||
| `<ORGANIZATION-NAME>` | Your company name as it appears in `companyName` |
|
||
|
||
`logging.path` was removed from the copy — it's optional and defaults to
|
||
`<current-directory>/logs/persona-engine-audit.ndjson`. Set it (or pass `-OutputPath`) only if you
|
||
want the audit log somewhere else.
|
||
|
||
> **`config/persona-engine.json` must never be committed.** It contains real group Object IDs and
|
||
> your tenant's attribute name. Keep it in a protected configuration store, and confirm `.gitignore`
|
||
> covers it. The sanitization gate scans untracked files too, so it will catch this — but do not rely
|
||
> on that as your only control.
|
||
|
||
### Step 6 — Validate, before connecting to anything
|
||
|
||
```bash
|
||
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly -NonInteractive
|
||
```
|
||
|
||
Exit code `0` required. Codes: `1` findings · `2` warnings with `-TreatWarningsAsErrors` ·
|
||
`3` file unreadable · `4` schema unusable.
|
||
|
||
### Step 7 — Preview
|
||
|
||
```bash
|
||
pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId <ACCOUNT-OBJECT-ID> -WhatIf -Verbose
|
||
```
|
||
|
||
One account first. Then the tenant:
|
||
|
||
```bash
|
||
pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf
|
||
```
|
||
|
||
`-WhatIf` is the **only** approved no-write control. `-Debug` does not imply read-only.
|
||
|
||
### Step 8 — Enforcement 🔒
|
||
|
||
**Blocked on the V-4 security sign-off** (T101). Do not run without `-WhatIf` against anything other
|
||
than purpose-created test accounts until that is recorded. See [docs/SecurityModel.md](docs/SecurityModel.md).
|
||
|
||
---
|
||
|
||
## Tenant validation checklist
|
||
|
||
Work through these in order. Each stage assumes the previous one passed. **Do not skip ahead** — the
|
||
whole point of the staging is that a failure is cheap at stage A1 and expensive at stage A3.
|
||
|
||
### Stage A1 — offline (no tenant, no credentials, no network)
|
||
|
||
Everything here runs on any machine with PowerShell 7.
|
||
|
||
- [ ] **Module manifest loads**
|
||
`pwsh -NoProfile -Command "Test-ModuleManifest ./PersonaEngine.psd1"`
|
||
Fails without `Microsoft.Graph.Authentication` installed. Expected on a bare machine.
|
||
|
||
- [ ] **Lint**
|
||
`pwsh -NoProfile -Command "Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1"`
|
||
|
||
- [ ] **Example configuration passes all four layers**
|
||
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive`
|
||
|
||
- [ ] **Your real configuration passes all four layers**
|
||
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly -NonInteractive`
|
||
|
||
- [ ] **Synthetic rule test produces the personas you expect**
|
||
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -TestDataPath <fixtures-dir> -ValidateOnly -NonInteractive`
|
||
Point `-TestDataPath` at a directory of synthetic user/membership fixtures. If any of them
|
||
simulate a failed membership lookup, that account must show `EvaluationError`, not a persona.
|
||
If it shows a persona instead, stop — FR-013 is broken.
|
||
|
||
- [ ] **Drift check against the deployed configuration** (once one exists)
|
||
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -PreviousConfigPath ./deployed/persona-engine.json -ValidateOnly -NonInteractive`
|
||
|
||
### Stage A2 — tenant preview, read-only
|
||
|
||
Requires delegated read scopes. **Sign in as a non-privileged account.**
|
||
|
||
- [ ] **Connect with read-only scopes and confirm no write scope was granted**
|
||
`Connect-MgGraph -Scopes 'User.Read.All','GroupMember.Read.All','RoleManagement.Read.Directory'`
|
||
then `(Get-MgContext).Scopes`
|
||
|
||
- [ ] **Single-user preview**
|
||
`pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId <ACCOUNT-OBJECT-ID> -WhatIf -Verbose`
|
||
Expect: one result line, `WouldUpdate` or `Unchanged`, exit `0`.
|
||
|
||
- [ ] **Single-user preview against each origin type — V-1 read half (T056)**
|
||
A cloud-only account, a currently-synced account, and a formerly-synced account. Record in
|
||
`specs/001-persona-engine/verification/V-1.md`. This is the check that proves the OTD-001
|
||
attribute choice actually works where `extensionAttributeN` would not.
|
||
|
||
- [ ] **Full tenant preview**
|
||
`pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf`
|
||
Expect exit `0` and reconciliation `PASS` at every summary.
|
||
|
||
- [ ] **Record the V-3 evidence (T055)**
|
||
Confirm the run completed as a **non-privileged** account. A Global Administrator run
|
||
invalidates this item. Record in `specs/001-persona-engine/verification/V-3.md`.
|
||
|
||
- [ ] **Review the impact before going further**
|
||
From the final summary: how many `WouldUpdate`? Which rules fired, and which fired zero times?
|
||
Is the `EvaluationError` count near zero? A high count means the rule set is asking for data
|
||
the tenant will not reliably give it.
|
||
|
||
- [ ] **Idempotence against the real tenant**
|
||
Run the full preview twice. The counters must be identical.
|
||
|
||
- [ ] **Performance baseline (NFR-002)**
|
||
Record `durationMs` from the `RunComplete` record and the account count. No target exists yet;
|
||
this run is how one gets set.
|
||
|
||
### Stage A3 — delegated write, test accounts only 🔒
|
||
|
||
**Gated on V-4** — the written security sign-off on the six OTD-003 compensating controls
|
||
(T101, `specs/001-persona-engine/verification/V-4.md`).
|
||
|
||
- [ ] **V-4 sign-off recorded** — nothing below may start before this
|
||
- [ ] **Purpose-created test accounts exist**, one per origin type, and no other account is in scope
|
||
- [ ] **Write scope added**: `User.ReadWrite.All`, still as a non-privileged account
|
||
- [ ] **Preview the test accounts first**, one at a time, with `-WhatIf`
|
||
- [ ] **Enforce one test account**
|
||
`pwsh ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId <TEST-ACCOUNT-OBJECT-ID>`
|
||
Confirm the confirmation prompt appears — `ConfirmImpact` is `High`.
|
||
- [ ] **Verify the write landed and touched nothing else**
|
||
Re-read the account and diff every property against a copy taken beforehand. Only the persona
|
||
attribute may have changed.
|
||
- [ ] **Confirm `previousValue` is on the audit record** — without it, rollback is impossible retroactively
|
||
- [ ] **Second run proposes zero changes** (SC-002, against a real directory this time)
|
||
- [ ] **V-1 write half (T102)** — one write per origin type; append to `V-1.md`
|
||
- [ ] **V-2 (T103)** — build a dynamic group on `user.extension_<appId>_<name>`, assign a Conditional
|
||
Access policy to it in **report-only** mode, and confirm it applies. This is what proves the
|
||
persona is actually useful rather than merely stored.
|
||
|
||
### Stage B — Azure Automation ⛔
|
||
|
||
Deferred; no Automation account available. T115–T121. Run **T116 first** (`Test-Json` behaviour in
|
||
the Automation runtime) — it is the cheapest item most likely to surprise, and V-5a already showed
|
||
this cmdlet behaves in a way nobody would guess.
|
||
|
||
### If something fails
|
||
|
||
| Symptom | Look at |
|
||
| --- | --- |
|
||
| Exit `1` | The findings printed above it. No connection was attempted. |
|
||
| Exit `2` | Scopes, consent, whether the account can sign in. |
|
||
| Exit `3` | Graph availability. **No accounts were processed** — a partial population is never used. |
|
||
| Exit `4` | Group/role endpoint health. Nothing was changed. |
|
||
| Exit `5` | **An engine defect.** File an issue with the `EngineDefect` record. |
|
||
| Every account `Skipped` | Target attribute blank or unapproved. |
|
||
| Every account `EvaluationError` | A required data source is disabled or unreachable. |
|
||
| Every account `Unclassified` | Every rule disabled, or no rule matches. The summary distinguishes these. |
|
||
|
||
[docs/OperationsRunbook.md](docs/OperationsRunbook.md) has the full table, the kill switch, and the
|
||
rollback procedure.
|
||
|
||
---
|
||
|
||
## Spec-driven development
|
||
|
||
This project follows the **GitHub Spec Kit** workflow. Nothing is implemented before it is specified, planned, and decomposed into tasks.
|
||
|
||
```
|
||
Specify -> Plan -> Tasks -> Implement
|
||
```
|
||
|
||
| Stage | Artifact | State |
|
||
| --- | --- | --- |
|
||
| Baseline | `Persona-Engine-Developer-Handoff.txt` | Approved |
|
||
| Constitution | `.specify/memory/constitution.md` | Ratified v1.0.0 |
|
||
| Specify | `specs/001-persona-engine/spec.md` | Complete |
|
||
| Plan | `plan.md`, `research.md` | Complete — OTD-001…007, 010 resolved |
|
||
| Contracts | `data-model.md`, `contracts/`, `persona-engine.schema.json` | Complete |
|
||
| Tasks | `specs/001-persona-engine/tasks.md` | Complete — 121 tasks |
|
||
| Implement | `src/`, `docs/` | **109 / 121** — remainder needs a tenant or Automation |
|
||
|
||
---
|
||
|
||
## Core design principles
|
||
|
||
| Principle | Meaning |
|
||
| --- | --- |
|
||
| **Deterministic** | The same input and configuration always produce the same persona. |
|
||
| **Exactly one result** | Every evaluated account receives one persona — never zero, never several. |
|
||
| **First match wins** | Rules are ordered; evaluation stops at the first match. |
|
||
| **Idempotent** | Re-running changes nothing unless the calculated value actually changed. |
|
||
| **Configuration-driven** | Business rules live in JSON, never in PowerShell source. |
|
||
| **Fail safe** | If evaluation cannot complete reliably, the existing persona is preserved. |
|
||
| **Explainable** | Every result identifies the matched rule, run ID, UPN, and Account Object ID. |
|
||
| **Modular** | The pure rule engine has no dependency on Graph, Azure Automation, or the console. |
|
||
|
||
### The one that matters most
|
||
|
||
**Unknown is not false.** If a membership lookup fails, the condition is `Unknown`, not `False` — so a
|
||
`notMemberOf` condition does not become satisfied when the lookup fails.
|
||
|
||
Without that, a transient Graph outage would make every privileged account look like a non-member of
|
||
its Tier 0 group, and one run would quietly demote the entire administrative population.
|
||
|
||
---
|
||
|
||
## Components
|
||
|
||
### `Invoke-PersonaEngine.ps1`
|
||
|
||
| Parameter | Notes |
|
||
| --- | --- |
|
||
| `-ConfigPath <string>` | Required. Validated through all four layers before any connection. |
|
||
| `-WhatIf` | **The approved no-write control.** |
|
||
| `-UserObjectId <GUID>` | Single-user execution. |
|
||
| `-OutputPath <string>` | Overrides `logging.path`. Both default to `<current-directory>/logs/persona-engine-audit.ndjson`. |
|
||
| `-CorrelationId <GUID>` | Run identifier; generated when absent. |
|
||
| `-SchemaPath <string>` | Schema override. |
|
||
| `-PreviousConfigPath <string>` | Enables the VR-003 drift checks. |
|
||
| `-Verbose` / `-Debug` | Common parameters. **Neither implies read-only.** |
|
||
|
||
**Exit codes**: `0` success · `1` config invalid · `2` auth · `3` enumeration · `4` data / threshold ·
|
||
`5` reconciliation · `6` unexpected.
|
||
|
||
### `Edit-PersonaEngineConfig.ps1`
|
||
|
||
| Parameter | Notes |
|
||
| --- | --- |
|
||
| `-ConfigPath <string>` | Required. |
|
||
| `-ValidateOnly` | Validate; never enter the editor. |
|
||
| `-NonInteractive` | Pipeline mode. Never prompts, never hangs. |
|
||
| `-SchemaPath` / `-OutputPath` | Schema override; Save-As target. |
|
||
| `-TreatWarningsAsErrors` | Escalate warnings (VR-005). |
|
||
| `-TestDataPath <string>` | Synthetic rule testing, no tenant. |
|
||
| `-PreviousConfigPath` / `-EnforcementEnabled` | Drift checks; raise safety severities. |
|
||
|
||
**Exit codes**: `0` valid · `1` errors · `2` warnings escalated · `3` file unreadable · `4` schema unusable.
|
||
|
||
Validation runs in four layers: **JSON syntax → JSON Schema → semantic → safety**, stopping at the
|
||
first that produces errors.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Invoke-PersonaEngine.ps1 thin wrapper: parameters, ShouldProcess, exit code
|
||
└── src/Engine/ Invoke-PersonaEngineRun (the run loop, testable offline)
|
||
├── Configuration Import, Test (4 layers), Resolve-TargetAttribute
|
||
├── Authentication Connect-PersonaGraphInteractive
|
||
├── DataProviders Get-PersonaUsers, GroupMembership, DirectoryRoles, retry, cache
|
||
├── Normalization ConvertTo-PersonaUserRecord, ConvertTo-PersonaMembershipRecord
|
||
├── RuleEngine Test-PersonaCondition/ConditionGroup/Rule, Resolve-UserPersona
|
||
├── Persistence Compare-PersonaValue, New-PersonaWriteBody, Set-UserPersonaAttribute
|
||
├── Presentation Write-UserPersonaResult, Write-PersonaSummary, reconciliation
|
||
└── Audit New-PersonaAuditRecord, Write-PersonaAuditRecord, Export-PersonaRunReport
|
||
```
|
||
|
||
The **rule engine is pure** — no Graph, no auth, no console, no filesystem, no clock. That purity is
|
||
what lets it be evaluated offline against synthetic fixtures with no tenant connection.
|
||
|
||
---
|
||
|
||
## Repository layout
|
||
|
||
```
|
||
Invoke-PersonaEngine.ps1 Edit-PersonaEngineConfig.ps1
|
||
PersonaEngine.psd1 PersonaEngine.psm1
|
||
|
||
config/ persona-engine.example.json, persona-engine.schema.json
|
||
src/ Configuration/ Authentication/ DataProviders/ Normalization/
|
||
RuleEngine/ Persistence/ Presentation/ Engine/ Audit/
|
||
docs/ Architecture.md SecurityModel.md ConfigurationReference.md
|
||
RuleAuthoringGuide.md OperationsRunbook.md BusinessRules.md Logging.md
|
||
specs/001-persona-engine/
|
||
spec.md plan.md tasks.md research.md data-model.md
|
||
quickstart.md traceability.md contracts/ verification/
|
||
```
|
||
|
||
---
|
||
|
||
## Security model
|
||
|
||
The short version; the full one is [docs/SecurityModel.md](docs/SecurityModel.md).
|
||
|
||
**Graph application permissions have no per-property write scope** (OTD-003). An identity that can
|
||
write the persona attribute can write any writable user property. The directory will not stop a
|
||
malformed request on our behalf, so six compensating controls hold that line, and each is tested:
|
||
|
||
1. `New-PersonaWriteBody` throws for any attribute other than the configured target.
|
||
2. The target must appear in `approvedWritableAttributes` — checked at validation **and** at write time.
|
||
3. Validation rejects any approved attribute that is not a directory extension property (`PE-SAF-002`).
|
||
4. One function builds the request body, and it returns a hashtable whose `Count` is exactly 1.
|
||
5. Tests inspect every body issued during a full enforcing run.
|
||
6. Code owners and branch policies gate changes to `src/Persistence/`.
|
||
|
||
**V-4 — written security sign-off on these controls — gates all enforcement.**
|
||
|
||
Never logged: tokens, `Authorization` headers, secrets, certificates, raw Graph responses. The
|
||
guarantee is structural: the record builder accepts only named, typed values, so there is nothing for
|
||
a secret to ride in on.
|
||
|
||
---
|
||
|
||
## Sanitization (SC-013)
|
||
|
||
No organization name, real domain, tenant or subscription ID, real UPN or Object ID, real group or
|
||
role identifier, environment-specific attribute name, or secret may appear in any file this
|
||
repository would commit.
|
||
|
||
Reserved domains (`example.com`, `.invalid`, `.test`) and the module manifest's own identity GUID are
|
||
exempt; nothing else is. See [docs/SecurityModel.md](docs/SecurityModel.md) for the full policy.
|
||
|
||
Placeholders: `<ORGANIZATION-NAME>` · `<PRIMARY-DOMAIN>` · `<TENANT-ID>` · `<ACCOUNT-OBJECT-ID>` ·
|
||
`<GROUP-OBJECT-ID>` · `<APPROVED-PERSONA-ATTRIBUTE-NAME>` · `<AUTOMATION-ACCOUNT-NAME>`
|
||
|
||
---
|
||
|
||
## Open decisions and verification
|
||
|
||
| ID | Status |
|
||
| --- | --- |
|
||
| OTD-001 persona attribute | **Resolved** — directory extension property |
|
||
| OTD-002 least-privilege permissions | **Resolved** |
|
||
| OTD-003 per-attribute write scope | **Resolved: not possible.** Six compensating controls; V-4 outstanding |
|
||
| OTD-004 Graph access approach | **Resolved** — `Invoke-MgGraphRequest` |
|
||
| OTD-005 schema validation | **Resolved locally** — `Test-Json`; V-5b open for Automation |
|
||
| OTD-006 log transport | **Resolved** — NDJSON via a single sink |
|
||
| OTD-007 retry policy | **Resolved** |
|
||
| OTD-008 incremental processing | Open — full enumeration only in v1 |
|
||
| OTD-009 concurrency lock | Open — deferred with Stage B |
|
||
| OTD-010 rollback | **Data captured**; tool out of scope for v1 |
|
||
|
||
| Verification | Status |
|
||
| --- | --- |
|
||
| V-1 read / write | Open — needs a tenant / test accounts |
|
||
| V-2 dynamic group + CA | Open |
|
||
| V-3 non-privileged preview | Open |
|
||
| **V-4 security sign-off** | **Open — gates enforcement** |
|
||
| V-5a `Test-Json` behaviour | **Closed** — [V-5a.md](specs/001-persona-engine/verification/V-5a.md) |
|
||
| V-5b `Test-Json` in Automation | Deferred |
|
||
|
||
Full requirement-to-test mapping, including the gaps: [traceability.md](specs/001-persona-engine/traceability.md).
|
||
|
||
---
|
||
|
||
## Documentation
|
||
|
||
| Document | For |
|
||
| --- | --- |
|
||
| [Architecture.md](docs/Architecture.md) | Boundaries, and why the rule engine is pure |
|
||
| [SecurityModel.md](docs/SecurityModel.md) | OTD-003, the six controls, V-4 |
|
||
| [ConfigurationReference.md](docs/ConfigurationReference.md) | Every field and every finding code |
|
||
| [RuleAuthoringGuide.md](docs/RuleAuthoringGuide.md) | User manual: every condition type and operator, worked examples, the interactive editor |
|
||
| [BusinessRules.md](docs/BusinessRules.md) | Writing and changing rules — the judgement calls |
|
||
| [OperationsRunbook.md](docs/OperationsRunbook.md) | Kill switch, rollback, incidents |
|
||
| [Logging.md](docs/Logging.md) | Record types and querying |
|
||
|
||
---
|
||
|
||
## Scope (version 1)
|
||
|
||
**In scope** — Entra **user objects only**; ordered first-match rules in JSON; property,
|
||
group-membership, and role conditions with nested `all`/`any`; `-WhatIf` as the no-write control;
|
||
structured audit logging; local PowerShell 7 and (deferred) Azure Automation.
|
||
|
||
**Out of scope** — service principals, managed identities, workload identities, agentic identities;
|
||
non-JSON configuration; delta processing; condition-level case-sensitivity; PIM-eligible role
|
||
assignments; a rollback tool.
|
||
|
||
### Candidate persona catalogue
|
||
|
||
`Guest` · `BreakGlass-Admin` · `Tier0-Admin` · `Tier1-Admin` · `Tier2-Admin` · `Restricted-User` ·
|
||
`Test-Account` · `Service-Account` · `Shared-Functional-Account` · `Meeting-Room-Device` · `Employee` ·
|
||
`Contractor` · `Student`
|
||
|
||
Two values are processing results, not rule outcomes:
|
||
|
||
- **`Unclassified`** — evaluation succeeded, no rule matched.
|
||
- **`EvaluationError`** — evaluation could not complete; the existing persona is preserved.
|