# Dedicated DNS analytical lifecycle. Ordinary channel setters remain restricted. function Get-WelaDnsAnalyticalSources { foreach($path in @('config/native_provider_packs.json','config/security_rules.json','scripts/NativeProviderPacks.ps1','scripts/Configuration.ps1','scripts/ControlApplicability.ps1','modules/AuditProfiles.psm1','modules/NativeProviders.psm1','scripts/DnsAnalytical.ps1','scripts/DnsAnalyticalArchive.cs')) { [pscustomobject]@{Path=$path;Sha256=(Get-FileHash -LiteralPath (Join-Path $PSScriptRoot "../$path") -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()} } foreach($file in @(Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot '../config/provider_rule_sources') -Filter '*.yml' -File | Sort-Object Name)){ [pscustomobject]@{Path=('config/provider_rule_sources/'+$file.Name);Sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()} } } function Get-WelaDnsAnalyticalDefinition { $sources=@(Get-WelaDnsAnalyticalSources) $catalog=Get-WelaProviderPackCatalog $pack=@($catalog.packs|Where-Object id -ceq 'dns-server-analytical') if($pack.Count -ne 1 -or $pack[0].provider -cne 'Microsoft-Windows-DNSServer' -or $pack[0].channel -cne 'Microsoft-Windows-DNSServer/Analytical' -or $pack[0].requiredService -cne 'DNS'){throw 'Reviewed DNS analytical catalog identity differs.'} $definition=[pscustomobject]@{Id='dns-server-analytical';CatalogId=$catalog.id;Pack=$pack[0];Builds=$catalog.buildFamilies.Server;Sources=$sources;RuleCommit=$catalog.ruleCommit;Rules=@($catalog.ruleReviews|Where-Object {$pack[0].ruleIds -contains $_.id})} Assert-WelaDnsAnalyticalSources $definition $definition } function Assert-WelaDnsAnalyticalSources { param($Definition) $actual=@(Get-WelaDnsAnalyticalSources) if($actual.Count -ne $Definition.Sources.Count){throw 'DNS source inventory changed.'} for($i=0;$i -lt $actual.Count;$i++){if($actual[$i].Path -cne $Definition.Sources[$i].Path -or $actual[$i].Sha256 -cne $Definition.Sources[$i].Sha256){throw 'Reviewed DNS sources changed; no further writes permitted.'}} } function Get-WelaDnsAnalyticalContext { $role=Get-WelaHostContext;$detail=Get-WelaDefaultContext if(-not(Test-WelaDefaultContextComplete $detail) -or $role.Build -ne $detail.Build){throw 'Complete consistent actual Windows context is required.'} $valid=switch($role.Role){MemberServer {$detail.ProductType -eq 3 -and $detail.DomainRole -in @(2,3)} DomainController {$detail.ProductType -eq 2 -and $detail.DomainRole -in @(4,5)} ADCS {$detail.ProductType -eq 3 -and $detail.DomainRole -in @(2,3) -and $detail.InstalledRoles -contains 'ADCS-Cert-Authority'} default {$false}} if(-not $valid){throw 'DNS analytical configuration requires a supported actual server role.'} [pscustomobject]@{Computer=[Environment]::MachineName;Role=$role.Role;Build=$role.Build;Detail=$detail;Key=([Environment]::MachineName+'|'+$role.Role+'|'+(Get-WelaDefaultContextKey $detail))} } function Assert-WelaDnsAnalyticalCapability { param($Definition,$Context,$Service,$Schema) if($context.Build -notin $Definition.Builds -or $context.Role -notin $Definition.Pack.roles){throw 'Server build/role is outside the reviewed provider profile.'} if($service.State -ne 'Running'){throw "DNS service is not running or observable ($($service.State)); no feature/service is installed or started."} if($schema.State -ne 'Observed' -or $schema.ChannelType -ne 'Analytical' -or $schema.ProviderGuid -ine 'eb79061a-a566-4698-9119-3ed2807060e7'){throw "Exact DNS analytical provider/schema is not established. $($schema.Diagnostic)"} $events=@($schema.Events|Where-Object Id -eq 257) if(-not $events.Count){throw 'Native DNS event257 is absent from the exact analytical channel.'} foreach($event in $events){$fields=@($event.Fields|Where-Object Name -ceq 'QNAME');if($fields.Count -ne 1 -or $fields[0].InType -notin @('win:UnicodeString','win:AnsiString')){throw 'Native event257 lacks its exact QNAME string schema.'}} } function Get-WelaDnsAnalyticalState { param($Definition) if($env:OS -ne 'Windows_NT' -or -not[Environment]::Is64BitProcess){throw 'DNS analytical operations require native 64-bit Windows.'} $context=Get-WelaDnsAnalyticalContext $service=Get-WelaNativeService DNS $schema=Get-WelaProviderPackSchema $Definition.Pack Assert-WelaDnsAnalyticalCapability $Definition $context $service $schema $logs=@() try { $logs=@(Get-WinEvent -ListLog $Definition.Pack.channel -ErrorAction Stop|Where-Object LogName -ceq $Definition.Pack.channel) if($logs.Count -ne 1){throw 'Exact DNS analytical channel registration was not returned.'} $log=$logs[0] if($log.IsEnabled -isnot [bool] -or $log.MaximumSizeInBytes -lt 1048576 -or [string]$log.LogMode -notin @('Circular','Retain') -or -not $log.SecurityDescriptor -or -not $log.LogFilePath){throw 'DNS channel enable/size/retention/ACL/path metadata is unknown or unsupported.'} $path=[Environment]::ExpandEnvironmentVariables([string]$log.LogFilePath) if($path -notmatch '^[A-Za-z]:\\' -or $path -match '[*?<>|]|[ .](\\|$)' -or $path -match '%[^%]+%' -or $path.Substring(2).Contains(':') -or [IO.Path]::GetFullPath($path) -ine $path){throw 'DNS trace path must be an unambiguous canonical local drive path.'} if(([IO.DriveInfo]::new([IO.Path]::GetPathRoot($path))).DriveType -ne [IO.DriveType]::Fixed){throw 'DNS trace must reside on a local fixed drive.'} [pscustomobject]@{Context=$context;Channel=$Definition.Pack.channel;Provider=$schema.Provider;ProviderGuid=$schema.ProviderGuid;ChannelType=$schema.ChannelType;ServiceState=$service.State;Schema=$schema;IsEnabled=[bool]$log.IsEnabled;MaximumSizeInBytes=[long]$log.MaximumSizeInBytes;LogMode=[string]$log.LogMode;SecurityDescriptor=[string]$log.SecurityDescriptor;LogFilePath=$path;RegisteredLogFilePath=[string]$log.LogFilePath} } finally {foreach($log in $logs){if($log -is [IDisposable]){$log.Dispose()}}} } function Get-WelaDnsAnalyticalStateKey { param($State) $parts=@($State.Context.Key,$State.Channel,$State.Provider,$State.ProviderGuid,$State.ChannelType,$State.ServiceState,[string]$State.IsEnabled,[string]$State.MaximumSizeInBytes,$State.LogMode,$State.SecurityDescriptor,$State.LogFilePath,$State.RegisteredLogFilePath) foreach($event in @($State.Schema.Events|Sort-Object Id,Version)){$parts+=@([string]$event.Id,[string]$event.Version,$event.TemplateSha256)} ($parts|ForEach-Object {[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_))}) -join '|' } function Assert-WelaDnsAnalyticalCurrent { param($Definition,$Expected) Assert-WelaDnsAnalyticalSources $Definition $current=Get-WelaDnsAnalyticalState $Definition if((Get-WelaDnsAnalyticalStateKey $current) -cne (Get-WelaDnsAnalyticalStateKey $Expected)){throw 'DNS role/schema/channel state changed; no further transition is authorized.'} $current } function Initialize-WelaDnsTraceArchive { if($env:OS -ne 'Windows_NT'){throw 'Native trace archives require Windows.'} $path=Join-Path $PSScriptRoot 'DnsAnalyticalArchive.cs';$hash=(Get-FileHash $path -Algorithm SHA256).Hash if(-not('Wela.DnsAnalytical.TraceArchive' -as [type])){Add-Type -Path $path -ErrorAction Stop;$script:WelaDnsArchiveSourceHash=$hash} if($script:WelaDnsArchiveSourceHash -cne $hash){throw 'Loaded native archive implementation differs from its current source.'} } function Copy-WelaDnsAnalyticalTrace { param([string]$Source,[string]$Destination,[long]$MaximumBytes) Initialize-WelaDnsTraceArchive # Separate native entry point avoids PowerShell coercing a null string to empty. if($Destination){[Wela.DnsAnalytical.TraceArchive]::Read($Source,$Destination,$MaximumBytes)} else{[Wela.DnsAnalytical.TraceArchive]::Inspect($Source,$MaximumBytes)} } function Assert-WelaDnsAnalyticalArchive { param($Archive,[long]$MaximumBytes) $fresh=Copy-WelaDnsAnalyticalTrace -Source $Archive.SourcePath -MaximumBytes $MaximumBytes if($Archive.State -eq 'ObservedAbsent') {if($fresh.State -ne 'ObservedAbsent'){throw 'A trace appeared after observed absence; archive it before reset.'};return} if($Archive.State -ne 'ArchivedBytes' -or $fresh.State -ne 'ObservedBytes' -or $fresh.Identity -cne $Archive.Identity -or $fresh.Length -ne $Archive.Length -or $fresh.Sha256 -cne $Archive.Sha256){throw 'Source trace changed after its verified archive.'} $saved=Copy-WelaDnsAnalyticalTrace -Source $Archive.ArchivePath -MaximumBytes $MaximumBytes if($saved.State -ne 'ObservedBytes' -or $saved.Length -ne $Archive.Length -or $saved.Sha256 -cne $Archive.Sha256){throw 'Recovery trace archive is no longer verified.'} } function Resolve-WelaDnsAnalyticalOutput { param([string]$Path) if($Path -match '[\x00-\x1f*?<>|"\[\]]' -or $Path -match '(?