From 558ba6f2a85844ef7aa701fcfbb2b924e15b261b Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Tue, 22 Sep 2026 09:41:04 +0900 Subject: [PATCH] Verify public SMB audit policy configuration on native Windows --- .github/workflows/native-smb-policy.yml | 47 +++++++++++ CHANGELOG-Japanese.md | 2 + CHANGELOG.md | 2 + docs/smb-auditing.md | 6 ++ tests/SmbPolicyConfigure.Windows.Tests.ps1 | 96 ++++++++++++++++++++++ website/docs/resources/changelog.ja.md | 2 + website/docs/resources/changelog.md | 2 + 7 files changed, 157 insertions(+) create mode 100644 .github/workflows/native-smb-policy.yml create mode 100644 tests/SmbPolicyConfigure.Windows.Tests.ps1 diff --git a/.github/workflows/native-smb-policy.yml b/.github/workflows/native-smb-policy.yml new file mode 100644 index 00000000..7221014a --- /dev/null +++ b/.github/workflows/native-smb-policy.yml @@ -0,0 +1,47 @@ +name: Native public SMB policy configuration +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + native-smb-policy: + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + engine: [powershell, pwsh] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Fixtures and public guards in Windows PowerShell5.1 + if: matrix.engine == 'powershell' + shell: powershell + run: | + ./tests/SmbAuditing.Tests.ps1 + ./tests/SmbAuditing.Windows.Tests.ps1 + - name: Native public SMB policy configuration in Windows PowerShell5.1 + if: matrix.engine == 'powershell' + shell: powershell + run: ./tests/SmbPolicyConfigure.Windows.Tests.ps1 -AllowDisposablePolicyWrite + - name: Fixtures and public guards in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: | + ./tests/SmbAuditing.Tests.ps1 + ./tests/SmbAuditing.Windows.Tests.ps1 + - name: Native public SMB policy configuration in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: ./tests/SmbPolicyConfigure.Windows.Tests.ps1 -AllowDisposablePolicyWrite + - name: Retain owned fixture evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: native-smb-policy-${{ matrix.os }}-${{ matrix.engine }} + path: ${{ runner.temp }}/wela-smb-policy-*/ + if-no-files-found: warn + retention-days: 7 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index ef9b8bad..24a6bdcd 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,8 @@ **改善:** +- Server 2022/2025とPowerShell 5.1/7でSMBポリシー設定の公開CLIを検証します。対応ホストで6項目の適用・再読取・再実行、非対応ホストのスキップ、元の型付き記録、無関係な設定の維持と完全な復元を確認します。イベント生成と実行時の有効化は別途検証します。(関連 #377) (@Shirofune-Security) + - Server 2022/2025 と Windows PowerShell 5.1/PowerShell 7 の破棄可能な環境で、Securityログ警告設定の公開CLIを検証します。未設定・0・高いしきい値、早い警告値の維持、DryRun、再実行、不正型の拒否と完全な復元を確認し、無関係な設定は保持します。ログ枯渇や警告イベント生成は検証範囲外です。 (@Shirofune-Security) - Server 2022/2025 と両 PowerShell エンジンで、公開 `targeted-sacl` のレジストリ操作を検証する使い捨てテストを追加しました。テスト専用の新規ハイブをマウントし、対象選択、DryRun、監査 ACE の追加、古い計画・前提条件不足の拒否、冪等性と厳密に対応付けた Security4657 を確認します。無関係な ACE・型付き値の保持、監査ポリシー・トークンの復元、所有ハイブのアンロードと削除の証跡を保存します。製品側のハイブ読み込みや Sigma 準備完了の判定は追加しません。(関連 #373) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d66a87..80e261c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Improvements:** +- Add native public SMB policy configuration acceptance on Server 2022/2025 and PowerShell 5.1/7: six-policy apply/readback and idempotence on supported hosts, unsupported-host skips, typed original journals, unrelated-state preservation and exact cleanup. Event generation and runtime activation remain separate. (Related #377) (@Shirofune-Security) + - Verify public Security-log warning configuration on disposable Server 2022/2025 hosts under Windows PowerShell 5.1/PowerShell 7: absent/zero/higher thresholds, earlier-threshold preservation, dry run, idempotence, wrong-type refusal and exact cleanup. Preserve unrelated registry values, channel configuration, audit masks and CrashOnAuditFail; no log-exhaustion or warning-event claim. (@Shirofune-Security) - Added disposable public `targeted-sacl` registry lifecycle validation on Server 2022/2025 and both PowerShell engines. A fixture-owned mounted hive exercises reviewed selection, DryRun, additive configuration, stale/prerequisite refusal and idempotence, followed by one precisely attributed Security4657. Retained native receipts verify unrelated ACE/value preservation and exact audit-policy, token and owned-hive cleanup; production does not load hives or gain Sigma credit. (Related #373) (@Shirofune-Security) diff --git a/docs/smb-auditing.md b/docs/smb-auditing.md index 153fa76f..fe2a4484 100644 --- a/docs/smb-auditing.md +++ b/docs/smb-auditing.md @@ -72,3 +72,9 @@ On isolated supported client/server snapshots, retain OS build/revision, PowerSh 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). + +## Native public configuration acceptance + +The separately opted-in `SmbPolicyConfigure.Windows.Tests.ps1` fixture runs public Plan, DryRun and Configure on disposable, unjoined Server 2022/2025 hosts with PowerShell 5.1/7. Server 2022 must skip all six unsupported controls without policy writes. On Server 2025, exact local ADMX and runtime observations must qualify before preparing six DWORD 0 values. Public Configure then writes six DWORD 1 values, preserves full native SMB configuration, siblings, access descriptors, service state and all 59 audit masks, records exact typed original journals, and repeats without writes. Cleanup restores the original values and removes only fixture-created empty policy keys. Native results retain the actual build/UBR and PowerShell version. + +This acceptance establishes policy registry behavior only. It generates no SMB traffic, performs no runtime activation or policy refresh, and does not establish Windows client/DC/AD CS, event, forwarding or Sigma readiness. diff --git a/tests/SmbPolicyConfigure.Windows.Tests.ps1 b/tests/SmbPolicyConfigure.Windows.Tests.ps1 new file mode 100644 index 00000000..8571d19f --- /dev/null +++ b/tests/SmbPolicyConfigure.Windows.Tests.ps1 @@ -0,0 +1,96 @@ +param([switch]$AllowDisposablePolicyWrite) +$ErrorActionPreference='Stop' +if(-not $AllowDisposablePolicyWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit opt-in on a disposable GitHub-hosted Windows runner is required.'} +$repo=Split-Path $PSScriptRoot -Parent +. (Join-Path $repo 'scripts/Configuration.ps1') +. (Join-Path $repo 'scripts/SmbAuditing.ps1') +Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force +$os=Get-CimInstance Win32_OperatingSystem;$computer=Get-CimInstance Win32_ComputerSystem +if($os.ProductType -ne 3 -or [int]$os.BuildNumber -notin @(20348,26100) -or $computer.DomainRole -ne 2 -or $computer.PartOfDomain){throw 'An unjoined disposable Server 2022/2025 is required.'} +$root=Join-Path $env:RUNNER_TEMP ('wela-smb-policy-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root +$engine=(Get-Process -Id $PID).Path;$definitions=@(Get-WelaSmbAuditDefinitions);$count=0;$failure=$null;$errors=@() +function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++} +function Key($Value){ConvertTo-Json -InputObject $Value -Depth 24 -Compress} +function Save($Name,$Value){ConvertTo-Json -InputObject $Value -Depth 24|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8} +function Masks { $m=Get-WelaEffectiveAuditPolicy;@($m.Keys|Sort-Object|ForEach-Object{"$_=$($m[$_])"}) -join ';' } +function Runtime { + foreach($side in @('Server','Client')){ + $cmd="Get-Smb${side}Configuration";$c=& $cmd -ErrorAction Stop + [pscustomobject][ordered]@{Side=$side;Properties=@($c.CimInstanceProperties|Sort-Object Name|ForEach-Object{[pscustomobject][ordered]@{Name=$_.Name;Type=$_.CimType.ToString();Value=$_.Value}})} + } +} +function Policies {foreach($d in $definitions){[pscustomobject]@{Definition=$d;Policy=Get-WelaRegistryState $d.Path $d.Name}}} +function Keys { + foreach($component in @('LanmanServer','LanmanWorkstation')){ + $base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64);$k=$null + try{ + $k=$base.OpenSubKey("SOFTWARE\Policies\Microsoft\Windows\$component") + if(-not $k){[pscustomobject][ordered]@{Component=$component;Exists=$false;Values=@();Children=@();Access=$null};continue} + $acl=if($PSVersionTable.PSVersion.Major -ge 6){[Microsoft.Win32.RegistryAclExtensions]::GetAccessControl($k)}else{$k.GetAccessControl()} + [pscustomobject][ordered]@{Component=$component;Exists=$true;Values=@($k.GetValueNames()|Sort-Object|ForEach-Object{[pscustomobject][ordered]@{Name=$_;Type=$k.GetValueKind($_).ToString();Value=$k.GetValue($_,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}});Children=@($k.GetSubKeyNames()|Sort-Object);Access=$acl.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access -bor [Security.AccessControl.AccessControlSections]::Owner -bor [Security.AccessControl.AccessControlSections]::Group)} + }finally{if($k){$k.Dispose()};$base.Dispose()} + } +} +function OtherKeys { + $all=@(Keys) + foreach($k in $all){$names=@($definitions|Where-Object Component -eq $k.Component|ForEach-Object Name);$k.Values=@($k.Values|Where-Object Name -NotIn $names)} + return $all +} +function Public([string]$Name,[string[]]$Arguments,[int]$Expected=0){ + $prior=$ErrorActionPreference + try{$ErrorActionPreference='Continue';$output=& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') smb-auditing @Arguments 2>&1|Out-String;$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior} + $output|Set-Content -LiteralPath (Join-Path $root ($Name+'.txt')) -Encoding UTF8 + Assert ($code -eq $Expected) "Public $Name exited $code : $output" + Get-Content -Raw -LiteralPath (Join-Path $root ($Name+'.json'))|ConvertFrom-Json +} +$before=@(Policies);$keys=@(Keys);$runtime=@(Runtime);$masks=Masks +$services=@(Get-Service LanmanServer,LanmanWorkstation|Sort-Object Name|Select-Object Name,Status) +Save 'original.json' @{Policies=$before;Keys=$keys;Runtime=$runtime;Masks=$masks;Services=$services;Build=[int]$os.BuildNumber;UBR=(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR;Engine=$PSVersionTable.PSVersion.ToString();PartOfDomain=$computer.PartOfDomain;DomainRole=$computer.DomainRole} +try{ + $initial=@(Get-WelaSmbAuditPlan) + if([int]$os.BuildNumber -eq 20348){Assert (@($initial|Where-Object Status -ne NotApplicable).Count -eq 0) 'All six policies are genuinely not applicable on Server 2022.'} + else{ + Assert (@($initial|Where-Object {$_.Status -notin @('ChangeRequired','PolicyConfigured')}).Count -eq 0) 'All six policies require exact local ADMX and readable native runtime before fixture writes.' + foreach($d in $definitions){New-WelaRegistryKey $d.Path;$null=New-ItemProperty -LiteralPath $d.Path -Name $d.Name -Value 0 -PropertyType DWord -Force} + } + $prepared=@(Policies);$other=@(OtherKeys);Save 'prepared.json' $prepared + $plan=Public plan @('-SmbAction','Plan','-ResultsPath',(Join-Path $root 'plan.json')) + Assert ($plan.Controls.Count -eq 6) 'Public Plan accounts for exactly six controls.' + $dry=Public dry @('-SmbAction','Configure','-DryRun','-BackupPath',(Join-Path $root 'dry-backup'),'-ResultsPath',(Join-Path $root 'dry.json')) + Assert ($dry.DryRun -and @($dry.Results|Where-Object Status -eq Applied).Count -eq 0 -and -not(Test-Path (Join-Path $root 'dry-backup'))) 'Dry run does not change policy or create original journals.' + Assert ((Key @(Policies)) -ceq (Key $prepared) -and (Key @(Runtime)) -ceq (Key $runtime)) 'Plan and DryRun preserve exact typed policy and full native runtime.' + $applied=Public apply @('-SmbAction','Configure','-Auto','-BackupPath',(Join-Path $root 'apply-backup'),'-ResultsPath',(Join-Path $root 'apply.json')) + Assert ($applied.Scope -ceq 'smb-audit-policies-only' -and $applied.Results.Count -eq 6) 'Public Configure retains narrow scope and all six outcomes.' + if([int]$os.BuildNumber -eq 20348){ + Assert (@($applied.Results|Where-Object Status -ne Skipped).Count -eq 0 -and -not(Test-Path (Join-Path $root 'apply-backup/before.jsonl'))) 'Unsupported Server 2022 has six skipped controls and no policy writes.' + }else{ + Assert (@($applied.Results|Where-Object Status -ne Applied).Count -eq 0) 'Server 2025 actually applied all six policy DWORDs.' + $journal=@(Get-Content (Join-Path $root 'apply-backup/before.jsonl')|ConvertFrom-Json);Assert ($journal.Count -eq 6) 'Every actual write has an original journal entry.' + foreach($row in $applied.Results){ + $j=@($journal|Where-Object Id -eq $row.Id);$p=@($prepared|Where-Object {$_.Definition.Path -ceq $row.Target.Path -and $_.Definition.Name -ceq $row.Target.Name}) + Assert ($j.Count -eq 1 -and $p.Count -eq 1 -and (Key $j[0].Before.Policy) -ceq (Key $p[0].Policy)) 'Each journal matches the actual typed original policy.' + Assert ($row.After.Policy.Type -ceq 'DWord' -and $row.After.Policy.Value -eq 1 -and $row.After.PolicyRegistryConfigured) 'Actual native readback verifies each DWORD without inferring runtime state.' + } + $repeat=Public repeat @('-SmbAction','Configure','-Auto','-BackupPath',(Join-Path $root 'repeat-backup'),'-ResultsPath',(Join-Path $root 'repeat.json')) + Assert (@($repeat.Results|Where-Object Status -ne AlreadyCompliant).Count -eq 0 -and -not(Test-Path (Join-Path $root 'repeat-backup/before.jsonl'))) 'Repeated public Configure is idempotent without another journal.' + } + Assert ((Key @(OtherKeys)) -ceq (Key $other) -and (Key @(Runtime)) -ceq (Key $runtime) -and (Masks) -ceq $masks) 'Sibling values, access descriptors, children, complete SMB runtime and all59 audit masks are preserved.' + Save 'completed.json' @{Status='Passed';Assertions=$count;ActualPolicyWrites=$(if([int]$os.BuildNumber -eq 26100){6}else{0});Scope='Policy registry only; no SMB traffic, activation, GPO refresh, event generation or Sigma proof.'} +}catch{$failure=$_.ToString();throw}finally{ + foreach($row in $before){try{ + $d=$row.Definition;$old=$row.Policy;$now=Get-WelaRegistryState $d.Path $d.Name + if($old.ValueExists){$null=New-ItemProperty -LiteralPath $d.Path -Name $d.Name -Value $old.Value -PropertyType $old.Type -Force} + elseif($now.ValueExists){Remove-ItemProperty -LiteralPath $d.Path -Name $d.Name -ErrorAction Stop} + }catch{$errors+=$_.ToString()}} + foreach($k in $keys|Where-Object {-not $_.Exists}){try{ + $path="HKLM:\SOFTWARE\Policies\Microsoft\Windows\$($k.Component)" + if(Test-Path -LiteralPath $path){$item=Get-Item -LiteralPath $path;if($item.ValueCount -ne 0 -or $item.SubKeyCount -ne 0){throw 'A fixture-created key is not empty; it was preserved.'};Remove-Item -LiteralPath $path -ErrorAction Stop} + }catch{$errors+=$_.ToString()}} + $checks=[ordered]@{} + foreach($pair in @(@('Policies',{(Key @(Policies)) -ceq (Key $before)}),@('Keys',{(Key @(Keys)) -ceq (Key $keys)}),@('Runtime',{(Key @(Runtime)) -ceq (Key $runtime)}),@('AuditMasks',{(Masks) -ceq $masks}),@('Services',{(Key @(Get-Service LanmanServer,LanmanWorkstation|Sort-Object Name|Select-Object Name,Status)) -ceq (Key $services)}))){try{$checks[$pair[0]]=& $pair[1]}catch{$checks[$pair[0]]=$false;$errors+=$_.ToString()}} + $complete=$errors.Count -eq 0 -and @($checks.Values|Where-Object {-not $_}).Count -eq 0 + Save 'cleanup.json' @{Complete=$complete;Checks=$checks;Errors=$errors;Failure=$failure;Assertions=$count} + if(-not $complete){throw 'SMB native policy fixture cleanup failed; inspect retained receipts.'} +} +Write-Host "PASS: $count native public SMB policy assertions and exact cleanup." +exit 0 diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index b4f75fc6..afda0843 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,8 @@ **改善:** +- Server 2022/2025とPowerShell 5.1/7でSMBポリシー設定の公開CLIを検証します。対応ホストで6項目の適用・再読取・再実行、非対応ホストのスキップ、元の型付き記録、無関係な設定の維持と完全な復元を確認します。イベント生成と実行時の有効化は別途検証します。(関連 #377) (@Shirofune-Security) + - Server 2022/2025 と Windows PowerShell 5.1/PowerShell 7 の破棄可能な環境で、Securityログ警告設定の公開CLIを検証します。未設定・0・高いしきい値、早い警告値の維持、DryRun、再実行、不正型の拒否と完全な復元を確認し、無関係な設定は保持します。ログ枯渇や警告イベント生成は検証範囲外です。 (@Shirofune-Security) - Server 2022/2025 と両 PowerShell エンジンで、公開 `targeted-sacl` のレジストリ操作を検証する使い捨てテストを追加しました。テスト専用の新規ハイブをマウントし、対象選択、DryRun、監査 ACE の追加、古い計画・前提条件不足の拒否、冪等性と厳密に対応付けた Security4657 を確認します。無関係な ACE・型付き値の保持、監査ポリシー・トークンの復元、所有ハイブのアンロードと削除の証跡を保存します。製品側のハイブ読み込みや Sigma 準備完了の判定は追加しません。(関連 #373) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 1ba91895..d65ae69a 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,8 @@ **Improvements:** +- Add native public SMB policy configuration acceptance on Server 2022/2025 and PowerShell 5.1/7: six-policy apply/readback and idempotence on supported hosts, unsupported-host skips, typed original journals, unrelated-state preservation and exact cleanup. Event generation and runtime activation remain separate. (Related #377) (@Shirofune-Security) + - Verify public Security-log warning configuration on disposable Server 2022/2025 hosts under Windows PowerShell 5.1/PowerShell 7: absent/zero/higher thresholds, earlier-threshold preservation, dry run, idempotence, wrong-type refusal and exact cleanup. Preserve unrelated registry values, channel configuration, audit masks and CrashOnAuditFail; no log-exhaustion or warning-event claim. (@Shirofune-Security) - Added disposable public `targeted-sacl` registry lifecycle validation on Server 2022/2025 and both PowerShell engines. A fixture-owned mounted hive exercises reviewed selection, DryRun, additive configuration, stale/prerequisite refusal and idempotence, followed by one precisely attributed Security4657. Retained native receipts verify unrelated ACE/value preservation and exact audit-policy, token and owned-hive cleanup; production does not load hives or gain Sigma credit. (Related #373) (@Shirofune-Security)