diff --git a/.github/workflows/native-validation.yml b/.github/workflows/native-validation.yml new file mode 100644 index 00000000..ab1f7067 --- /dev/null +++ b/.github/workflows/native-validation.yml @@ -0,0 +1,37 @@ +name: Native 4688 validation collection +on: + push: + branches: ['**'] + paths: + - 'WELA.ps1' + - 'scripts/NativeValidation.ps1' + - 'scripts/ControlApplicability.ps1' + - 'scripts/Configuration.ps1' + - 'modules/AuditProfiles.psm1' + - 'tests/NativeValidation*' + - '.github/workflows/native-validation.yml' + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + native-probe: + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Synthetic rejection regressions in Windows PowerShell 5.1 + shell: powershell + run: ./tests/NativeValidation.Tests.ps1 + - name: Actual native 4688 probe with exact disposable policy restoration + shell: powershell + run: ./tests/NativeValidation.Windows.Tests.ps1 -AllowDisposablePolicyWrite + - name: Synthetic rejection regressions in PowerShell 7 + shell: pwsh + run: ./tests/NativeValidation.Tests.ps1 + - name: Actual native 4688 probe from PowerShell 7 with restoration + shell: pwsh + run: ./tests/NativeValidation.Windows.Tests.ps1 -AllowDisposablePolicyWrite diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 71e269ef..7d48e8e1 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,7 @@ **改善:** +- ネイティブ Security 4688 の無害な固定プローブを収集する明示的な `native-validation` を追加。型付き前提条件、イベントの厳密な照合、前後の状態、ハッシュ付き証拠を新規の非公開ディレクトリに記録します。不完全・上限到達・曖昧・ドリフトの結果は未検証のままです。監査ポリシー変更や Sigma 利用可能ルール数の加算は行いません。使い捨て Server 2022/2025 テストで実イベントとポリシー復元を確認し、Windows 11/DC/ADCS とバックエンドの検証は別途必要です。 (#413) (@Shirofune-Security) - ローカル監査権限と`CrashOnAuditFail`を監査・計画・出典別に設定する任意実行の`audit-integrity`を追加しました。実際のクライアント・メンバーサーバー・DCを区別し、Microsoft SCTで省略された設定は保持します。対象SIDの一覧、権限削除の明示指定、権限単位のLSA更新、完全な復旧記録と変更直前・変更後の検証により、無関係な権限を保持します。復旧状態では変更せず、ネイティブCIは読み取りだけを行います。トークン・GPO・サービス・イベントの検証は別途ラボで必要となり、Sigma検知範囲には加算しません。 (#412) (@Shirofune-Security) - 読み取り専用の`retention-health`を追加し、送信元/収集サーバーの標準ログバッファ、確認した先頭レコードの経過日数、申告されたアーカイブ方針、件数を制限したローカルEVTX/ACL一覧、各言語のWEF・時刻情報とログ消失・消去・満杯の兆候をJSONと単独で表示できるHTMLに分けて記録します。保持されたイベントの時刻に基づく件数率とUTF-8 XMLバイト数の試算には上限・前提を明示し、容量・完全な保持・実効読み取り権限・時刻同期・転送成功の証明とは扱いません。複数ホストでのロールオーバー・復旧・到着検証は別途必要です。 (#410) (@Shirofune-Security) - 読み取り専用の`control-applicability`を追加し、旧CISのApplication Guard監査要件を保持しつつ、Windows 11 24H2以降では削除済み・対象外と表示します。`default-evidence`で正確なビルド・パッチ・ドメイン参加・役割を含む現状を記録し、出典とレビュー情報が一致する参照環境と比較できます。旧ベースラインの既定値は履歴情報として保持し、未検証の既定値はUnknownと表示します。合成テストと読み取り専用CIからクリーンインストールやイベント生成の証明は行いません。 (#409) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2534cc..1a23f806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Improvements:** +- Added opt-in `native-validation` to collect a fixed benign Security 4688 probe with typed prerequisites, exact native event matching, before/after state and hashed components in a new private directory. Partial, capped, ambiguous and drifted results remain unverified; the collector changes no audit policy and grants no Sigma readiness credit. Disposable Server 2022/2025 tests exercise real events with verified policy restoration; Windows 11/DC/ADCS and backend acceptance remain separate. (#413) (@Shirofune-Security) - Added opt-in `audit-integrity` audit, plan and source-profile configuration for local audit privileges and `CrashOnAuditFail`, with separate actual client/member/DC scope and preserved Microsoft SCT omissions. Exact affected SIDs, explicit privilege-removal consent, per-right LSA updates, complete recovery journals and fresh/readback checks preserve unrelated privileges. Recovery states are blocked; native CI reads policy only, while token, GPO, service and event validation remains a lab requirement without Sigma credit. (#412) (@Shirofune-Security) - Added read-only `retention-health` source/collector JSON and self-contained HTML reports separating native buffers, observed record-boundary ages, declared archive policy, bounded local EVTX/ACL inventory, localized WEF/time evidence and loss/clear/full indicators. Retained-event timestamp rates and UTF-8 XML-byte scenarios expose caps and assumptions; none establish archive capacity, complete retention, effective reader access, synchronized time or successful forwarding. Multi-host rollover/recovery/arrival validation remains pending. (#410) (@Shirofune-Security) - Added read-only `control-applicability` for the historical CIS Application Guard audit requirement, reporting removal on Windows 11 24H2+ without remediation. Added exact build/patch/join/role native snapshots and provenance-bound reference comparison through `default-evidence`. Legacy baseline default strings are retained as historical hints while public defaults remain Unknown without reviewed scenario evidence. Synthetic fixtures and native read-only CI do not claim clean-install or event-generation proof. (#409) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index 41837522..92683453 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -62,6 +62,9 @@ [ValidateSet('OneSettings','SecurityWarning')][string[]]$NotificationControl, [ValidateRange(1,90)][int]$WarningPercent = 90, [switch]$EnablePrivacyChannel, + [ValidateSet('Plan','Run')][string]$ProbeAction = 'Plan', + [string]$ProbeOutputPath, + [ValidateRange(1,30)][int]$ProbeTimeoutSeconds = 15, [switch]$Help ) @@ -81,6 +84,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" . (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1") . (Join-Path $ScriptRoot "scripts/LdapDiagnostics.ps1") . (Join-Path $ScriptRoot "scripts/ControlApplicability.ps1") +. (Join-Path $ScriptRoot "scripts/NativeValidation.ps1") . (Join-Path $ScriptRoot "scripts/AuditNotifications.ps1") . (Join-Path $ScriptRoot "scripts/AdObjectSacl.ps1") . (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1") @@ -1814,6 +1818,7 @@ Usage: ./WELA.ps1 control-applicability # Read-only historical native feature/build assessment ./WELA.ps1 default-evidence -Help # Exact-context observed snapshots and reviewed reference comparison ./WELA.ps1 audit-notifications -Help # OneSettings audit and Security warning policy + ./WELA.ps1 native-validation -Help # Collect a fixed native 4688 probe without changing policy ./WELA.ps1 version # Show the WELA version ./WELA.ps1 help # Show this help "@ @@ -1825,6 +1830,13 @@ Write-Host "" Write-Host "WELA v$WELAVersion - $WELAReleaseName" Write-Host "" +if ($Cmd -ne 'native-validation' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('ProbeAction','ProbeOutputPath','ProbeTimeoutSeconds') }).Count) { + throw 'Probe options require native-validation. No command was run.' +} +if ($Cmd -eq 'native-validation' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','ProbeAction','ProbeOutputPath','ProbeTimeoutSeconds','Help') }).Count) { + throw 'native-validation accepts only its dedicated probe options. No command was run.' +} + if ($Cmd -ne 'audit-integrity' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('IntegrityAction','IntegrityProfile','AllowPrivilegeRemoval') }).Count) { throw 'Integrity options require the dedicated audit-integrity command. No command was run.' } @@ -1917,6 +1929,13 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi } switch ($Cmd.ToLower()) { + 'native-validation' { + if ($Help) { Write-Host 'Usage: ./WELA.ps1 native-validation [-ProbeAction Plan|Run] [-ProbeOutputPath new-directory] [-ProbeTimeoutSeconds 1..30]. Plan reads prerequisites; Run launches a fixed benign cmd.exe probe and collects exact native Security 4688 XML. No policy changes or Sigma readiness credit. See docs/native-validation.md.'; return } + $report=Invoke-WelaNativeValidation -Action $ProbeAction -OutputPath $ProbeOutputPath -TimeoutSeconds $ProbeTimeoutSeconds + $report + if ($report.ExitCode -ne 0) { exit 1 } + } + 'audit-integrity' { if ($Help) { Write-Host 'Usage: ./WELA.ps1 audit-integrity [-IntegrityAction Audit|Plan|Configure] [-IntegrityProfile source-id] [-AllowPrivilegeRemoval] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath report.json]. Audit is read-only; Plan/Configure require an exact source profile. See docs/audit-integrity.md.'; return } if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath) { throw 'audit-integrity observes the actual local Windows host; use -IntegrityProfile and -ResultsPath, without Security profiles, role/build overrides or HTML.' } diff --git a/docs/native-validation.md b/docs/native-validation.md new file mode 100644 index 00000000..4f6ad58f --- /dev/null +++ b/docs/native-validation.md @@ -0,0 +1,36 @@ +# Native event validation components + +`native-validation` collects a fixed, benign native Security 4688 process-creation event. Sysmon is out of scope. It is the first executable collector for issue #387; it does not complete every role, event family or Sigma/backend acceptance case. + +```powershell +# Read-only prerequisite assessment. No child process or output directory. +./WELA.ps1 native-validation +# Explicitly launch one fixed System32 cmd.exe echo command and collect its event. +./WELA.ps1 native-validation -ProbeAction Run -ProbeOutputPath C:\Lab\evidence\probe-001 +``` + +Use 64-bit elevated Windows PowerShell 5.1 or PowerShell 7 on a reviewed Windows 11 build (22000, 22621, 22631, 26100 or 26200), Server 2022 (20348) or Server 2025 (26100). The collector detects the actual client/member/DC/ADCS context; combined DC+CA hosts and unknown builds remain unsupported. Exact patch, edition, architecture, join state and installed roles must be readable. Policy administration remains a separate operator action: this command does not enable auditing, refresh GPO, change SACLs, clear logs, restart services or contact a backend. + +Prerequisites are effective Process Creation Success auditing, DWORD `SCENoApplyLegacyAuditPolicy=1`, DWORD `ProcessCreationIncludeCmdLine_Enabled=1` and an enabled/readable Security channel. Plan reports observed prerequisites only; Run repeats the observations before launch and after collection. A changed audit mask, prerequisite or host context prevents a successful result. Observations do not establish persistence through a later policy refresh. + +The two native host readers must agree on build. The reported role, patch, join +state and installed-role summary must also agree with the detailed host +observations; a client/member observation cannot be labeled as a DC, and an AD CS +label additionally requires the installed CA role. Contradictory evidence is +reported as Unverified even if the process event itself matches. + +Run launches the Windows system `cmd.exe` with `/d /c echo WELA_PROBE_`; callers cannot supply executable paths or commands. It retains only the single matching event XML, using native provider identity, EventID 4688/version 2, successful-audit keyword, local computer name (or joined FQDN), time window, child PID, creator PID, executable path and complete fixed command line. Duplicate matches, unknown schema, access denial, drift, timeout or a 512-event query cap produce `Unverified` and exit 1. Event polling defaults to 15 seconds and supports `-ProbeTimeoutSeconds 1..30`; each synchronous Windows query can take additional time. The child process has a separate 10-second limit. The collector never exports the entire Security log. + +The destination must be a new directory under an existing parent. On Windows, its ACL is restricted to the current user, SYSTEM and local Administrators. Files use CreateNew and cannot overwrite prior evidence. A completed collection contains `before-state.json`, `process.json`, `event.xml`, `after-state.json` and `manifest.json`. The manifest fingerprints each component using SHA-256 and preserves the collector status. Partial artifacts and diagnostics remain available after failure. These hashes detect changes relative to the manifest; they are not signatures or proof against a malicious evidence author. Review and protect the directory before sharing its host/policy metadata. + +## Completing rule and backend evidence + +`NativeEventObserved` means the fixed probe produced the expected local telemetry in the recorded context and time. `ReadyRuleCredit` always remains zero. The random benign command will ordinarily not satisfy an existing detection rule. + +The component bundle is deliberately a different kind from `WelaNativeRuleEvidence` and cannot be supplied directly as a completed readiness bundle. To validate a specific rule, use the [native rule eligibility contract](native-rule-eligibility.md), retain its full original rule, obtain an independent complete normalization review, generate a suitable controlled scenario, and preserve backend ingestion, the translated query and its actual successful result. Do not relabel this probe as a rule match or invent the missing artifacts. A supported native event and state snapshot can inform that lab work; the original component manifest remains the provenance record. No backend name, rule match or query result is manufactured by this command. + +## Verification and remaining acceptance + +Synthetic tests exercise identity/schema/time/command mismatches, disabled or mistyped prerequisites, drift, caps, denied access, output collisions and public command isolation. They provide no native event evidence. A separate explicitly opted-in GitHub-hosted runner test temporarily enables the three prerequisites, captures a real matching 4688 event on Server 2022/2025 under PowerShell 5.1/7 and restores the exact previous audit mask and registry value kinds/absence in `finally`, verifying restoration. The production collector contains no policy writer. CI does not provision a DC or CA, test Windows 11, forward an event or execute a Sigma query. Those cases remain isolated-lab acceptance work before closing #387. + +Microsoft documents the event schema and command-line prerequisite in [4688: A new process has been created](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/auditing/event-4688). The collector requires the reviewed modern version 2 schema rather than extrapolating from older versions. diff --git a/scripts/NativeValidation.ps1 b/scripts/NativeValidation.ps1 new file mode 100644 index 00000000..e343ecff --- /dev/null +++ b/scripts/NativeValidation.ps1 @@ -0,0 +1,183 @@ +# Fixed, opt-in native telemetry probe. This collector never changes Windows policy. +function Get-WelaProbeState { + if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not [Environment]::Is64BitProcess) { throw 'Native validation requires 64-bit Windows PowerShell.' } + $context = Get-WelaDefaultContext + if (-not (Test-WelaDefaultContextComplete $context)) { throw "Complete native host context is required: $($context.Diagnostic)" } + $hostContext = Get-WelaHostContext + if ($hostContext.Build -ne $context.Build) { throw 'Native host readers disagree about the Windows build.' } + if (($hostContext.Role -eq 'Client' -and $hostContext.Build -notin @(22000,22621,22631,26100,26200)) -or + ($hostContext.Role -ne 'Client' -and $hostContext.Build -notin @(20348,26100))) { throw 'Host build is outside the reviewed Windows 11 / Server 2022 and 2025 probe scope.' } + $policies = Get-WelaEffectiveAuditPolicy + $precedence = Get-WelaRegistryState -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa' -Name 'SCENoApplyLegacyAuditPolicy' + $commandLine = Get-WelaRegistryState -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit' -Name 'ProcessCreationIncludeCmdLine_Enabled' + $channel = Get-WinEvent -ListLog Security -ErrorAction Stop + if (@($channel).Count -ne 1 -or $channel.LogName -ne 'Security') { throw 'Exact Security log configuration is unavailable.' } + [pscustomobject][ordered]@{ + capturedAtUtc = [DateTime]::UtcNow.ToString('o') + context = [pscustomobject][ordered]@{computer=[Environment]::MachineName;role=$hostContext.Role;build=$context.Build;patch="$($context.Build).$($context.UBR)";domainJoined=$context.DomainJoined;installedRoles=@($context.InstalledRoles)} + hostObservation = $context + auditPolicies = $policies + auditPrecedence = $precedence + commandLineCapture = $commandLine + securityChannelEnabled = [bool]$channel.IsEnabled + } +} +function Assert-WelaProbePrerequisites { + param($State) + $guid = '0cce922b-69ae-11d9-bed3-505054503030' + if (-not $State -or -not (Test-WelaDefaultContextComplete $State.hostObservation)) { throw 'Complete observed host context is required.' } + if ($State.context.role -notin @('Client','MemberServer','DomainController','ADCS') -or $State.context.build -ne $State.hostObservation.Build) { throw 'Host context is inconsistent.' } + $observed=$State.hostObservation + $roleMatches=switch ($State.context.role) { + 'Client' { $observed.ProductType -eq 1 -and $observed.DomainRole -in @(0,1) } + 'DomainController' { $observed.ProductType -eq 2 -and $observed.DomainRole -in @(4,5) } + 'MemberServer' { $observed.ProductType -eq 3 -and $observed.DomainRole -in @(2,3) } + 'ADCS' { $observed.ProductType -eq 3 -and $observed.DomainRole -in @(2,3) -and $observed.InstalledRoles -contains 'ADCS-Cert-Authority' } + } + if (-not $roleMatches -or $State.context.domainJoined -isnot [bool] -or + $State.context.domainJoined -ne $observed.DomainJoined -or + $State.context.patch -cne "$($observed.Build).$($observed.UBR)" -or + $State.context.installedRoles -isnot [array] -or + (@($State.context.installedRoles | Sort-Object -Unique) -join "`n") -cne (@($observed.InstalledRoles | Sort-Object -Unique) -join "`n")) { + throw 'Probe role, patch, join or installed-role summary contradicts the detailed host observation.' + } + $mask = $State.auditPolicies[$guid] + if (($mask -isnot [int] -and $mask -isnot [long]) -or $mask -notin @(1,3)) { throw 'Effective Process Creation success auditing is required; no policy was changed.' } + foreach ($entry in @($State.auditPrecedence,$State.commandLineCapture)) { + if (-not $entry.ValueExists -or $entry.Type -ne 'DWord' -or ($entry.Value -isnot [int] -and $entry.Value -isnot [long]) -or $entry.Value -ne 1) { throw 'Typed DWORD=1 audit precedence and process command-line capture are required; no policy was changed.' } + } + if ($State.securityChannelEnabled -isnot [bool] -or -not $State.securityChannelEnabled) { throw 'Security channel must be observed enabled.' } +} +function Get-WelaProbeStateKey { + param($State) + # Timestamps differ; every observed policy and exact host context must remain stable. + [ordered]@{context=$State.context;host=(Get-WelaDefaultContextKey $State.hostObservation);policies=@($State.auditPolicies.GetEnumerator() | Sort-Object Key | ForEach-Object { "$($_.Key)=$($_.Value)" });precedence=$State.auditPrecedence;commandLine=$State.commandLineCapture;security=$State.securityChannelEnabled} | ConvertTo-Json -Depth 10 -Compress +} +function Start-WelaProbeProcess { + $executable = Join-Path ([Environment]::GetFolderPath('System')) 'cmd.exe' + if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) { throw 'Native System32 cmd.exe is unavailable.' } + $marker = 'WELA_PROBE_' + [guid]::NewGuid().ToString('N') + $start = New-Object Diagnostics.ProcessStartInfo + $start.FileName=$executable; $start.Arguments='/d /c echo ' + $marker + $start.UseShellExecute=$false; $start.CreateNoWindow=$true + $start.RedirectStandardOutput=$true; $start.RedirectStandardError=$true + $began=[DateTime]::UtcNow + $process=[Diagnostics.Process]::Start($start) + try { + $processId=$process.Id + if (-not $process.WaitForExit(10000)) { $process.Kill(); throw 'Fixed benign cmd.exe probe timed out.' } + $output=$process.StandardOutput.ReadToEnd(); $diagnostic=$process.StandardError.ReadToEnd() + if ($process.ExitCode -ne 0 -or $output.Trim() -cne $marker) { throw "Fixed benign probe failed: $diagnostic" } + [pscustomobject]@{ProcessId=$processId;ParentProcessId=$PID;Executable=$executable;Arguments=$start.Arguments;Marker=$marker;StartedUtc=$began.ToString('o');CompletedUtc=[DateTime]::UtcNow.ToString('o');ExitCode=$process.ExitCode} + } finally { $process.Dispose() } +} +function Read-WelaProbeEvents { + param([DateTime]$StartUtc,[DateTime]$EndUtc,[int]$MaximumEvents=512) + try { + $records=@(Get-WinEvent -FilterHashtable @{LogName='Security';ProviderName='Microsoft-Windows-Security-Auditing';Id=4688;StartTime=$StartUtc;EndTime=$EndUtc} -MaxEvents $MaximumEvents -ErrorAction Stop) + $xml=@(foreach ($record in $records) { try { [string]$record.ToXml() } finally { $record.Dispose() } }) + [pscustomobject]@{Xml=$xml;Capped=($records.Count -ge $MaximumEvents)} + } catch { + if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') { return [pscustomobject]@{Xml=@();Capped=$false} } + throw + } +} +function Test-WelaProbeEvent { + param([string]$Xml,$Process,$State,[DateTime]$EndUtc) + $reader=$null + try { + $settings=New-Object Xml.XmlReaderSettings + $settings.DtdProcessing=[Xml.DtdProcessing]::Prohibit; $settings.XmlResolver=$null; $settings.MaxCharactersInDocument=4194304 + $reader=[Xml.XmlReader]::Create([IO.StringReader]::new($Xml),$settings) + $doc=New-Object Xml.XmlDocument; $doc.XmlResolver=$null; $doc.Load($reader) + $ns=New-Object Xml.XmlNamespaceManager($doc.NameTable); $ns.AddNamespace('e','http://schemas.microsoft.com/win/2004/08/events/event') + if ($doc.DocumentElement.LocalName -cne 'Event' -or $doc.DocumentElement.NamespaceURI -cne $ns.LookupNamespace('e')) { return $false } + if ($doc.SelectNodes('/e:Event/e:System',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:EventData',$ns).Count -ne 1 -or $doc.SelectNodes('/e:Event/e:UserData',$ns).Count) { return $false } + $system=@{} + foreach ($name in @('Provider','EventID','Version','EventRecordID','Channel','Computer','Keywords','TimeCreated')) { + $nodes=$doc.SelectNodes("/e:Event/e:System/e:$name",$ns) + if ($nodes.Count -ne 1) { return $false }; $system[$name]=$nodes[0] + } + if ($system.Provider.GetAttribute('Name') -cne 'Microsoft-Windows-Security-Auditing' -or + $system.Provider.GetAttribute('Guid').Trim('{}') -ine '54849625-5478-4994-a5ba-3e3b0328c30d' -or + $system.EventID.InnerText -cne '4688' -or $system.Version.InnerText -cne '2' -or $system.Channel.InnerText -cne 'Security' -or + $system.Keywords.InnerText -ine '0x8020000000000000' -or $system.EventRecordID.InnerText -notmatch '^[1-9][0-9]*$') { return $false } + $computers=@($State.context.computer) + if ($State.context.domainJoined) { $computers+= "$($State.context.computer).$($State.hostObservation.Domain)" } + if ($system.Computer.InnerText -notin $computers) { return $false } + $eventTime=[DateTimeOffset]::Parse($system.TimeCreated.GetAttribute('SystemTime'),[Globalization.CultureInfo]::InvariantCulture) + if ($eventTime.UtcDateTime -lt ([DateTimeOffset]::Parse($Process.StartedUtc)).UtcDateTime -or $eventTime.UtcDateTime -gt $EndUtc) { return $false } + $data=@{} + foreach ($node in $doc.SelectNodes('/e:Event/e:EventData/e:Data',$ns)) { + $name=$node.GetAttribute('Name'); if (-not $name -or $data.ContainsKey($name)) { return $false }; $data[$name]=$node.InnerText + } + if ($data.NewProcessId -notmatch '^0x[0-9a-f]+$' -or $data.ProcessId -notmatch '^0x[0-9a-f]+$') { return $false } + if ([Convert]::ToInt64($data.NewProcessId.Substring(2),16) -ne $Process.ProcessId -or [Convert]::ToInt64($data.ProcessId.Substring(2),16) -ne $Process.ParentProcessId -or $data.NewProcessName -ine $Process.Executable) { return $false } + # Exactly the fixed invocation, permitting the native runtime's image quoting. + return $data.CommandLine -ieq ($Process.Executable + ' ' + $Process.Arguments) -or $data.CommandLine -ieq ('"' + $Process.Executable + '" ' + $Process.Arguments) + } catch { return $false } finally { if ($reader) { $reader.Dispose() } } +} +function Write-WelaProbeArtifact { + param([string]$Root,[string]$Name,[string]$Text) + $path=Join-Path $Root $Name + $bytes=[Text.UTF8Encoding]::new($false).GetBytes($Text) + $stream=[IO.File]::Open($path,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None) + try { $stream.Write($bytes,0,$bytes.Length) } finally { $stream.Dispose() } + [pscustomobject]@{path=$Name;sha256=(Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()} +} +function Invoke-WelaNativeValidation { + param([ValidateSet('Plan','Run')][string]$Action='Plan',[string]$OutputPath,[ValidateRange(1,30)][int]$TimeoutSeconds=15) + if (($Action -eq 'Run') -ne (-not [string]::IsNullOrWhiteSpace($OutputPath))) { throw 'Run requires a new -ProbeOutputPath directory; Plan does not write artifacts.' } + $report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaNativeProbeComponents';Probe='security-4688-command-line-v1';Action=$Action;Status='Unverified';ExitCode=0;GeneratedUtc=[DateTime]::UtcNow.ToString('o');PolicyChanges=0;ReadyRuleCredit=0;Scope='Built-in Windows only. Sysmon excluded. Native event collection is not complete-rule or backend validation.';RequiredEvidence=@('Reviewed complete rule and normalization','Backend ingestion','Translated query and successful query result');BeforeState=$null;AfterState=$null;Process=$null;Artifacts=@();Diagnostic='';OutputPath=$null} + # Reserve a new private directory before any process is launched. New-Item fails on collisions. + if ($Action -eq 'Run') { + # PowerShell location can differ from the process working directory. + $provider=$null; $drive=$null + $full=$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath,[ref]$provider,[ref]$drive) + if ($provider.Name -ne 'FileSystem') { throw 'Probe output must use the filesystem.' } + $parent=Split-Path $full -Parent + if (-not (Test-Path -LiteralPath $parent -PathType Container)) { throw 'Output parent directory must already exist.' } + $null=New-Item -ItemType Directory -Path $full -ErrorAction Stop + $report.OutputPath=$full + if ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) { + $acl=New-Object Security.AccessControl.DirectorySecurity + $acl.SetAccessRuleProtection($true,$false) + foreach ($sid in @([Security.Principal.WindowsIdentity]::GetCurrent().User.Value,'S-1-5-18','S-1-5-32-544') | Select-Object -Unique) { + $acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new($sid),'FullControl','ContainerInherit,ObjectInherit','None','Allow')) + } + Set-Acl -LiteralPath $full -AclObject $acl -ErrorAction Stop + } + } + try { + $before=Get-WelaProbeState; $report.BeforeState=$before + $beforeKey=Get-WelaProbeStateKey $before + if ($Action -eq 'Run') { $report.Artifacts+=Write-WelaProbeArtifact $full 'before-state.json' ($before | ConvertTo-Json -Depth 16) } + Assert-WelaProbePrerequisites $before + if ($Action -eq 'Plan') { $report.Status='PrerequisitesObserved'; return $report } + $fresh=Get-WelaProbeState; Assert-WelaProbePrerequisites $fresh + if ((Get-WelaProbeStateKey $fresh) -cne $beforeKey) { throw 'Native prerequisite or context drift before probe launch.' } + $process=Start-WelaProbeProcess; $report.Process=$process + $report.Artifacts+=Write-WelaProbeArtifact $full 'process.json' ($process | ConvertTo-Json -Depth 6) + $timer=[Diagnostics.Stopwatch]::StartNew(); $matches=@() + do { + $end=[DateTime]::UtcNow + $batch=Read-WelaProbeEvents -StartUtc ([DateTimeOffset]::Parse($process.StartedUtc).UtcDateTime) -EndUtc $end + if ($batch.Capped) { throw '4688 query reached its 512-event cap; collection is incomplete. Retry in a quieter isolated environment.' } + $matches=@($batch.Xml | Where-Object { Test-WelaProbeEvent -Xml $_ -Process $process -State $before -EndUtc $end }) + if ($matches.Count -gt 1) { throw 'Ambiguous native probe events; no event was selected.' } + if ($matches.Count -eq 1) { break } + if ($timer.Elapsed.TotalSeconds -lt $TimeoutSeconds) { Start-Sleep -Milliseconds 250 } + } while ($timer.Elapsed.TotalSeconds -lt $TimeoutSeconds) + if ($matches.Count -ne 1) { throw 'No exact native 4688 event with the probe PID, creator PID, path and full command line arrived within the timeout.' } + $report.Artifacts+=Write-WelaProbeArtifact $full 'event.xml' ([string]$matches[0]) + $after=Get-WelaProbeState; $report.AfterState=$after + $report.Artifacts+=Write-WelaProbeArtifact $full 'after-state.json' ($after | ConvertTo-Json -Depth 16) + Assert-WelaProbePrerequisites $after + if ((Get-WelaProbeStateKey $after) -cne $beforeKey) { throw 'Observed host or policy drift during native probe collection.' } + $report.Status='NativeEventObserved' + } catch { $report.Status='Unverified'; $report.ExitCode=1; $report.Diagnostic=$_.Exception.Message } + if ($Action -eq 'Run') { + $null=Write-WelaProbeArtifact $full 'manifest.json' ($report | ConvertTo-Json -Depth 20) + } + return $report +} diff --git a/tests/NativeValidation.Tests.ps1 b/tests/NativeValidation.Tests.ps1 new file mode 100644 index 00000000..6d522438 --- /dev/null +++ b/tests/NativeValidation.Tests.ps1 @@ -0,0 +1,88 @@ +$ErrorActionPreference='Stop' +$repo=Split-Path $PSScriptRoot -Parent +. (Join-Path $repo 'scripts/ControlApplicability.ps1') +. (Join-Path $repo 'scripts/NativeValidation.ps1') +$script:checks=0 +function Assert($Condition,[string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:checks++ } +function Throws([scriptblock]$Code,[string]$Message) { $caught=$false; try { & $Code | Out-Null } catch { $caught=$true }; Assert $caught $Message } +$context=[pscustomobject]@{Status='Observed';RolesStatus='Observed';ProductType=3;DomainRole=2;DomainJoined=$false;Build=20348;UBR=4000;Edition='ServerDatacenter';Domain='WORKGROUP';Architecture='64-bit';ProcessorArchitecture=9;InstalledRoles=@('Web-Server')} +$script:state=[pscustomobject]@{capturedAtUtc=[DateTime]::UtcNow.ToString('o');context=[pscustomobject]@{computer='test-host';role='MemberServer';build=20348;patch='20348.4000';domainJoined=$false;installedRoles=@('Web-Server')};hostObservation=$context;auditPolicies=@{'0cce922b-69ae-11d9-bed3-505054503030'=1};auditPrecedence=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Type='DWord';Value=1};commandLineCapture=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Type='DWord';Value=1};securityChannelEnabled=$true} +$script:process=[pscustomobject]@{ProcessId=123;ParentProcessId=456;Executable='C:\Windows\System32\cmd.exe';Arguments='/d /c echo WELA_PROBE_0123456789abcdef0123456789abcdef';Marker='WELA_PROBE_0123456789abcdef0123456789abcdef';StartedUtc=[DateTime]::UtcNow.AddSeconds(-1).ToString('o');ExitCode=0} +$time=[DateTime]::UtcNow.ToString('o') +$script:xml=@" +46882100Securitytest-host0x80200000000000000x7b0x1c8C:\Windows\System32\cmd.exe"C:\Windows\System32\cmd.exe" /d /c echo WELA_PROBE_0123456789abcdef0123456789abcdef +"@ +Assert (Test-WelaProbeEvent $xml $process $state ([DateTime]::UtcNow)) 'complete native-shaped event matches' +foreach ($replacement in @(@('0x7b','0x7c'),@('0x1c8','0x1c9'),@('WELA_PROBE_0123456789abcdef0123456789abcdef','WELA_PROBE_wrong'),@('4688','4689'),@('2','1'),@('test-host','other-host'),@('0x8020000000000000','0x8010000000000000'),@('Security','System'),@('100','0'),@('Name="Microsoft-Windows-Security-Auditing"','Name="Other"'),@('0x1c8','0x1c80x1c8'))) { + Assert (-not (Test-WelaProbeEvent ($xml.Replace($replacement[0],$replacement[1])) $process $state ([DateTime]::UtcNow))) "mismatch rejected: $($replacement[0])" +} +Assert (-not (Test-WelaProbeEvent (']>'+$xml) $process $state ([DateTime]::UtcNow))) 'external entity rejected' +Assert (-not (Test-WelaProbeEvent $xml $process $state ([DateTime]::UtcNow.AddDays(-1)))) 'future relative event rejected' +$prior=$process.StartedUtc; $process.StartedUtc=[DateTime]::UtcNow.AddDays(1).ToString('o') +Assert (-not (Test-WelaProbeEvent $xml $process $state ([DateTime]::UtcNow))) 'event predating probe rejected'; $process.StartedUtc=$prior +Assert-WelaProbePrerequisites $state +foreach ($field in @('role','patch','domainJoined','installedRoles')) { + $prior=$state.context.$field + switch ($field) { + 'role' { $state.context.role='DomainController' } + 'patch' { $state.context.patch='20348.9999' } + 'domainJoined' { $state.context.domainJoined=$true } + 'installedRoles' { $state.context.installedRoles=@('ADCS-Cert-Authority') } + } + Throws { Assert-WelaProbePrerequisites $state } "contradictory native context $field rejected" + $state.context.$field=$prior +} +$state.context.role='ADCS' +Throws { Assert-WelaProbePrerequisites $state } 'CA label without its installed role cannot establish a CA probe context' +$state.context.role='MemberServer' +foreach ($mask in @(0,2,$null,'1')) { $state.auditPolicies['0cce922b-69ae-11d9-bed3-505054503030']=$mask; Throws { Assert-WelaProbePrerequisites $state } 'missing success bit or unknown mask rejected' } +$state.auditPolicies['0cce922b-69ae-11d9-bed3-505054503030']=1 +$state.auditPrecedence.Type='String'; Throws { Assert-WelaProbePrerequisites $state } 'string masquerading as DWORD rejected'; $state.auditPrecedence.Type='DWord' +$state.commandLineCapture.Value=0; Throws { Assert-WelaProbePrerequisites $state } 'missing command line capture rejected'; $state.commandLineCapture.Value=1 +$state.securityChannelEnabled=$false; Throws { Assert-WelaProbePrerequisites $state } 'disabled Security rejected'; $state.securityChannelEnabled=$true +$state.hostObservation.RolesStatus='Unknown'; Throws { Assert-WelaProbePrerequisites $state } 'unknown roles rejected'; $state.hostObservation.RolesStatus='Observed' +$script:launched=0; $script:reads=0; $script:scenario='success' +function Get-WelaProbeState { $script:reads++; if ($scenario -eq 'denied') { throw 'denied' }; if ($scenario -eq 'drift' -and $reads -gt 2) { $script:state.auditPolicies['0cce922b-69ae-11d9-bed3-505054503030']=3 }; return $script:state } +function Start-WelaProbeProcess { $script:launched++; return $script:process } +function Read-WelaProbeEvents { + if ($scenario -eq 'query-denied') { throw 'Security access denied' } + if ($scenario -eq 'ambiguous') { return [pscustomobject]@{Xml=@($script:xml,$script:xml);Capped=$false} } + if ($scenario -eq 'timeout') { return [pscustomobject]@{Xml=@();Capped=$false} } + [pscustomobject]@{Xml=@($script:xml);Capped=($scenario -eq 'cap')} +} +$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-native-probe-tests-'+[guid]::NewGuid().ToString('N')); $null=New-Item -ItemType Directory -Path $root +try { + # PowerShell's current location is independent of the process working directory. + $relativeName='relative-probe-'+[guid]::NewGuid().ToString('N') + Push-Location $root + try { + $relative=Invoke-WelaNativeValidation -Action Run -OutputPath $relativeName -TimeoutSeconds 1 + Assert ($relative.ExitCode -eq 0 -and $relative.OutputPath -eq (Join-Path $root $relativeName)) 'relative probe destination resolves against the PowerShell location' + Assert (Test-Path -LiteralPath (Join-Path $root "$relativeName/manifest.json")) 'relative probe manifest is written in the requested location' + } finally { Pop-Location } + $script:launched=0 + $plan=Invoke-WelaNativeValidation + Assert ($plan.Status -eq 'PrerequisitesObserved' -and $launched -eq 0 -and $plan.Artifacts.Count -eq 0) 'default Plan observes without launching or exporting' + Throws { Invoke-WelaNativeValidation -Action Run } 'Run needs new destination' + Throws { Invoke-WelaNativeValidation -OutputPath (Join-Path $root 'unused') } 'Plan rejects output destination' + foreach ($case in @('success','cap','ambiguous','timeout','denied','query-denied','drift')) { + $script:scenario=$case; $script:reads=0; $script:state.auditPolicies['0cce922b-69ae-11d9-bed3-505054503030']=1 + $destination=Join-Path $root $case + $result=Invoke-WelaNativeValidation -Action Run -OutputPath $destination -TimeoutSeconds 1 + Assert (Test-Path (Join-Path $destination 'manifest.json')) 'partial and completed manifests are retained' + Assert ($result.ReadyRuleCredit -eq 0 -and $result.PolicyChanges -eq 0) 'no policy changes or readiness credit' + if ($case -eq 'success') { + Assert ($result.Status -eq 'NativeEventObserved' -and $result.ExitCode -eq 0 -and $result.Artifacts.Count -eq 4) 'exact native probe artifact set collected' + foreach ($artifact in $result.Artifacts) { Assert ((Get-FileHash -LiteralPath (Join-Path $destination $artifact.path)).Hash.ToLowerInvariant() -ceq $artifact.sha256) 'artifact hashes verify' } + } else { Assert ($result.Status -eq 'Unverified' -and $result.ExitCode -eq 1 -and $result.Diagnostic) "failure cannot claim telemetry: $case" } + Throws { Invoke-WelaNativeValidation -Action Run -OutputPath $destination } 'existing output directory refused' + } +} finally { Remove-Item -LiteralPath $root -Recurse -Force } +# The public dispatcher must reject probe options before it reaches unrelated mutation paths. +foreach ($args in @(@('configure','-Profile','wela','-ProbeAction','Plan'),@('native-validation','-Auto'),@('native-validation','-Role','Client','-Build','26100'))) { + $saved=$ErrorActionPreference; $ErrorActionPreference='Continue' + try { $output=& (Get-Process -Id $PID).Path -NoProfile -File (Join-Path $repo 'WELA.ps1') @args 2>&1; $code=$LASTEXITCODE } finally { $ErrorActionPreference=$saved } + Assert ($code -ne 0 -and ($output -join ' ') -match 'No command') 'public scope guard fails before execution' +} +$global:LASTEXITCODE=0 +Write-Host "Passed $script:checks native-validation assertions. Fixtures are synthetic; no native telemetry is claimed." diff --git a/tests/NativeValidation.Windows.Tests.ps1 b/tests/NativeValidation.Windows.Tests.ps1 new file mode 100644 index 00000000..033d4caf --- /dev/null +++ b/tests/NativeValidation.Windows.Tests.ps1 @@ -0,0 +1,62 @@ +param([switch]$AllowDisposablePolicyWrite) +$ErrorActionPreference='Stop' +if ($env:OS -ne 'Windows_NT') { Write-Host 'Skipped: native Windows is required.'; exit 0 } +if (-not $AllowDisposablePolicyWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted') { throw 'This native event test requires explicit policy-write opt-in on a disposable GitHub-hosted runner.' } +$repo=Split-Path $PSScriptRoot -Parent +. (Join-Path $repo 'scripts/Configuration.ps1') +. (Join-Path $repo 'scripts/ControlApplicability.ps1') +. (Join-Path $repo 'scripts/NativeValidation.ps1') +Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force +$guid='0cce922b-69ae-11d9-bed3-505054503030' +$controls=@( + [pscustomobject]@{Path='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa';Name='SCENoApplyLegacyAuditPolicy'}, + [pscustomobject]@{Path='HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit';Name='ProcessCreationIncludeCmdLine_Enabled'} +) +$beforeMask=(Get-WelaEffectiveAuditPolicy)[$guid] +foreach ($control in $controls) { $control | Add-Member NoteProperty Before (Get-WelaRegistryState -Path $control.Path -Name $control.Name) } +$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-native-4688-'+[guid]::NewGuid().ToString('N')) +$null=New-Item -ItemType Directory -Path $root +$receipt=Join-Path $root 'policy-before.json' +[pscustomobject]@{AuditMask=$beforeMask;Controls=$controls} | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $receipt -Encoding UTF8 +$touched=$false; $restored=$false +try { + $touched=$true + foreach ($control in $controls) { + if (-not (Test-Path -LiteralPath $control.Path)) { $null=New-WelaRegistryKey -Path $control.Path } + $null=New-ItemProperty -LiteralPath $control.Path -Name $control.Name -Value 1 -PropertyType DWord -Force + } + Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 1 -Mode minimum + $destination=Join-Path $root 'collection' + $result=Invoke-WelaNativeValidation -Action Run -OutputPath $destination -TimeoutSeconds 30 + if ($result.ExitCode -ne 0 -or $result.Status -ne 'NativeEventObserved') { throw ($result | ConvertTo-Json -Depth 20) } + if ($result.ReadyRuleCredit -ne 0 -or $result.Artifacts.Count -ne 4) { throw 'Collector mislabeled incomplete evidence.' } + foreach ($artifact in $result.Artifacts) { + if ((Get-FileHash -LiteralPath (Join-Path $destination $artifact.path)).Hash.ToLowerInvariant() -cne $artifact.sha256) { throw 'Native artifact hash mismatch.' } + } + $event=[IO.File]::ReadAllText((Join-Path $destination 'event.xml')) + if (-not (Test-WelaProbeEvent $event $result.Process $result.BeforeState ([DateTime]::UtcNow))) { throw 'Native event cannot be independently matched.' } + Write-Host "Native 4688 event observed on $($result.BeforeState.context.role) $($result.BeforeState.context.patch) under PowerShell $($PSVersionTable.PSVersion). Complete-rule, backend and other-role validation remain pending." +} finally { + if ($touched) { + $errors=@() + try { Set-WelaEffectiveAuditPolicy -Guid $guid -Mask $beforeMask -Mode exact } catch { $errors+=$_.Exception.Message } + foreach ($control in $controls) { + try { + if ($control.Before.ValueExists) { $null=New-ItemProperty -LiteralPath $control.Path -Name $control.Name -Value $control.Before.Value -PropertyType $control.Before.Type -Force } + else { Remove-ItemProperty -LiteralPath $control.Path -Name $control.Name -ErrorAction SilentlyContinue } + if (-not $control.Before.KeyExists -and (Test-Path -LiteralPath $control.Path)) { + $key=Get-Item -LiteralPath $control.Path + if ($key.ValueCount -eq 0 -and $key.SubKeyCount -eq 0) { Remove-Item -LiteralPath $control.Path -ErrorAction Stop } + } + $after=Get-WelaRegistryState -Path $control.Path -Name $control.Name + if (($after | ConvertTo-Json -Compress) -cne ($control.Before | ConvertTo-Json -Compress)) { throw "Registry restoration differs: $($control.Name)" } + } catch { $errors+=$_.Exception.Message } + } + try { if ((Get-WelaEffectiveAuditPolicy)[$guid] -ne $beforeMask) { throw 'Audit mask restoration differs.' } } catch { $errors+=$_.Exception.Message } + $restored=$errors.Count -eq 0 + if (-not $restored) { throw "Policy restoration failed; receipt retained at $receipt : $($errors -join '; ')" } + } + if ($restored) { Remove-Item -LiteralPath $root -Recurse -Force } +} +$global:LASTEXITCODE=0 +Write-Host 'Native 4688 collection and exact policy restoration passed.' diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 675c4ce1..e3a398d7 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,7 @@ **改善:** +- ネイティブ Security 4688 の無害な固定プローブを収集する明示的な `native-validation` を追加。型付き前提条件、イベントの厳密な照合、前後の状態、ハッシュ付き証拠を新規の非公開ディレクトリに記録します。不完全・上限到達・曖昧・ドリフトの結果は未検証のままです。監査ポリシー変更や Sigma 利用可能ルール数の加算は行いません。使い捨て Server 2022/2025 テストで実イベントとポリシー復元を確認し、Windows 11/DC/ADCS とバックエンドの検証は別途必要です。 (#413) (@Shirofune-Security) - ローカル監査権限と`CrashOnAuditFail`を監査・計画・出典別に設定する任意実行の`audit-integrity`を追加しました。実際のクライアント・メンバーサーバー・DCを区別し、Microsoft SCTで省略された設定は保持します。対象SIDの一覧、権限削除の明示指定、権限単位のLSA更新、完全な復旧記録と変更直前・変更後の検証により、無関係な権限を保持します。復旧状態では変更せず、ネイティブCIは読み取りだけを行います。トークン・GPO・サービス・イベントの検証は別途ラボで必要となり、Sigma検知範囲には加算しません。 (#412) (@Shirofune-Security) - 読み取り専用の`retention-health`を追加し、送信元/収集サーバーの標準ログバッファ、確認した先頭レコードの経過日数、申告されたアーカイブ方針、件数を制限したローカルEVTX/ACL一覧、各言語のWEF・時刻情報とログ消失・消去・満杯の兆候をJSONと単独で表示できるHTMLに分けて記録します。保持されたイベントの時刻に基づく件数率とUTF-8 XMLバイト数の試算には上限・前提を明示し、容量・完全な保持・実効読み取り権限・時刻同期・転送成功の証明とは扱いません。複数ホストでのロールオーバー・復旧・到着検証は別途必要です。 (#410) (@Shirofune-Security) - 読み取り専用の`control-applicability`を追加し、旧CISのApplication Guard監査要件を保持しつつ、Windows 11 24H2以降では削除済み・対象外と表示します。`default-evidence`で正確なビルド・パッチ・ドメイン参加・役割を含む現状を記録し、出典とレビュー情報が一致する参照環境と比較できます。旧ベースラインの既定値は履歴情報として保持し、未検証の既定値はUnknownと表示します。合成テストと読み取り専用CIからクリーンインストールやイベント生成の証明は行いません。 (#409) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index f4d83bbb..77c695da 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,7 @@ **Improvements:** +- Added opt-in `native-validation` to collect a fixed benign Security 4688 probe with typed prerequisites, exact native event matching, before/after state and hashed components in a new private directory. Partial, capped, ambiguous and drifted results remain unverified; the collector changes no audit policy and grants no Sigma readiness credit. Disposable Server 2022/2025 tests exercise real events with verified policy restoration; Windows 11/DC/ADCS and backend acceptance remain separate. (#413) (@Shirofune-Security) - Added opt-in `audit-integrity` audit, plan and source-profile configuration for local audit privileges and `CrashOnAuditFail`, with separate actual client/member/DC scope and preserved Microsoft SCT omissions. Exact affected SIDs, explicit privilege-removal consent, per-right LSA updates, complete recovery journals and fresh/readback checks preserve unrelated privileges. Recovery states are blocked; native CI reads policy only, while token, GPO, service and event validation remains a lab requirement without Sigma credit. (#412) (@Shirofune-Security) - Added read-only `retention-health` source/collector JSON and self-contained HTML reports separating native buffers, observed record-boundary ages, declared archive policy, bounded local EVTX/ACL inventory, localized WEF/time evidence and loss/clear/full indicators. Retained-event timestamp rates and UTF-8 XML-byte scenarios expose caps and assumptions; none establish archive capacity, complete retention, effective reader access, synchronized time or successful forwarding. Multi-host rollover/recovery/arrival validation remains pending. (#410) (@Shirofune-Security) - Added read-only `control-applicability` for the historical CIS Application Guard audit requirement, reporting removal on Windows 11 24H2+ without remediation. Added exact build/patch/join/role native snapshots and provenance-bound reference comparison through `default-evidence`. Legacy baseline default strings are retained as historical hints while public defaults remain Unknown without reviewed scenario evidence. Synthetic fixtures and native read-only CI do not claim clean-install or event-generation proof. (#409) (@Shirofune-Security)