Files

201 lines
14 KiB
PowerShell

# Optional audit-only SMB policies. Requires Configuration.ps1; compatible with PowerShell 5.1.
function Get-WelaSmbAuditDefinitions {
foreach ($component in @('LanmanServer', 'LanmanWorkstation')) {
$peer = if ($component -eq 'LanmanServer') { 'Client' } else { 'Server' }
foreach ($name in @("Audit${peer}DoesNotSupportEncryption", "Audit${peer}DoesNotSupportSigning", 'AuditInsecureGuestLogon')) {
[pscustomobject]@{ Component = $component; Name = $name; Path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\$component"; Admx = "$component.admx"; PolicyName = "Pol_$name"; DesiredValue = 1; DesiredType = 'DWord' }
}
}
}
function Get-WelaSmbAuditHost {
try {
if (-not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell to read and write the native policy registry view.' }
$os = Get-CimInstance -ClassName Win32_OperatingSystem -Property ProductType, BuildNumber, Version, Caption -ErrorAction Stop
if (-not $os -or [string]$os.BuildNumber -notmatch '^\d+$' -or $os.ProductType -notin @(1, 2, 3)) { throw 'OS build or product type is unknown.' }
$build = [int]$os.BuildNumber
$state = if ($build -lt 26100) { 'NotApplicable' }
elseif (($os.ProductType -eq 1 -and $build -in @(26100, 26200)) -or ($os.ProductType -in @(2, 3) -and $build -eq 26100)) { 'Candidate' }
else { 'Unknown' }
[pscustomobject]@{ Status = $state; Build = $build; ProductType = [int]$os.ProductType; Caption = [string]$os.Caption; Version = [string]$os.Version; Diagnostic = $(if ($state -eq 'NotApplicable') { 'These six audit switches require Windows 11 24H2/25H2 or Server 2025; older releases, including Server 2022, are not configured.' } elseif ($state -eq 'Unknown') { 'This OS build has not been reviewed; no policies will be created.' } else { 'Build is eligible; each local ADMX mapping is checked separately.' }) }
} catch { [pscustomobject]@{ Status = 'Unknown'; Build = $null; ProductType = $null; Caption = $null; Version = $null; Diagnostic = $_.Exception.Message } }
}
function Read-WelaSmbAuditAdmx {
param([string]$Path)
$settings = New-Object Xml.XmlReaderSettings
$settings.DtdProcessing = [Xml.DtdProcessing]::Prohibit
$settings.XmlResolver = $null
$reader = [Xml.XmlReader]::Create($Path, $settings)
try {
$document = New-Object Xml.XmlDocument
$document.XmlResolver = $null
$document.Load($reader)
return $document
} finally { $reader.Dispose() }
}
function Get-WelaSmbAuditCapability {
param($Definition, $HostState)
$path = Join-Path (Join-Path $env:windir 'PolicyDefinitions') $Definition.Admx
$result = [pscustomobject]@{ Status = $HostState.Status; Host = $HostState; AdmxPath = $path; AdmxSha256 = $null; SupportedOn = $null; Diagnostic = $HostState.Diagnostic }
if ($HostState.Status -ne 'Candidate') { return $result }
try {
if (-not (Test-Path -LiteralPath $path -PathType Leaf -ErrorAction Stop)) { throw "Local policy definition is missing: $path" }
$document = Read-WelaSmbAuditAdmx -Path $path
$policies = @($document.SelectNodes("//*[local-name()='policy']") | Where-Object {
$_.GetAttribute('name') -eq $Definition.PolicyName -and $_.GetAttribute('class') -eq 'Machine' -and
$_.GetAttribute('key') -eq ($Definition.Path -replace '^HKLM:\\', '') -and $_.GetAttribute('valueName') -eq $Definition.Name
})
if ($policies.Count -ne 1) { throw 'Exact machine ADMX policy/key/value mapping is missing or ambiguous.' }
$enabled = $policies[0].SelectSingleNode("./*[local-name()='enabledValue']/*[local-name()='decimal']")
if (-not $enabled -or $enabled.GetAttribute('value') -ne '1') { throw 'ADMX does not define the requested enabled DWORD value 1.' }
$supported = $policies[0].SelectSingleNode("./*[local-name()='supportedOn']")
if ($supported) { $result.SupportedOn = $supported.GetAttribute('ref') }
$result.AdmxSha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash
$result.Status = 'Supported'
$result.Diagnostic = 'Reviewed host build and exact local machine ADMX mapping found. Runtime observation and event validation are separate.'
} catch { $result.Status = 'Unknown'; $result.Diagnostic = $_.Exception.Message }
return $result
}
function Get-WelaSmbAuditRuntime {
param($Definition)
$command = if ($Definition.Component -eq 'LanmanServer') { 'Get-SmbServerConfiguration' } else { 'Get-SmbClientConfiguration' }
$result = [pscustomobject]@{ Command = $command; Property = $Definition.Name; Status = 'NotExposed'; Value = $null; Diagnostic = '' }
try {
if (-not (Get-Command -Name $command -ErrorAction SilentlyContinue)) {
$result.Diagnostic = 'Runtime cmdlet is unavailable; registry-only verification cannot establish effective auditing.'
return $result
}
$configuration = & $command -ErrorAction Stop
if (-not $configuration) { throw 'Runtime cmdlet returned no configuration.' }
$property = $configuration.PSObject.Properties[$Definition.Name]
if ($null -eq $property) {
$result.Diagnostic = 'This runtime object does not expose the audit property; registry-only verification cannot establish effective auditing.'
return $result
}
if ($property.Value -isnot [bool]) { throw 'Runtime audit property is not a Boolean.' }
$result.Status = 'Observed'; $result.Value = $property.Value
$result.Diagnostic = if ($property.Value) {
'Runtime audit Boolean is True; generated or collected events have not been verified.'
} else {
'Runtime audit Boolean is False; enabled auditing is not currently observed. The cause and activation timing are unknown; a policy refresh or restart is not assumed to resolve this.'
}
} catch { $result.Status = 'Unknown'; $result.Diagnostic = $_.Exception.Message }
return $result
}
function Get-WelaSmbAuditState {
param($Definition)
$capability = Get-WelaSmbAuditCapability -Definition $Definition -HostState (Get-WelaSmbAuditHost)
$policy = $null; $runtime = $null
if ($capability.Status -eq 'Supported') {
$policy = Get-WelaRegistryState -Path $Definition.Path -Name $Definition.Name
$runtime = Get-WelaSmbAuditRuntime -Definition $Definition
}
$policyConfigured = $capability.Status -eq 'Supported' -and $policy.ValueExists -and $policy.Type -eq 'DWord' -and $policy.Value -eq 1
$runtimeState = if ($capability.Status -eq 'NotApplicable') { 'NotApplicable' }
elseif ($runtime -and $runtime.Status -eq 'Observed' -and $runtime.Value) { 'Active' }
elseif ($runtime -and $runtime.Status -eq 'Observed' -and $policyConfigured) { 'PendingVerification' }
elseif ($runtime -and $runtime.Status -eq 'Observed') { 'NotActive' }
else { 'Unknown' }
[pscustomobject]@{
Capability = $capability; Policy = $policy; Runtime = $runtime
PolicyRegistryConfigured = [bool]$policyConfigured; RuntimeState = $runtimeState
VerificationScope = $(if ($runtimeState -eq 'Active') { 'Policy registry and runtime audit flag observed separately; event generation not established' }
elseif ($runtimeState -eq 'PendingVerification') { 'Policy registry configured; runtime verification pending (observed False)' }
else { 'Policy registry only; effective auditing not established' })
}
}
function Test-WelaSmbAuditCompliance {
param($Snapshot)
# The mutation requests a policy DWORD, not synchronous runtime activation.
# Runtime evidence stays separate; read errors still fail in the read callback.
return $Snapshot.Capability.Status -eq 'Supported' -and $Snapshot.Policy.ValueExists -and
$Snapshot.Policy.Type -eq 'DWord' -and $Snapshot.Policy.Value -eq 1
}
function Get-WelaSmbAuditPlan {
foreach ($definition in Get-WelaSmbAuditDefinitions) {
$state = $null
try {
$state = Get-WelaSmbAuditState -Definition $definition
$status = if ($state.Capability.Status -ne 'Supported') { $state.Capability.Status }
elseif ($state.Runtime.Status -eq 'Unknown') { 'Unknown' }
elseif (Test-WelaSmbAuditCompliance $state) { 'PolicyConfigured' } else { 'ChangeRequired' }
$diagnostic = if ($state.Capability.Status -ne 'Supported') { $state.Capability.Diagnostic } else { $state.Runtime.Diagnostic }
[pscustomobject]@{ Definition = $definition; Status = $status; Before = $state; Diagnostic = $diagnostic }
} catch { [pscustomobject]@{ Definition = $definition; Status = 'Unknown'; Before = $state; Diagnostic = $_.Exception.Message } }
}
}
function Set-WelaSmbAuditControls {
param($Context, [array]$Plan)
foreach ($entry in $Plan) {
$definition = $entry.Definition
$id = "SmbAudit/$($definition.Component)/$($definition.Name)"
if ($entry.Status -in @('NotApplicable', 'Unknown')) {
$Context.Results.Add([pscustomobject]@{ Id = $id; Kind = 'SmbAudit'; Target = @{ Path = $definition.Path; Name = $definition.Name }; Desired = @{ Value = 1; Type = 'DWord' }; Before = $entry.Before; After = $entry.Before; Status = $(if ($entry.Status -eq 'NotApplicable') { 'Skipped' } else { 'Failed' }); Diagnostic = "$($entry.Status): $($entry.Diagnostic)" })
continue
}
$callback = @{ Definition = $definition; Observed = $null }
$read = {
param($state)
$snapshot = Get-WelaSmbAuditState -Definition $state.Definition
if ($snapshot.Capability.Status -ne 'Supported') { throw "SMB policy capability changed: $($snapshot.Capability.Diagnostic)" }
if ($snapshot.Runtime.Status -eq 'Unknown') { throw "Runtime observation failed: $($snapshot.Runtime.Diagnostic)" }
$state.Observed = $snapshot
return $snapshot
}
$test = { param($snapshot) Test-WelaSmbAuditCompliance $snapshot }
$apply = {
param($state)
$fresh = Get-WelaSmbAuditState -Definition $state.Definition
if ($fresh.Capability.Status -ne 'Supported' -or $fresh.Runtime.Status -eq 'Unknown') { throw 'Capability/runtime could no longer be read; no policy was written.' }
foreach ($field in @('KeyExists', 'ValueExists', 'Value', 'Type')) {
if ($fresh.Policy.$field -ne $state.Observed.Policy.$field) { throw 'Policy changed after the recovery snapshot; review policy and retry.' }
}
New-WelaRegistryKey -Path $state.Definition.Path
Set-ItemProperty -LiteralPath $state.Definition.Path -Name $state.Definition.Name -Type DWord -Value 1 -ErrorAction Stop
'Audit policy DWORD written. Runtime activation is observed separately; its cause/timing, event generation/collection and policy persistence remain unverified.'
}
Invoke-WelaConfigurationControl -Context $Context -Id $id -Kind SmbAudit -Target @{ Path = $definition.Path; Name = $definition.Name } `
-Desired @{ Value = 1; Type = 'DWord' } -Read $read -Compliant $test -Apply $apply -CallbackState $callback `
-Description 'Set this supported SMB audit policy to DWORD 1 without changing security requirements.'
}
}
function Invoke-WelaSmbAuditCommand {
param([ValidateSet('Audit', 'Plan', 'Configure')][string]$Action = 'Audit', [switch]$Auto, [switch]$DryRun, [string]$BackupPath, [string]$ResultsPath)
if ($env:OS -ne 'Windows_NT') { throw 'SMB auditing requires Windows.' }
if ($DryRun -and $Action -ne 'Configure') { throw '-DryRun applies only to SmbAction Configure; Audit and Plan are read-only.' }
$plan = @(Get-WelaSmbAuditPlan)
if ($Action -eq 'Configure') {
$context = New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
Set-WelaSmbAuditControls -Context $context -Plan $plan
$report = Complete-WelaConfiguration -Context $context -Scope 'smb-audit-policies-only' `
-SuccessMessage 'SMB policy registry values verified. Runtime activation and event generation are reported separately.'
$runtimeSummary = [ordered]@{ Active = 0; PendingVerification = 0; NotActive = 0; Unknown = 0; NotApplicable = 0 }
foreach ($row in $report.Results) {
# A failed final read can leave an earlier snapshot in After. Do not
# promote that stale observation to a successful runtime summary.
$snapshot = if ($row.Status -eq 'Failed') { $null } elseif ($row.After) { $row.After } else { $row.Before }
$runtimeState = if ($snapshot -and $snapshot.RuntimeState) { $snapshot.RuntimeState } else { 'Unknown' }
$runtimeSummary[$runtimeState]++
}
$report | Add-Member NoteProperty VerificationScope 'Policy registry write/read-back verification; runtime activation and event generation are separate observations.'
$report | Add-Member NoteProperty RuntimeVerification ([pscustomobject]$runtimeSummary)
Write-Host "SMB runtime observations: $($runtimeSummary.Active) active, $($runtimeSummary.PendingVerification) pending verification, $($runtimeSummary.NotActive) not active, $($runtimeSummary.Unknown) unknown, $($runtimeSummary.NotApplicable) not applicable. Pending means observed False despite policy DWORD 1; the cause and activation timing are unknown. No refresh or restart was performed." -ForegroundColor Yellow
if ($ResultsPath) {
try { $report | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
catch { $report.ExitCode = 1; Write-Host "[Failed] Writing SMB results: $_" -ForegroundColor Red }
}
return $report
}
$report = [pscustomobject]@{ Scope = 'smb-audit-policies-only'; Action = $Action; Controls = $plan; ExitCode = $(if (@($plan | Where-Object Status -eq Unknown).Count) { 1 } else { 0 }) }
if ($ResultsPath) { $report | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
return $report
}