updated docs, removed testing files, fixed logging
This commit is contained in:
@@ -6,24 +6,24 @@ The engine enumerates Entra user accounts, evaluates each one against an ordered
|
||||
|
||||
> **Project status: Stage A implementation complete — ready for tenant validation.**
|
||||
>
|
||||
> 109 of 121 tasks are done. **354 offline tests pass**, along with the engine-purity and
|
||||
> sanitization gates. 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).
|
||||
> 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 [testing checklist](#testing-checklist) in order.
|
||||
> — 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 -TestDataPath ./tests/TestData -ValidateOnly -NonInteractive
|
||||
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive
|
||||
```
|
||||
|
||||
That validates the configuration through all four layers and runs the real rule engine against
|
||||
synthetic fixtures. No tenant, no credentials, no network. It is the fastest way to see what the
|
||||
engine does.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -34,14 +34,12 @@ engine does.
|
||||
| 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 offline suites or the config editor. |
|
||||
| Pester 5.0+ | Only to run the tests. Developed against 6.1.0. |
|
||||
| PSScriptAnalyzer | Only for the lint gate. |
|
||||
| `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 Pester -MinimumVersion 5.0 -Scope CurrentUser -SkipPublisherCheck
|
||||
Install-Module PSScriptAnalyzer -Scope CurrentUser
|
||||
```
|
||||
|
||||
@@ -60,11 +58,11 @@ cd persona-engine
|
||||
### Step 2 — Prove the machine can run it, before touching a tenant
|
||||
|
||||
```bash
|
||||
pwsh -NoProfile -Command '& { $c = & ./tests/PesterConfiguration.ps1 -Suite Offline; Invoke-Pester -Configuration $c }'
|
||||
pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive
|
||||
```
|
||||
|
||||
Expect **354 passed, 0 failed**. This needs no credentials and no network. If it does not pass, stop
|
||||
— nothing downstream is trustworthy.
|
||||
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
|
||||
|
||||
@@ -122,7 +120,10 @@ Then replace every placeholder:
|
||||
| `<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` |
|
||||
| `<LOG-OUTPUT-PATH>` | Audit log path |
|
||||
|
||||
`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`
|
||||
@@ -152,19 +153,14 @@ 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 — CI
|
||||
|
||||
Two pipelines are included. `pipelines/validate.yml` gates every pull request and runs entirely
|
||||
offline; `pipelines/test.yml` publishes test results and coverage.
|
||||
|
||||
### Step 9 — Enforcement 🔒
|
||||
### 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).
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
## 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.
|
||||
@@ -177,28 +173,9 @@ Everything here runs on any machine with PowerShell 7.
|
||||
`pwsh -NoProfile -Command "Test-ModuleManifest ./PersonaEngine.psd1"`
|
||||
Fails without `Microsoft.Graph.Authentication` installed. Expected on a bare machine.
|
||||
|
||||
- [ ] **Offline suite: 354 passed, 0 failed**
|
||||
`pwsh -NoProfile -Command "& { $c = & ./tests/PesterConfiguration.ps1 -Suite Offline; Invoke-Pester -Configuration $c }"`
|
||||
|
||||
- [ ] **Safety suite passes and is not empty**
|
||||
`pwsh -NoProfile -Command "& { $c = & ./tests/PesterConfiguration.ps1 -Suite Safety; Invoke-Pester -Configuration $c }"`
|
||||
A zero-test green run is the most dangerous possible result — it is what a mis-tagged file looks
|
||||
like, and the assertions it silently drops are the zero-write and single-attribute ones.
|
||||
|
||||
- [ ] **Engine purity (Principle IV)**
|
||||
`pwsh ./tests/Test-EnginePurity.ps1`
|
||||
|
||||
- [ ] **Sanitization (SC-013)**
|
||||
`pwsh ./tests/Test-Sanitization.ps1`
|
||||
Scans tracked **and** untracked non-ignored files, so it catches a leak before the commit.
|
||||
|
||||
- [ ] **Lint**
|
||||
`pwsh -NoProfile -Command "Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1"`
|
||||
|
||||
- [ ] **No Graph module was loaded during the offline suite**
|
||||
`pwsh -NoProfile -Command "& { $c = & ./tests/PesterConfiguration.ps1 -Suite Offline; $c.Output.Verbosity='None'; $null = Invoke-Pester -Configuration $c; Get-Module Microsoft.Graph* }"`
|
||||
Must print nothing. This is the proof that SC-008 holds.
|
||||
|
||||
- [ ] **Example configuration passes all four layers**
|
||||
`pwsh ./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.example.json -ValidateOnly -NonInteractive`
|
||||
|
||||
@@ -206,9 +183,10 @@ Everything here runs on any machine with PowerShell 7.
|
||||
`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 ./tests/TestData -ValidateOnly -NonInteractive`
|
||||
The fixtures include two accounts whose membership lookups failed. Both must show
|
||||
`EvaluationError`, not a persona. If they show a persona, stop — FR-013 is broken.
|
||||
`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`
|
||||
@@ -312,7 +290,7 @@ Specify -> Plan -> Tasks -> Implement
|
||||
| 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/`, `tests/`, `pipelines/`, `docs/` | **109 / 121** — remainder needs a tenant or Automation |
|
||||
| Implement | `src/`, `docs/` | **109 / 121** — remainder needs a tenant or Automation |
|
||||
|
||||
---
|
||||
|
||||
@@ -336,7 +314,6 @@ Specify -> Plan -> Tasks -> Implement
|
||||
|
||||
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.
|
||||
`tests/RuleEngine/UnknownNotFalse.Tests.ps1` exists solely to prevent that regression.
|
||||
|
||||
---
|
||||
|
||||
@@ -349,7 +326,7 @@ its Tier 0 group, and one run would quietly demote the entire administrative pop
|
||||
| `-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`. |
|
||||
| `-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. |
|
||||
@@ -392,9 +369,8 @@ Invoke-PersonaEngine.ps1 thin wrapper: parameters, ShouldProcess, exit
|
||||
└── Audit New-PersonaAuditRecord, Write-PersonaAuditRecord, Export-PersonaRunReport
|
||||
```
|
||||
|
||||
The **rule engine is pure** — no Graph, no auth, no console, no filesystem, no clock. Enforced on
|
||||
every build by `tests/Test-EnginePurity.ps1`, which parses each file with the PowerShell AST parser
|
||||
and inspects only code tokens. That purity is why 354 tests run with no tenant.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -407,11 +383,8 @@ PersonaEngine.psd1 PersonaEngine.psm1
|
||||
config/ persona-engine.example.json, persona-engine.schema.json
|
||||
src/ Configuration/ Authentication/ DataProviders/ Normalization/
|
||||
RuleEngine/ Persistence/ Presentation/ Engine/ Audit/
|
||||
tests/ Unit/ RuleEngine/ Configuration/ Safety/ TestData/
|
||||
Test-EnginePurity.ps1 Test-Sanitization.ps1 TestHelpers.ps1
|
||||
docs/ Architecture.md SecurityModel.md ConfigurationReference.md
|
||||
OperationsRunbook.md BusinessRules.md Logging.md
|
||||
pipelines/ validate.yml test.yml
|
||||
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/
|
||||
@@ -448,12 +421,11 @@ No organization name, real domain, tenant or subscription ID, real UPN or Object
|
||||
role identifier, environment-specific attribute name, or secret may appear in any file this
|
||||
repository would commit.
|
||||
|
||||
`tests/Test-Sanitization.ps1` scans tracked **and** untracked non-ignored files. Reserved domains
|
||||
(`example.com`, `.invalid`, `.test`) and the module manifest's own identity GUID are exempt; nothing
|
||||
else is.
|
||||
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>` · `<LOG-OUTPUT-PATH>`
|
||||
`<GROUP-OBJECT-ID>` · `<APPROVED-PERSONA-ATTRIBUTE-NAME>` · `<AUTOMATION-ACCOUNT-NAME>`
|
||||
|
||||
---
|
||||
|
||||
@@ -492,7 +464,8 @@ Full requirement-to-test mapping, including the gaps: [traceability.md](specs/00
|
||||
| [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 |
|
||||
| [BusinessRules.md](docs/BusinessRules.md) | Writing and changing rules |
|
||||
| [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 |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user