diff --git a/.github/workflows/audit-notifications.yml b/.github/workflows/audit-notifications.yml new file mode 100644 index 00000000..b4232fd6 --- /dev/null +++ b/.github/workflows/audit-notifications.yml @@ -0,0 +1,28 @@ +name: Audit notification regressions +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + audit-notifications: + strategy: + matrix: + os: [windows-2022, windows-2025] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Fixtures on Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditNotifications.Tests.ps1 + - name: Native read-only observations on Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditNotifications.Windows.Tests.ps1 + - name: Fixtures on PowerShell 7 + shell: pwsh + run: ./tests/AuditNotifications.Tests.ps1 + - name: Native read-only observations on PowerShell 7 + shell: pwsh + run: ./tests/AuditNotifications.Windows.Tests.ps1 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 9167bedb..3d2a8ef9 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,8 @@ **改善:** +- 任意実行の`audit-notifications`を追加し、OneSettings監査とSecurityログ警告しきい値の監査・計画・設定に対応しました。対象とチャネル変更の明示指定、OS・ADMX確認、型付き復旧記録と変更検出を行い、既存の低いしきい値とチャネルACL・保存方式を保持します。ポリシー一致と実イベント生成を分け、Windowsラボの証拠とSigma利用可能性は未検証と表示します。 (#408) (@Shirofune-Security) + - 読み取り専用の`rule-eligibility`を追加し、ルール・対応表のハッシュ、ルールごとの判定理由、対象外の理由、分子・分母を明示します。任意で取り込んだラボ資料を、対応範囲を限定した完全なルール定義、ネイティブXML、設定、収集・クエリ実行の証拠と照合し、未対応・未確認の項目はConditionalとします。監査のCSV/JSON/HTMLとNavigator出力では、設定が有効なだけでルールを利用可能と判定しません。取り込んだReady判定は記録された環境・時点に限られ、実環境での一連の検証を保証しません。 (#407) (@Shirofune-Security) - Windows標準のドメイン/Kerberos環境向けに、送信元設定と明示的に選択した収集サーバーのサブスクリプションを監査・計画・設定する任意実行の`wef-source`と`wec-collector`を追加しました。実際の収集先ID、既存リスナーと範囲を限定した受信規則、送信元SID、メンバーホストでの読み取り権限の追加、明示的なASDのWSMan強化設定と共通チャネル設定を、復旧記録・変更検出・読み戻しで確認します。DCのグループ管理権限と異なる既存サブスクリプションは変更しません。JSONにはクエリ・チャネル・実行状態の証拠を保持し、実効読み取り権限・イベント到着・転送後のSigma検知範囲は未検証と表示します。隔離Windows環境での配備検証は別途必要です。 (#406) (@Shirofune-Security) - CIS v4.0.0 Level 2向けに、Windows PowerShell 5.1トランスクリプトの明示的な監査・計画・設定機能を追加しました。管理者が選択した既存の出力ディレクトリを確認し、ポリシーとは分けて報告します。型を含むレジストリ変更前の状態を保存し、共有される32/64ビットのビューと変更後の状態を検証します。呼び出しヘッダーの設定は保持し、ACL・共有・保存期間は変更せず、Sigma EVTX検知範囲の向上も自動加算しません。使い捨て環境の実トランスクリプトテストでは元のポリシーを復元します。中央保存先の権限と収集は別途検証が必要です。 (#405) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7d062a0..b54ca6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Improvements:** +- Added opt-in `audit-notifications` audit/plan/configure for OneSettings auditing and Security log warning thresholds, with explicit control/channel selections, reviewed host and ADMX gates, typed recovery journals and drift checks. Earlier warning thresholds and channel ACL/retention are preserved. Reports separate policy matches from warning/event generation; Windows lab evidence and Sigma eligibility remain unverified. (#408) (@Shirofune-Security) + - Added read-only `rule-eligibility` reports with pinned corpus/mapping hashes, per-rule reasons, explicit scope exclusions and numerator/denominator totals. Optional imported lab artifacts are checked against a narrow complete-rule parser, native XML, configuration, ingestion and query evidence; unsupported or incomplete cases stay Conditional. Audit CSV/JSON/HTML and Navigator outputs no longer treat enabled settings as proven usable rules. Imported Ready results apply only to their recorded context/time; no live end-to-end validation is implied. (#407) (@Shirofune-Security) - Added separate opt-in `wef-source` and `wec-collector` audit, plan and configure commands for native domain/Kerberos source settings and explicitly selected collector subscriptions. Actual collector identity, scoped existing ingress/listener prerequisites, explicit source SIDs, additive member-host read permissions, optional ASD WSMan hardening and shared channel controls are checked with journals, drift guards and readback. DC group authority and different existing subscriptions are preserved. JSON retains query/channel/runtime evidence without claiming effective read access, event arrival or forwarded Sigma coverage; isolated Windows deployment validation remains pending. (#406) (@Shirofune-Security) - Added explicit CIS v4.0.0 Level 2 Windows PowerShell 5.1 transcription audit, plan and configure actions. An operator-selected existing output directory is checked and reported separately from policy; typed canonical registry writes are journaled, verified through shared 32/64-bit views and checked for drift while preserving invocation-header preferences. No ACL/share/retention changes or automatic Sigma EVTX credit are introduced; disposable native transcript tests restore original policy, and central authorization/collection remains a deployment check. (#405) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index e9f80d08..fc5f8c69 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -49,6 +49,10 @@ [ValidateRange(1,2147483647)][int]$LdapSearchTimeMs, [ValidateRange(1,2147483647)][int]$LdapExpensiveThreshold, [ValidateRange(1,2147483647)][int]$LdapInefficientThreshold, + [ValidateSet('Audit','Plan','Configure')][string]$NotificationAction = 'Audit', + [ValidateSet('OneSettings','SecurityWarning')][string[]]$NotificationControl, + [ValidateRange(1,90)][int]$WarningPercent = 90, + [switch]$EnablePrivacyChannel, [switch]$Help ) @@ -66,6 +70,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" . (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1") . (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1") . (Join-Path $ScriptRoot "scripts/LdapDiagnostics.ps1") +. (Join-Path $ScriptRoot "scripts/AuditNotifications.ps1") . (Join-Path $ScriptRoot "scripts/AdObjectSacl.ps1") . (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1") . (Join-Path $ScriptRoot "scripts/WmiNamespaceAuditing.ps1") @@ -1781,6 +1786,7 @@ Usage: ./WELA.ps1 configure-sacl # Add targeted File System/Registry audit SACLs (ASEP keys + sensitive files) needed by the rules, without global auditing ./WELA.ps1 configure-sacl -Auto # ...automatically without prompts ./WELA.ps1 update-rules # Update rule config files from https://github.com/Yamato-Security/WELA + ./WELA.ps1 audit-notifications -Help # OneSettings audit and Security warning policy ./WELA.ps1 version # Show the WELA version ./WELA.ps1 help # Show this help "@ @@ -1792,6 +1798,14 @@ Write-Host "" Write-Host "WELA v$WELAVersion - $WELAReleaseName" Write-Host "" +if ($Cmd -eq 'audit-notifications' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','NotificationAction','NotificationControl','WarningPercent','EnablePrivacyChannel','Auto','DryRun','BackupPath','ResultsPath','Help') }).Count) { + throw 'audit-notifications accepts only notification, consent/dry-run, recovery and JSON output options. No command was run.' +} + +if ($Cmd -ne 'audit-notifications' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('NotificationAction','NotificationControl','WarningPercent','EnablePrivacyChannel') }).Count) { + throw 'Notification options require audit-notifications. No command was run.' +} + if ($Cmd -ne 'rule-eligibility' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('RuleEvidencePath', 'RuleCorpusPath', 'RuleManifestPath') }).Count) { throw '-RuleEvidencePath, -RuleCorpusPath and -RuleManifestPath require the read-only rule-eligibility command. No command was run.' } @@ -1820,7 +1834,7 @@ if ($Cmd -ne 'ad-object-sacl' -and @($PSBoundParameters.Keys | Where-Object { }).Count) { throw 'AD object SACL options require the dedicated ad-object-sacl command. No command was run.' } -if ($DryRun -and -not ($Cmd -eq 'ldap-diagnostics' -and $LdapAction -eq 'Configure') -and -not ($Cmd -eq 'applocker-readiness' -and $AppLockerAction -eq 'Import') -and $Cmd -notin @('configure', 'configure-eventlogs') -and +if ($DryRun -and -not ($Cmd -eq 'audit-notifications' -and $NotificationAction -eq 'Configure') -and -not ($Cmd -eq 'ldap-diagnostics' -and $LdapAction -eq 'Configure') -and -not ($Cmd -eq 'applocker-readiness' -and $AppLockerAction -eq 'Import') -and $Cmd -notin @('configure', 'configure-eventlogs') -and -not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure') -and -not ($Cmd -eq 'smb-auditing' -and $SmbAction -eq 'Configure') -and -not ($Cmd -eq 'powershell-transcription' -and $TranscriptionAction -eq 'Configure') -and @@ -1828,7 +1842,7 @@ if ($DryRun -and -not ($Cmd -eq 'ldap-diagnostics' -and $LdapAction -eq 'Configu -not ($Cmd -in @('wef-source','wec-collector') -and $WefAction -eq 'Configure') -and -not ($Cmd -eq 'ad-object-sacl' -and $AdSaclAction -in @('Configure', 'Rollback')) -and -not ($Cmd -eq 'wmi-auditing' -and $WmiAction -eq 'Configure')) { - throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs, firewall-logging -FirewallAction Configure, smb-auditing -SmbAction Configure, powershell-transcription -TranscriptionAction Configure, wmi-auditing -WmiAction Configure, channel-settings -ChannelAction Configure, wef-source/wec-collector -WefAction Configure, applocker-readiness -AppLockerAction Import, ad-object-sacl -AdSaclAction Configure|Rollback, and ldap-diagnostics -LdapAction Configure. No command was run." + throw "-DryRun is supported only by configure (including configure -Profile), configure-eventlogs, firewall-logging -FirewallAction Configure, smb-auditing -SmbAction Configure, powershell-transcription -TranscriptionAction Configure, wmi-auditing -WmiAction Configure, channel-settings -ChannelAction Configure, wef-source/wec-collector -WefAction Configure, applocker-readiness -AppLockerAction Import, ad-object-sacl -AdSaclAction Configure|Rollback, ldap-diagnostics -LdapAction Configure, and audit-notifications -NotificationAction Configure. No command was run." } if (($WmiNamespace -or $WmiIncludeChildren -or $PSBoundParameters.ContainsKey('WmiAction')) -and $Cmd -ne 'wmi-auditing') { throw '-WmiAction, -WmiNamespace and -WmiIncludeChildren require wmi-auditing. No command was run.' @@ -1858,6 +1872,15 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi } switch ($Cmd.ToLower()) { + 'audit-notifications' { + if ($Help) { Write-Host 'Usage: ./WELA.ps1 audit-notifications [-NotificationAction Audit|Plan|Configure] [-NotificationControl OneSettings,SecurityWarning] [-WarningPercent 1..90] [-EnablePrivacyChannel] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath report.json]. See docs/audit-notifications.md.'; return } + if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath) { throw 'audit-notifications uses actual host context and -ResultsPath; profile/role/build overrides and HTML are unsupported.' } + if ($NotificationAction -eq 'Configure' -and -not (TestAdministrator)) { throw 'Notification Configure requires Administrator privileges.' } + $report=Invoke-WelaNotificationCommand -Action $NotificationAction -Control $NotificationControl -WarningPercent $WarningPercent -EnablePrivacyChannel:$EnablePrivacyChannel -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $report + if ($report.ExitCode) { exit $report.ExitCode } + } + 'rule-eligibility' { if ($Profile -or $Baseline -or $Auto -or $PlanPath) { throw 'rule-eligibility reviews native rule metadata and optional lab artifacts; use -ResultsPath/-HtmlPath, not configuration options.' } $arguments = @{} diff --git a/docs/audit-notifications.md b/docs/audit-notifications.md new file mode 100644 index 00000000..3f16df0a --- /dev/null +++ b/docs/audit-notifications.md @@ -0,0 +1,83 @@ +# OneSettings auditing and Security log warnings + +`audit-notifications` is a separate, read-only-by-default command for two native +Windows controls. It does not change normal `configure` behavior or add Sigma +eligibility. Sysmon is out of scope. + +```powershell +./WELA.ps1 audit-notifications -ResultsPath notifications.json +./WELA.ps1 audit-notifications -NotificationAction Plan -NotificationControl OneSettings,SecurityWarning -EnablePrivacyChannel +./WELA.ps1 audit-notifications -NotificationAction Configure -NotificationControl OneSettings,SecurityWarning -EnablePrivacyChannel -WarningPercent 90 -DryRun +./WELA.ps1 audit-notifications -NotificationAction Configure -NotificationControl SecurityWarning -WarningPercent 80 -Auto -BackupPath C:\Evidence\warnings-before -ResultsPath C:\Evidence\warnings.json +``` + +Configure requires Administrator, 64-bit PowerShell and explicit control selection. +Audit and Plan do not modify settings. Options used with another command fail +before dispatch. `-WarningPercent` is a maximum (1–90); an existing positive DWORD +at or below that maximum is preserved. An absent/zero/higher threshold is set to +the selected maximum. An unexpected registry type is preserved and fails the +configuration. Missing values remain missing in the recovery evidence, rather +than being conflated with documented defaults. + +| Control | Registry policy | Reviewed source | +|---|---|---| +| OneSettings | `HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection\EnableOneSettingsAuditing`, DWORD 1 | CIS Windows 11 Enterprise and Windows Server 2022 v4.0.0, 18.10.16.5; Microsoft System CSP | +| SecurityWarning | `HKLM\SYSTEM\CurrentControlSet\Services\Eventlog\Security\WarningLevel`, DWORD 1–90 | CIS Windows 11 Enterprise v4.0.0 18.5.13 / Server 2022 v4.0.0 18.5.12; Microsoft Windows guest baseline | + +Microsoft documents OneSettings for Windows 11 21H2 onward. WELA reviews client +builds 22000/22621/22631/26100/26200 and Server 2022 build 20348 (the latter is an +explicit CIS recommendation, not an inference from client CSP support). It also +requires the exact local `DataCollection.admx` machine policy/key/DWORD mapping +and readable `Microsoft-Windows-Privacy-Auditing/Operational` metadata. The ADMX +hash is retained. Server 2025 OneSettings remains Unknown pending reviewed +support evidence. Unknown builds, policy values other than 0/1, missing templates, +or missing/unreadable channels block OneSettings changes. Merely installing an +ADMX is not enough to pass the host/source gate. + +SecurityWarning supports the listed Windows 11 builds and Server 2022/2025 +(20348/26100), including DC and CA hosts. The actual OS product type and domain +role must agree. This is the same longstanding Eventlog registry control; Server +2025 support does not imply a CIS Server 2025 recommendation was reviewed. + +`-EnablePrivacyChannel` separately authorizes enabling the Privacy Operational +channel after OneSettings policy succeeds. It rechecks producer prerequisites at the shared channel read/write/final-check +boundaries and uses shared channel journaling, stale-state checks and readback, retaining the ACL, log mode and existing size +(the shared technical minimum is 64 KiB, not a CIS sizing recommendation). Without +this switch, a disabled channel is reported and preserved. No diagnostic-data +level, OneSettings download/network policy, service state or forwarding setting +is changed. + +Security channel metadata and warning usefulness are separate from registry +compliance. Circular overwrite suppresses this warning; Retain is only a +conditional prerequisite. AutoBackup behavior and unreadable modes are Unknown. +This command never changes retention, fills/clears a log, changes +CrashOnAuditFail, or claims disk-space, archival or forwarding health. + +Every mutation uses the shared typed pre-change journal, fresh-state guards, +readback and final drift verification. Recover only the recorded named values +(and channel state if requested) using the [recovery procedure](configuration-results.md). +A partial failure is reported; successful earlier changes are not automatically +rolled back over another administrator's work. Domain GPO/MDM may subsequently +override local policy. Registry verification is not proof of effective producer +behavior or policy persistence. + +## Validation and remaining evidence + +Fixture tests cover absent/typed values, threshold preservation, role/source and +ADMX/channel gates, stale plans, races, failed/ignored writes, final drift, +idempotence, dry-run and command dispatch. Windows Server 2022/2025 CI observes +native registry/CIM/channel state without changing policy, under PowerShell 5.1 +and 7. This is not a Windows 11, DC or AD CS event-generation test. + +Before closing issue #378, retain isolated Windows 11 and Server 2022 evidence of +an authorized benign OneSettings attempt with exact build/patch, policy, channel, +native event XML and collection result. Do not assume an EventID without inspecting +the provider on that build. Test warning behavior using disposable Security logs +under explicitly controlled retention/CrashOnAuditFail conditions; never exhaust +a production Security log. Record GPO refresh behavior and verify the intended +reader/collector. Neither registry DWORD grants usable-rule credit. + +Sources: [Microsoft System CSP](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-system#enableonesettingsauditing), +[Microsoft Windows guest baseline](https://learn.microsoft.com/en-us/azure/governance/policy/samples/guest-configuration-baseline-windows), +[CIS Windows benchmarks](https://www.cisecurity.org/benchmark/microsoft_windows_desktop). +Reviewed CIS documents are v4.0.0 (client 18.10.16.5/18.5.13; Server 18.10.16.5/18.5.12). diff --git a/scripts/AuditNotifications.ps1 b/scripts/AuditNotifications.ps1 new file mode 100644 index 00000000..0b9518a8 --- /dev/null +++ b/scripts/AuditNotifications.ps1 @@ -0,0 +1,187 @@ +# Explicit native audit policy controls. Read-only unless Configure is selected. +function Get-WelaNotificationHost { + if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { return [pscustomobject]@{Status='NotApplicable';Diagnostic='Windows required.'} } + try { + if (-not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell for the native policy registry view.' } + $os=Get-CimInstance Win32_OperatingSystem -ErrorAction Stop + $computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop + if (($os.ProductType -eq 1 -and $computer.DomainRole -notin @(0,1)) -or + ($os.ProductType -eq 2 -and $computer.DomainRole -notin @(4,5)) -or + ($os.ProductType -eq 3 -and $computer.DomainRole -notin @(2,3)) -or $os.ProductType -notin @(1,2,3)) { throw 'Unknown or conflicting Windows role observations.' } + $build=[int]$os.BuildNumber + $supported=($os.ProductType -eq 1 -and $build -in @(22000,22621,22631,26100,26200)) -or + ($os.ProductType -in @(2,3) -and $build -in @(20348,26100)) + [pscustomobject]@{Status=$(if ($supported) {'Supported'} else {'Unknown'});Build=$build;ProductType=[int]$os.ProductType;DomainRole=[int]$computer.DomainRole;Caption=[string]$os.Caption;Diagnostic='Reviewed Windows 11 / Server 2022 and 2025 host families; individual controls have additional gates.'} + } catch { [pscustomobject]@{Status='Unknown';Diagnostic=$_.Exception.Message} } +} + +function Get-WelaNotificationDefinitions { + @( + [pscustomobject]@{Id='OneSettings';Path='HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection';Name='EnableOneSettingsAuditing';Channel='Microsoft-Windows-Privacy-Auditing/Operational';Source='CIS Windows 11 Enterprise / Windows Server 2022 v4.0.0, 18.10.16.5';DocumentedDefault='Disabled (source reference; not a clean-host observation)'} + [pscustomobject]@{Id='SecurityWarning';Path='HKLM:\SYSTEM\CurrentControlSet\Services\Eventlog\Security';Name='WarningLevel';Channel='Security';Source='CIS Windows 11 Enterprise v4.0.0 18.5.13 / Windows Server 2022 v4.0.0 18.5.12';DocumentedDefault='0 percent (source reference; not a clean-host observation)'} + ) +} + +function Get-WelaOneSettingsDefinitionEvidence { + $path=Join-Path $env:windir 'PolicyDefinitions/DataCollection.admx' + $settings=New-Object Xml.XmlReaderSettings + $settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit; $settings.XmlResolver=$null + $reader=[Xml.XmlReader]::Create($path,$settings) + try { + $doc=New-Object Xml.XmlDocument; $doc.XmlResolver=$null; $doc.Load($reader) + $policies=@($doc.SelectNodes("//*[local-name()='policy']") | Where-Object { + $_.GetAttribute('name') -eq 'EnableOneSettingsAuditing' -and $_.GetAttribute('class') -eq 'Machine' -and + $_.GetAttribute('key') -eq 'Software\Policies\Microsoft\Windows\DataCollection' -and $_.GetAttribute('valueName') -eq 'EnableOneSettingsAuditing' + }) + if ($policies.Count -ne 1) { throw 'Exact OneSettings machine policy mapping is absent or ambiguous.' } + $enabled=$policies[0].SelectSingleNode("./*[local-name()='enabledValue']/*[local-name()='decimal']") + if (-not $enabled -or $enabled.GetAttribute('value') -ne '1') { throw 'OneSettings enabled DWORD definition is not 1.' } + [pscustomobject]@{Path=$path;Sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash;Mapping='EnableOneSettingsAuditing DWORD 1'} + } finally { $reader.Dispose() } +} + +function Get-WelaNotificationSnapshot { + param($Definition) + $hostState=Get-WelaNotificationHost + $result=[pscustomobject]@{Host=$hostState;Status=$hostState.Status;Policy=$null;Channel=$null;DefinitionEvidence=$null;WarningGeneration='Not applicable to this control';Diagnostic=$hostState.Diagnostic} + if ($hostState.Status -ne 'Supported') { return $result } + try { + if ($Definition.Id -eq 'OneSettings') { + # CIS Server 2022 explicitly includes this control. The client CSP alone + # does not establish Server 2025 support; leave that family unverified. + if ($hostState.ProductType -ne 1 -and $hostState.Build -ne 20348) { throw 'OneSettings on this server release lacks reviewed source support; no policy will be created.' } + $result.DefinitionEvidence=Get-WelaOneSettingsDefinitionEvidence + } + $result.Policy=Get-WelaRegistryState -Path $Definition.Path -Name $Definition.Name + $result.Channel=Get-WelaNativeChannel -Name $Definition.Channel + if ($result.Policy.ValueExists -and $result.Policy.Type -ne 'DWord') { throw 'Existing policy value has an unexpected registry type; preserved for manual review.' } + if ($Definition.Id -eq 'OneSettings') { + if ($result.Policy.ValueExists -and $result.Policy.Value -notin @(0,1)) { throw 'Unknown OneSettings DWORD value; preserved for manual review.' } + if (-not (Test-WelaNativeChannelSnapshot $result.Channel)) { throw 'Privacy-Auditing channel metadata is absent or unreadable; event logging prerequisites are not established.' } + $result.Diagnostic='Registry policy and native channel observed; OneSettings event generation and forwarding remain unverified.' + } else { + $result.WarningGeneration=switch ($result.Channel.LogMode) { + 'Circular' {'Not expected: overwrite mode suppresses this warning'} + 'Retain' {'Conditional: retained log mode; threshold event still requires lab validation'} + 'AutoBackup' {'Unknown: automatic archive rollover behavior requires lab validation'} + default {'Unknown: Security log retention mode unreadable'} + } + $result.Diagnostic='Threshold compliance is independent of warning generation, disk space, archive retention and forwarding health.' + } + } catch { $result.Status='Unknown'; $result.Diagnostic=$_.Exception.Message } + return $result +} + +function Test-WelaNotificationValueEqual { + param($First,$Second) + return $First.ValueExists -eq $Second.ValueExists -and $First.Type -ceq $Second.Type -and + (ConvertTo-Json $First.Value -Compress) -ceq (ConvertTo-Json $Second.Value -Compress) +} + +function Get-WelaNotificationPlan { + param([ValidateSet('OneSettings','SecurityWarning')][string[]]$Control=@('OneSettings','SecurityWarning'),[ValidateRange(1,90)][int]$WarningPercent=90) + foreach ($definition in Get-WelaNotificationDefinitions) { + if ($definition.Id -notin $Control) { continue } + $snapshot=Get-WelaNotificationSnapshot $definition + $desired=if ($definition.Id -eq 'OneSettings') {1} else {$WarningPercent} + # Threshold is an upper bound: preserve an existing earlier warning. + if ($definition.Id -eq 'SecurityWarning' -and $snapshot.Policy.ValueExists -and $snapshot.Policy.Type -eq 'DWord' -and + $snapshot.Policy.Value -ge 1 -and $snapshot.Policy.Value -le $WarningPercent) { $desired=[int]$snapshot.Policy.Value } + $matches=$snapshot.Status -eq 'Supported' -and $snapshot.Policy.ValueExists -and $snapshot.Policy.Type -eq 'DWord' -and $snapshot.Policy.Value -eq $desired + [pscustomobject]@{Definition=$definition;Before=$snapshot;Desired=$desired;Status=$(if ($snapshot.Status -ne 'Supported') {$snapshot.Status} elseif ($matches) {'PolicyMatches'} else {'ChangeRequired'});ThresholdMaximum=$WarningPercent} + } +} + +function Set-WelaNotificationControls { + param($Context,[array]$Plan) + foreach ($entry in $Plan) { + $state=@{Entry=$entry;Initial=$true;JournalState=$null} + $read={ + param($state) + $current=Get-WelaNotificationSnapshot $state.Entry.Definition + if ($current.Status -ne 'Supported') { throw "Control prerequisites unavailable: $($current.Diagnostic)" } + if ($state.Initial) { + if ((ConvertTo-Json $current.Host -Compress) -cne (ConvertTo-Json $state.Entry.Before.Host -Compress) -or + -not (Test-WelaNotificationValueEqual $current.Policy $state.Entry.Before.Policy)) { throw 'Notification plan is stale; review a fresh plan.' } + $state.JournalState=$current; $state.Initial=$false + } + return $current + } + $test={param($current,$state) $current.Policy.ValueExists -and $current.Policy.Type -eq 'DWord' -and $current.Policy.Value -eq $state.Entry.Desired} + $apply={ + param($state) + $fresh=Get-WelaNotificationSnapshot $state.Entry.Definition + if ($fresh.Status -ne 'Supported' -or + (ConvertTo-Json $fresh.Host -Compress) -cne (ConvertTo-Json $state.JournalState.Host -Compress) -or + (ConvertTo-Json $fresh.DefinitionEvidence -Compress) -cne (ConvertTo-Json $state.JournalState.DefinitionEvidence -Compress) -or + -not (Test-WelaNotificationValueEqual $fresh.Policy $state.JournalState.Policy)) { throw 'Notification state changed after journaling; write refused.' } + New-WelaRegistryKey -Path $state.Entry.Definition.Path + $value=Get-WelaRegistryState -Path $state.Entry.Definition.Path -Name $state.Entry.Definition.Name + if (-not (Test-WelaNotificationValueEqual $value $fresh.Policy)) { throw 'Notification registry value changed immediately before writing.' } + Set-ItemProperty -LiteralPath $state.Entry.Definition.Path -Name $state.Entry.Definition.Name -Value $state.Entry.Desired -Type DWord -ErrorAction Stop + } + Invoke-WelaConfigurationControl -Context $Context -Id "AuditNotifications/$($entry.Definition.Id)" -Kind Registry -Target @{Path=$entry.Definition.Path;Name=$entry.Definition.Name} -Desired @{Type='DWord';Value=$entry.Desired} -Read $read -Compliant $test -Apply $apply -CallbackState $state + } +} + +function Invoke-WelaNotificationCommand { + param([ValidateSet('Audit','Plan','Configure')][string]$Action='Audit', + [ValidateSet('OneSettings','SecurityWarning')][string[]]$Control, + [ValidateRange(1,90)][int]$WarningPercent=90,[switch]$EnablePrivacyChannel, + [switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath) + if ($Action -eq 'Configure' -and -not $Control) { throw 'Configure requires an explicit NotificationControl selection.' } + if (-not $Control) { $Control=@('OneSettings','SecurityWarning') } + if ($EnablePrivacyChannel -and 'OneSettings' -notin $Control) { throw 'EnablePrivacyChannel requires the OneSettings control.' } + if ($DryRun -and $Action -ne 'Configure') { throw 'DryRun requires Configure.' } + $plan=@(Get-WelaNotificationPlan -Control $Control -WarningPercent $WarningPercent) + $channelPlan=@() + if ($EnablePrivacyChannel) { + $one=@($plan | Where-Object {$_.Definition.Id -eq 'OneSettings'})[0] + if ($one.Before.Status -ne 'Supported') { throw "Privacy channel configuration unavailable: $($one.Before.Diagnostic)" } + # WELA's technical minimum only; CIS does not prescribe a channel size here. + $profile=[pscustomobject]@{controls=@([pscustomobject]@{channel=$one.Definition.Channel;enabled=$true;sourceExampleBytes=65536;readerSid=$null})} + $channelPlan=@(Get-WelaNativeChannelPlan -Profile $profile) + } + $report=[pscustomobject]@{Scope='audit-notifications';ExitCode=$(if (@($plan | Where-Object Status -eq 'Unknown').Count) {1} else {0})} + if ($Action -eq 'Configure') { + $context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath + Set-WelaNotificationControls $context $plan + if ($EnablePrivacyChannel) { + $policy=@($context.Results | Where-Object Id -eq 'AuditNotifications/OneSettings')[0] + if ($policy.Status -in @('AlreadyCompliant','Applied') -or ($DryRun -and $policy.Status -eq 'Skipped' -and $policy.Diagnostic -like 'Dry run:*')) { + # Recheck the complete producer prerequisites before any channel action. + try { + $fresh=Get-WelaNotificationSnapshot $one.Definition + if ($fresh.Status -ne 'Supported' -or (-not $DryRun -and + (-not $fresh.Policy.ValueExists -or $fresh.Policy.Type -ne 'DWord' -or $fresh.Policy.Value -ne 1))) { + throw 'Privacy channel prerequisites changed or OneSettings policy is no longer enabled; dependent channel action refused.' + } + foreach ($channel in $channelPlan) { + $channel | Add-Member NoteProperty NotificationDefinition $one.Definition + $channel | Add-Member NoteProperty NotificationPreview ([bool]$DryRun) + } + $guard={ param($channel) + $producer=Get-WelaNotificationSnapshot $channel.NotificationDefinition + if ($producer.Status -ne 'Supported' -or (-not $channel.NotificationPreview -and + (-not $producer.Policy.ValueExists -or $producer.Policy.Type -ne 'DWord' -or $producer.Policy.Value -ne 1))) { + throw 'OneSettings policy/prerequisites changed before the dependent channel action or verification.' + } + } + Set-WelaNativeChannelControls -Context $context -Plan $channelPlan -Profile 'audit-notifications' -ValidatePrerequisites $guard + } catch { + # Keep the policy journal, final drift check and JSON export available. + $context.Results.Add([pscustomobject]@{Id='AuditNotifications/PrivacyChannelDependency';Kind='NativeChannel';Target=$one.Definition.Channel;Desired='Enable after verified OneSettings policy';Before=$fresh;After=$null;Status='Failed';Diagnostic=$_.Exception.Message}) + } + } + } + $report=Complete-WelaConfiguration -Context $context -SuccessMessage 'Notification configuration finished. Applied/AlreadyCompliant rows verify settings; skipped rows do not. Events and forwarding remain unverified.' + $report.Scope='audit-notifications' + } + $report | Add-Member NoteProperty Action $Action + $report | Add-Member NoteProperty Plan $plan + $report | Add-Member NoteProperty PrivacyChannelPlan $channelPlan + $report | Add-Member NoteProperty Current @(Get-WelaNotificationPlan -Control $Control -WarningPercent $WarningPercent) + $report | Add-Member NoteProperty EventGeneration 'Not verified; no Sigma eligibility increase. Sysmon is out of scope.' + if ($ResultsPath) { $report | ConvertTo-Json -Depth 18 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } + $report +} diff --git a/scripts/NativeChannelConfiguration.ps1 b/scripts/NativeChannelConfiguration.ps1 index ef10f8b7..e198e934 100644 --- a/scripts/NativeChannelConfiguration.ps1 +++ b/scripts/NativeChannelConfiguration.ps1 @@ -44,21 +44,23 @@ function Get-WelaNativeChannelPlan { } function Set-WelaNativeChannelControls { - param($Context, [array]$Plan, [string]$Profile) + param($Context, [array]$Plan, [string]$Profile, [scriptblock]$ValidatePrerequisites) foreach ($entry in $Plan) { $channel = $entry.Definition.channel $id = "NativeChannel/$channel/Settings" if ($entry.Status -in @('NotInstalled', 'Unknown', 'ManualReview')) { + $prerequisiteDiagnostic = if ($entry.PSObject.Properties['PrerequisiteDiagnostic']) { $entry.PrerequisiteDiagnostic } else { '' } $Context.Results.Add([pscustomobject]@{ Id = $id; Kind = 'NativeChannel'; Target = @{ Channel = $channel; Profile = $Profile } Desired = $entry.Desired; Before = $entry.Before; After = $null; Status = 'Failed' - Diagnostic = "$($entry.Status): channel metadata/ACL cannot safely be configured. $($entry.Access.Diagnostic)" + Diagnostic = "$($entry.Status): channel metadata/ACL cannot safely be configured. $($entry.Access.Diagnostic) $prerequisiteDiagnostic" }) continue } - $state = @{ Entry = $entry; InitialRead = $true; Snapshot = $null } + $state = @{ Entry = $entry; InitialRead = $true; Snapshot = $null; ValidatePrerequisites = $ValidatePrerequisites } $read = { param($state) + if ($state.ValidatePrerequisites) { & $state.ValidatePrerequisites $state.Entry } $current = Get-WelaNativeChannel -Name $state.Entry.Definition.channel if (-not (Test-WelaNativeChannelSnapshot $current)) { throw 'Channel settings became unreadable; no assumed defaults are used.' } if ($state.InitialRead) { @@ -78,6 +80,7 @@ function Set-WelaNativeChannelControls { $apply = { param($state) $entry = $state.Entry + if ($state.ValidatePrerequisites) { & $state.ValidatePrerequisites $entry } $fresh = Get-WelaNativeChannel -Name $entry.Definition.channel if (-not (Test-WelaNativeChannelSnapshotEqual $state.Snapshot $fresh)) { throw 'Channel settings changed after the recovery snapshot; no channel write was attempted.' } $arguments = @('sl', $entry.Definition.channel) diff --git a/tests/AuditNotifications.Tests.ps1 b/tests/AuditNotifications.Tests.ps1 new file mode 100644 index 00000000..a384b78a --- /dev/null +++ b/tests/AuditNotifications.Tests.ps1 @@ -0,0 +1,143 @@ +$ErrorActionPreference='Stop' +$root=Split-Path $PSScriptRoot -Parent +. (Join-Path $root 'scripts/Configuration.ps1') +. (Join-Path $root 'scripts/NativeChannelConfiguration.ps1') +. (Join-Path $root 'scripts/AuditNotifications.ps1') +$script:count=0; $script:cleanup=@() +function Assert($Condition,$Message) { if (-not $Condition) { throw $Message }; $script:count++ } +function Reject([scriptblock]$Action,[string]$Pattern) { + $message=''; try { & $Action | Out-Null } catch { $message=$_.Exception.Message } + Assert ($message -match $Pattern) "Expected '$Pattern', got '$message'." +} +function Reset-Fixture { + $script:values=@{}; $script:types=@{}; $script:writes=@(); $script:journal=$null + $script:hostStatus='Supported'; $script:build=22631; $script:product=1 + $script:channel='Enabled'; $script:logMode='Circular'; $script:admxError=$false + $script:channelPromptDrift=$false; $script:policyDrift=$false; $script:postWriteReads=0 + $script:readError=$false; $script:writeError=$false; $script:ignored=$false; $script:race=$false +} +function Get-WelaNotificationHost { [pscustomobject]@{Status=$script:hostStatus;Build=$script:build;ProductType=$script:product;Diagnostic='fixture'} } +function Get-WelaOneSettingsDefinitionEvidence { + if ($script:admxError) { throw 'mapping missing' } + [pscustomobject]@{Path='fixture';Sha256='abc';Mapping='DWORD1'} +} +function Get-WelaRegistryState { + param($Path,$Name) + if ($script:readError) { throw 'read denied' } + if ($script:policyDrift -and $script:writes.Count -gt 0 -and $Name -eq 'EnableOneSettingsAuditing') { $script:postWriteReads++; if ($script:postWriteReads -ge 2) { $script:values[$Name]=0 } } + if ($script:race -and $script:journal -and (Test-Path $script:journal)) { $script:values[$Name]=0; $script:types[$Name]='DWord' } + [pscustomobject]@{KeyExists=$true;ValueExists=$script:values.ContainsKey($Name);Value=$script:values[$Name];Type=$script:types[$Name]} +} +function Get-WelaNativeChannel { + param($Name) + [pscustomobject]@{Name=$Name;State=$script:channel;IsEnabled=($script:channel -eq 'Enabled');MaximumSizeInBytes=1048576;LogMode=$script:logMode;SecurityDescriptor='O:SYG:SYD:(A;;0x1;;;SY)'} +} +function New-WelaRegistryKey { param($Path) } +function Set-ItemProperty { + param($LiteralPath,$Name,$Value,$Type,$ErrorAction) + Assert ($script:journal -and (Test-Path $script:journal)) 'Write requires a pre-change journal.' + if ($script:writeError) { throw 'write denied' } + $script:writes+=$Name + if (-not $script:ignored) { $script:values[$Name]=$Value; $script:types[$Name]=$Type } +} +function New-FixtureContext([switch]$DryRun) { + $path=Join-Path ([IO.Path]::GetTempPath()) ('wela-notification-'+[guid]::NewGuid().ToString('N')) + $script:cleanup+=$path; $script:journal=Join-Path $path 'before.jsonl' + New-WelaConfigurationContext -Auto -DryRun:$DryRun -BackupPath $path +} +try { + Reset-Fixture + $plan=@(Get-WelaNotificationPlan) + Assert ($plan.Count -eq 2 -and $script:writes.Count -eq 0) 'Audit/plan enumerate two independent controls without writing.' + Assert ($plan[1].Before.WarningGeneration -like 'Not expected*') 'Circular overwrite cannot claim threshold warnings.' + $script:logMode='AutoBackup' + Assert ((Get-WelaNotificationPlan SecurityWarning).Before.WarningGeneration -like 'Unknown*') 'AutoBackup event behavior requires validation.' + $script:logMode='Retain' + Assert ((Get-WelaNotificationPlan SecurityWarning).Before.WarningGeneration -like 'Conditional*') 'Retain policy is no event proof.' + Reject { Invoke-WelaNotificationCommand -Action Configure } 'explicit NotificationControl' + Reject { Invoke-WelaNotificationCommand -Control SecurityWarning -EnablePrivacyChannel } 'requires the OneSettings' + Reject { Invoke-WelaNotificationCommand -DryRun } 'requires Configure' + foreach ($v in @(0,91)) { Reject { Get-WelaNotificationPlan -WarningPercent $v } 'validat' } + $script:values.WarningLevel=70; $script:types.WarningLevel='DWord' + $row=Get-WelaNotificationPlan SecurityWarning -WarningPercent 90 + Assert ($row.Status -eq 'PolicyMatches' -and $row.Desired -eq 70) 'Preserve an existing stricter threshold.' + $row=Get-WelaNotificationPlan SecurityWarning -WarningPercent 60 + Assert ($row.Status -eq 'ChangeRequired' -and $row.Desired -eq 60) 'Explicit lower maximum is actionable.' + foreach ($scenario in @('apply','dry','stale','race','read','write','ignored','drift','type','admx','channel','unsupported','unknownvalue')) { + Reset-Fixture + if ($scenario -eq 'type') { $script:values.EnableOneSettingsAuditing='1'; $script:types.EnableOneSettingsAuditing='String' } + if ($scenario -eq 'unknownvalue') { $script:values.EnableOneSettingsAuditing=2; $script:types.EnableOneSettingsAuditing='DWord' } + if ($scenario -eq 'admx') { $script:admxError=$true } + if ($scenario -eq 'channel') { $script:channel='Not installed' } + if ($scenario -eq 'unsupported') { $script:product=3; $script:build=26100 } + $plan=@(Get-WelaNotificationPlan OneSettings) + if ($scenario -eq 'stale') { $script:values.EnableOneSettingsAuditing=0; $script:types.EnableOneSettingsAuditing='DWord' } + if ($scenario -eq 'race') { $script:race=$true } + if ($scenario -eq 'read') { $script:readError=$true } + if ($scenario -eq 'write') { $script:writeError=$true } + if ($scenario -eq 'ignored') { $script:ignored=$true } + $context=New-FixtureContext -DryRun:($scenario -eq 'dry') + Set-WelaNotificationControls -Context $context -Plan $plan + if ($scenario -eq 'drift') { $script:values.EnableOneSettingsAuditing=0 } + $report=Complete-WelaConfiguration $context + switch ($scenario) { + 'apply' { + Assert ($report.ExitCode -eq 0 -and $script:values.EnableOneSettingsAuditing -eq 1) 'Write is read back and verified.' + $journal=Get-Content $script:journal -Raw | ConvertFrom-Json + Assert (-not $journal.Before.Policy.ValueExists -and $journal.Before.DefinitionEvidence.Sha256 -eq 'abc') 'Journal retains absent state and policy mapping evidence.' + $plan=@(Get-WelaNotificationPlan OneSettings) + Set-WelaNotificationControls $context $plan + Assert ($script:writes.Count -eq 1 -and $context.Results[1].Status -eq 'AlreadyCompliant') 'Repeat configuration is idempotent.' + } + 'dry' { Assert ($report.ExitCode -eq 0 -and $script:writes.Count -eq 0 -and -not (Test-Path $context.BackupPath)) 'Dry-run does not write settings or journal.' } + 'ignored' { Assert ($report.ExitCode -eq 1 -and $script:writes.Count -eq 1) 'Ignored native write fails verification.' } + 'drift' { Assert ($report.ExitCode -eq 1 -and $context.Results[0].Status -eq 'Overridden') 'Final policy drift fails.' } + default { Assert ($report.ExitCode -eq 1 -and $script:writes.Count -eq 0) "$scenario must block the write." } + } + } + Reset-Fixture + $script:product=2; $script:build=20348 + Assert ((Get-WelaNotificationPlan OneSettings).Before.Status -eq 'Supported') 'CIS explicitly includes Server 2022 DC; template and channel still required.' + $script:build=26100 + $rows=@(Get-WelaNotificationPlan) + Assert ($rows[0].Status -eq 'Unknown' -and $rows[1].Status -eq 'ChangeRequired') 'Unknown OneSettings applicability cannot hide the independent Security threshold.' + # Exercise command orchestration with a real runner and stubbed channel writes. + function Get-WelaNativeChannelPlan { param($Profile) [pscustomobject]@{Channel=$Profile.controls[0].channel} } + function Set-WelaNativeChannelControls { + param($Context,$Plan,$Profile,$ValidatePrerequisites) + Assert ($null -ne $ValidatePrerequisites) 'Shared channel runner must receive the producer prerequisite callback.' + if ($script:channelPromptDrift) { $script:values.EnableOneSettingsAuditing=0 } + & $ValidatePrerequisites $Plan[0] + $script:channelCalls++ + } + Reset-Fixture; $script:channelCalls=0 + $context=New-FixtureContext -DryRun + $report=Invoke-WelaNotificationCommand -Action Configure -Control OneSettings -EnablePrivacyChannel -Auto -BackupPath $context.BackupPath + Assert ($report.ExitCode -eq 0 -and $script:channelCalls -eq 1) 'Explicit channel request follows successful policy write.' + Reset-Fixture; $script:channelCalls=0; $script:writeError=$true + $context=New-FixtureContext -DryRun + $report=Invoke-WelaNotificationCommand -Action Configure -Control OneSettings -EnablePrivacyChannel -Auto -BackupPath $context.BackupPath + Assert ($report.ExitCode -eq 1 -and $script:channelCalls -eq 0) 'Failed policy must not enable the channel.' + Reset-Fixture; $script:channelCalls=0; $script:policyDrift=$true + $context=New-FixtureContext -DryRun + $resultPath=Join-Path ([IO.Path]::GetTempPath()) ('wela-notification-export-'+[guid]::NewGuid().ToString('N')+'.json') + $script:cleanup+=$resultPath + $report=Invoke-WelaNotificationCommand -Action Configure -Control OneSettings -EnablePrivacyChannel -Auto -BackupPath $context.BackupPath -ResultsPath $resultPath + $export=Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json + Assert ($report.ExitCode -eq 1 -and $script:channelCalls -eq 0) 'OneSettings drift to zero blocks the dependent channel action.' + Assert ($export.ExitCode -eq 1 -and @($export.Results | Where-Object Id -eq 'AuditNotifications/PrivacyChannelDependency').Count -eq 1) 'Dependency failure retains final drift results and JSON export.' + Reset-Fixture; $script:channelCalls=0 + $context=New-FixtureContext -DryRun + $report=Invoke-WelaNotificationCommand -Action Configure -Control OneSettings -EnablePrivacyChannel -Auto -DryRun -BackupPath $context.BackupPath + Assert ($report.ExitCode -eq 0 -and $script:channelCalls -eq 1 -and $script:writes.Count -eq 0) 'Dry-run may preview a channel after a policy change that has not been written.' + Reset-Fixture; $script:channelCalls=0; $script:channelPromptDrift=$true + $context=New-FixtureContext -DryRun + $report=Invoke-WelaNotificationCommand -Action Configure -Control OneSettings -EnablePrivacyChannel -Auto -BackupPath $context.BackupPath + Assert ($report.ExitCode -eq 1 -and $script:channelCalls -eq 0) 'Producer callback blocks policy drift at the shared channel prompt/write boundary.' + $exe=(Get-Process -Id $PID).Path + $ErrorActionPreference='Continue' + try { $output=& $exe -NoProfile -File (Join-Path $root 'WELA.ps1') configure -Profile wela-2.2.0 -NotificationControl SecurityWarning 2>&1; $code=$LASTEXITCODE } finally { $ErrorActionPreference='Stop' } + Assert ($code -ne 0 -and ($output -join "`n") -match 'Notification options require') 'Wrong-command options fail before profile mutation.' + Write-Host "PASS: $script:count notification checks." + $global:LASTEXITCODE=0 +} finally { foreach ($path in $script:cleanup) { if (Test-Path $path) { Remove-Item -LiteralPath $path -Recurse -Force } } } diff --git a/tests/AuditNotifications.Windows.Tests.ps1 b/tests/AuditNotifications.Windows.Tests.ps1 new file mode 100644 index 00000000..a7bac550 --- /dev/null +++ b/tests/AuditNotifications.Windows.Tests.ps1 @@ -0,0 +1,18 @@ +$ErrorActionPreference='Stop' +$root=Split-Path $PSScriptRoot -Parent +. (Join-Path $root 'scripts/Configuration.ps1') +. (Join-Path $root 'scripts/NativeChannelConfiguration.ps1') +. (Join-Path $root 'scripts/AuditNotifications.ps1') +Import-Module (Join-Path $root 'modules/NativeProviders.psm1') +$before=@(Get-WelaNotificationDefinitions | ForEach-Object { Get-WelaRegistryState -Path $_.Path -Name $_.Name }) | ConvertTo-Json -Depth 8 +$report=Invoke-WelaNotificationCommand -Action Audit +if ($report.Current.Count -ne 2) { throw 'Audit omitted a control.' } +$hostState=Get-WelaNotificationHost +if ($hostState.Status -ne 'Supported') { throw "Unexpected native host state: $($hostState.Diagnostic)" } +$warning=@($report.Current | Where-Object {$_.Definition.Id -eq 'SecurityWarning'})[0] +if ($warning.Before.Channel.Name -ne 'Security' -or $warning.Before.Status -ne 'Supported') { throw 'Native Security policy/channel observation failed.' } +$after=@(Get-WelaNotificationDefinitions | ForEach-Object { Get-WelaRegistryState -Path $_.Path -Name $_.Name }) | ConvertTo-Json -Depth 8 +if ($before -cne $after) { throw 'Read-only audit changed policy.' } +$report | ConvertTo-Json -Depth 18 | Write-Host +Write-Host 'PASS: native read-only policy/channel observations; event generation not tested.' +$global:LASTEXITCODE=0 diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index e63f4be0..d9bdf110 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,8 @@ **改善:** +- 任意実行の`audit-notifications`を追加し、OneSettings監査とSecurityログ警告しきい値の監査・計画・設定に対応しました。対象とチャネル変更の明示指定、OS・ADMX確認、型付き復旧記録と変更検出を行い、既存の低いしきい値とチャネルACL・保存方式を保持します。ポリシー一致と実イベント生成を分け、Windowsラボの証拠とSigma利用可能性は未検証と表示します。 (#408) (@Shirofune-Security) + - 読み取り専用の`rule-eligibility`を追加し、ルール・対応表のハッシュ、ルールごとの判定理由、対象外の理由、分子・分母を明示します。任意で取り込んだラボ資料を、対応範囲を限定した完全なルール定義、ネイティブXML、設定、収集・クエリ実行の証拠と照合し、未対応・未確認の項目はConditionalとします。監査のCSV/JSON/HTMLとNavigator出力では、設定が有効なだけでルールを利用可能と判定しません。取り込んだReady判定は記録された環境・時点に限られ、実環境での一連の検証を保証しません。 (#407) (@Shirofune-Security) - Windows標準のドメイン/Kerberos環境向けに、送信元設定と明示的に選択した収集サーバーのサブスクリプションを監査・計画・設定する任意実行の`wef-source`と`wec-collector`を追加しました。実際の収集先ID、既存リスナーと範囲を限定した受信規則、送信元SID、メンバーホストでの読み取り権限の追加、明示的なASDのWSMan強化設定と共通チャネル設定を、復旧記録・変更検出・読み戻しで確認します。DCのグループ管理権限と異なる既存サブスクリプションは変更しません。JSONにはクエリ・チャネル・実行状態の証拠を保持し、実効読み取り権限・イベント到着・転送後のSigma検知範囲は未検証と表示します。隔離Windows環境での配備検証は別途必要です。 (#406) (@Shirofune-Security) - CIS v4.0.0 Level 2向けに、Windows PowerShell 5.1トランスクリプトの明示的な監査・計画・設定機能を追加しました。管理者が選択した既存の出力ディレクトリを確認し、ポリシーとは分けて報告します。型を含むレジストリ変更前の状態を保存し、共有される32/64ビットのビューと変更後の状態を検証します。呼び出しヘッダーの設定は保持し、ACL・共有・保存期間は変更せず、Sigma EVTX検知範囲の向上も自動加算しません。使い捨て環境の実トランスクリプトテストでは元のポリシーを復元します。中央保存先の権限と収集は別途検証が必要です。 (#405) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 1e8818aa..2273998a 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,8 @@ **Improvements:** +- Added opt-in `audit-notifications` audit/plan/configure for OneSettings auditing and Security log warning thresholds, with explicit control/channel selections, reviewed host and ADMX gates, typed recovery journals and drift checks. Earlier warning thresholds and channel ACL/retention are preserved. Reports separate policy matches from warning/event generation; Windows lab evidence and Sigma eligibility remain unverified. (#408) (@Shirofune-Security) + - Added read-only `rule-eligibility` reports with pinned corpus/mapping hashes, per-rule reasons, explicit scope exclusions and numerator/denominator totals. Optional imported lab artifacts are checked against a narrow complete-rule parser, native XML, configuration, ingestion and query evidence; unsupported or incomplete cases stay Conditional. Audit CSV/JSON/HTML and Navigator outputs no longer treat enabled settings as proven usable rules. Imported Ready results apply only to their recorded context/time; no live end-to-end validation is implied. (#407) (@Shirofune-Security) - Added separate opt-in `wef-source` and `wec-collector` audit, plan and configure commands for native domain/Kerberos source settings and explicitly selected collector subscriptions. Actual collector identity, scoped existing ingress/listener prerequisites, explicit source SIDs, additive member-host read permissions, optional ASD WSMan hardening and shared channel controls are checked with journals, drift guards and readback. DC group authority and different existing subscriptions are preserved. JSON retains query/channel/runtime evidence without claiming effective read access, event arrival or forwarded Sigma coverage; isolated Windows deployment validation remains pending. (#406) (@Shirofune-Security) - Added explicit CIS v4.0.0 Level 2 Windows PowerShell 5.1 transcription audit, plan and configure actions. An operator-selected existing output directory is checked and reported separately from policy; typed canonical registry writes are journaled, verified through shared 32/64-bit views and checked for drift while preserving invocation-header preferences. No ACL/share/retention changes or automatic Sigma EVTX credit are introduced; disposable native transcript tests restore original policy, and central authorization/collection remains a deployment check. (#405) (@Shirofune-Security)