Files
WELA/modules/AuditCatalog.psm1

73 lines
3.5 KiB
PowerShell

# Identifier validation is separate from recommendations and detection readiness.
Set-StrictMode -Version 2.0
function ConvertTo-WelaCanonicalAuditName {
param([string]$Name)
# Only reviewed spelling aliases; do not accept arbitrary fuzzy matches.
switch -CaseSensitive ($Name) {
'Non-Sensitive Privilege Use' { return 'Non Sensitive Privilege Use' }
'User / Device Claims' { return 'User/Device Claims' }
'Central Policy Staging' { return 'Central Access Policy Staging' }
default { return $Name }
}
}
function Assert-WelaAuditCatalog {
[CmdletBinding()]
param([Parameter(Mandatory)]$Catalog, [Parameter(Mandatory)]$CanonicalCatalog)
$canonical = @{}; $canonicalGuids = @{}; $seen = @{}; $ids = @{}
foreach ($row in $CanonicalCatalog) {
if (-not $row.id -or $canonical.ContainsKey($row.id) -or
$row.guid -notmatch '^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$' -or $canonicalGuids.ContainsKey($row.guid)) {
throw 'Invalid or duplicate canonical audit identifier.'
}
$canonical[$row.id] = $row.guid; $canonicalGuids[$row.guid] = $true
}
foreach ($row in @($Catalog | Where-Object { $_.currentSetting.type -eq 'auditpol' })) {
$name = ConvertTo-WelaCanonicalAuditName $row.subCategory
if (-not $row.id -or $ids.ContainsKey($row.id)) { throw "Duplicate or empty legacy audit id: $($row.id)" }
if ($row.select.type -ne 'guid' -or -not $canonical.ContainsKey($name) -or $canonical[$name] -ine $row.select.guid) {
throw "Audit catalog name/GUID mismatch: $($row.subCategory) / $($row.select.guid)"
}
if ($seen.ContainsKey($row.select.guid)) { throw "Duplicate legacy audit GUID: $($row.select.guid)" }
$seen[$row.select.guid] = $true; $ids[$row.id] = $true
}
}
function Get-WelaEventMappingReview {
[CmdletBinding()]
param(
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Mappings,
[Parameter(Mandatory)]$CanonicalCatalog,
[Parameter(Mandatory)][ValidateRange(0,65535)][int]$EventId
)
$byGuid = @{}
foreach ($row in $CanonicalCatalog) { $byGuid[$row.guid] = $row.id }
# Empty category-heading rows must never become EventID 0 through a cast.
$matches = @($Mappings | Where-Object { $_.'Event ID' -match '^\d+$' -and [int]$_.'Event ID' -eq $EventId })
$candidates = @(); $uncertain = @()
foreach ($row in $matches) {
if (-not $row.Subcategory -or -not $byGuid.ContainsKey($row.GUID)) {
$uncertain += 'CategoryOnlyOrUnknownGuid'; continue
}
if ((ConvertTo-WelaCanonicalAuditName $row.Subcategory) -cne $byGuid[$row.GUID]) {
$uncertain += 'NameGuidMismatch'; continue
}
$candidates += [pscustomobject]@{ Name=$byGuid[$row.GUID]; Guid=$row.GUID }
}
$candidates = @($candidates | Sort-Object Guid -Unique)
$reasons = @($uncertain | Select-Object -Unique)
if (-not $matches.Count) { $reasons += 'NoMapping' }
if ($candidates.Count -gt 1) { $reasons += 'MultipleSubcategories' }
# Even an unambiguous mapping does not prove outcome, object, fields or source role.
$reasons += 'OutcomeObjectAndRoleUnverified'
[pscustomobject]@{
EventId=$EventId
State=$(if (-not $matches.Count -or -not $candidates.Count) { 'Unknown' } else { 'Conditional' })
MappingCount=$matches.Count; Candidates=$candidates; Reasons=$reasons
DetectionReady=$false
}
}
Export-ModuleMember -Function Assert-WelaAuditCatalog, Get-WelaEventMappingReview