mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-01 20:14:43 +02:00
201 lines
14 KiB
PowerShell
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
|
|
}
|