diff --git a/.github/workflows/native-profile-configure.yml b/.github/workflows/native-profile-configure.yml new file mode 100644 index 00000000..f1ebdfe2 --- /dev/null +++ b/.github/workflows/native-profile-configure.yml @@ -0,0 +1,47 @@ +name: Native public audit profile configuration +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + native-profile-configure: + 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/audit-profiles.Tests.ps1 + ./tests/IntegrationProfileConfiguration.Tests.ps1 + - name: Native profile configuration in Windows PowerShell5.1 + if: matrix.engine == 'powershell' + shell: powershell + run: ./tests/ProfileConfigure.Windows.Tests.ps1 -AllowDisposablePolicyWrite + - name: Fixtures and public guards in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: | + ./tests/audit-profiles.Tests.ps1 + ./tests/IntegrationProfileConfiguration.Tests.ps1 + - name: Native profile configuration in PowerShell7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: ./tests/ProfileConfigure.Windows.Tests.ps1 -AllowDisposablePolicyWrite + - name: Retain owned fixture evidence + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: native-profile-configure-${{ matrix.os }}-${{ matrix.engine }} + path: ${{ runner.temp }}/wela-profile-configure-*/ + if-no-files-found: warn + retention-days: 7 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index aef86c3e..83edb0b6 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,7 @@ **改善:** +- カスタム監査プロファイルの公開 Plan、DryRun、Configure、任意項目、再実行、Audit を Server 2022/2025 と Windows PowerShell 5.1/PowerShell 7 の破棄可能な環境で検証します。実際の優先設定、厳密値・最小値・維持の動作、変更前ジャーナル、全59監査マスクと復元を確認します。 (@Shirofune-Security) - `wec-listener` の Plan/Apply を追加し、割り当て済みIPv4に限定した新規HTTP5985リスナーを作成できるようにしました。ホスト・実行ユーザー・ソース・WinRMとファイアウォールの状態、計画ハッシュ、実行前記録とネイティブ再読取で変更を検証します。両PowerShellホストから固定のWindows PowerShell 5.1ワーカーを使用し、既存リスナーや状態変化を検出した場合は拒否します。転送到着やSigma対応は別途検証が必要です。 (@Shirofune-Security) - 明示的な`file-access-probe` Plan/Runを追加し、既存のReadData成功監査SACLが適用される通常のローカルファイルから1バイトだけ読み取ります。実装・実行中エンジンの選択をハッシュ処理前に拒否し、同じハンドルのDOS/NTパスと実体、読み取りと実体再確認の実測区間、実際のワーカー・トークン・ハンドル、ポリシー・セキュリティ・実装の一致を確認し、ローカルSecurity4663と永続化した専用の証拠を必要とします。内容は保持せず、ポリシー・ACL・ファイルデータを変更しません。失敗監査・転送・Sigma利用可能性は未検証です。Server 2022/2025と両PowerShellの使い捨てテストで実イベントと正確な復元を検証します。 (関連 #373) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4555ae93..d83efff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Improvements:** +- Add disposable native acceptance for public custom-profile Plan, DryRun, Configure, optional controls, idempotence and Audit on Server 2022/2025 under Windows PowerShell 5.1/PowerShell 7. Verify exact/minimum/preserve semantics, real precedence, original journals and all 59 masks, with exact fixture restoration. (@Shirofune-Security) - Added opt-in `wec-listener` Plan/Apply for one new assigned-IPv4 HTTP5985 listener. Reviewed host/operator/source and WinRM/firewall snapshots, explicit plan hashes, pending evidence and native readback guard creation and preserve existing settings. A fixed native Windows PowerShell 5.1 worker provides the creation adapter under both PowerShell host versions. Existing listeners and drift require review; forwarding arrival and Sigma readiness are not inferred. (@Shirofune-Security) - Added explicit `file-access-probe` Plan/Run for one byte read from one existing ordinary local leaf with a matching pre-existing ReadData success SACL. Source/engine targets are refused before hashing. Same-handle DOS/NT identity, exact worker/token/handle attribution over the measured read and identity-readback phase, full policy/security/source guards and durable private evidence require an actual local Security4663. No content is retained and no policy, ACL or file-data changes are made; failure, forwarding and Sigma readiness remain unverified. Disposable Server 2022/2025 tests cover both PowerShell engines and exact cleanup. (Related #373) (@Shirofune-Security) diff --git a/docs/audit-profiles.md b/docs/audit-profiles.md index afa8d512..3da93f00 100644 --- a/docs/audit-profiles.md +++ b/docs/audit-profiles.md @@ -90,3 +90,5 @@ RSoP schema references: [registry policy](https://learn.microsoft.com/en-us/prev Targeted file/registry SACL prerequisites are included as a read-only companion plan. See [targeted SACL planning](targeted-sacl-planning.md) for per-user gaps, source distinctions and `-SaclMode Skip`. The stronger profile's optional IPsec Main Mode control additionally requires positive local native prerequisite evidence during shared planning/configuration. See [conditional IPsec prerequisites](ipsec-prerequisites.md) for scope, statuses and fresh pre-write checks. + +A separate [public custom-profile native acceptance fixture](custom-audit-profiles.md#verification-and-recovery) exercises the shared configuration/precedence engine with actual writes on disposable Server 2022/2025 hosts. It verifies all 59 effective masks and exact cleanup without claiming full baseline, GPO, event or Sigma acceptance. diff --git a/docs/custom-audit-profiles.md b/docs/custom-audit-profiles.md index de29a981..ef5cfb4d 100644 --- a/docs/custom-audit-profiles.md +++ b/docs/custom-audit-profiles.md @@ -110,7 +110,14 @@ undo partially applied changes, restore a GPO, or invoke policy refresh. Re-run assessment after GPO/MDM refresh to verify effective state. Tests exercise malformed files, preservation modes, validation ordering, mocked -writes, prompt-time file changes and final drift. Windows CI performs real read-only -custom-profile audits on Server 2022/2025 with PowerShell 5.1/7. Configuration and -benign event/backend acceptance on Windows 11, DC and AD CS labs remain separate; +writes, prompt-time file changes and final drift. Windows CI also exercises the actual public Plan, DryRun, Configure and Audit +commands on disposable Server 2022/2025 hosts with PowerShell 5.1/7. A fixture-owned +custom file selects four canonical controls: minimum and exact masks, an explicit +optional control, and Not Configured preservation. Tests compare all 59 masks, +typed precedence, source fingerprints, native channels and original journals, +then verify exact fixture restoration. Invalid role selection is refused before +configuration, and repeated configuration makes no further native change. +These are hosted standalone servers classified by the shared profile engine as +MemberServer; no domain join or GPO refresh is simulated. Configuration and +benign event/backend acceptance on Windows 11, domain-joined servers, DC and AD CS labs remain separate; no clean-install or detection-coverage claim is made. diff --git a/tests/ProfileConfigure.Windows.Tests.ps1 b/tests/ProfileConfigure.Windows.Tests.ps1 new file mode 100644 index 00000000..5106a4aa --- /dev/null +++ b/tests/ProfileConfigure.Windows.Tests.ps1 @@ -0,0 +1,105 @@ +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 +Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force +Import-Module (Join-Path $repo 'modules/NativeProviders.psm1') -Force +. (Join-Path $repo 'scripts/Configuration.ps1') +$engine=(Get-Process -Id $PID).Path +$root=Join-Path $env:RUNNER_TEMP ('wela-profile-configure-'+[guid]::NewGuid().ToString('N')) +$null=New-Item -ItemType Directory -Path $root +$count=0;$failure=$null;$cleanupErrors=@() +function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++} +function Save($Name,$Value){ConvertTo-Json -InputObject $Value -Depth 30 | Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8} +function Key($Value){ConvertTo-Json -InputObject $Value -Depth 25 -Compress} +function Masks($Value){@($Value.Keys|Sort-Object|ForEach-Object{"$_=$($Value[$_])"}) -join ';'} +function Public([string]$Label,[string[]]$Arguments,[int]$Expected=0){ + $prior=$ErrorActionPreference + try{$ErrorActionPreference='Continue';$output=& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') @Arguments 2>&1|Out-String;$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior} + $output|Set-Content -LiteralPath (Join-Path $root ($Label+'.txt')) -Encoding UTF8 + Assert ($code -eq $Expected) "Public $Label exited $code, expected $Expected : $output" +} +function Channels { @(foreach($name in @('Security','System','Application','ForwardedEvents','Microsoft-Windows-CAPI2/Operational')){Get-WelaNativeChannel $name}) } +function TypedPrecedence {Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' SCENoApplyLegacyAuditPolicy} +$before=Get-WelaEffectiveAuditPolicy;$precedence=TypedPrecedence;$channels=Channels +$hostState=Get-WelaHostContext +Assert ($hostState.Role -eq 'MemberServer' -and $hostState.Build -in @(20348,26100)) 'Only the actual hosted server context is supported by this fixture.' +$catalog=Join-Path $repo 'config/audit_profiles.json';$example=Join-Path $repo 'config/custom-audit-profile.example.json' +$catalogHash=(Get-FileHash $catalog).Hash;$exampleHash=(Get-FileHash $example).Hash +$profilePath=Join-Path $root 'profile.json' +$custom=Get-Content $example -Raw|ConvertFrom-Json +$custom.profiles[0].id='custom-native-acceptance' +$custom.profiles[0].appliesTo=@([pscustomobject]@{roles=@($hostState.Role);minBuild=$hostState.Build;maxBuild=$hostState.Build}) +$custom.profiles[0].note='Disposable native acceptance fixture; no baseline or detection claim.' +Save 'profile.json' $custom +$sourceHash=(Get-FileHash $profilePath).Hash.ToLowerInvariant() +$ids=@{Creation='0CCE922B-69AE-11D9-BED3-505054503030';Termination='0CCE922C-69AE-11D9-BED3-505054503030';Share='0CCE9244-69AE-11D9-BED3-505054503030';File='0CCE921D-69AE-11D9-BED3-505054503030'} +$base=@('-Profile','custom-native-acceptance','-ProfileFile',$profilePath,'-SaclMode','Skip') +Save 'original.json' @{Host=$hostState;Engine=$PSVersionTable.PSVersion.ToString();Masks=$before;Precedence=$precedence;Channels=$channels;Sources=@{Custom=$sourceHash;Catalog=$catalogHash;Example=$exampleHash}} +try{ + # Fixture-only initial values distinguish exact, minimum, optional and NC semantics. + $null=New-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name SCENoApplyLegacyAuditPolicy -PropertyType DWord -Value 0 -Force + Set-WelaEffectiveAuditPolicy -Guid $ids.Creation -Mask 2 -Mode exact + Set-WelaEffectiveAuditPolicy -Guid $ids.Termination -Mask 3 -Mode exact + Set-WelaEffectiveAuditPolicy -Guid $ids.Share -Mask 1 -Mode exact + Set-WelaEffectiveAuditPolicy -Guid $ids.File -Mask 0 -Mode exact + $seed=Get-WelaEffectiveAuditPolicy;$seedPrecedence=TypedPrecedence + Save 'seeded.json' @{Masks=$seed;Precedence=$seedPrecedence} + $planPath=Join-Path $root 'plan.json' + Public 'plan' (@('plan')+$base+@('-PlanPath',$planPath)) + $plan=Get-Content $planPath -Raw|ConvertFrom-Json + Assert ($plan.role -eq $hostState.Role -and $plan.build -eq $hostState.Build -and $plan.policies.Count -eq 59) 'Public Plan retains actual context and all59 controls.' + Assert ($plan.CustomProfileSource.Sha256 -ceq $sourceHash) 'Plan binds the selected custom source bytes.' + $dryPath=Join-Path $root 'dry.json';$dryBackup=Join-Path $root 'dry-backup' + Public 'dry' (@('configure')+$base+@('-Auto','-DryRun','-BackupPath',$dryBackup,'-ResultsPath',$dryPath)) + $dry=Get-Content $dryPath -Raw|ConvertFrom-Json + Assert ($dry.DryRun -and $dry.ExitCode -eq 0 -and -not(Test-Path $dryBackup)) 'DryRun returns explicit preview without creating a journal.' + Assert ((Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $seed) -and (Key (TypedPrecedence)) -ceq (Key $seedPrecedence)) 'Plan/DryRun preserve all59 masks and typed precedence.' + Public 'wrong-role' (@('configure')+$base+@('-Auto','-Role','Client','-Build',[string]$hostState.Build,'-BackupPath',(Join-Path $root 'wrong-backup'),'-ResultsPath',(Join-Path $root 'wrong.json'))) 1 + Assert (-not(Test-Path (Join-Path $root 'wrong-backup')) -and (Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $seed)) 'Mismatched actual role refuses before native writes or a journal.' + $backup=Join-Path $root 'configure-backup';$resultPath=Join-Path $root 'configured.json' + Public 'configure' (@('configure')+$base+@('-Auto','-BackupPath',$backup,'-ResultsPath',$resultPath)) + $result=Get-Content $resultPath -Raw|ConvertFrom-Json + $expected=$seed.Clone();$expected[$ids.Creation]=3;$expected[$ids.Termination]=1 + Assert ((Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $expected)) 'Actual Configure enables minimum Success without clearing Failure, applies exact Success, and preserves optional/NC/omitted controls.' + Assert ((TypedPrecedence).Type -eq 'DWord' -and (TypedPrecedence).Value -eq 1) 'Public Configure applies and verifies actual DWORD precedence before audit writes.' + Assert ($result.ExitCode -eq 0 -and $result.Scope -ceq 'advanced-audit-policy-and-precedence' -and $result.ProfileScope -ceq 'advanced-audit-policy-only') 'Completed public results retain the narrow scope and success.' + Assert ($result.CustomProfileSource.Sha256 -ceq $sourceHash -and $result.CustomProfileSource.CanonicalSha256 -ieq $catalogHash) 'Completed result retains source and canonical catalog fingerprints.' + $journal=@(Get-Content (Join-Path $backup 'before.jsonl')|ConvertFrom-Json) + Assert ($journal.Count -eq 3 -and $journal[0].Target.Name -ceq 'SCENoApplyLegacyAuditPolicy') 'Only precedence and the two changed subcategories are journaled, in prerequisite order.' + foreach($entry in $journal){ + $row=@($result.Results|Where-Object Id -ceq $entry.Id) + Assert ($row.Count -eq 1 -and $row[0].Status -ceq 'Applied' -and (Key $row[0].Before) -ceq (Key $entry.Before)) 'Every native write has matching original journal and Applied result.' + } + $repeatPath=Join-Path $root 'repeat.json';$repeatBackup=Join-Path $root 'repeat-backup' + Public 'repeat' (@('configure')+$base+@('-Auto','-BackupPath',$repeatBackup,'-ResultsPath',$repeatPath)) + $repeat=Get-Content $repeatPath -Raw|ConvertFrom-Json + Assert ($repeat.ExitCode -eq 0 -and @($repeat.Results|Where-Object Status -eq 'Applied').Count -eq 0 -and (Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $expected)) 'Repeated public Configure is idempotent with no native write.' + $optionalPath=Join-Path $root 'optional.json' + Public 'optional' (@('configure')+$base+@('-Auto','-IncludeOptional','-BackupPath',(Join-Path $root 'optional-backup'),'-ResultsPath',$optionalPath)) + $optional=Get-Content $optionalPath -Raw|ConvertFrom-Json;$expected[$ids.File]=3 + Assert ($optional.ExitCode -eq 0 -and (Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $expected)) 'Explicit IncludeOptional changes only File System; all other masks are preserved.' + Assert (@($optional.Results|Where-Object Status -eq 'Applied').Count -eq 1) 'Optional second stage records exactly one applied control.' + $auditPath=Join-Path $root 'audit.json' + Public 'audit' (@('audit-settings')+$base+@('-IncludeOptional','-PlanPath',$auditPath)) + $audit=Get-Content $auditPath -Raw|ConvertFrom-Json + Assert ($audit.policies.Count -eq 59 -and $audit.CustomProfileSource.Sha256 -ceq $sourceHash) 'Post-configure public Audit reads the same59 controls and source.' + Assert ((Key (Channels)) -ceq (Key $channels)) 'Advanced-audit-only configuration preserves native channel configuration.' + Assert ((Get-FileHash $profilePath).Hash -ieq $sourceHash -and (Get-FileHash $catalog).Hash -ceq $catalogHash -and (Get-FileHash $example).Hash -ceq $exampleHash) 'No input policy or canonical source file was changed.' + Save 'completed.json' @{Status='Passed';Assertions=$count;ExpectedMasks=$expected;ObservedMasks=Get-WelaEffectiveAuditPolicy;Scope='Actual public custom-profile advanced policy/precedence only; no GPO refresh, event generation or Sigma claim.'} +}catch{$failure=$_.ToString();throw}finally{ + try{ + $now=Get-WelaEffectiveAuditPolicy + foreach($guid in $before.Keys){if($now[$guid] -ne $before[$guid]){Set-WelaEffectiveAuditPolicy -Guid $guid -Mask $before[$guid] -Mode exact}} + }catch{$cleanupErrors+=$_.ToString()} + try{ + if($precedence.ValueExists){$null=New-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name SCENoApplyLegacyAuditPolicy -PropertyType $precedence.Type -Value $precedence.Value -Force} + else{Remove-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name SCENoApplyLegacyAuditPolicy -ErrorAction Stop} + }catch{$cleanupErrors+=$_.ToString()} + $masksOk=$false;$precedenceOk=$false;$channelsOk=$false + try{$masksOk=(Masks (Get-WelaEffectiveAuditPolicy)) -ceq (Masks $before);$precedenceOk=(Key (TypedPrecedence)) -ceq (Key $precedence);$channelsOk=(Key (Channels)) -ceq (Key $channels)}catch{$cleanupErrors+=$_.ToString()} + Save 'cleanup.json' @{Failure=$failure;Errors=$cleanupErrors;All59MasksRestored=$masksOk;TypedPrecedenceRestored=$precedenceOk;ChannelsPreserved=$channelsOk;Complete=($masksOk -and $precedenceOk -and $channelsOk -and -not $cleanupErrors.Count)} + if(-not $masksOk -or -not $precedenceOk -or -not $channelsOk -or $cleanupErrors.Count){throw 'Native profile fixture cleanup failed; inspect retained evidence.'} +} +Write-Host "PASS: $count public native profile configuration assertions and exact cleanup." +exit 0 diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index b7c56a3d..b4281717 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,7 @@ **改善:** +- カスタム監査プロファイルの公開 Plan、DryRun、Configure、任意項目、再実行、Audit を Server 2022/2025 と Windows PowerShell 5.1/PowerShell 7 の破棄可能な環境で検証します。実際の優先設定、厳密値・最小値・維持の動作、変更前ジャーナル、全59監査マスクと復元を確認します。 (@Shirofune-Security) - `wec-listener` の Plan/Apply を追加し、割り当て済みIPv4に限定した新規HTTP5985リスナーを作成できるようにしました。ホスト・実行ユーザー・ソース・WinRMとファイアウォールの状態、計画ハッシュ、実行前記録とネイティブ再読取で変更を検証します。両PowerShellホストから固定のWindows PowerShell 5.1ワーカーを使用し、既存リスナーや状態変化を検出した場合は拒否します。転送到着やSigma対応は別途検証が必要です。 (@Shirofune-Security) - 明示的な`file-access-probe` Plan/Runを追加し、既存のReadData成功監査SACLが適用される通常のローカルファイルから1バイトだけ読み取ります。実装・実行中エンジンの選択をハッシュ処理前に拒否し、同じハンドルのDOS/NTパスと実体、読み取りと実体再確認の実測区間、実際のワーカー・トークン・ハンドル、ポリシー・セキュリティ・実装の一致を確認し、ローカルSecurity4663と永続化した専用の証拠を必要とします。内容は保持せず、ポリシー・ACL・ファイルデータを変更しません。失敗監査・転送・Sigma利用可能性は未検証です。Server 2022/2025と両PowerShellの使い捨てテストで実イベントと正確な復元を検証します。 (関連 #373) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 2e489bf6..dcd52f80 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,7 @@ **Improvements:** +- Add disposable native acceptance for public custom-profile Plan, DryRun, Configure, optional controls, idempotence and Audit on Server 2022/2025 under Windows PowerShell 5.1/PowerShell 7. Verify exact/minimum/preserve semantics, real precedence, original journals and all 59 masks, with exact fixture restoration. (@Shirofune-Security) - Added opt-in `wec-listener` Plan/Apply for one new assigned-IPv4 HTTP5985 listener. Reviewed host/operator/source and WinRM/firewall snapshots, explicit plan hashes, pending evidence and native readback guard creation and preserve existing settings. A fixed native Windows PowerShell 5.1 worker provides the creation adapter under both PowerShell host versions. Existing listeners and drift require review; forwarding arrival and Sigma readiness are not inferred. (@Shirofune-Security) - Added explicit `file-access-probe` Plan/Run for one byte read from one existing ordinary local leaf with a matching pre-existing ReadData success SACL. Source/engine targets are refused before hashing. Same-handle DOS/NT identity, exact worker/token/handle attribution over the measured read and identity-readback phase, full policy/security/source guards and durable private evidence require an actual local Security4663. No content is retained and no policy, ACL or file-data changes are made; failure, forwarding and Sigma readiness remain unverified. Disposable Server 2022/2025 tests cover both PowerShell engines and exact cleanup. (Related #373) (@Shirofune-Security)