diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 52989c99..20d22f1c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,7 @@ jobs: Copy-Item -Recurse -Path ./scripts -Destination release-binaries/ Copy-Item -Recurse -Path ./modules -Destination release-binaries/ New-Item -ItemType Directory -Path release-binaries/docs -Force | Out-Null - Copy-Item -Path ./docs/gpo-audit-packages.md, ./docs/gpo-package-deployment.md, ./docs/gpo-creation.md, ./docs/wmi-probe.md, ./docs/firewall-logging.md, ./docs/firewall-logging-recovery.md, ./docs/ipsec-prerequisites.md, ./docs/wec-state.md, ./docs/wec-update.md, ./docs/wec-runtime.md, ./docs/wef-deployment.md, ./docs/native-channel-access.md, ./docs/eventlog-settings.md, ./docs/channel-read.md, ./docs/native-rule-eligibility.md, ./docs/native-validation.md, ./docs/wef-arrival.md, ./docs/smb-runtime-activation.md, ./docs/smb-auditing.md, ./docs/wec-ingress.md, ./docs/capi2-probe.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/eventlog-recovery.md, ./docs/failed-logon-probe.md, ./docs/wec-listener.md, ./docs/file-access-probe.md, ./docs/applocker-script-probe.md, ./docs/applocker-probe.md, ./docs/selected-sacl-configuration.md, ./docs/registry-sacl-recovery.md, ./docs/targeted-sacl-planning.md, ./docs/native-registry-sacl-validation.md, ./docs/wec-authorization.md, ./docs/channel-recovery.md, ./docs/outgoing-ntlm.md, ./docs/wec-collector-observation.md, ./docs/native-provider-packs.md, ./docs/native-provider-acceptance.md -Destination release-binaries/docs/ + Copy-Item -Path ./docs/gpo-audit-packages.md, ./docs/gpo-package-deployment.md, ./docs/gpo-creation.md, ./docs/wmi-probe.md, ./docs/firewall-logging.md, ./docs/firewall-logging-recovery.md, ./docs/ipsec-prerequisites.md, ./docs/wec-state.md, ./docs/wec-update.md, ./docs/wec-runtime.md, ./docs/wef-deployment.md, ./docs/native-channel-access.md, ./docs/eventlog-settings.md, ./docs/channel-read.md, ./docs/native-rule-eligibility.md, ./docs/native-validation.md, ./docs/wef-arrival.md, ./docs/smb-runtime-activation.md, ./docs/smb-auditing.md, ./docs/wec-ingress.md, ./docs/capi2-probe.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/eventlog-recovery.md, ./docs/failed-logon-probe.md, ./docs/wec-listener.md, ./docs/file-access-probe.md, ./docs/applocker-script-probe.md, ./docs/applocker-probe.md, ./docs/selected-sacl-configuration.md, ./docs/registry-sacl-recovery.md, ./docs/targeted-sacl-planning.md, ./docs/native-registry-sacl-validation.md, ./docs/native-filesystem-sacl-validation.md, ./docs/wec-authorization.md, ./docs/channel-recovery.md, ./docs/outgoing-ntlm.md, ./docs/wec-collector-observation.md, ./docs/native-provider-packs.md, ./docs/native-provider-acceptance.md -Destination release-binaries/docs/ - name: Set Artifact Name if: contains(matrix.info.os, 'windows') == true diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 3b6874eb..e893c489 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,8 @@ **改善:** +- Server 2022/2025 と PowerShell 5.1/7 の使い捨て環境で、リダイレクトされたユーザーフォルダーの実カタログを使う公開ファイルシステム SACL 設定を検証します。Plan/DryRun/Configure、子の変化による拒否、再実行時の無変更を確認し、無関係なセキュリティ情報と保護された子孫を保持します。継承された末端ファイルの SACL を公開プローブの正確な Security4663 と照合し、型付きプロファイル、ハイブ、トークン、監査ポリシー、所有ファイルの後始末を記録とハッシュで確認します。本番のプロファイル読み込み、遠隔ユーザー、将来の子孫、Sigma の保証は行いません。 (関連 #373) (@Shirofune-Security) + - 明示的な `registry-sacl-recovery` を追加しました。整合する4つの元記録、レビュー済み計画のハッシュ、過去・現在ともに空の子キー観測、監査縮小と継承への個別同意を必須とし、追加が証明されたレジストリルートの明示的な監査ACEを1つだけ削除します。SACLのみのネイティブ書き込みで他の記述子フィールドとACEの順序を保持し、部分的な書き込みの証跡と元の記述子バイトとの一致を別々に報告します。過去のキー・操作者の同一性認証、ツリー全体の原子性、イベント生成、Sigmaの準備完了は保証しません。 (Related #373) (@Shirofune-Security) - Server 2022/2025とPowerShell 5.1/7でSMBポリシー設定の公開CLIを検証します。対応ホストで6項目の適用・再読取・再実行、非対応ホストのスキップ、元の型付き記録、無関係な設定の維持と完全な復元を確認します。イベント生成と実行時の有効化は別途検証します。(関連 #377) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7129758..dc7ac96f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Improvements:** +- Added native public filesystem SACL lifecycle validation for one genuine redirected per-user catalog target on disposable Server 2022/2025 with PowerShell 5.1/7. Actual Plan/DryRun/Configure, stale-child refusal and idempotence preserve unrelated security and protected descendants; an inherited leaf SACL is checked against an exact public-probe Security4663. Retained receipts and hashes verify full typed profile, hive, token, audit-policy and owned-file cleanup without production profile loading, remote-user, future-child or Sigma claims. (Related #373) (@Shirofune-Security) + - Added opt-in `registry-sacl-recovery` for one proven explicit registry-root audit ACE, requiring four matching original records, a reviewed plan hash, empty historical/current descendants and separate audit-reduction/inheritance consent. Native SACL-only removal preserves unrelated descriptor fields and ACE order, retains partial-write evidence and reports original-byte equality separately; no authenticated historical key/operator identity, atomic-tree, event or Sigma claim. (Related #373) (@Shirofune-Security) - 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) diff --git a/docs/native-filesystem-sacl-validation.md b/docs/native-filesystem-sacl-validation.md new file mode 100644 index 00000000..42d4347a --- /dev/null +++ b/docs/native-filesystem-sacl-validation.md @@ -0,0 +1,38 @@ +# Public filesystem SACL lifecycle validation + +The `Native public filesystem SACL lifecycle` workflow validates the public `targeted-sacl` command against its real built-in per-user Signal directory definition. It runs on disposable Server 2022/2025 hosts with Windows PowerShell 5.1 and PowerShell 7. It uses an owned redirected folder, its existing ordinary descendants and a protected subtree, then runs the public `file-access-probe` against one inherited leaf SACL. + +## Owned fixture boundary + +The test creates a fresh private directory on the system volume, a newly saved hive mounted under a nonce-derived synthetic SID, and a matching new `ProfileList` entry. The entry contains only its ownership marker and typed `ProfileImagePath`. Its loaded hive supplies a redirected `AppData` known-folder value. The ordinary catalog must independently discover exactly that SID's Signal directory and classify it `Redirected`; no alternate catalog, arbitrary target switch or mocked resolver supplies selection authority. + +The synthetic SID is a fixture identity, not a created Windows account or proof of another user's effective access. The read worker uses the actual elevated runner account. Existing users, offline hives and system catalog targets are not modified. Profile registration, hive loading, initial unrelated ACE/protection setup and temporary audit policy are test-only operations; public WELA commands do not perform them. + +The test alone prepares File System success/failure auditing and typed advanced-audit precedence. It requires a complete observation of all 59 masks, the original full process token and the complete bounded `ProfileList` key/value inventory. Profile values retain their registry types and unexpanded data. The test never restores a whole saved system registry tree over current state. + +## Public operations and retained proof + +The fixture exercises this sequence with bounded, separately launched public WELA processes: + +1. Discover the actual redirected catalog target. Plan without child consent must block inheritance. +2. Plan with explicit child consent must capture the exact parent and all four existing descendants: one ordinary directory/leaf pair and one protected directory/leaf pair. +3. DryRun must leave every descriptor unchanged and create no recovery directory. +4. Create one owned unreviewed child. Configure using the earlier plan must refuse before journaling or writing. Remove that fixture child and generate a fresh plan. +5. Configure the fresh selection. A successful result must contain one `Applied` row and matching distinct Pending, Confirmed and descendant-observation records. +6. Independently read the parent and children. Exactly one required root ACE is added; its unrelated ACE, owner, group, DACL and other observed descriptor components remain. Two ordinary descendants show the inherited ACE, while both protected descendants retain their original security. +7. A fresh Plan/Configure reports `AlreadyCompliant`, adds no duplicate ACE or receipt, and preserves the complete observed tree. +8. Public file-probe Plan/Run on the ordinary leaf must observe the existing inherited ReadData SACL and exactly one attributable local Security 4663. The protected leaf must remain uncovered and its read probe must refuse before a read operation. + +The probe retains raw XML and binds the actual worker PID, handle, subject SID/logon, native file identity/path, access mask and measured one-byte-read/held-identity-readback phase. It reads exactly one byte and retains no file content. Only the fixture hashes its known harmless files to check byte preservation. The public configuration still reports `GenerationReadiness=Conditional` and `UsableRuleCredit=0`; the probe grants no Sigma credit. + +Review `fresh-plan.json`, `results.json`, `journal/`, the independent before/after/final descendant snapshots, `probe-result.json`, `probe/event.xml`, `cleanup.json` and `artifact-hashes.json` together. A process exit or printed status alone is insufficient. A failed run can retain partial evidence and is not a successful lifecycle result. + +## Cleanup and limits + +Cleanup restores the original selected audit mask and exact typed precedence, then independently compares every original audit mask, full token, `ProfileList` inventory/data and loaded-hive names. The ProfileList adapter removes only its exact unchanged two-value, childless, marker-owned entry. Changed ownership or partial setup prevents unproven deletion and is retained as a cleanup error. The owned hive is unloaded, its original seed removed, and the private hive files/target tree removed only after profile and hive restoration is verified. Each independent verification is guarded so one failure does not hide other cleanup observations. Registry parent last-write metadata is not restored or claimed unchanged. + +This proves the observed fixture cases on the tested builds. It does not establish arbitrary redirected-user access, remote shares, offline profiles, future children, an atomic tree transaction, Windows 11, domain/DC/CA behavior, forwarding, retention or backend Sigma execution. No receipt authorizes removing inherited ACEs from production descendants. The selected command's existing concurrency and partial-write limits still apply. + +The system-volume fixture is deliberate: some hosted data volumes emit the Removable Storage task even when `DriveInfo` reports Fixed. The existing probe accepts File System task 12800 only. Neither a protected branch nor another volume receives event credit from the successful ordinary leaf. + +See [selected SACL configuration](selected-sacl-configuration.md), [file-access probe](file-access-probe.md) and the separate [public registry lifecycle](native-registry-sacl-validation.md). Microsoft documents [SetSecurityInfo inheritance behavior](https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setsecurityinfo) and the [4663 access-use event fields](https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/event-4663). diff --git a/docs/selected-sacl-configuration.md b/docs/selected-sacl-configuration.md index f9f7f3a4..af105b77 100644 --- a/docs/selected-sacl-configuration.md +++ b/docs/selected-sacl-configuration.md @@ -88,7 +88,9 @@ Mocked tests cover selection, source-specific masks, unsupported consent, source The separate [public registry lifecycle fixture](native-registry-sacl-validation.md) mounts a newly saved, fixture-owned hive under a fresh synthetic user SID. The unchanged public catalog resolves its RunOnce key, then actual CLI Plan/DryRun/Configure calls exercise the reviewed lifecycle and one exact local 4657. Only the fixture loads/unloads hives and prepares auditing; the product behavior above is unchanged. This leaf fixture does not replace populated-tree inheritance validation. -Native CI results must be reviewed before claiming those test cases passed. Windows 11, DC/CA, user redirection, large/changing production trees, forwarding and actual Sigma/backend execution remain separate acceptance work. Every report remains `GenerationReadiness=Conditional` with `UsableRuleCredit=0`. +The [public filesystem lifecycle fixture](native-filesystem-sacl-validation.md) resolves a genuine built-in Signal target through an owned synthetic profile and redirected known folder. It exercises actual public selection, Plan/DryRun/Configure, stale-child refusal and idempotence on a populated tree, checks protected descendants, and matches one public leaf-read probe to local4663 XML. Only the disposable fixture registers its profile and mounts its hive. + +Native CI results must be reviewed before claiming those test cases passed. Windows 11, DC/CA, other user-redirection/access scenarios, large/changing production trees, forwarding and actual Sigma/backend execution remain separate acceptance work. Every report remains `GenerationReadiness=Conditional` with `UsableRuleCredit=0`. Primary API references: [GetSecurityInfo](https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-getsecurityinfo), [SetSecurityInfo and inheritance](https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setsecurityinfo), [registry open/link behavior](https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regopenkeyexw), [file handle and sharing flags](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew). diff --git a/tests/FileSaclLifecycle.Windows.Tests.ps1 b/tests/FileSaclLifecycle.Windows.Tests.ps1 index 72828744..d9f63dd0 100644 --- a/tests/FileSaclLifecycle.Windows.Tests.ps1 +++ b/tests/FileSaclLifecycle.Windows.Tests.ps1 @@ -4,14 +4,11 @@ $ErrorActionPreference='Stop' if(-not $AllowDisposableProfileWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted' -or [Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess){throw 'Explicit disposable hosted native Windows fixture only.'} $script:ScriptRoot=Split-Path $PSScriptRoot -Parent Import-Module (Join-Path $script:ScriptRoot 'modules/AuditProfiles.psm1') -ErrorAction Stop -foreach($name in @('Configuration','WefArrival','WmiProbe','ChannelRead','SelectedSaclConfiguration','FileAccessProbe')){. (Join-Path $script:ScriptRoot ('scripts/'+$name+'.ps1'))} +foreach($name in @('Configuration','WefArrival','WmiProbe','ChannelRead','ControlApplicability','TargetedSaclPlanning','SelectedSaclConfiguration','FileAccessProbe')){. (Join-Path $script:ScriptRoot ('scripts/'+$name+'.ps1'))} Initialize-WelaWmiProbeNative Add-Type -Path (Join-Path $PSScriptRoot 'RegistrySaclFixtureNative.cs') -ErrorAction Stop Add-Type -Path (Join-Path $PSScriptRoot 'FileSaclProfileFixture.cs') -ErrorAction Stop $nonce=[guid]::NewGuid().ToString('N') -$evidence=New-WelaArrivalOutput (Join-Path $env:RUNNER_TEMP ('wela-filesystem-lifecycle-'+$nonce)) $script:ScriptRoot -$targetRoot=New-WelaArrivalOutput (Join-Path (Join-Path $env:SystemRoot 'Temp') ('wela-filesystem-sacl-'+$nonce)) $script:ScriptRoot -$files=Join-Path $evidence 'owned-hive-files';$null=New-Item -ItemType Directory $files function Save([string]$Name,$Value){[IO.File]::WriteAllText((Join-Path $evidence $Name),(ConvertTo-Json -InputObject $Value -Depth 32),[Text.UTF8Encoding]::new($false))} function Key($Value){ConvertTo-Json -InputObject $Value -Depth 32 -Compress} function Hives {@([Microsoft.Win32.Registry]::Users.GetSubKeyNames()|Sort-Object)} @@ -46,11 +43,27 @@ $script:assertions=0 function Assert($Condition,[string]$Message){if(-not $Condition){throw $Message};$script:assertions++} $beforeProfiles=[Wela.FileSaclFixture.Profile]::Snapshot();$beforeHives=Hives;$beforeToken=[Wela.WmiProbe.Native]::Snapshot() $beforeMasks=Get-WelaEffectiveAuditPolicy;$precedencePath='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa';$precedenceName='SCENoApplyLegacyAuditPolicy';$beforePrecedence=Get-WelaRegistryState $precedencePath $precedenceName +$evidence=New-WelaArrivalOutput (Join-Path $env:RUNNER_TEMP ('wela-filesystem-lifecycle-'+$nonce)) $script:ScriptRoot +$targetRoot=New-WelaArrivalOutput (Join-Path (Join-Path $env:SystemRoot 'Temp') ('wela-filesystem-sacl-'+$nonce)) $script:ScriptRoot +$files=Join-Path $evidence 'owned-hive-files';$null=New-Item -ItemType Directory $files Save 'before-profiles.json' $beforeProfiles;Save 'before-hives.json' $beforeHives;Save 'before-token.json' $beforeToken;Save 'before-masks.json' $beforeMasks;Save 'before-precedence.json' $beforePrecedence $hive=[Wela.RegistrySaclFixture.Hive]::new($nonce,(Join-Path $files 'owned.dat'));$profile=$null;$failure=$null;$cleanupErrors=@();$policyTouched=$false;$auditGuid='0CCE921D-69AE-11D9-BED3-505054503030' try { $hive.Prepare();$profile=[Wela.FileSaclFixture.Profile]::new($nonce,$hive.Sid,$targetRoot);$profile.Prepare() $signal=Join-Path $profile.AppDataPath 'Signal';$null=New-Item -ItemType Directory $signal + $preparedProfiles=Key ([Wela.FileSaclFixture.Profile]::Snapshot()) + $collision=[Wela.FileSaclFixture.Profile]::new($nonce,$hive.Sid,$targetRoot);$refusal='' + try{$collision.Prepare()}catch{$refusal=$_.Exception.Message}finally{$collision.Dispose()} + Assert ($refusal -match 'already exists' -and -not $collision.Created -and (Key ([Wela.FileSaclFixture.Profile]::Snapshot())) -ceq $preparedProfiles) 'A real colliding ProfileList entry is never claimed, altered or removed by a new fixture owner.' + $ownedProfilePath='Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\'+$hive.Sid + try{ + Set-ItemProperty -LiteralPath $ownedProfilePath -Name ProfileImagePath -Type String -Value $profile.ProfilePath + $refusal='';try{$profile.Dispose()}catch{$refusal=$_.Exception.Message} + Assert ($refusal -match 'changed' -and $profile.Created -and (Test-Path -LiteralPath $ownedProfilePath)) 'Typed ownership drift refuses profile deletion despite identical text.' + }finally{Set-ItemProperty -LiteralPath $ownedProfilePath -Name ProfileImagePath -Type ExpandString -Value $profile.ProfilePath} + $profile.AssertOwned() + Assert ((Key ([Wela.FileSaclFixture.Profile]::Snapshot())) -ceq $preparedProfiles) 'Fixture-only ownership refusal test restores its exact registered profile tuple.' + Public 'catalog' @('targeted-sacl','-TargetSaclProfile','asd-native-2021-10','-IncludeOptional','-ResultsPath',(Join-Path $evidence 'catalog.json')) $catalog=Read-Receipt 'catalog.json' Save 'owned-profile.json' ([pscustomobject]@{Sid=$hive.Sid;Nonce=$nonce;Root=$targetRoot;ProfilePath=$profile.ProfilePath;AppDataPath=$profile.AppDataPath;SelectedPath=$signal}) diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 854679ee..c2099cb1 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 の使い捨て環境で、リダイレクトされたユーザーフォルダーの実カタログを使う公開ファイルシステム SACL 設定を検証します。Plan/DryRun/Configure、子の変化による拒否、再実行時の無変更を確認し、無関係なセキュリティ情報と保護された子孫を保持します。継承された末端ファイルの SACL を公開プローブの正確な Security4663 と照合し、型付きプロファイル、ハイブ、トークン、監査ポリシー、所有ファイルの後始末を記録とハッシュで確認します。本番のプロファイル読み込み、遠隔ユーザー、将来の子孫、Sigma の保証は行いません。 (関連 #373) (@Shirofune-Security) + - 明示的な `registry-sacl-recovery` を追加しました。整合する4つの元記録、レビュー済み計画のハッシュ、過去・現在ともに空の子キー観測、監査縮小と継承への個別同意を必須とし、追加が証明されたレジストリルートの明示的な監査ACEを1つだけ削除します。SACLのみのネイティブ書き込みで他の記述子フィールドとACEの順序を保持し、部分的な書き込みの証跡と元の記述子バイトとの一致を別々に報告します。過去のキー・操作者の同一性認証、ツリー全体の原子性、イベント生成、Sigmaの準備完了は保証しません。 (Related #373) (@Shirofune-Security) - Server 2022/2025とPowerShell 5.1/7でSMBポリシー設定の公開CLIを検証します。対応ホストで6項目の適用・再読取・再実行、非対応ホストのスキップ、元の型付き記録、無関係な設定の維持と完全な復元を確認します。イベント生成と実行時の有効化は別途検証します。(関連 #377) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 6b533916..648293c2 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,8 @@ **Improvements:** +- Added native public filesystem SACL lifecycle validation for one genuine redirected per-user catalog target on disposable Server 2022/2025 with PowerShell 5.1/7. Actual Plan/DryRun/Configure, stale-child refusal and idempotence preserve unrelated security and protected descendants; an inherited leaf SACL is checked against an exact public-probe Security4663. Retained receipts and hashes verify full typed profile, hive, token, audit-policy and owned-file cleanup without production profile loading, remote-user, future-child or Sigma claims. (Related #373) (@Shirofune-Security) + - Added opt-in `registry-sacl-recovery` for one proven explicit registry-root audit ACE, requiring four matching original records, a reviewed plan hash, empty historical/current descendants and separate audit-reduction/inheritance consent. Native SACL-only removal preserves unrelated descriptor fields and ACE order, retains partial-write evidence and reports original-byte equality separately; no authenticated historical key/operator identity, atomic-tree, event or Sigma claim. (Related #373) (@Shirofune-Security) - 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)