Files
personaEngine2/docs/ConfigurationReference.md
T
dave cdc6bb33d3 Implement Stage A: rule engine, validation, audit, and safety gates
Completes 109 of 121 tasks. Every remaining task needs a tenant connection
(T055, T056, T101-T103) or an Azure Automation account (T115-T121).

  354 offline Pester tests      PASS
  Engine purity (Principle IV)  PASS
  Sanitization (SC-013)         PASS  (156 files)
  Graph module loaded in tests  none  (SC-008 holds)

What landed
  - Four-layer configuration validation with stable finding codes, covering
    every VR-002 and VR-003 condition, plus a 23-fixture invalid-config corpus
  - Run loop, audit records (NDJSON through a single sink), summaries,
    reconciliation, and exit codes 0-6
  - Persistence behind a single write-body builder whose result always has
    exactly one key
  - Invoke-PersonaEngine.ps1 and Edit-PersonaEngineConfig.ps1
  - Six docs, two pipelines, traceability matrix, V-5a and sanitization records

Three deviations from tasks.md, each recorded in its status block

  T033 is not in Resolve-UserPersona. evaluationErrorThreshold is run-level
  state and the rule engine is pure; a counter there would break Principle IV.
  It lives in New-PersonaRunCounter and is applied in the run loop.

  A new src/Engine/ layer holds Invoke-PersonaEngineRun. The entry script
  imports the manifest, which requires Microsoft.Graph.Authentication, so a
  loop living only inside it could not run on a machine without the Graph SDK
  and SC-004 could not be proven at all. The entry script is now a thin
  wrapper and what ships is what is tested.

  The invalid-config corpus is generated by a committed script, with the
  generated fixtures committed too, so a reviewer sees the fixture in the diff.

Defects found by running the code, not by reading it

  Group and role ID lists were double-wrapped: @(Get-PersonaGroupIdPage ...)
  around a comma-returned array collapsed every membership list into one
  bogus space-joined entry. That is a silent false non-match, exactly what
  FR-013 exists to prevent.

  A 403 whose status appears only in the exception message parsed as $null,
  which the retry policy treats as a transport error - five requests per
  account against a tenant already refusing. Status extraction now falls back
  to the message text, bounded to 400-599.

  The sanitization scan walked tracked files only, so it covered 34 of 156
  files and none of this phase's code. It now scans untracked non-ignored
  files too, and a negative control confirms it catches a planted leak.

  Test-Json reports one error per violating location, not first-failure-only
  as the V-5a draft claimed. Record and pin corrected.

Enforcement remains blocked on the V-4 security sign-off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:48:19 -04:00

11 KiB
Raw Blame History

Configuration reference

Every field in persona-engine.json, and every finding code the validator can produce.

The authoritative schema is config/persona-engine.schema.json (JSON Schema draft-07). A working example is config/persona-engine.example.json, which is validated by CI against its own schema — if the example the documentation points at could not pass, every reader's first run would fail.

Top level

Field Required Notes
configVersion yes Semantic version, major.minor.patch. A downgrade is a safety finding.
metadata no owner, changeReference, description. Free-form; not read by the engine.
engine yes Engine behaviour.
dataSources yes Which directory data may be retrieved.
logging no Audit output.
personas yes The declared persona catalogue.
rules yes Ordered business rules.

engine

Field Required Default Notes
targetAttribute yes The single attribute the engine may write. Must be a directory extension property and must appear in approvedWritableAttributes.
approvedWritableAttributes yes The allow-list. Comparison is ordinal — extension property names are case-sensitive in Graph.
maxConditionDepth no 5 RE-004. Minimum 1, hard ceiling 10.
summaryInterval no 25 Interim summary frequency. 0 suppresses interim summaries; a final summary always appears.
defaultMembershipMode no direct Mode for membership conditions that do not specify one.
evaluationErrorThreshold no unset Count of EvaluationError results above which the run reports exit code 4. Unset means report, do not fail.

Setting evaluationErrorThreshold to 0 makes a single transient lookup failure fail the run. That is occasionally what you want; it is rarely what you meant.

dataSources

Field Required Notes
groups.enabled yes When false, no membership facet is retrieved. Rules needing it become EvaluationError.
groups.membershipMode no Pins a mode globally. Leave it out unless you mean to restrict — absent means "any mode is acceptable", and RE-007 makes mode a per-condition choice. Pinning it makes every per-condition override a PE-SEM-014 warning.
roles.enabled yes Directory role assignments.
roles.includeEligible no PIM-eligible assignments. Out of scope for v1; no provider is implemented.

The engine retrieves only the facets enabled rules actually reference. A configuration with no role conditions never calls the role endpoint, so a tenant where role reads are unavailable can still run property-only rules.

logging

Field Required Default Notes
destination no both file, stream, both, or none. stream writes records to the PowerShell Information stream.
path no NDJSON output file. One record per line.
traceConditionValues no false Writes evaluated attribute values into audit records.
acknowledgeConditionTracing no false Required whenever traceConditionValues is true (VR-003).

personas

The declared catalogue. A rule assigning a persona absent from this list is a PE-SEM-010 error — the catalogue is what stops a typo from writing a new persona value into the directory.

Unclassified and EvaluationError are processing results and may never be declared or assigned.

rules

Field Required Notes
id yes Unique. Appears in every audit record; this is how a decision is traced to its rule.
name yes Human-readable.
description yes Why the rule exists. Required, because a rule nobody can explain cannot be safely changed.
enabled yes Disabled rules are excluded from evaluation but still appear in summaries with zero matches.
priority yes Unique integer. Lower evaluates first.
persona yes Must appear in personas.
match yes The root condition group.
tags, owner, changeReference, effectiveDate, notes, testCases no Metadata. effectiveDate is not evaluated — a date-dependent decision would break determinism.

Priorities must be unique among enabled rules. The engine breaks ties by rule ID so results stay deterministic, but the resulting order is an accident rather than a decision, so PE-SEM-002 blocks it.

Condition groups and conditions

A group has operator (all or any) and a conditions array. Each entry is either another group or a condition.

Field Applies to Notes
type all property, membership, or role.
property property One of the supported names below, or an extension property.
operator all See the operator table.
value most Single comparison value.
values in, notIn Comparison set.
groupObjectIds membership Group Object IDs. Names are mutable; IDs are not (RE-009).
roleIds role Role template IDs, which are stable across tenants.
membershipMode membership direct or transitive, per condition.
caseSensitive Reserved. Not implemented in v1; the schema accepts the key so a later version needs no breaking change.

Supported properties

AccountObjectId · UserPrincipalName · DisplayName · UserType · AccountEnabled · CompanyName · JobTitle · Department

Plus any directory extension property named extension_<32-hex-app-id>_<name>. Anything else is PE-SEM-015: unsupported properties are never retrieved, so the condition would compare against a permanently absent value and quietly never match.

Operators (RE-005)

Operator Applies to Notes
equals, notEquals property Case-insensitive (RE-006).
contains, notContains property Case-insensitive substring.
startsWith, endsWith property Case-insensitive.
matchesRegex property Pattern compiled at validation time. An invalid pattern is PE-SEM-016, not a runtime failure.
in, notIn property Requires values.
isNull, isNotNull property Tests presence. Must not carry a value — it would be silently ignored (PE-SEM-009).
memberOf, notMemberOf membership, role Requires groupObjectIds or roleIds.

Null and absent properties are treated as empty for ordinary comparisons and never cause an evaluation failure (FR-012). Intentional null matching uses isNull / isNotNull.

Tri-state evaluation

Conditions return True, False, or Unknown. Unknown means required data could not be retrieved, and it propagates:

Group Contains Result
all any False False
all only True plus at least one Unknown Unknown
any any True True
any only False plus at least one Unknown Unknown

An Unknown at a rule's root makes the account EvaluationError: the stored persona is preserved and no write is attempted (FR-013, FR-014).

Validation layers

Run in order, stopping at the first that produces Error findings. Running semantic checks over a structurally invalid document yields noise, not signal.

Layer Mechanism Codes
1 Syntax ConvertFrom-Json PE-SYN-nnn
2 Schema Test-Json -SchemaFile PE-SCH-nnn
3 Semantic PowerShell checks PE-SEM-nnn
4 Safety PowerShell checks PE-SAF-nnn

Codes are stable. Pipelines and runbooks match on them, so a code is never reused for a different condition and never renumbered.

Syntax — PE-SYN

Code Condition
PE-SYN-001 Configuration file not found, or is not a file
PE-SYN-002 File exists but could not be read
PE-SYN-003 File is not valid JSON

Schema — PE-SCH

Code Condition
PE-SCH-001 Document violates the schema
PE-SCH-002 Schema file not found
PE-SCH-003 Schema file exists but is not valid JSON Schema

PE-SCH-003 exists because of V-5a: Test-Json returns $true when the schema itself cannot be parsed. A wrapper trusting the return value would report every configuration as schema-valid against a schema that never ran.

Semantic — PE-SEM (VR-002)

Code Condition Severity
PE-SEM-001 Duplicate rule ID Error
PE-SEM-002 Duplicate priority among enabled rules Error
PE-SEM-003 No rules, or no enabled rules Error
PE-SEM-004 Blank target attribute Error
PE-SEM-005 Target attribute absent from the approved list Error
PE-SEM-006 Rule references a disabled data source Error
PE-SEM-007 memberOf / notMemberOf with no group or role IDs Error
PE-SEM-008 in / notIn with no values Error
PE-SEM-009 isNull / isNotNull carrying a comparison value Error
PE-SEM-010 Persona not in the declared catalogue Error
PE-SEM-011 Unclassified used as a rule persona Error
PE-SEM-012 Nesting deeper than maxConditionDepth Error
PE-SEM-013 maxConditionDepth outside 110 Error
PE-SEM-014 Condition mode differs from an explicitly pinned global mode Warning
PE-SEM-015 Unsupported property name Error
PE-SEM-016 Invalid regular expression Error

Several of these are also enforced by the schema. The overlap is deliberate: layer 2 can be bypassed with -SchemaPath, and V-5a showed an unparseable schema passes silently. Anything that can misclassify a privileged account is checked twice.

Safety — PE-SAF (VR-003)

Code Condition Severity
PE-SAF-001 Blank target attribute Error enforcing, Warning in preview
PE-SAF-002 Approved list contains a non-extension attribute Error
PE-SAF-003 Enabled rules need data the data sources do not provide Error
PE-SAF-004 configVersion lower than the deployed version Error enforcing, Warning in preview
PE-SAF-005 Rules removed or reordered with no version change Error enforcing, Warning in preview
PE-SAF-006 Tracing enabled without acknowledgement Error
PE-SAF-007 Save would overwrite an existing configuration with no backup Error

PE-SAF-004 and PE-SAF-005 need -PreviousConfigPath. Without it they are skipped, and an Information finding says so — silence would be read as approval.

Escalation (VR-005)

Error blocks execution and saving. Warning blocks only under -TreatWarningsAsErrors. Information never blocks. Passing -TreatWarningsAsErrors does not change a finding's severity; it changes the caller's tolerance for it.