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:
@@ -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
|
||||
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
|
||||
The business rule collection, used to seed RuleCounts.
|
||||
|
||||
@@ -67,6 +72,7 @@ function New-PersonaRunCounter {
|
||||
Skipped = 0
|
||||
|
||||
RuleCounts = $ruleCounts
|
||||
Results = [System.Collections.Generic.List[object]]::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,4 +123,12 @@ function Add-PersonaRunResult {
|
||||
'UpdateFailed' { $Counters.UpdateFailed++ }
|
||||
'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
|
||||
Preview or Enforce, shown in the header so a screenshot of a summary is
|
||||
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()]
|
||||
param(
|
||||
@@ -37,14 +42,25 @@ function Write-PersonaSummary {
|
||||
[string] $SummaryType = 'Interim',
|
||||
|
||||
[ValidateSet('Preview', 'Enforce')]
|
||||
[string] $Mode = 'Preview'
|
||||
[string] $Mode = 'Preview',
|
||||
|
||||
[Nullable[datetime]] $StartedUtc
|
||||
)
|
||||
|
||||
$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 ('=' * 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 ('{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