Add elapsed run time, per-account results CSV, and a default config path
Summaries now show elapsed wall-clock time since the run started, and every summary (interim and final) overwrites a results.csv (Object ID, UPN, persona/status) next to the audit log, so an operator has a plain export without parsing NDJSON. ConfigPath also now defaults to ./config/persona-engine.json instead of requiring -ConfigPath every run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,7 +27,8 @@
|
|||||||
|
|
||||||
.PARAMETER ConfigPath
|
.PARAMETER ConfigPath
|
||||||
Path to the JSON configuration. Validated through all four layers before any
|
Path to the JSON configuration. Validated through all four layers before any
|
||||||
connection is attempted (FR-002).
|
connection is attempted (FR-002). Defaults to ./config/persona-engine.json,
|
||||||
|
resolved against the current directory, when omitted.
|
||||||
|
|
||||||
.PARAMETER UserObjectId
|
.PARAMETER UserObjectId
|
||||||
Evaluate a single user instead of enumerating the tenant. The recommended first
|
Evaluate a single user instead of enumerating the tenant. The recommended first
|
||||||
@@ -73,9 +74,8 @@
|
|||||||
#>
|
#>
|
||||||
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
|
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[ValidateNotNullOrEmpty()]
|
[ValidateNotNullOrEmpty()]
|
||||||
[string] $ConfigPath,
|
[string] $ConfigPath = (Join-Path (Get-Location).Path 'config' 'persona-engine.json'),
|
||||||
|
|
||||||
[guid] $UserObjectId,
|
[guid] $UserObjectId,
|
||||||
|
|
||||||
@@ -162,6 +162,13 @@ try {
|
|||||||
$logPath = $OutputPath ? $OutputPath : ($configuredPath ? $configuredPath : $defaultLogPath)
|
$logPath = $OutputPath ? $OutputPath : ($configuredPath ? $configuredPath : $defaultLogPath)
|
||||||
$auditParams = @{ Destination = $destination; Path = $logPath; State = $sinkState }
|
$auditParams = @{ Destination = $destination; Path = $logPath; State = $sinkState }
|
||||||
|
|
||||||
|
# Lives next to the audit log rather than under its own config key for path -
|
||||||
|
# one directory to point an operator at, not two. The file name alone is
|
||||||
|
# configurable because "results.csv" may collide with something else already
|
||||||
|
# written there.
|
||||||
|
$resultsFileName = $config.Logging.resultsFileName ? [string]$config.Logging.resultsFileName : 'results.csv'
|
||||||
|
$resultsPath = Join-Path (Split-Path -Parent $logPath) $resultsFileName
|
||||||
|
|
||||||
# Tracing is enabled by -Debug or by configuration, and requires acknowledgement
|
# Tracing is enabled by -Debug or by configuration, and requires acknowledgement
|
||||||
# in the configuration either way (VR-003, enforced in validation layer 4).
|
# in the configuration either way (VR-003, enforced in validation layer 4).
|
||||||
$traceRequested = $PSBoundParameters.ContainsKey('Debug') -or
|
$traceRequested = $PSBoundParameters.ContainsKey('Debug') -or
|
||||||
@@ -222,6 +229,8 @@ try {
|
|||||||
IsEnforcing = $runConfirmed
|
IsEnforcing = $runConfirmed
|
||||||
ShouldProcessGate = $gate
|
ShouldProcessGate = $gate
|
||||||
Tracing = $tracing
|
Tracing = $tracing
|
||||||
|
StartedUtc = $startedUtc
|
||||||
|
ResultsPath = $resultsPath
|
||||||
}
|
}
|
||||||
if ($UserObjectId -and $UserObjectId -ne [guid]::Empty) { $runParams['UserObjectId'] = $UserObjectId.ToString() }
|
if ($UserObjectId -and $UserObjectId -ne [guid]::Empty) { $runParams['UserObjectId'] = $UserObjectId.ToString() }
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
'Add-PersonaRunResult'
|
'Add-PersonaRunResult'
|
||||||
'Test-PersonaReconciliation'
|
'Test-PersonaReconciliation'
|
||||||
'Get-PersonaReconciliationDetail'
|
'Get-PersonaReconciliationDetail'
|
||||||
|
'Export-PersonaResultsCsv'
|
||||||
# Audit
|
# Audit
|
||||||
'New-PersonaAuditContext'
|
'New-PersonaAuditContext'
|
||||||
'New-PersonaAuditRecord'
|
'New-PersonaAuditRecord'
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ its Tier 0 group, and one run would quietly demote the entire administrative pop
|
|||||||
|
|
||||||
| Parameter | Notes |
|
| Parameter | Notes |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `-ConfigPath <string>` | Required. Validated through all four layers before any connection. |
|
| `-ConfigPath <string>` | Defaults to `./config/persona-engine.json`. Validated through all four layers before any connection. |
|
||||||
| `-WhatIf` | **The approved no-write control.** |
|
| `-WhatIf` | **The approved no-write control.** |
|
||||||
| `-UserObjectId <GUID>` | Single-user execution. |
|
| `-UserObjectId <GUID>` | Single-user execution. |
|
||||||
| `-OutputPath <string>` | Overrides `logging.path`. Both default to `<current-directory>/logs/persona-engine-audit.ndjson`. |
|
| `-OutputPath <string>` | Overrides `logging.path`. Both default to `<current-directory>/logs/persona-engine-audit.ndjson`. |
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"logging": {
|
"logging": {
|
||||||
"destination": "both",
|
"destination": "both",
|
||||||
|
"resultsFileName": "results.csv",
|
||||||
"traceConditionValues": false
|
"traceConditionValues": false
|
||||||
},
|
},
|
||||||
"personas": [
|
"personas": [
|
||||||
|
|||||||
@@ -105,6 +105,12 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "NDJSON output file for 'file'/'both' destinations. Defaults to <current-directory>/logs/persona-engine-audit.ndjson when unset."
|
"description": "NDJSON output file for 'file'/'both' destinations. Defaults to <current-directory>/logs/persona-engine-audit.ndjson when unset."
|
||||||
},
|
},
|
||||||
|
"resultsFileName": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"default": "results.csv",
|
||||||
|
"description": "Per-account results CSV (AccountObjectId, UserPrincipalName, persona/status), written alongside the audit log and overwritten on every summary."
|
||||||
|
},
|
||||||
"traceConditionValues": {
|
"traceConditionValues": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ property-only rules.
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `destination` | no | `both` | `file`, `stream`, `both`, or `none`. `stream` writes records to the PowerShell Information stream. |
|
| `destination` | no | `both` | `file`, `stream`, `both`, or `none`. `stream` writes records to the PowerShell Information stream. |
|
||||||
| `path` | no | `<current-directory>/logs/persona-engine-audit.ndjson` | NDJSON output file. One record per line. |
|
| `path` | no | `<current-directory>/logs/persona-engine-audit.ndjson` | NDJSON output file. One record per line. |
|
||||||
|
| `resultsFileName` | no | `results.csv` | Per-account results CSV, written next to `path`'s directory. Lists `AccountObjectId`, `UserPrincipalName`, `PersonaStatus` for every account processed so far. Overwritten on every summary, interim and final. |
|
||||||
| `traceConditionValues` | no | `false` | Writes evaluated attribute values into audit records. |
|
| `traceConditionValues` | no | `false` | Writes evaluated attribute values into audit records. |
|
||||||
| `acknowledgeConditionTracing` | no | `false` | **Required whenever `traceConditionValues` is true** (VR-003). |
|
| `acknowledgeConditionTracing` | no | `false` | **Required whenever `traceConditionValues` is true** (VR-003). |
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,19 @@ warning, **once** per run, and processing continues.
|
|||||||
Once, not once per user: a run over five thousand accounts with a locked log file should warn once,
|
Once, not once per user: a run over five thousand accounts with a locked log file should warn once,
|
||||||
or the warning that matters is buried in the noise it generates.
|
or the warning that matters is buried in the noise it generates.
|
||||||
|
|
||||||
|
## Results CSV
|
||||||
|
|
||||||
|
Alongside the NDJSON audit log, every summary — interim and final — (re)writes a plain CSV listing
|
||||||
|
every account processed so far: `AccountObjectId`, `UserPrincipalName`, `PersonaStatus` (the assigned
|
||||||
|
persona for `Matched` accounts, otherwise `Unclassified` or `EvaluationError`). It is overwritten in
|
||||||
|
full each time, not appended, so it always reflects the whole run to that point rather than only the
|
||||||
|
accounts since the last summary.
|
||||||
|
|
||||||
|
It is written next to the audit log — same directory as `logging.path` — under `logging.resultsFileName`
|
||||||
|
(default `results.csv`). Export failure never ends a run, for the same reason a sink failure doesn't:
|
||||||
|
a locked file or full disk is an operational problem, not a reason to abandon a classification run
|
||||||
|
mid-population.
|
||||||
|
|
||||||
## Record types
|
## Record types
|
||||||
|
|
||||||
| Type | When | Carries |
|
| Type | When | Carries |
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ the header line and `PE-SAF-001`.
|
|||||||
A summary appears every `summaryInterval` accounts and once at the end, listing **every** rule
|
A summary appears every `summaryInterval` accounts and once at the end, listing **every** rule
|
||||||
including disabled and zero-match ones. A rule that never fired and a rule that is not in the
|
including disabled and zero-match ones. A rule that never fired and a rule that is not in the
|
||||||
configuration look identical if zero-match rules are omitted, and that distinction is usually what
|
configuration look identical if zero-match rules are omitted, and that distinction is usually what
|
||||||
you are looking for.
|
you are looking for. Its header shows elapsed wall-clock time since the run started.
|
||||||
|
|
||||||
|
Each summary also (re)writes `logging.resultsFileName` (default `results.csv`, next to the audit log)
|
||||||
|
with one row per account processed so far — Object ID, UPN, and assigned persona/status — for an
|
||||||
|
operator who wants the current population breakdown without parsing NDJSON.
|
||||||
|
|
||||||
## Exit codes
|
## Exit codes
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ The engine entry point. Retrieval, evaluation, reporting, and controlled persist
|
|||||||
```powershell
|
```powershell
|
||||||
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
|
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string] $ConfigPath,
|
[Parameter()][string] $ConfigPath = './config/persona-engine.json',
|
||||||
[Parameter()][guid] $UserObjectId,
|
[Parameter()][guid] $UserObjectId,
|
||||||
[Parameter()][string] $OutputPath,
|
[Parameter()][string] $OutputPath,
|
||||||
[Parameter()][guid] $CorrelationId
|
[Parameter()][guid] $CorrelationId
|
||||||
@@ -21,7 +21,7 @@ parameters and are **not** declared.
|
|||||||
|
|
||||||
| Parameter | Required | Behaviour |
|
| Parameter | Required | Behaviour |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `-ConfigPath` | Yes | Path to the JSON configuration. Validated through all four layers before any connection is made (FR-002). |
|
| `-ConfigPath` | No | Path to the JSON configuration. Defaults to `./config/persona-engine.json`, resolved against the current directory, when omitted. Validated through all four layers before any connection is made (FR-002). |
|
||||||
| `-WhatIf` | No | **The approved no-write control.** Reads, evaluation, comparison, console output, summaries, and audit records all behave identically to enforcement; zero write requests are issued (FR-017, SC-004). |
|
| `-WhatIf` | No | **The approved no-write control.** Reads, evaluation, comparison, console output, summaries, and audit records all behave identically to enforcement; zero write requests are issued (FR-017, SC-004). |
|
||||||
| `-UserObjectId` | No | Single-user execution for validation. Skips enumeration; retrieves one user. |
|
| `-UserObjectId` | No | Single-user execution for validation. Skips enumeration; retrieves one user. |
|
||||||
| `-OutputPath` | No | Overrides the configured audit output path where permitted. |
|
| `-OutputPath` | No | Overrides the configured audit output path where permitted. |
|
||||||
@@ -56,9 +56,12 @@ Failing any of these yields `Unchanged`, `WouldUpdate`, or `Skipped` — never a
|
|||||||
- **Per user, immediately after evaluation** (FR-018, SC-012): one console line carrying UPN,
|
- **Per user, immediately after evaluation** (FR-018, SC-012): one console line carrying UPN,
|
||||||
Account Object ID, outcome, matched rule ID, stored value, calculated value, and action.
|
Account Object ID, outcome, matched rule ID, stored value, calculated value, and action.
|
||||||
- **Every `summaryInterval` users** (FR-019): a table of all business rules with match counts, plus
|
- **Every `summaryInterval` users** (FR-019): a table of all business rules with match counts, plus
|
||||||
outcome totals and a reconciliation check.
|
outcome totals, elapsed wall-clock time since the run started, and a reconciliation check.
|
||||||
- **At completion**: a final summary regardless of interval, including when the interval is `0`
|
- **At completion**: a final summary regardless of interval, including when the interval is `0`
|
||||||
(FR-020).
|
(FR-020).
|
||||||
|
- **Every summary, interim and final**: `logging.resultsFileName` (default `results.csv`, written
|
||||||
|
next to the audit log) is overwritten with one row per account processed so far — Account Object
|
||||||
|
ID, UPN, and the assigned persona or outcome status.
|
||||||
- **Reconciliation** at every summary: `Processed = Matched + Unclassified + EvaluationError`
|
- **Reconciliation** at every summary: `Processed = Matched + Unclassified + EvaluationError`
|
||||||
(FR-021). A mismatch is logged as an engine defect, at `Error` severity.
|
(FR-021). A mismatch is logged as an engine defect, at `Error` severity.
|
||||||
- **Audit records**: see [audit-record.md](audit-record.md).
|
- **Audit records**: see [audit-record.md](audit-record.md).
|
||||||
|
|||||||
@@ -105,6 +105,12 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "NDJSON output file for 'file'/'both' destinations. Defaults to <current-directory>/logs/persona-engine-audit.ndjson when unset."
|
"description": "NDJSON output file for 'file'/'both' destinations. Defaults to <current-directory>/logs/persona-engine-audit.ndjson when unset."
|
||||||
},
|
},
|
||||||
|
"resultsFileName": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"default": "results.csv",
|
||||||
|
"description": "Per-account results CSV (AccountObjectId, UserPrincipalName, persona/status), written alongside the audit log and overwritten on every summary."
|
||||||
|
},
|
||||||
"traceConditionValues": {
|
"traceConditionValues": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ function Invoke-PersonaEngineRun {
|
|||||||
.PARAMETER Tracing
|
.PARAMETER Tracing
|
||||||
Include ConditionTrace on results and audit records.
|
Include ConditionTrace on results and audit records.
|
||||||
|
|
||||||
|
.PARAMETER StartedUtc
|
||||||
|
Run start timestamp, threaded down to Write-PersonaSummary so each summary
|
||||||
|
can show elapsed wall-clock time. Optional; a caller that omits it just gets
|
||||||
|
summaries without an elapsed figure.
|
||||||
|
|
||||||
|
.PARAMETER ResultsPath
|
||||||
|
Destination for the per-account results CSV. When supplied, it is
|
||||||
|
(re)written after every summary - interim and final. Optional; a caller
|
||||||
|
that omits it skips the CSV export entirely.
|
||||||
|
|
||||||
.OUTPUTS
|
.OUTPUTS
|
||||||
PersonaEngine.RunOutcome carrying the counters and the exit code.
|
PersonaEngine.RunOutcome carrying the counters and the exit code.
|
||||||
#>
|
#>
|
||||||
@@ -74,7 +84,11 @@ function Invoke-PersonaEngineRun {
|
|||||||
|
|
||||||
[string] $UserObjectId,
|
[string] $UserObjectId,
|
||||||
|
|
||||||
[switch] $Tracing
|
[switch] $Tracing,
|
||||||
|
|
||||||
|
[Nullable[datetime]] $StartedUtc,
|
||||||
|
|
||||||
|
[string] $ResultsPath
|
||||||
)
|
)
|
||||||
|
|
||||||
$EXIT_OK = 0
|
$EXIT_OK = 0
|
||||||
@@ -190,7 +204,9 @@ function Invoke-PersonaEngineRun {
|
|||||||
|
|
||||||
if ($Configuration.SummaryInterval -gt 0 -and ($counters.Processed % $Configuration.SummaryInterval) -eq 0) {
|
if ($Configuration.SummaryInterval -gt 0 -and ($counters.Processed % $Configuration.SummaryInterval) -eq 0) {
|
||||||
|
|
||||||
Write-PersonaSummary -Counters $counters -SummaryType 'Interim' -Mode $Context.Mode
|
Write-PersonaSummary -Counters $counters -SummaryType 'Interim' -Mode $Context.Mode -StartedUtc $StartedUtc
|
||||||
|
|
||||||
|
if ($ResultsPath) { Export-PersonaResultsCsv -Counters $counters -Path $ResultsPath }
|
||||||
|
|
||||||
New-PersonaAuditRecord -Context $Context -RecordType 'Summary' -Counters $counters `
|
New-PersonaAuditRecord -Context $Context -RecordType 'Summary' -Counters $counters `
|
||||||
-Properties @{ summaryType = 'Interim' } | Write-PersonaAuditRecord @AuditParameters
|
-Properties @{ summaryType = 'Interim' } | Write-PersonaAuditRecord @AuditParameters
|
||||||
@@ -206,7 +222,9 @@ function Invoke-PersonaEngineRun {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Always emitted, whatever the interval - including 0 (FR-020).
|
# Always emitted, whatever the interval - including 0 (FR-020).
|
||||||
Write-PersonaSummary -Counters $counters -SummaryType 'Final' -Mode $Context.Mode
|
Write-PersonaSummary -Counters $counters -SummaryType 'Final' -Mode $Context.Mode -StartedUtc $StartedUtc
|
||||||
|
|
||||||
|
if ($ResultsPath) { Export-PersonaResultsCsv -Counters $counters -Path $ResultsPath }
|
||||||
|
|
||||||
New-PersonaAuditRecord -Context $Context -RecordType 'Summary' -Counters $counters `
|
New-PersonaAuditRecord -Context $Context -RecordType 'Summary' -Counters $counters `
|
||||||
-Properties @{ summaryType = 'Final' } | Write-PersonaAuditRecord @AuditParameters
|
-Properties @{ summaryType = 'Final' } | Write-PersonaAuditRecord @AuditParameters
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
function Export-PersonaResultsCsv {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Writes the per-account results CSV (object ID, UPN, persona/status).
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Called after every summary, interim and final, and overwrites the file each
|
||||||
|
time. Counters.Results accumulates for the whole run, so the file on disk
|
||||||
|
always lists every account processed so far, not just those since the last
|
||||||
|
summary.
|
||||||
|
|
||||||
|
Export failure never ends the run, matching Write-PersonaAuditRecord's
|
||||||
|
failure handling for the same reason: a locked file or a full disk is an
|
||||||
|
operational problem with the export, not a reason to abandon a
|
||||||
|
classification run mid-population.
|
||||||
|
|
||||||
|
.PARAMETER Counters
|
||||||
|
The run counter set.
|
||||||
|
|
||||||
|
.PARAMETER Path
|
||||||
|
Destination CSV file. The parent directory is created if it does not exist.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[object] $Counters,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string] $Path
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Counters.Results.Count -eq 0) { return }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$directory = Split-Path -Parent $Path
|
||||||
|
if ($directory -and -not (Test-Path -LiteralPath $directory)) {
|
||||||
|
$null = New-Item -ItemType Directory -Path $directory -Force -WhatIf:$false -Confirm:$false
|
||||||
|
}
|
||||||
|
|
||||||
|
# -WhatIf:$false / -Confirm:$false pin this write regardless of any ambient
|
||||||
|
# $WhatIfPreference in the caller's session, the same reason
|
||||||
|
# Write-PersonaAuditRecord pins its own sink writes: this is a report, not a
|
||||||
|
# directory mutation, and must not silently no-op under -WhatIf.
|
||||||
|
$Counters.Results | Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding utf8NoBOM -Force -WhatIf:$false -Confirm:$false
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "Results CSV export failed; the run continues without it: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,11 @@ function New-PersonaRunCounter {
|
|||||||
makes a zero-match rule distinguishable from an absent one - an operator
|
makes a zero-match rule distinguishable from an absent one - an operator
|
||||||
asking "did RULE-0030 fire?" gets "no, zero matches" rather than silence.
|
asking "did RULE-0030 fire?" gets "no, zero matches" rather than silence.
|
||||||
|
|
||||||
|
Results accumulates one row per processed account (AccountObjectId,
|
||||||
|
UserPrincipalName, persona/status) for Export-PersonaResultsCsv. It grows
|
||||||
|
for the life of the run, not just since the last summary, so the CSV a
|
||||||
|
summary writes always reflects every account processed so far.
|
||||||
|
|
||||||
.PARAMETER Rules
|
.PARAMETER Rules
|
||||||
The business rule collection, used to seed RuleCounts.
|
The business rule collection, used to seed RuleCounts.
|
||||||
|
|
||||||
@@ -67,6 +72,7 @@ function New-PersonaRunCounter {
|
|||||||
Skipped = 0
|
Skipped = 0
|
||||||
|
|
||||||
RuleCounts = $ruleCounts
|
RuleCounts = $ruleCounts
|
||||||
|
Results = [System.Collections.Generic.List[object]]::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,4 +123,12 @@ function Add-PersonaRunResult {
|
|||||||
'UpdateFailed' { $Counters.UpdateFailed++ }
|
'UpdateFailed' { $Counters.UpdateFailed++ }
|
||||||
'Skipped' { $Counters.Skipped++ }
|
'Skipped' { $Counters.Skipped++ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Matched carries the assigned persona; Unclassified/EvaluationError carry
|
||||||
|
# their outcome name, since CalculatedPersona is 'Unclassified' or $null there.
|
||||||
|
$Counters.Results.Add([pscustomobject]@{
|
||||||
|
AccountObjectId = [string]$Result.AccountObjectId
|
||||||
|
UserPrincipalName = [string]$Result.UserPrincipalName
|
||||||
|
PersonaStatus = [string]$Result.Outcome -eq 'Matched' ? [string]$Result.CalculatedPersona : [string]$Result.Outcome
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ function Write-PersonaSummary {
|
|||||||
.PARAMETER Mode
|
.PARAMETER Mode
|
||||||
Preview or Enforce, shown in the header so a screenshot of a summary is
|
Preview or Enforce, shown in the header so a screenshot of a summary is
|
||||||
self-describing.
|
self-describing.
|
||||||
|
|
||||||
|
.PARAMETER StartedUtc
|
||||||
|
Run start timestamp. When supplied, the header shows elapsed wall-clock
|
||||||
|
time since the run began. Optional so callers that only care about counts
|
||||||
|
are not forced to thread a clock through.
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
@@ -37,14 +42,25 @@ function Write-PersonaSummary {
|
|||||||
[string] $SummaryType = 'Interim',
|
[string] $SummaryType = 'Interim',
|
||||||
|
|
||||||
[ValidateSet('Preview', 'Enforce')]
|
[ValidateSet('Preview', 'Enforce')]
|
||||||
[string] $Mode = 'Preview'
|
[string] $Mode = 'Preview',
|
||||||
|
|
||||||
|
[Nullable[datetime]] $StartedUtc
|
||||||
)
|
)
|
||||||
|
|
||||||
$reconciled = Test-PersonaReconciliation -Counters $Counters
|
$reconciled = Test-PersonaReconciliation -Counters $Counters
|
||||||
|
|
||||||
|
# $StartedUtc arrives here already unwrapped to a plain [datetime] - PowerShell
|
||||||
|
# collapses [Nullable[datetime]] to DateTime (or $null) at the call boundary, so
|
||||||
|
# a null check is used rather than .Value / .HasValue.
|
||||||
|
$elapsed = ($null -ne $StartedUtc) ? ('{0:hh\:mm\:ss}' -f ([DateTime]::UtcNow - $StartedUtc)) : $null
|
||||||
|
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Host ('=' * 100) -ForegroundColor DarkGray
|
Write-Host ('=' * 100) -ForegroundColor DarkGray
|
||||||
Write-Host ("{0} summary - mode: {1} - processed: {2}" -f $SummaryType, $Mode, $Counters.Processed) -ForegroundColor Cyan
|
Write-Host (
|
||||||
|
$elapsed `
|
||||||
|
? ("{0} summary - mode: {1} - elapsed: {2} - processed: {3}" -f $SummaryType, $Mode, $elapsed, $Counters.Processed) `
|
||||||
|
: ("{0} summary - mode: {1} - processed: {2}" -f $SummaryType, $Mode, $Counters.Processed)
|
||||||
|
) -ForegroundColor Cyan
|
||||||
Write-Host ('=' * 100) -ForegroundColor DarkGray
|
Write-Host ('=' * 100) -ForegroundColor DarkGray
|
||||||
|
|
||||||
Write-Host ('{0,-28} {1,-40} {2,-9} {3,10} {4,8}' -f 'Rule ID', 'Name', 'Priority', 'Enabled', 'Matches') -ForegroundColor DarkGray
|
Write-Host ('{0,-28} {1,-40} {2,-9} {3,10} {4,8}' -f 'Rule ID', 'Name', 'Priority', 'Enabled', 'Matches') -ForegroundColor DarkGray
|
||||||
|
|||||||
Reference in New Issue
Block a user