diff --git a/.github/workflows/smb-auditing.yml b/.github/workflows/smb-auditing.yml new file mode 100644 index 00000000..6077684a --- /dev/null +++ b/.github/workflows/smb-auditing.yml @@ -0,0 +1,31 @@ +name: SMB audit policy regressions +on: + push: + branches: ['**'] + paths: + - 'WELA.ps1' + - 'scripts/Configuration.ps1' + - 'scripts/SmbAuditing.ps1' + - 'tests/SmbAuditing*' + - '.github/workflows/smb-auditing.yml' + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + smb-auditing: + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Mocked policy/runtime and real ADMX parsing in Windows PowerShell 5.1 + shell: powershell + run: ./tests/SmbAuditing.Tests.ps1 + - name: Actual Windows read-only smoke in Windows PowerShell 5.1 + shell: powershell + run: ./tests/SmbAuditing.Windows.Tests.ps1 + - name: Mocked policy/runtime and real ADMX parsing in PowerShell 7 + shell: pwsh + run: ./tests/SmbAuditing.Tests.ps1 + - name: Actual Windows read-only smoke in PowerShell 7 + shell: pwsh + run: ./tests/SmbAuditing.Windows.Tests.ps1 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index b2f7d539..c79f4a86 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -7,6 +7,7 @@ - ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (#394) (@Shirofune-Security) - イベントログのサイズ監査と設定に共通のバイト単位プロファイルを導入し、AppLocker・ファイアウォールログの256 MiB、Setupの32 MiB、ASD推奨のSecurityログ2048 MiBに対応した。`-LogProfile`と`configure-eventlogs`で送信元と収集サーバーのサイズ・保存方式を選択できる。明示的に指定しない限り、既存の大きいバッファと保存方式は維持する。結果には検証した設定を記録し、未測定の保存日数は不明と表示する。 (#396) (@Shirofune-Security) +- 6つのネイティブSMB監査ポリシーを監査・計画・設定する任意実行の`smb-auditing`を追加しました。OSビルドとローカルADMXの正確な定義を確認してから書き込み、ポリシーのDWORD値と取得可能な実行時設定を分けて表示します。Dry-run、復旧用記録、ポリシーレジストリの検証に対応します。実行時設定は有効・検証待ち・不明を区別し、Falseが観測されてもレジストリへの書き込み成功を失敗とは扱いません。署名・暗号化要件やゲストアクセスは変更しません。実イベント生成と収集の検証は別途必要です。 (#397) (@Shirofune-Security) - `audit-settings`、`plan`、`configure`で共有するバージョン付きの詳細監査ポリシープロファイルを追加した。59のサブカテゴリと14のプロファイルで、WELA、文書に基づくWindows既定値、Microsoft、確認済みのCIS v4.0.0、ASDのWindows標準機能向け監査ガイドに対応する。ホストの役割とビルドの検証、オフラインでの設定計画、出典と前提条件を含むJSON出力をサポートする。完全一致、最低限、任意、変更なし、未構成、適用対象外を区別する。Windows既定値は参照専用で、プロファイルの対象はSecurityログの詳細監査ポリシーに限定される。 (#390) (@Shirofune-Security) - WELAのプロファイルにWindows標準の監査サブカテゴリを6つ追加した。Group MembershipとAuthorization Policy Changeは成功、Application Group Management、MPSSVC Rule-Level Policy Change、IPsec Driver、Kernel Objectは成功と失敗を監査する。各ガイドに対応するプロファイルでは、それぞれの監査設定と前提条件を維持する。Kernel Objectのイベント生成には対象オブジェクトに適切なSACLが必要であり、この変更ではそのSACLを作成しない。 (#391) (@Shirofune-Security) - `configure`と`configure -Profile`に`-DryRun`と`-ResultsPath`を追加し、Windows設定を変更せずに変更内容を確認し、設定項目ごとの結果をJSONで出力できるようにした。`-DryRun`に対応していないコマンドは、実行前にエラーで停止する。 (#392) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83105006..ab71489f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (#394) (@Shirofune-Security) - Unified event-log size auditing and configuration with shared byte-based profiles, including 256 MiB AppLocker/firewall logs, 32 MiB Setup and ASD 2048 MiB Security. Added separate source and collector size/mode choices through `-LogProfile` and `configure-eventlogs`; larger buffers and existing retention modes are preserved unless explicitly changed. Results include verified state and unknown retention duration. (#396) (@Shirofune-Security) +- Added opt-in `smb-auditing` audit, plan and configure actions for six native SMB audit policies. Host builds and exact local ADMX mappings gate writes; policy DWORDs and available runtime values are reported separately, with dry-run, recovery journaling and policy-registry verification. Runtime activation is reported separately as active, pending verification or unknown; an observed False does not turn a verified registry write into a failure. Signing/encryption requirements and guest access are not changed; runtime event/ingestion validation remains required. (#397) (@Shirofune-Security) - Added versioned advanced audit-policy profiles shared by `audit-settings`, `plan` and `configure`: 59 subcategories and 14 profiles covering WELA, documented Windows defaults, Microsoft, reviewed CIS v4.0.0 and ASD native guidance. Profiles support role/build validation, offline planning and JSON exports with sources and prerequisites. Exact, minimum, optional, unchanged, Not Configured and not-applicable settings remain distinct. Windows defaults are reference-only; profile scope is advanced Security audit policy. (#390) (@Shirofune-Security) - Added six native Windows audit subcategories to WELA's profile: Group Membership and Authorization Policy Change (Success), plus Application Group Management, MPSSVC Rule-Level Policy Change, IPsec Driver and Kernel Object (Success and Failure). Source-specific profiles retain their own audit settings and prerequisites; Kernel Object events require matching object SACLs, which this change does not create. (#391) (@Shirofune-Security) - Added `-DryRun` and `-ResultsPath` to `configure` and `configure -Profile` to preview changes without modifying Windows settings and export per-control results as JSON. Commands that do not support `-DryRun` reject it before running. (#392) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index aebda907..c8196e0a 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -21,6 +21,7 @@ [ValidateSet('Preserve', 'CisV4')][string]$FirewallPathMode = 'Preserve', [ValidateRange(16384, 32767)][int]$FirewallMinimumSizeKiB = 16384, [string]$HtmlPath, + [ValidateSet('Audit', 'Plan', 'Configure')][string]$SmbAction = 'Audit', [switch]$Help ) @@ -36,6 +37,7 @@ $AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt" $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" . (Join-Path $ScriptRoot "scripts/Configuration.ps1") . (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1") +. (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1") Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop Import-Module (Join-Path $ScriptRoot "modules/NativeProviders.psm1") -ErrorAction Stop Import-Module (Join-Path $ScriptRoot "modules/EventLogSettings.psm1") -ErrorAction Stop @@ -1668,6 +1670,10 @@ Usage: ./WELA.ps1 firewall-logging -FirewallAction Plan -FirewallPathMode CisV4 ./WELA.ps1 firewall-logging -FirewallAction Configure -DryRun # Firewall text logging is opt-in; it does not change firewall enforcement or rules. + ./WELA.ps1 smb-auditing -SmbAction Audit -ResultsPath smb-audit.json + ./WELA.ps1 smb-auditing -SmbAction Plan + ./WELA.ps1 smb-auditing -SmbAction Configure -DryRun + # SMB auditing is opt-in and never changes signing/encryption requirements or guest access. ./WELA.ps1 profiles # List versioned advanced audit-policy profiles ./WELA.ps1 plan -Profile wela-2.2.0 -Role Client -Build 26100 -PlanPath plan.json ./WELA.ps1 audit-settings -Profile microsoft-sct-win11-24h2 -PlanPath audit.json @@ -1698,8 +1704,9 @@ Write-Host "" # Reject unsupported dry-run requests before reaching any command's mutation path. if ($DryRun -and $Cmd -notin @('configure', 'configure-eventlogs') -and - -not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure')) { - throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs and firewall-logging -FirewallAction Configure. No command was run." + -not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure') -and + -not ($Cmd -eq 'smb-auditing' -and $SmbAction -eq 'Configure')) { + throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs, firewall-logging -FirewallAction Configure and smb-auditing -SmbAction Configure. No command was run." } if ($Profile -and $Cmd -in @('eventlog-profiles', 'audit-filesize', 'configure-eventlogs')) { throw '-Profile selects advanced audit policy only. Use -LogProfile for event-log size/mode settings.' @@ -1730,6 +1737,19 @@ switch ($Cmd.ToLower()) { if ($report.ExitCode) { exit $report.ExitCode } } catch { Write-Host "[Failed] Firewall logging: $_" -ForegroundColor Red; exit 1 } } + 'smb-auditing' { + if ($Help) { + Write-Host 'Usage: ./WELA.ps1 smb-auditing [-SmbAction Audit|Plan|Configure] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath file.json]' + Write-Host 'Checks six version-aware SMB audit policies against local ADMX and available runtime properties. Configure writes supported audit DWORDs only. See docs/smb-auditing.md.' + return + } + if ($Profile -or $Baseline) { throw 'smb-auditing uses -SmbAction; -Profile and -Baseline apply to Security audit settings.' } + try { + $report = Invoke-WelaSmbAuditCommand -Action $SmbAction -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $report + if ($report.ExitCode) { exit $report.ExitCode } + } catch { Write-Host "[Failed] SMB auditing: $_" -ForegroundColor Red; exit 1 } + } "profiles" { (Import-WelaAuditProfiles).profiles | Select-Object id, version, scope, appliesTo | Format-List } diff --git a/docs/smb-auditing.md b/docs/smb-auditing.md new file mode 100644 index 00000000..cd600f45 --- /dev/null +++ b/docs/smb-auditing.md @@ -0,0 +1,72 @@ +# Version-aware native SMB audit policies + +The opt-in `smb-auditing` command audits, plans and configures six built-in Windows audit policies. It does not enable insecure guest access, weaken signing/encryption, change SMB dialects or shares, restart services, or install Sysmon. It does not configure event forwarding, change channel settings or claim a Sigma coverage increase. + +Run in elevated **64-bit** Windows PowerShell 5.1 or PowerShell 7: + +```powershell +.\WELA.ps1 smb-auditing -SmbAction Audit -ResultsPath smb-audit.json +.\WELA.ps1 smb-auditing -SmbAction Plan -ResultsPath smb-plan.json +.\WELA.ps1 smb-auditing -SmbAction Configure -DryRun -ResultsPath smb-preview.json +.\WELA.ps1 smb-auditing -SmbAction Configure -Auto -BackupPath .\smb-before -ResultsPath smb-results.json +``` + +`-Profile` and `-Baseline` are rejected for this command: their advanced Security audit-policy semantics do not include these SMB policies. Audit and Plan both read the current host and show desired DWORD values; the plan is evidence, not an offline authorization file. They write only the explicitly requested results JSON. Configure dry run performs no policy/key writes and creates no recovery directory. Unknown read failures give exit code 1; known unsupported controls are skipped explicitly. Audit/Plan uses `PolicyConfigured` for the desired registry DWORD, with runtime state reported separately. A readable assessment with `ChangeRequired` has exit code 0 because the assessment completed. `Applied` and `AlreadyCompliant` rows verify the requested policy-registry DWORD. Exit code 0 means there are no failed/overridden controls; dry-run, declined and unsupported/skipped controls do not establish configuration. None of these results proves runtime auditing or event generation. + +## Exact controls and capability gates + +Each value below is enabled as **REG_DWORD 1**. Numeric strings are neither compliant nor accepted during read-back. + +| Registry key under HKLM | Values | +| --- | --- | +| `SOFTWARE\Policies\Microsoft\Windows\LanmanServer` | `AuditClientDoesNotSupportEncryption`, `AuditClientDoesNotSupportSigning`, `AuditInsecureGuestLogon` | +| `SOFTWARE\Policies\Microsoft\Windows\LanmanWorkstation` | `AuditServerDoesNotSupportEncryption`, `AuditServerDoesNotSupportSigning`, `AuditInsecureGuestLogon` | + +The reviewed build families are Windows 11 24H2 (26100), Windows 11 25H2 (26200), and Windows Server 2025 including domain controllers (26100). Host role/build is read from Win32_OperatingSystem. Older releases, including Server 2022 (20348), are `NotApplicable` even if someone has copied newer ADMX files onto them. Unreviewed future builds or unknown host information are `Unknown`; they receive no policy writes. + +A qualifying build is only a candidate. For **each** control WELA must also read the local `%windir%\PolicyDefinitions\LanmanServer.admx` or `LanmanWorkstation.admx` and find exactly one matching Machine policy, official `Pol_...` name, exact registry key and value name, and enabled decimal DWORD value 1. Missing, inaccessible, malformed, mismatched or ambiguous definitions are `Unknown`. DTD/external entities are prohibited. The report records the local ADMX path, SHA-256 hash and supportedOn reference. WELA does not download templates or assume that a Central Store proves local capability. + +Microsoft's Policy CSP pages list **26100.3613** as the availability floor for that CSP delivery surface. This tool writes the documented registry policy, not the CSP. It does not treat every 26100 host as supported based on its build alone or use the CSP minor build as a substitute for local policy/capability evidence. Local templates can still be replaced independently of the OS; available native runtime properties and lab events provide additional evidence. + +## Policy registry versus effective runtime + +Reports keep `Policy` (the actual policy-registry value/type) separate from `Runtime` (the corresponding property of `Get-SmbServerConfiguration` or `Get-SmbClientConfiguration`). WELA never substitutes the policy DWORD for a runtime observation: + +- `Observed`: the getter exposes an actual Boolean. `RuntimeState=Active` means that Boolean was True, not that representative events were generated. False is `NotActive` before the desired policy exists, or `PendingVerification` when the policy registry contains DWORD 1. A correctly written/read-back policy therefore succeeds even when the runtime Boolean remains False. Pending verification does **not** assert propagation delay, a future activation deadline, or that a policy refresh/restart will fix the discrepancy. Its cause and activation timing are unknown; investigate and repeat Audit independently. WELA performs no refresh/restart and never weakens security to make a Boolean change. +- `NotExposed`: the getter or property is unavailable. `RuntimeState=Unknown` distinguishes this from an observed False. With the exact local ADMX mapping, WELA can verify the registry policy only. The snapshot explicitly says **effective auditing not established**. A successful registry result is not proof of runtime activation or event generation. +- `Unknown`: a runtime read fails or returns an unexpected type. Configuration fails closed without treating the state as a default. + +Configure uses the common recovery journal and result runner. It rechecks capabilities/current values before writing, verifies the DWORD afterward, and reads policy/runtime again at completion. Registry value/type drift becomes `Overridden`; actual read/write failures remain failures and give a nonzero exit code while other controls continue. A runtime Boolean that remains or becomes False is reported separately as pending verification, rather than a registry write failure. An existing DWORD 1 is not rewritten merely to try to make the runtime Boolean change. A later audit can observe `Active` without any additional writes. A prompt-time policy change is refused so recovery evidence does not silently describe a stale value. + +Configure JSON includes an explicit `VerificationScope` and `RuntimeVerification` counts for Active, PendingVerification, NotActive, Unknown and NotApplicable, alongside each actual before/after observation. Failed controls count as Unknown in that summary so an older snapshot cannot be mistaken for a successful final runtime read. Console output scopes success to policy-registry verification and prints the runtime counts. None of these statuses proves event generation or central collection. + +The registry policy is a current observation, not proof of GPO/MDM ownership or long-term persistence. Future policy refresh can replace it. A direct local policy write also is not an edit to the domain GPO or its authoritative registry.pol source. + +## Manual recovery + +There is no automatic rollback. Review results and `before.jsonl` before selecting an entry. Its `Before.Policy` contains the original value existence, data and registry kind; `Before.Runtime` is observation only and must not be blindly passed to SMB setters. Example for one reviewed entry: + +```powershell +$entry = Get-Content -LiteralPath .\smb-before\before.jsonl | ConvertFrom-Json | + Where-Object { $_.Kind -eq 'SmbAudit' -and $_.Target.Name -eq 'AuditClientDoesNotSupportSigning' } | + Select-Object -First 1 +if (-not $entry) { throw 'Recovery entry not found' } +$old = $entry.Before.Policy +if ($old.ValueExists) { + Set-ItemProperty -LiteralPath $entry.Target.Path -Name $entry.Target.Name -Value $old.Value -Type $old.Type -ErrorAction Stop +} else { + Remove-ItemProperty -LiteralPath $entry.Target.Path -Name $entry.Target.Name -ErrorAction Stop +} +``` + +Review concurrent administrator changes and GPO/MDM ownership first; a failed write may have left the original state unchanged. Restore controls individually and rerun Audit. Keep newly created parent policy keys unless separately reviewed as empty and safe to remove; never delete the entire LanmanServer/Workstation policy key. Registry and runtime observations can differ, and this implementation has not established why or when they converge; recovery also requires a later runtime check. This command has not changed guest access, signing/encryption requirements, shares or service state. + +## Evidence still needed before closing issue #377 + +Mocked tests exercise OS/build and per-policy ADMX gating, all six exact policy targets, DWORD types, supported/unsupported/unknown states, runtime read errors and unavailable properties, dry run, recovery ordering, idempotence, pending runtime verification despite successful DWORD writes, later runtime activation without rewriting, actual runtime read failures and final registry drift. Windows CI adds read-only registry/local-ADMX/native-runtime observations and dry-run checks under PowerShell 5.1 and 7. It does not generate SMB traffic or alter runner policy. + +On isolated supported client/server snapshots, retain OS build/revision, PowerShell version, WELA commit, ADMX hashes and before/after reports. Confirm a policy refresh does not unexpectedly override the requested setting. Capture representative native SMB audit events under the existing security requirements and verify collector delivery. Microsoft's signing/encryption guide identifies SMBClient/Audit 31998/31999 and SMBServer/Audit 3021/3022; validate the event actually corresponds to the test condition. Inspect guest-audit behavior without enabling guest access or weakening signing/encryption. If the existing secure configuration prevents a guest-session event, record that limitation rather than changing the security posture merely to obtain a test event. Also verify manual recovery. These traffic and ingestion tests remain pending; keep the issue open until the required evidence exists. + +Microsoft documents the policy-to-registry mappings and the SMB configuration cmdlets, but the cited pages do not establish synchronous propagation of a direct policy-registry write into the getter or promise that refreshing Group Policy resolves any discrepancy. WELA makes neither assumption. + +Sources: [LanmanServer Policy CSP mappings](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-lanmanserver), [LanmanWorkstation Policy CSP mappings](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-lanmanworkstation), [SMB signing and encryption auditing](https://learn.microsoft.com/en-us/windows-server/storage/file-server/smb-signing-overview), [SMB feature availability](https://learn.microsoft.com/en-us/windows-server/storage/file-server/file-server-smb-overview), [SMB configuration getter](https://learn.microsoft.com/en-us/powershell/module/smbshare/get-smbclientconfiguration?view=windowsserver2025-ps), [SMB server audit parameters](https://learn.microsoft.com/en-us/powershell/module/smbshare/set-smbserverconfiguration?view=windowsserver2025-ps), and [issue #377](https://github.com/Yamato-Security/WELA/issues/377). diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 index 3c51a233..b0fe1121 100644 --- a/scripts/Configuration.ps1 +++ b/scripts/Configuration.ps1 @@ -94,8 +94,9 @@ function Invoke-WelaConfigurationControl { function Complete-WelaConfiguration { param($Context, [string]$ResultsPath, $Plan, - [ValidateSet("native-windows-configuration", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only")] - [string]$Scope = "native-windows-configuration") + [ValidateSet("native-windows-configuration", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only", "smb-audit-policies-only")] + [string]$Scope = "native-windows-configuration", + [string]$SuccessMessage = 'Configuration completed; all requested controls verified.') # A second read detects a value that was compliant earlier but changed during # this run. It does not establish whether GPO or another writer caused drift. foreach ($check in $Context.Checks) { @@ -133,7 +134,7 @@ function Complete-WelaConfiguration { if ($report.ExitCode) { Write-Host "Configuration incomplete: $failed failed or overridden control(s). Review results and recovery journal." -ForegroundColor Red } elseif ($Context.DryRun) { Write-Host 'Dry run completed. No Windows configuration was changed.' -ForegroundColor Cyan } elseif ($skipped) { Write-Host "Configuration completed with $skipped skipped control(s)." -ForegroundColor Yellow } - else { Write-Host 'Configuration completed; all requested controls verified.' -ForegroundColor Green } + else { Write-Host $SuccessMessage -ForegroundColor Green } return $report } diff --git a/scripts/SmbAuditing.ps1 b/scripts/SmbAuditing.ps1 new file mode 100644 index 00000000..c831ffd9 --- /dev/null +++ b/scripts/SmbAuditing.ps1 @@ -0,0 +1,200 @@ +# 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 +} diff --git a/tests/SmbAuditing.Tests.ps1 b/tests/SmbAuditing.Tests.ps1 new file mode 100644 index 00000000..86c8d7af --- /dev/null +++ b/tests/SmbAuditing.Tests.ps1 @@ -0,0 +1,198 @@ +$ErrorActionPreference = 'Stop' +$repo = Split-Path $PSScriptRoot -Parent +$script:ScriptRoot = $repo +. (Join-Path $repo 'scripts/Configuration.ps1') +. (Join-Path $repo 'scripts/SmbAuditing.ps1') +$script:assertions = 0; $script:cleanup = @() +$root = Join-Path ([IO.Path]::GetTempPath()) ('wela-smb-' + [guid]::NewGuid().ToString('N')) +$script:cleanup += $root +$null = New-Item -ItemType Directory -Path (Join-Path $root 'PolicyDefinitions') -Force +$savedWindir = $env:windir; $savedOS = $env:OS +$env:windir = $root +function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ } +function Write-Admx([string]$Omit = '', [string]$WrongType = '') { + foreach ($component in @('LanmanServer', 'LanmanWorkstation')) { + $policies = @() + foreach ($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $component)) { + if ($definition.Name -eq $Omit) { continue } + $enabled = if ($definition.Name -eq $WrongType) { '1' } else { '' } + $policies += '' + $enabled + '' + } + '' + ($policies -join '') + '' | Set-Content -LiteralPath (Join-Path (Join-Path $root 'PolicyDefinitions') "$component.admx") -Encoding UTF8 + } +} +function Reset-Mocks { + $script:build = 26100; $script:productType = 1 + $script:hostFails = $false; $script:registryFails = $false; $script:runtimeFails = $false; $script:writeFails = $false + $script:runtimeMissing = $false; $script:runtimeFollows = $true; $script:wrongTypeWrite = $false + $script:writes = 0; $script:keysCreated = 0; $script:registry = @{}; $script:runtime = @{}; $script:onPrompt = $null + foreach ($definition in Get-WelaSmbAuditDefinitions) { + $id = "$($definition.Component)/$($definition.Name)" + $script:registry[$id] = [pscustomobject]@{ KeyExists = $false; ValueExists = $false; Value = $null; Type = $null } + $script:runtime[$id] = $false + } + Write-Admx +} +function Get-CimInstance { + param($ClassName, $Property, $ErrorAction) + if ($script:hostFails) { throw 'OS query denied' } + [pscustomobject]@{ ProductType = $script:productType; BuildNumber = [string]$script:build; Version = "10.0.$script:build"; Caption = 'Mock Windows' } +} +function Get-WelaRegistryState { + param($Path, $Name) + if ($script:registryFails) { throw 'Policy read denied' } + $component = ($Path -split '\\')[-1] + $source = $script:registry["$component/$Name"] + if (-not $source) { throw "Unexpected policy path $Path/$Name" } + [pscustomobject]@{ KeyExists = $source.KeyExists; ValueExists = $source.ValueExists; Value = $source.Value; Type = $source.Type } +} +function New-WelaRegistryKey { param($Path) $script:keysCreated++ } +function Set-ItemProperty { + param($LiteralPath, $Name, $Value, $Type, $ErrorAction) + $definition = @(Get-WelaSmbAuditDefinitions | Where-Object { $_.Path -eq $LiteralPath -and $_.Name -eq $Name }) + Assert ($definition.Count -eq 1 -and $Value -eq 1 -and $Type -eq 'DWord') 'Only six exact audit DWORD paths can be written' + $entries = @(Get-Content -LiteralPath (Join-Path $script:context.BackupPath 'before.jsonl') | ConvertFrom-Json) + Assert ($entries[-1].Target.Name -eq $Name -and $entries[-1].Target.Path -eq $LiteralPath) 'Matching recovery evidence precedes the write' + Assert ($entries[-1].Before.Capability.AdmxSha256 -and $entries[-1].Before.Policy) 'Journal retains registry state and ADMX evidence' + if ($script:writeFails) { throw 'Policy write denied' } + $script:writes++ + $id = "$($definition[0].Component)/$Name" + $script:registry[$id] = [pscustomobject]@{ KeyExists = $true; ValueExists = $true; Value = 1; Type = $(if ($script:wrongTypeWrite) { 'String' } else { 'DWord' }) } + if ($script:runtimeFollows) { $script:runtime[$id] = $true } +} +function Get-Runtime($Component) { + if ($script:runtimeFails) { throw 'Runtime query denied' } + $values = @{ RequireSecuritySignature = $true; EnableInsecureGuestLogons = $false } + if (-not $script:runtimeMissing) { + foreach ($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $Component)) { $values[$definition.Name] = $script:runtime["$Component/$($definition.Name)"] } + } + [pscustomobject]$values +} +function Get-SmbClientConfiguration { param($ErrorAction) Get-Runtime 'LanmanWorkstation' } +function Get-SmbServerConfiguration { param($ErrorAction) Get-Runtime 'LanmanServer' } +function Read-Host { param($Prompt) if ($script:onPrompt) { & $script:onPrompt }; 'Y' } +function New-TestContext([switch]$DryRun, [switch]$Prompt) { + $path = Join-Path $root ([guid]::NewGuid().ToString('N')) + $script:context = New-WelaConfigurationContext -DryRun:$DryRun -Auto:(-not $Prompt) -BackupPath $path + return $script:context +} +try { + Reset-Mocks + foreach ($case in @(@(26100, 1), @(26200, 1), @(26100, 2), @(26100, 3))) { + $script:build = $case[0]; $script:productType = $case[1] + $plan = @(Get-WelaSmbAuditPlan) + Assert ($plan.Count -eq 6 -and @($plan | Where-Object Status -eq ChangeRequired).Count -eq 6) 'Reviewed client/server/DC builds require the six policies when exact ADMX exists' + } + Reset-Mocks + $script:build = 20348; $script:productType = 3 + $plan = @(Get-WelaSmbAuditPlan) + Assert (@($plan | Where-Object Status -eq NotApplicable).Count -eq 6) 'Server 2022 stays unsupported even with copied newer ADMX' + $context = New-TestContext + Set-WelaSmbAuditControls $context $plan + Assert ($script:writes -eq 0 -and $script:keysCreated -eq 0 -and @($context.Results | Where-Object Status -eq Skipped).Count -eq 6) 'Unsupported host gets explicit skips and no created policy keys' + $script:build = 30000 + Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 6) 'Unreviewed future builds are Unknown' + + Reset-Mocks + Write-Admx -Omit AuditInsecureGuestLogon + Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 2) 'Missing local ADMX controls are individually gated' + Write-Admx -WrongType AuditInsecureGuestLogon + Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 2) 'An ADMX string value 1 does not authorize a DWORD policy write' + Remove-Item -LiteralPath (Join-Path $root 'PolicyDefinitions/LanmanServer.admx') + Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 4) 'Missing ADMX file blocks its component while preserving other controls' + ']>&external;' | Set-Content -LiteralPath (Join-Path $root 'PolicyDefinitions/LanmanServer.admx') + Assert (@(Get-WelaSmbAuditPlan | Where-Object { $_.Definition.Component -eq 'LanmanServer' -and $_.Status -eq 'Unknown' }).Count -eq 3) 'ADMX external entities are rejected' + + Reset-Mocks + $plan = @(Get-WelaSmbAuditPlan) + $context = New-TestContext -DryRun + Set-WelaSmbAuditControls $context $plan + Assert ($script:writes -eq 0 -and $script:keysCreated -eq 0 -and -not (Test-Path $context.BackupPath)) 'Dry run creates no registry policy or recovery directory' + $context = New-TestContext + Set-WelaSmbAuditControls $context $plan + Assert ($script:writes -eq 6 -and (Complete-WelaConfiguration $context -Scope smb-audit-policies-only).ExitCode -eq 0) 'All six exact policies are applied and runtime observations confirm them' + $context = New-TestContext + Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan) + Assert ($script:writes -eq 6 -and @($context.Results | Where-Object Status -eq AlreadyCompliant).Count -eq 6) 'Confirmed policy/runtime state is idempotent' + $script:registry['LanmanServer/AuditInsecureGuestLogon'].Value = 0 + Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1 -and $context.Results[2].Status -eq 'Overridden') 'Final policy refresh drift changes status and exit code' + + Reset-Mocks + $script:runtimeFollows = $false + $context = New-TestContext + Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan) + $result = Complete-WelaConfiguration $context + Assert ($script:writes -eq 6 -and $result.Failed -eq 0 -and $result.ExitCode -eq 0) 'Correct policy writes succeed independently of synchronous runtime activation' + Assert ($context.Results[0].After.Policy.Value -eq 1 -and $context.Results[0].After.Runtime.Value -eq $false) 'Successful policy read-back retains the actual false runtime observation' + Assert ($context.Results[0].After.PolicyRegistryConfigured -and $context.Results[0].After.RuntimeState -eq 'PendingVerification') 'Policy configuration and pending runtime verification are distinct' + Assert ($context.Results[0].After.Runtime.Diagnostic -match 'cause and activation timing are unknown') 'False runtime state does not assume a refresh will resolve it' + $plan = @(Get-WelaSmbAuditPlan) + Assert (@($plan | Where-Object Status -eq PolicyConfigured).Count -eq 6) 'Audit/plan reports configured policy despite pending runtime' + $context = New-TestContext + Set-WelaSmbAuditControls $context $plan + Assert ($script:writes -eq 6 -and @($context.Results | Where-Object Status -eq AlreadyCompliant).Count -eq 6) 'Pending runtime alone never causes redundant DWORD writes' + $env:OS = 'Windows_NT' + $pendingJson = Join-Path $root 'pending-runtime.json' + $pendingReport = Invoke-WelaSmbAuditCommand -Action Configure -Auto -BackupPath (Join-Path $root 'pending-runtime-backup') -ResultsPath $pendingJson + $savedPending = Get-Content $pendingJson -Raw | ConvertFrom-Json + Assert ($pendingReport.ExitCode -eq 0 -and $savedPending.RuntimeVerification.PendingVerification -eq 6 -and $savedPending.RuntimeVerification.Active -eq 0) 'Public JSON explicitly summarizes pending runtime without a policy failure' + Assert ($savedPending.VerificationScope -match 'Policy registry.*runtime activation.*separate') 'Public result success is explicitly scoped to the policy registry' + foreach ($id in @($script:runtime.Keys)) { $script:runtime[$id] = $true } + $later = @(Get-WelaSmbAuditPlan) + Assert (@($later | Where-Object { $_.Status -eq 'PolicyConfigured' -and $_.Before.RuntimeState -eq 'Active' }).Count -eq 6) 'A later independent audit observes runtime activation without rewriting policy' + $result = Complete-WelaConfiguration $context + Assert ($result.ExitCode -eq 0 -and $script:writes -eq 6 -and @($result.Results | Where-Object { $_.After.RuntimeState -eq 'Active' }).Count -eq 6) 'Final recheck records later activation independently of write success' + $script:runtime['LanmanServer/AuditClientDoesNotSupportEncryption'] = $false + $result = Complete-WelaConfiguration $context + Assert ($result.ExitCode -eq 0 -and $result.Results[0].After.RuntimeState -eq 'PendingVerification') 'Runtime returning false remains visible without relabeling an unchanged policy as overridden' + $script:runtimeFails = $true + Assert ((Complete-WelaConfiguration $context).Failed -eq 6) 'An actual runtime read error still fails the final verification instead of being treated as pending False' + $unknownReport = Invoke-WelaSmbAuditCommand -Action Configure -Auto -BackupPath (Join-Path $root 'unknown-runtime-backup') + Assert ($unknownReport.ExitCode -eq 1 -and $unknownReport.RuntimeVerification.Unknown -eq 6 -and $unknownReport.RuntimeVerification.Active -eq 0) 'Failed observations summarize as unknown and never reuse an older active runtime snapshot' + + Reset-Mocks + $script:runtimeMissing = $true + $context = New-TestContext + Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan) + Assert ((Complete-WelaConfiguration $context).ExitCode -eq 0) 'Exact ADMX permits registry-only configuration when runtime property is absent' + Assert ($context.Results[0].After.Runtime.Status -eq 'NotExposed' -and $context.Results[0].After.VerificationScope -like '*effective auditing not established*') 'Registry-only outcome never claims runtime confirmation' + Assert ($context.Results[0].After.RuntimeState -eq 'Unknown') 'An unavailable runtime property is Unknown rather than Active or pending False' + + Reset-Mocks + $script:wrongTypeWrite = $true + $context = New-TestContext + Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan) + Assert ((Complete-WelaConfiguration $context).Failed -eq 6) 'Read-back rejects wrong registry type despite a numeric match' + foreach ($errorKind in @('hostFails', 'registryFails', 'runtimeFails')) { + Reset-Mocks + Set-Variable -Name $errorKind -Scope Script -Value $true + $context = New-TestContext + Set-WelaSmbAuditControls $context @(Get-WelaSmbAuditPlan) + Assert ($script:writes -eq 0 -and (Complete-WelaConfiguration $context).Failed -eq 6) "$errorKind becomes an explicit failure without writes" + } + Reset-Mocks + $script:runtime['LanmanServer/AuditInsecureGuestLogon'] = 'False' + Assert (@(Get-WelaSmbAuditPlan | Where-Object Status -eq Unknown).Count -eq 1) 'String False is not coerced into a true runtime Boolean' + Reset-Mocks + $script:writeFails = $true + $context = New-TestContext + Set-WelaSmbAuditControls $context @((Get-WelaSmbAuditPlan)[0]) + Assert ((Complete-WelaConfiguration $context).Failed -eq 1) 'Write errors propagate to results' + Reset-Mocks + $script:onPrompt = { $script:registry['LanmanServer/AuditClientDoesNotSupportEncryption'].Value = 42 } + $context = New-TestContext -Prompt + Set-WelaSmbAuditControls $context @((Get-WelaSmbAuditPlan)[0]) + Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Policy changes during confirmation are not overwritten with stale recovery data' + + Reset-Mocks + $env:OS = 'Windows_NT' + $json = Join-Path $root 'plan.json' + $report = Invoke-WelaSmbAuditCommand -Action Plan -ResultsPath $json + Assert ($report.ExitCode -eq 0 -and (Get-Content $json -Raw | ConvertFrom-Json).Controls.Count -eq 6) 'Public plan writes six complete controls to JSON' + $report = Invoke-WelaSmbAuditCommand -Action Configure -DryRun -BackupPath (Join-Path $root 'dry') + Assert ($report.DryRun -and $script:writes -eq 0 -and $report.Scope -eq 'smb-audit-policies-only') 'Public entrypoint keeps SMB scope and dry-run semantics' + Write-Host "PASS: $script:assertions SMB audit assertions (mocked policies/runtime; real temporary ADMX parsing)." +} finally { + $env:windir = $savedWindir; $env:OS = $savedOS + foreach ($path in $script:cleanup) { if (Test-Path $path) { Remove-Item -LiteralPath $path -Recurse -Force } } +} diff --git a/tests/SmbAuditing.Windows.Tests.ps1 b/tests/SmbAuditing.Windows.Tests.ps1 new file mode 100644 index 00000000..0270fafc --- /dev/null +++ b/tests/SmbAuditing.Windows.Tests.ps1 @@ -0,0 +1,31 @@ +# Native Windows read-only evidence: policy registry, local ADMX and SMB getters only. +$ErrorActionPreference = 'Stop' +if ($env:OS -ne 'Windows_NT') { Write-Host 'SKIP: Windows only'; return } +$repo = Split-Path $PSScriptRoot -Parent +. (Join-Path $repo 'scripts/Configuration.ps1') +. (Join-Path $repo 'scripts/SmbAuditing.ps1') +function Read-PolicySnapshot { + foreach ($definition in Get-WelaSmbAuditDefinitions) { + [pscustomobject]@{ Component = $definition.Component; Name = $definition.Name; State = Get-WelaRegistryState -Path $definition.Path -Name $definition.Name } + } +} +$before = @(Read-PolicySnapshot) | ConvertTo-Json -Depth 8 +$serverBefore = Get-SmbServerConfiguration | Select-Object RequireSecuritySignature, EncryptData, RejectUnencryptedAccess | ConvertTo-Json +$clientBefore = Get-SmbClientConfiguration | Select-Object RequireSecuritySignature, RequireEncryption, EnableInsecureGuestLogons | ConvertTo-Json +$plan = @(Get-WelaSmbAuditPlan) +if ($plan.Count -ne 6) { throw 'Expected all six SMB audit controls.' } +foreach ($entry in $plan) { + if ($entry.Status -notin @('NotApplicable', 'Unknown', 'ChangeRequired', 'PolicyConfigured')) { throw 'Unexpected assessment status.' } + Write-Host "$($entry.Definition.Component)/$($entry.Definition.Name): $($entry.Status); $($entry.Diagnostic)" + if ($entry.Before.Runtime) { Write-Host "Runtime: $($entry.Before.Runtime.Status) / $($entry.Before.Runtime.Value)" } +} +$path = Join-Path ([IO.Path]::GetTempPath()) ('wela-smb-readonly-' + [guid]::NewGuid().ToString('N')) +$context = New-WelaConfigurationContext -DryRun -Auto -BackupPath $path +Set-WelaSmbAuditControls -Context $context -Plan $plan +if (Test-Path -LiteralPath $path) { throw 'Dry run created an unexpected recovery directory.' } +if (@($context.Results | Where-Object Status -eq Applied).Count) { throw 'Dry run applied a control.' } +$after = @(Read-PolicySnapshot) | ConvertTo-Json -Depth 8 +$serverAfter = Get-SmbServerConfiguration | Select-Object RequireSecuritySignature, EncryptData, RejectUnencryptedAccess | ConvertTo-Json +$clientAfter = Get-SmbClientConfiguration | Select-Object RequireSecuritySignature, RequireEncryption, EnableInsecureGuestLogons | ConvertTo-Json +if ($before -ne $after -or $serverBefore -ne $serverAfter -or $clientBefore -ne $clientAfter) { throw 'Policy or security requirements changed during read-only test.' } +Write-Host 'PASS: native policy/ADMX/runtime observation and dry-run evidence; audit policies and security requirements unchanged. No event generation or ingestion test performed.' diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index da38eec2..4d03a568 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -10,6 +10,7 @@ - ネイティブのDomain/Private/Publicテキストログを監査・計画・設定する任意実行の`firewall-logging`を追加しました。許可・破棄ログの有効化、最小サイズの確認、既存パスと大きな上限値の保持、CIS v4.0.0のパスの明示的な選択に対応します。ファイアウォールサービスのディレクトリ権限を確認し、ローカル設定と実効設定を記録して変更後の実効設定を検証します。通信制御やACLは変更しません。実通信によるログ生成と収集の検証は別途必要です。 (#394) (@Shirofune-Security) - イベントログのサイズ監査と設定に共通のバイト単位プロファイルを導入し、AppLocker・ファイアウォールログの256 MiB、Setupの32 MiB、ASD推奨のSecurityログ2048 MiBに対応した。`-LogProfile`と`configure-eventlogs`で送信元と収集サーバーのサイズ・保存方式を選択できる。明示的に指定しない限り、既存の大きいバッファと保存方式は維持する。結果には検証した設定を記録し、未測定の保存日数は不明と表示する。 (#396) (@Shirofune-Security) +- 6つのネイティブSMB監査ポリシーを監査・計画・設定する任意実行の`smb-auditing`を追加しました。OSビルドとローカルADMXの正確な定義を確認してから書き込み、ポリシーのDWORD値と取得可能な実行時設定を分けて表示します。Dry-run、復旧用記録、ポリシーレジストリの検証に対応します。実行時設定は有効・検証待ち・不明を区別し、Falseが観測されてもレジストリへの書き込み成功を失敗とは扱いません。署名・暗号化要件やゲストアクセスは変更しません。実イベント生成と収集の検証は別途必要です。 (#397) (@Shirofune-Security) - `audit-settings`、`plan`、`configure`で共有するバージョン付きの詳細監査ポリシープロファイルを追加した。59のサブカテゴリと14のプロファイルで、WELA、文書に基づくWindows既定値、Microsoft、確認済みのCIS v4.0.0、ASDのWindows標準機能向け監査ガイドに対応する。ホストの役割とビルドの検証、オフラインでの設定計画、出典と前提条件を含むJSON出力をサポートする。完全一致、最低限、任意、変更なし、未構成、適用対象外を区別する。Windows既定値は参照専用で、プロファイルの対象はSecurityログの詳細監査ポリシーに限定される。 (#390) (@Shirofune-Security) - WELAのプロファイルにWindows標準の監査サブカテゴリを6つ追加した。Group MembershipとAuthorization Policy Changeは成功、Application Group Management、MPSSVC Rule-Level Policy Change、IPsec Driver、Kernel Objectは成功と失敗を監査する。各ガイドに対応するプロファイルでは、それぞれの監査設定と前提条件を維持する。Kernel Objectのイベント生成には対象オブジェクトに適切なSACLが必要であり、この変更ではそのSACLを作成しない。 (#391) (@Shirofune-Security) - `configure`と`configure -Profile`に`-DryRun`と`-ResultsPath`を追加し、Windows設定を変更せずに変更内容を確認し、設定項目ごとの結果をJSONで出力できるようにした。`-DryRun`に対応していないコマンドは、実行前にエラーで停止する。 (#392) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 7ab939d2..50139bca 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -10,6 +10,7 @@ - Added opt-in `firewall-logging` audit, plan and configure actions for native Domain/Private/Public text logs, with allowed/dropped logging, minimum size checks, preserved operator paths/larger limits, and explicit CIS v4.0.0 paths. Configuration checks firewall service directory permissions, journals local/effective state and verifies effective policy without changing firewall enforcement or ACLs. Traffic and ingestion validation remains required. (#394) (@Shirofune-Security) - Unified event-log size auditing and configuration with shared byte-based profiles, including 256 MiB AppLocker/firewall logs, 32 MiB Setup and ASD 2048 MiB Security. Added separate source and collector size/mode choices through `-LogProfile` and `configure-eventlogs`; larger buffers and existing retention modes are preserved unless explicitly changed. Results include verified state and unknown retention duration. (#396) (@Shirofune-Security) +- Added opt-in `smb-auditing` audit, plan and configure actions for six native SMB audit policies. Host builds and exact local ADMX mappings gate writes; policy DWORDs and available runtime values are reported separately, with dry-run, recovery journaling and policy-registry verification. Runtime activation is reported separately as active, pending verification or unknown; an observed False does not turn a verified registry write into a failure. Signing/encryption requirements and guest access are not changed; runtime event/ingestion validation remains required. (#397) (@Shirofune-Security) - Added versioned advanced audit-policy profiles shared by `audit-settings`, `plan` and `configure`: 59 subcategories and 14 profiles covering WELA, documented Windows defaults, Microsoft, reviewed CIS v4.0.0 and ASD native guidance. Profiles support role/build validation, offline planning and JSON exports with sources and prerequisites. Exact, minimum, optional, unchanged, Not Configured and not-applicable settings remain distinct. Windows defaults are reference-only; profile scope is advanced Security audit policy. (#390) (@Shirofune-Security) - Added six native Windows audit subcategories to WELA's profile: Group Membership and Authorization Policy Change (Success), plus Application Group Management, MPSSVC Rule-Level Policy Change, IPsec Driver and Kernel Object (Success and Failure). Source-specific profiles retain their own audit settings and prerequisites; Kernel Object events require matching object SACLs, which this change does not create. (#391) (@Shirofune-Security) - Added `-DryRun` and `-ResultsPath` to `configure` and `configure -Profile` to preview changes without modifying Windows settings and export per-control results as JSON. Commands that do not support `-DryRun` reject it before running. (#392) (@Shirofune-Security)