# 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: Phase 0 — approved requirements baseline.** > No implementation code exists yet. The authoritative baseline is > [`Persona-Engine-Developer-Handoff.txt`](Persona-Engine-Developer-Handoff.txt). > The next step is Phase 1: convert that baseline into `specs/001-persona-engine/spec.md`. --- ## 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 | Phase | State | | --- | --- | --- | --- | | Baseline | `Persona-Engine-Developer-Handoff.txt` | 0 | Approved | | Specify | `specs/001-persona-engine/spec.md` | 1 | Not started | | Plan | `specs/001-persona-engine/plan.md`, `research.md` | 2 | Not started | | Contracts | `data-model.md`, `contracts/`, `persona-engine.schema.json` | 3 | Not started | | Tasks | `specs/001-persona-engine/tasks.md` | 4 | Not started | | Implement | `src/`, `tests/`, `pipelines/` | 5–12 | Not started | Any item that is unresolved must be captured as an explicit **assumption, risk, or architecture decision**. It must never be silently implemented. --- ## 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. | --- ## Scope (version 1) **In scope** - Microsoft Entra **user objects only** - Ordered, first-match business rules defined in **JSON** (the only supported configuration format) - Property, group-membership, and role-based conditions with nested `All` / `Any` composition - Native PowerShell `-WhatIf` as the approved no-write control - Structured, audit-friendly logging plus immediate per-user output and periodic summaries - Local PowerShell 7 execution and Azure Automation PowerShell 7 runbook execution **Out of scope for v1** — the architecture must not assume these share user-object properties: - Service principals, managed identities, workload identities, agentic identities ### Candidate persona catalogue These are candidate business classifications, **not** hard-coded engine behaviour: `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 rather than rules: - **`Unclassified`** — evaluation succeeded, but no rule matched. - **`EvaluationError`** — evaluation could not complete; the existing persona is preserved. --- ## Components ### 1. `Invoke-PersonaEngine.ps1` Retrieval, evaluation, reporting, and controlled persistence. | Parameter | Notes | | --- | --- | | `-ConfigPath ` | Required | | `-WhatIf` | Native risk-mitigation parameter; the approved no-write control | | `-Verbose` / `-Debug` | Native common parameters; `-Debug` must **not** mean read-only | | `-UserObjectId ` | Optional single-user test execution | | `-OutputPath ` | Optional override, if permitted | | `-CorrelationId ` | Optional supplied run identifier | The script uses `CmdletBinding` with `SupportsShouldProcess`. ```powershell # Read-only evaluation ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf # Read-only with operational detail ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf -Verbose # Single-user validation ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId -WhatIf # Production, changed-values-only processing ./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json ``` **Exit codes** | Code | Meaning | | --- | --- | | `0` | Successful run; no fatal processing errors | | `1` | Configuration validation failure | | `2` | Authentication / authorization failure | | `3` | User enumeration failure | | `4` | Fatal required data-provider failure | | `5` | Reconciliation failure | | `6` | Unexpected fatal engine error | A per-user `EvaluationError` does not necessarily terminate the run, but the final status must report the number of affected accounts and may apply a configurable warning/failure threshold. ### 2. `Edit-PersonaEngineConfig.ps1` Configuration validation, interactive editing, synthetic rule testing, and pipeline enforcement. | Parameter | Notes | | --- | --- | | `-ConfigPath ` | Required | | `-ValidateOnly` | Validate without entering the editor | | `-NonInteractive` | Pipeline mode; returns codes instead of prompting | | `-SchemaPath ` | Optional schema override | | `-OutputPath ` | Optional Save-As target | | `-TreatWarningsAsErrors` | Escalate warnings | | `-TestDataPath ` | Optional synthetic sample input | ```powershell # Validate only ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly # Pipeline validation ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly -NonInteractive # Interactive editor ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json ``` Validation runs in four layers: **JSON syntax → JSON Schema → semantic → safety**. --- ## Architecture ``` Invoke-PersonaEngine.ps1 | +-- Configuration Import-PersonaConfiguration, Test-PersonaConfiguration, Resolve-TargetAttribute +-- Authentication Connect-PersonaGraphInteractive, Connect-PersonaGraphManagedIdentity +-- Data Providers Get-PersonaUsers, Get-PersonaGroupMembership, Get-PersonaDirectoryRoles +-- Normalization ConvertTo-PersonaUserRecord, ConvertTo-PersonaMembershipRecord +-- Rule Engine Test-PersonaCondition, Test-PersonaConditionGroup, Test-PersonaRule, Resolve-UserPersona +-- Persistence Compare-PersonaValue, Set-UserPersonaAttribute +-- Presentation Write-UserPersonaResult, Write-PersonaSummary +-- Audit New-PersonaAuditRecord, Export-PersonaRunReport ``` **Critical flow** ``` Graph acquisition -> normalized identity record -> pure rule engine -> persona decision result -> comparison -> optional persistence adapter -> console and structured audit output ``` The **pure rule engine must not depend** on Graph authentication, Azure Automation, or console rendering. It has to be testable offline with synthetic data. --- ## Planned repository structure ``` PersonaEngine/ |-- README.md |-- Invoke-PersonaEngine.ps1 |-- Edit-PersonaEngineConfig.ps1 |-- PersonaEngine.psd1 |-- PersonaEngine.psm1 | |-- config/ persona-engine.example.json, persona-engine.schema.json |-- src/ Authentication/ Configuration/ DataProviders/ Normalization/ | RuleEngine/ Persistence/ Presentation/ Audit/ |-- tests/ Unit/ Integration/ Configuration/ Safety/ TestData/ |-- docs/ Architecture.md BusinessRules.md ConfigurationReference.md | Logging.md SecurityModel.md OperationsRunbook.md |-- pipelines/ validate.yml test.yml release.yml |-- specs/ |-- 001-persona-engine/ |-- spec.md plan.md tasks.md research.md data-model.md quickstart.md |-- contracts/ checklists/ ``` --- ## Security model - **Authentication** — Azure Automation uses a **managed identity**. Local development uses an approved interactive or read-only application identity. **No client secret in source control.** - **Least privilege** — the execution identity gets only what the enabled rules require: in-scope user properties, configured group membership, configured role data, and the existing persona value. - **Write permissions** — the production identity is granted the minimum permission needed to update the configured target attribute. Whether Entra can enforce write scope at the **individual attribute level must be verified, never assumed** (see OTD-003). - **Compensating controls**, if the Graph permission proves broader than the single attribute: the persistence module accepts only the approved target attribute; that attribute must appear in `approvedWritableAttributes`; validation rejects all others; a dedicated function builds a request body containing only that attribute; unit and integration tests inspect the request body; code owners and branch policies gate persistence changes; directory audit logs are monitored for unexpected property writes. - **Data handling** — UPN and Account Object ID are approved for logs. Never log access tokens, authorization headers, secrets, or full Graph responses. Detailed condition values are diagnostic-only, behind `-Debug`. - **Kill switch** — disable the Automation schedule, run with `-WhatIf`, revoke production write permission, or disable write deployment stages. --- ## Sanitization requirements **Every** artifact in this repository — docs, examples, tests, configuration samples — must be free of organization names, real domains, tenant or subscription IDs, automation account names, real UPNs or Object IDs, real group/role identifiers, environment-specific attribute names, Log Analytics details, and any secret, token, certificate, or credential. Use placeholders only: `` · `` · `` · `` · `` · `` · `` · `` Synthetic test data must be obviously fictional and must never reproduce real employee records. Tenant-specific configuration belongs in a protected repository or configuration store, not here. --- ## Open technical decisions Research items to be resolved in `research.md` or an ADR. **OTD-001 through OTD-005 must be closed before persistence implementation.** | ID | Decision | | --- | --- | | OTD-001 | Select the exact Entra persona attribute mechanism — data type, Graph read/update method, discoverability, Conditional Access compatibility | | OTD-002 | Confirm exact least-privilege Microsoft Graph permissions | | OTD-003 | Confirm whether write authorization can be restricted to the individual target attribute | | OTD-004 | Select the Graph access approach — SDK cmdlets, direct REST, or a controlled combination | | OTD-005 | Select a JSON Schema validation approach compatible with PS7 locally and in Azure Automation | | OTD-006 | Select structured-log destination and transport | | OTD-007 | Define retry policy — retryable status codes, max attempts, backoff, jitter, logging | | OTD-008 | Define full versus incremental processing roadmap | | OTD-009 | Define production schedule and concurrency lock | | OTD-010 | Define rollback implementation | --- ## Getting started Implementation has not begun. The current work item is Phase 1. 1. Initialize the Spec Kit project structure. 2. Convert the handoff baseline into `specs/001-persona-engine/spec.md`. 3. Build a requirements traceability list using FR/NFR identifiers. 4. Close OTD-001 through OTD-005 before any persistence work. 5. Create `persona-engine.schema.json` and a placeholder-only `persona-engine.example.json`. 6. Define normalized PowerShell object contracts. 7. **Build the pure rule engine first**, with offline Pester tests, before any Graph integration. 8. Implement configuration validation and non-interactive pipeline mode. 9. Implement Graph read adapters, then console and structured logging. 10. Implement the persistence adapter **last**, with `ShouldProcess` and tests proving zero writes under `-WhatIf`. Requirements: **PowerShell 7**. Offline unit testing must not require tenant connectivity. --- ## Delivery - Hosted in **Azure DevOps Git** — feature branches, pull requests, protected release branch, code owners on persistence, security configuration, and production rules. - **Validation pipeline** — repository hygiene checks, PowerShell static analysis, JSON Schema validation, semantic/safety configuration validation, Pester unit tests, Pester safety tests, test result publication, artifact packaging. - **Release pipeline** — validate approved branch/tag, repeat validation and tests, package, deploy to Azure Automation, import modules, publish runbook, **keep the schedule disabled**, execute `-WhatIf` validation, approval gate, then enable enforcement. - Changes to the target attribute, `approvedWritableAttributes`, rule priority, rule enablement, rule conditions, persona outputs, authentication permissions, persistence functions, logging destination, or `WhatIf`/`ShouldProcess` behaviour **all require review**. --- ## Definition of done (v1) Version 1 is complete when both PowerShell scripts are implemented; the JSON Schema exists and is documented; validation covers syntax, schema, semantic, and safety layers; ordered first-match evaluation and nested `All`/`Any` work within the configured depth; the initial property and membership operators are tested; null behaviour matches the approved decision; required group-lookup failures produce `EvaluationError` and preserve the existing persona; `Unclassified` users are reported distinctly; each user result displays immediately; interim and final summaries work — including interval `0` — and reconciliation checks pass; logs include UPN and Account Object ID; **`-WhatIf` produces zero Graph writes**; only changed valid values are written in enforcement mode; the write payload contains only the approved target attribute; local offline Pester, read-only production-tenant, Azure Automation PowerShell 7, and Azure DevOps pipeline runs all pass; security review confirms permissions and compensating controls; operational documentation, kill switch, and rollback procedure are complete; and `-WhatIf` impact evidence is reviewed before enforcement is enabled.