diff --git a/.gitattributes b/.gitattributes index 01033988..5fbbcc74 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,6 +24,11 @@ # Fixed public pending-request fixture is pinned by its exact byte hash. /tests/fixtures/adcs-pending-probe.csr text eol=lf +# DNS analytical receipts pin the catalog and lifecycle implementation bytes. +/config/native_provider_packs.json text eol=lf +/scripts/NativeProviderPacks.ps1 text eol=lf +/scripts/DnsAnalytical.ps1 text eol=lf +/scripts/DnsAnalyticalArchive.cs text eol=lf # Native WEC XML reader source identity remains identical across checkouts. /modules/WecSubscriptionXml.cs text eol=lf scripts/AppLockerProbe.ps1 text eol=lf diff --git a/.github/workflows/dns-analytical.yml b/.github/workflows/dns-analytical.yml new file mode 100644 index 00000000..25f0044b --- /dev/null +++ b/.github/workflows/dns-analytical.yml @@ -0,0 +1,30 @@ +name: DNS analytical lifecycle +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + dns-analytical: + timeout-minutes: 25 + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + engine: [powershell, pwsh] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - name: Safe lifecycle fixtures on Windows PowerShell 5.1 + if: matrix.engine == 'powershell' + shell: powershell + run: ./tests/DnsAnalytical.Tests.ps1 + - name: Safe lifecycle fixtures on PowerShell 7 + if: matrix.engine == 'pwsh' + shell: pwsh + run: ./tests/DnsAnalytical.Tests.ps1 + - name: Explicit disposable standalone DNS role and loopback event257 + shell: powershell + run: ./tests/DnsAnalytical.Windows.Tests.ps1 -AllowDisposableDns -TestEngine '${{ matrix.engine }}' diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 2d559c62..6102b171 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,6 +4,7 @@ **改善:** +- 任意実行の`dns-analytical`を追加し、DNS Serverの分析ログを監査・計画・明示選択で設定できるようにしました。トレース再設定への個別同意、永続的な変更前記録、容量を制限したネイティブETLの退避とハッシュ検証に対応し、ACL・パス・既存の大きいバッファを保持します。退避失敗で停止した状態を失敗として報告します。使い捨てDNS環境のループバックイベント257と設定復元のテストを追加し、転送・Sigmaの利用可能性は未検証のままです。 (#425) (@Shirofune-Security) - 読み取り専用の`wec-runtime`を追加し、WEC標準APIの稼働状態、数値エラー、UTC時刻と件数を制限した送信元別の観測結果を取得します。実行者・ホスト・定義の変更、不明な値と上限到達を明示し、既存のWEF・保存状態レポートでは元の文字列も保持します。過去の送信元一覧を現在の接続数とは扱わず、Activeからイベント到着やSigma利用可能性を推定しません。無効な使い捨てサブスクリプションで検証し、サービス状態とテスト対象を復元します。WEF・EVTXの合成テスト資料で時刻の末尾ゼロが失われる問題も修正しました。 (#424) (@Shirofune-Security) - 既存の監査専用ポリシーに対する固定ネイティブ EXE の実行と AppLocker イベントの厳密な照合、保護されたハッシュ付き証拠、変更検出を行うオプトインの `applocker-probe` を追加しました。ポリシー・サービス・チャネルの変更や Sigma の評価加算は行いません。使い捨て Windows CI は一時的な監査専用テストポリシーで実際の 8003 を収集し、管理状態の観測を保持して GP ポリシーとチャネル設定を復元します。 (#423) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc291ae..093cad4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Improvements:** +- Added opt-in `dns-analytical` auditing, planning and selective DNS Server channel configuration with explicit trace-reset consent, durable state records and bounded native ETL archives verified before resets. Preserve ACLs, paths and larger buffers; report stopped partial failures honestly. Added disposable standalone-DNS tests for loopback event 257 and exact configuration restoration; forwarding and Sigma readiness remain unverified. (#425) (@Shirofune-Security) - Added read-only `wec-runtime` with typed native WEC activity, numeric errors, UTC timestamps and bounded per-source observations. Actual reader/context and definition checks keep partial reads, caps and drift explicit; existing WEF/retention inventories retain raw text alongside typed fields. Historical source lists are not connection counts and Active grants no arrival or Sigma credit. Disposable disabled-subscription tests restore service state and remove only their owned fixture. Also fixed synthetic WEF/EVTX fixture timestamp roundtrips without weakening bundle validation. (#424) (@Shirofune-Security) - Added opt-in `applocker-probe` planning and fixed native EXE collection against existing audit-only policy, with exact AppLocker event correlation, private hashed evidence and drift checks. No policy/service/channel changes or Sigma credit; disposable Windows CI prepares one temporary audit-only fixture for real 8003 collection, preserving management observations and restoring GP policy/channel settings. (#423) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index 1c84045e..ded0ee9e 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -97,6 +97,12 @@ [string]$RecoveryOutputPath, [string]$ArrivalProbePath, [string]$ArrivalOutputPath, + [ValidateSet('Audit','Plan','Configure')][string]$DnsAction = 'Audit', + [ValidateSet('Enabled','Disabled')][string]$DnsState, + [ValidateSet('Preserve','Circular','Retain')][string]$DnsRetention = 'Preserve', + [ValidateRange(1048576,1073741824)][long]$DnsMinimumBytes = 33554432, + [ValidateRange(1048576,4294967296)][long]$DnsArchiveMaximumBytes = 1073741824, + [switch]$AllowDnsTraceReset, [string[]]$WecRuntimeId, [ValidateRange(1,512)][int]$WecRuntimeMaximumSources=128, [ValidateSet('Plan','Run')][string]$AppLockerProbeAction = 'Plan', @@ -140,6 +146,7 @@ Import-Module (Join-Path $ScriptRoot "modules/EventLogSettings.psm1") -ErrorActi Import-Module (Join-Path $ScriptRoot "modules/NativeChannelAccess.psm1") -ErrorAction Stop . (Join-Path $ScriptRoot "scripts/NativeChannelConfiguration.ps1") . (Join-Path $ScriptRoot "scripts/NativeProviderPacks.ps1") +. (Join-Path $ScriptRoot "scripts/DnsAnalytical.ps1") Import-Module (Join-Path $ScriptRoot "modules/WefSubscriptions.psm1") -ErrorAction Stop . (Join-Path $ScriptRoot "scripts/WefDeployment.ps1") . (Join-Path $ScriptRoot "scripts/RetentionHealth.ps1") @@ -1844,6 +1851,7 @@ function Get-WelaUserProfiles { $usage = @" Usage: + ./WELA.ps1 dns-analytical -Help # Dedicated DNS Server direct-channel lifecycle ./WELA.ps1 wec-runtime -WecRuntimeId subscription-id -ResultsPath new-runtime.json ./WELA.ps1 targeted-sacl -Help # Selected existing local SACL targets; read-only by default ./WELA.ps1 gpo-package -GpoAction Plan -GpoProfile wela-2.2.0 -Role Client -Build 26100 @@ -1927,6 +1935,12 @@ Write-Host "" Write-Host "WELA v$WELAVersion - $WELAReleaseName" Write-Host "" +if ($Cmd -ne 'dns-analytical' -and @($PSBoundParameters.Keys | Where-Object { $_ -like 'Dns*' -or $_ -eq 'AllowDnsTraceReset' }).Count) { + throw 'DNS analytical options require dns-analytical. No command was run.' +} +if ($Cmd -eq 'dns-analytical' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','DnsAction','DnsState','DnsRetention','DnsMinimumBytes','DnsArchiveMaximumBytes','AllowDnsTraceReset','Auto','DryRun','BackupPath','ResultsPath','Help') }).Count) { + throw 'dns-analytical accepts only dedicated DNS lifecycle and report options. No command was run.' +} if ($Cmd -ne 'targeted-sacl' -and @($PSBoundParameters.Keys | Where-Object { $_ -like 'TargetSacl*' }).Count) { throw 'TargetSacl options require targeted-sacl. No command was run.' } @@ -2045,7 +2059,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 'targeted-sacl' -and $TargetSaclAction -eq 'Configure') -and -not ($Cmd -eq 'audit-recovery' -and $RecoveryAction -eq 'Restore') -and -not ($Cmd -eq 'gpo-package' -and $GpoAction -eq 'Export') -and -not ($Cmd -eq 'audit-integrity' -and $IntegrityAction -eq 'Configure') -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 +if ($DryRun -and -not ($Cmd -eq 'dns-analytical' -and $DnsAction -eq 'Configure') -and -not ($Cmd -eq 'targeted-sacl' -and $TargetSaclAction -eq 'Configure') -and -not ($Cmd -eq 'audit-recovery' -and $RecoveryAction -eq 'Restore') -and -not ($Cmd -eq 'gpo-package' -and $GpoAction -eq 'Export') -and -not ($Cmd -eq 'audit-integrity' -and $IntegrityAction -eq 'Configure') -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 'provider-packs' -and $ProviderAction -eq 'Configure') -and -not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure') -and -not ($Cmd -eq 'smb-auditing' -and $SmbAction -eq 'Configure') -and @@ -2054,7 +2068,7 @@ if ($DryRun -and -not ($Cmd -eq 'targeted-sacl' -and $TargetSaclAction -eq 'Conf -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, ldap-diagnostics -LdapAction Configure, provider-packs -ProviderAction Configure, audit-integrity -IntegrityAction Configure, audit-notifications -NotificationAction Configure, and targeted-sacl -TargetSaclAction Configure; gpo-package -GpoAction Export writes component files only. 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, provider-packs -ProviderAction Configure, audit-integrity -IntegrityAction Configure, audit-notifications -NotificationAction Configure, and targeted-sacl -TargetSaclAction Configure; gpo-package -GpoAction Export writes component files only; dns-analytical supports -DnsAction 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.' @@ -2084,6 +2098,13 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi } switch ($Cmd.ToLower()) { + 'dns-analytical' { + if ($Help) { Write-Host 'Usage: ./WELA.ps1 dns-analytical [-DnsAction Audit|Plan|Configure] [-DnsState Enabled|Disabled] [-DnsRetention Preserve|Circular|Retain] [-DnsMinimumBytes bytes] [-DnsArchiveMaximumBytes bytes] [-AllowDnsTraceReset] [-Auto] [-DryRun] [-BackupPath new-private-directory] [-ResultsPath new.json]. Configure requires explicit DnsState and reset consent for changes; archives stopped traces before reset. See docs/dns-analytical.md.'; return } + $report=Invoke-WelaDnsAnalytical -Action $DnsAction -State $DnsState -Retention $DnsRetention -MinimumBytes $DnsMinimumBytes -ArchiveMaximumBytes $DnsArchiveMaximumBytes -AllowTraceReset:$AllowDnsTraceReset -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $report + if ($report.ExitCode) { exit $report.ExitCode } + } + 'wec-runtime' { if ($Help) {Write-Host 'Usage: ./WELA.ps1 wec-runtime -WecRuntimeId id1,id2 [-WecRuntimeMaximumSources 1..512] [-ResultsPath new.json]. Read-only local typed WEC activity/errors/times; historical source inventory is not a connection count. No arrival or Sigma claim. See docs/wec-runtime.md.';return} $report=Invoke-WelaWecRuntime -Ids $WecRuntimeId -MaximumSources $WecRuntimeMaximumSources -ResultsPath $ResultsPath diff --git a/docs/dns-analytical.md b/docs/dns-analytical.md new file mode 100644 index 00000000..9cfc000e --- /dev/null +++ b/docs/dns-analytical.md @@ -0,0 +1,64 @@ +# DNS Server analytical logging + +`dns-analytical` is a dedicated, opt-in adapter for `Microsoft-Windows-DNSServer/Analytical`. Audit is the read-only default. The existing `provider-packs` analytical entry remains manual-only; this command does not open the ordinary channel setter to arbitrary Analytical or Debug channels. Native Windows only; Sysmon is excluded. + +```powershell +./WELA.ps1 dns-analytical +./WELA.ps1 dns-analytical -DnsAction Plan -DnsState Enabled -DnsRetention Retain +./WELA.ps1 dns-analytical -DnsAction Configure -DnsState Enabled -DryRun + +# Read the plan and review the interruption, archive location and retention choice first. +./WELA.ps1 dns-analytical -DnsAction Configure -DnsState Enabled ` + -DnsRetention Retain -AllowDnsTraceReset -BackupPath C:\WELA-Recovery\dns-001 ` + -ResultsPath .\dns-configured.json + +# An explicit stop also archives the stopped trace; it does not clear it. +./WELA.ps1 dns-analytical -DnsAction Configure -DnsState Disabled ` + -AllowDnsTraceReset -BackupPath C:\WELA-Recovery\dns-002 +``` + +Use a 64-bit elevated Windows session on a reviewed server build with the DNS service already running. The actual role/build/patch context, provider GUID, channel links, Analytical type and event 257's exact `QNAME` string template are checked. Missing roles, services, manifests, fields or unknown state block changes. The command never installs DNS, starts a service, changes a zone, issues a DNS query or changes audit policy. Clients are outside this adapter's scope. + +The built-in provider profile is `native-provider-packs-v1`; its catalog and pinned full-rule artifacts are validated. Their source identities and the implementation hashes are recorded. This is a DNS provider profile, separate from advanced audit `-Profile`/`-ProfileFile`. Dedicated DNS options are rejected on unrelated commands. Configure requires an explicit `-DnsState Enabled|Disabled`. + +## Sizes and retention + +Enabling uses a default 32 MiB minimum, a WELA optional floor rather than a Microsoft baseline requirement. `-DnsMinimumBytes` accepts 1 MiB–1 GiB, rounds upward to native 64 KiB units and preserves larger existing buffers. Disabling does not resize the buffer. ACL, registered trace path, provider and other observed settings are preserved. + +`-DnsRetention Preserve` is the default. Explicit `Circular` permits oldest-event overwrite; `Retain` stops accepting new events when full. AutoBackup is outside this adapter's supported direct-channel modes. Neither mode proves a retention duration or complete collection. Microsoft's [DNS logging guidance](https://learn.microsoft.com/en-us/windows-server/networking/dns/dns-logging-and-diagnostics) describes the tradeoffs and recommends monitoring logging load. + +An enabled circular direct channel cannot be queried in the same way as an Operational log. A query failure in this state does not prove that logging failed. Microsoft documents [stopping a circular analytical channel before viewing/exporting its events](https://learn.microsoft.com/en-us/troubleshoot/windows-server/system-management-components/error-when-enabling-analytic-debug-event-log). This adapter reports configuration separately from native event generation. + +## Consent, durable archives and failures + +Every non-idempotent Configure requires `-AllowDnsTraceReset`, including stopping a running trace. Stopping creates a collection gap; enabling, resizing or changing retention can reset trace contents. `-Auto` only skips the ordinary confirmation and cannot supply reset consent. Dry-run requires Configure and writes neither recovery files nor channel settings. + +Before a transition, the adapter creates a **new private recovery directory** under an existing operator-controlled parent and durably records `01-before.json`. It rechecks source/context/schema/channel state, stops the channel if necessary, verifies the stopped configuration and records `02-stopped.json`. + +It then opens the registered local ETL through a stable native handle, refuses reparse points, non-fixed drives, remote paths and ambiguous stream/wildcard/control/trailing-dot-space paths and writer/delete sharing, and copies in 64 KiB blocks to a new `trace-before.etl`. `-DnsArchiveMaximumBytes` defaults to 1 GiB and accepts 1 MiB–4 GiB; an oversized trace blocks the reset rather than being truncated. File identity, length and SHA-256 are checked against source and archived bytes. `03-archive.json` distinguishes: + +- `ArchivedBytes`: the complete bytes available in that stopped file were copied and read back with the same hash. +- `ObservedAbsent`: native `FILE_NOT_FOUND` was observed under existing local parents. No empty archive or hash is fabricated. +- Failure: inaccessible, locked, empty existing, oversized, changed or unsupported trace state. This cannot authorize a reset. + +Both source and saved archive are revalidated immediately before the reset-capable native command. Readback must match the desired enable/size/retention values and preserved ACL/path/context. `04-applied.json` records that verified moment; `05-result.json` records final status and any later drift. New result paths resolve relative to PowerShell's current location, require unambiguous fixed-drive paths, and never overwrite existing evidence or write alternate data streams. The last source/archive recheck runs after the final context observation and immediately before the native reset call; it is not an atomic compare-and-reset operation. + +If archival fails after a stop, the result is **Failed** and the trace remains stopped. Automatically re-enabling could destroy the very evidence that could not be archived. Recovery files and the observed after-state remain available; a failed/partial operation is never unconditional success. A successful repeat is AlreadyCompliant and performs no stop, archive or reset. + +The archive establishes byte preservation at the recorded time, **not** EVTX validity, complete historic retention, continuity, forwarding or rule coverage. No compare-and-swap transaction exists against concurrent Event Log administration. Rechecks and stable archive handles narrow races; use an isolated change window. Later state changes are not prevented. + +## Recovery + +Read `01-before.json`, `03-archive.json` and the final result together. Preserve archived traces outside the live trace path, verify their hashes, and review the current native channel state. Never copy an old archive over an active log. Returning a size/retention/enable setting can itself reset the current trace: stop and archive that current trace first, then perform an explicitly reviewed restoration through the policy authority. This command preserves larger buffers and does not provide automatic shrinking or blind rollback. + +The ordinary audit-policy recovery workflow does not interpret these dedicated DNS receipts. A receipt records observations and completed transitions; it is not authorization to overwrite later settings or evidence. + +## Validation and remaining scope + +Mocked tests cover explicit selection/consent, role/service/schema blockers, larger buffers and rounding, native failures, source/channel/trace/archive races, journal failures, typed absence, stopped partial states, final drift and public guards. The native workflow uses separate disposable Server 2022/2025 hosts for PowerShell 5.1 and 7. It creates the standalone DNS role and one unique authoritative `.test` zone, resolves only a unique name through `127.0.0.1`, and checks event 257 with the exact provider GUID, machine, unique `QNAME`, loopback addresses and successful authoritative A response in the stopped ETL archive. Raw ETL XML can contain an empty `System.Channel`; the fixture retains it unchanged and establishes channel provenance from the verified registered trace path plus the exact provider event/version-to-channel manifest link. A contradictory nonempty XML channel is rejected. A separate probe receipt records this distinction; it is not a backend field mapping or readiness claim. Native file fixtures verify streaming copies, locks, absence, empty files and caps. + +The fixture restores exact channel configuration, verifies audit policies are unchanged, and removes only its owned zone/records. It requests removal of the feature it installed; a successful uninstall requiring a restart is recorded as pending disposal of the ephemeral GitHub runner, not complete live OS restoration. Native CI must pass before those cases are claimed as observed. + +Production configuration does not generate a probe automatically. Windows 11/DC/CA behavior, populated production DNS workloads, recursive events 260/261, collection continuity and backend execution remain separate acceptance work. The pinned rules currently use `Microsoft-Windows-DNS-Server/Analytical`, while the observed native channel is `Microsoft-Windows-DNSServer/Analytical`; no silent alias rewrite or readiness uplift is applied. Every result retains `ReadyRuleCredit=0` and `GenerationReadiness=Unverified`. + +Primary references: [DNS logging and event fields](https://learn.microsoft.com/en-us/windows-server/networking/dns/dns-logging-and-diagnostics), [direct-channel query restrictions](https://learn.microsoft.com/en-us/troubleshoot/windows-server/system-management-components/error-when-enabling-analytic-debug-event-log), [wevtutil parameters and byte rounding](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/wevtutil). diff --git a/docs/native-provider-packs.md b/docs/native-provider-packs.md index 6f69892d..1061ba35 100644 --- a/docs/native-provider-packs.md +++ b/docs/native-provider-packs.md @@ -50,3 +50,5 @@ Every attempted native write first records the original enabled flag, exact buff The mocked regression suite exercises missing fields/providers, unsupported types/builds, role/service gates, journal-before-write, dry-run, decline, idempotence, preserved ACL/retention/larger buffers, native failure/false success, prompt races and final schema drift. Windows Server 2022/2025 CI on PowerShell 5.1/7 reads real provider manifests and the public CLI plan and checks that channel settings stay unchanged. It creates no DNS queries, log entries, services or subscriptions. Windows 11/DC/CA event-generation and actual backend/collector validation remain pending acceptance work for issue #386. Primary references: [Microsoft WEF Appendix C/F](https://learn.microsoft.com/en-us/windows/security/operating-system-security/device-management/use-windows-event-forwarding-to-assist-in-intrusion-detection), [DNS logging and diagnostics](https://learn.microsoft.com/en-us/windows-server/networking/dns/dns-logging-and-diagnostics), [EventMetadata](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.eventing.reader.eventmetadata?view=windowsdesktop-10.0), [EventLogLink](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.eventing.reader.eventloglink?view=windowsdesktop-10.0), [Windows 11 release families](https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information), and [Windows Server release families](https://learn.microsoft.com/en-us/windows/release-health/windows-server-release-info). The WEF sample identifies event/channel candidates; it does not validate these rule definitions or this implementation on every build. + +For an explicitly reviewed DNS Server analytical transition with stopped-trace archival, use the separate [DNS analytical lifecycle](dns-analytical.md). The ordinary provider-pack setter continues to refuse Analytical/Debug configuration. diff --git a/scripts/DnsAnalytical.ps1 b/scripts/DnsAnalytical.ps1 new file mode 100644 index 00000000..36e7d950 --- /dev/null +++ b/scripts/DnsAnalytical.ps1 @@ -0,0 +1,198 @@ +# Dedicated DNS analytical lifecycle. Ordinary channel setters remain restricted. +function Get-WelaDnsAnalyticalSources { + foreach($path in @('config/native_provider_packs.json','config/security_rules.json','scripts/NativeProviderPacks.ps1','scripts/Configuration.ps1','scripts/ControlApplicability.ps1','modules/AuditProfiles.psm1','modules/NativeProviders.psm1','scripts/DnsAnalytical.ps1','scripts/DnsAnalyticalArchive.cs')) { + [pscustomobject]@{Path=$path;Sha256=(Get-FileHash -LiteralPath (Join-Path $PSScriptRoot "../$path") -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()} + } + foreach($file in @(Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot '../config/provider_rule_sources') -Filter '*.yml' -File | Sort-Object Name)){ + [pscustomobject]@{Path=('config/provider_rule_sources/'+$file.Name);Sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()} + } +} +function Get-WelaDnsAnalyticalDefinition { + $sources=@(Get-WelaDnsAnalyticalSources) + $catalog=Get-WelaProviderPackCatalog + $pack=@($catalog.packs|Where-Object id -ceq 'dns-server-analytical') + if($pack.Count -ne 1 -or $pack[0].provider -cne 'Microsoft-Windows-DNSServer' -or $pack[0].channel -cne 'Microsoft-Windows-DNSServer/Analytical' -or $pack[0].requiredService -cne 'DNS'){throw 'Reviewed DNS analytical catalog identity differs.'} + $definition=[pscustomobject]@{Id='dns-server-analytical';CatalogId=$catalog.id;Pack=$pack[0];Builds=$catalog.buildFamilies.Server;Sources=$sources;RuleCommit=$catalog.ruleCommit;Rules=@($catalog.ruleReviews|Where-Object {$pack[0].ruleIds -contains $_.id})} + Assert-WelaDnsAnalyticalSources $definition + $definition +} +function Assert-WelaDnsAnalyticalSources { + param($Definition) + $actual=@(Get-WelaDnsAnalyticalSources) + if($actual.Count -ne $Definition.Sources.Count){throw 'DNS source inventory changed.'} + for($i=0;$i -lt $actual.Count;$i++){if($actual[$i].Path -cne $Definition.Sources[$i].Path -or $actual[$i].Sha256 -cne $Definition.Sources[$i].Sha256){throw 'Reviewed DNS sources changed; no further writes permitted.'}} +} +function Get-WelaDnsAnalyticalContext { + $role=Get-WelaHostContext;$detail=Get-WelaDefaultContext + if(-not(Test-WelaDefaultContextComplete $detail) -or $role.Build -ne $detail.Build){throw 'Complete consistent actual Windows context is required.'} + $valid=switch($role.Role){MemberServer {$detail.ProductType -eq 3 -and $detail.DomainRole -in @(2,3)} DomainController {$detail.ProductType -eq 2 -and $detail.DomainRole -in @(4,5)} ADCS {$detail.ProductType -eq 3 -and $detail.DomainRole -in @(2,3) -and $detail.InstalledRoles -contains 'ADCS-Cert-Authority'} default {$false}} + if(-not $valid){throw 'DNS analytical configuration requires a supported actual server role.'} + [pscustomobject]@{Computer=[Environment]::MachineName;Role=$role.Role;Build=$role.Build;Detail=$detail;Key=([Environment]::MachineName+'|'+$role.Role+'|'+(Get-WelaDefaultContextKey $detail))} +} +function Assert-WelaDnsAnalyticalCapability { + param($Definition,$Context,$Service,$Schema) + if($context.Build -notin $Definition.Builds -or $context.Role -notin $Definition.Pack.roles){throw 'Server build/role is outside the reviewed provider profile.'} + if($service.State -ne 'Running'){throw "DNS service is not running or observable ($($service.State)); no feature/service is installed or started."} + if($schema.State -ne 'Observed' -or $schema.ChannelType -ne 'Analytical' -or $schema.ProviderGuid -ine 'eb79061a-a566-4698-9119-3ed2807060e7'){throw "Exact DNS analytical provider/schema is not established. $($schema.Diagnostic)"} + $events=@($schema.Events|Where-Object Id -eq 257) + if(-not $events.Count){throw 'Native DNS event257 is absent from the exact analytical channel.'} + foreach($event in $events){$fields=@($event.Fields|Where-Object Name -ceq 'QNAME');if($fields.Count -ne 1 -or $fields[0].InType -notin @('win:UnicodeString','win:AnsiString')){throw 'Native event257 lacks its exact QNAME string schema.'}} +} +function Get-WelaDnsAnalyticalState { + param($Definition) + if($env:OS -ne 'Windows_NT' -or -not[Environment]::Is64BitProcess){throw 'DNS analytical operations require native 64-bit Windows.'} + $context=Get-WelaDnsAnalyticalContext + $service=Get-WelaNativeService DNS + $schema=Get-WelaProviderPackSchema $Definition.Pack + Assert-WelaDnsAnalyticalCapability $Definition $context $service $schema + $logs=@() + try { + $logs=@(Get-WinEvent -ListLog $Definition.Pack.channel -ErrorAction Stop|Where-Object LogName -ceq $Definition.Pack.channel) + if($logs.Count -ne 1){throw 'Exact DNS analytical channel registration was not returned.'} + $log=$logs[0] + if($log.IsEnabled -isnot [bool] -or $log.MaximumSizeInBytes -lt 1048576 -or [string]$log.LogMode -notin @('Circular','Retain') -or -not $log.SecurityDescriptor -or -not $log.LogFilePath){throw 'DNS channel enable/size/retention/ACL/path metadata is unknown or unsupported.'} + $path=[Environment]::ExpandEnvironmentVariables([string]$log.LogFilePath) + if($path -notmatch '^[A-Za-z]:\\' -or $path -match '[*?<>|]|[ .](\\|$)' -or $path -match '%[^%]+%' -or $path.Substring(2).Contains(':') -or [IO.Path]::GetFullPath($path) -ine $path){throw 'DNS trace path must be an unambiguous canonical local drive path.'} + if(([IO.DriveInfo]::new([IO.Path]::GetPathRoot($path))).DriveType -ne [IO.DriveType]::Fixed){throw 'DNS trace must reside on a local fixed drive.'} + [pscustomobject]@{Context=$context;Channel=$Definition.Pack.channel;Provider=$schema.Provider;ProviderGuid=$schema.ProviderGuid;ChannelType=$schema.ChannelType;ServiceState=$service.State;Schema=$schema;IsEnabled=[bool]$log.IsEnabled;MaximumSizeInBytes=[long]$log.MaximumSizeInBytes;LogMode=[string]$log.LogMode;SecurityDescriptor=[string]$log.SecurityDescriptor;LogFilePath=$path;RegisteredLogFilePath=[string]$log.LogFilePath} + } finally {foreach($log in $logs){if($log -is [IDisposable]){$log.Dispose()}}} +} +function Get-WelaDnsAnalyticalStateKey { + param($State) + $parts=@($State.Context.Key,$State.Channel,$State.Provider,$State.ProviderGuid,$State.ChannelType,$State.ServiceState,[string]$State.IsEnabled,[string]$State.MaximumSizeInBytes,$State.LogMode,$State.SecurityDescriptor,$State.LogFilePath,$State.RegisteredLogFilePath) + foreach($event in @($State.Schema.Events|Sort-Object Id,Version)){$parts+=@([string]$event.Id,[string]$event.Version,$event.TemplateSha256)} + ($parts|ForEach-Object {[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$_))}) -join '|' +} +function Assert-WelaDnsAnalyticalCurrent { + param($Definition,$Expected) + Assert-WelaDnsAnalyticalSources $Definition + $current=Get-WelaDnsAnalyticalState $Definition + if((Get-WelaDnsAnalyticalStateKey $current) -cne (Get-WelaDnsAnalyticalStateKey $Expected)){throw 'DNS role/schema/channel state changed; no further transition is authorized.'} + $current +} +function Initialize-WelaDnsTraceArchive { + if($env:OS -ne 'Windows_NT'){throw 'Native trace archives require Windows.'} + $path=Join-Path $PSScriptRoot 'DnsAnalyticalArchive.cs';$hash=(Get-FileHash $path -Algorithm SHA256).Hash + if(-not('Wela.DnsAnalytical.TraceArchive' -as [type])){Add-Type -Path $path -ErrorAction Stop;$script:WelaDnsArchiveSourceHash=$hash} + if($script:WelaDnsArchiveSourceHash -cne $hash){throw 'Loaded native archive implementation differs from its current source.'} +} +function Copy-WelaDnsAnalyticalTrace { + param([string]$Source,[string]$Destination,[long]$MaximumBytes) + Initialize-WelaDnsTraceArchive + # Separate native entry point avoids PowerShell coercing a null string to empty. + if($Destination){[Wela.DnsAnalytical.TraceArchive]::Read($Source,$Destination,$MaximumBytes)} + else{[Wela.DnsAnalytical.TraceArchive]::Inspect($Source,$MaximumBytes)} +} +function Assert-WelaDnsAnalyticalArchive { + param($Archive,[long]$MaximumBytes) + $fresh=Copy-WelaDnsAnalyticalTrace -Source $Archive.SourcePath -MaximumBytes $MaximumBytes + if($Archive.State -eq 'ObservedAbsent') {if($fresh.State -ne 'ObservedAbsent'){throw 'A trace appeared after observed absence; archive it before reset.'};return} + if($Archive.State -ne 'ArchivedBytes' -or $fresh.State -ne 'ObservedBytes' -or $fresh.Identity -cne $Archive.Identity -or $fresh.Length -ne $Archive.Length -or $fresh.Sha256 -cne $Archive.Sha256){throw 'Source trace changed after its verified archive.'} + $saved=Copy-WelaDnsAnalyticalTrace -Source $Archive.ArchivePath -MaximumBytes $MaximumBytes + if($saved.State -ne 'ObservedBytes' -or $saved.Length -ne $Archive.Length -or $saved.Sha256 -cne $Archive.Sha256){throw 'Recovery trace archive is no longer verified.'} +} +function Resolve-WelaDnsAnalyticalOutput { + param([string]$Path) + if($Path -match '[\x00-\x1f*?<>|"\[\]]' -or $Path -match '(?|""\[\]]|[ .](\\|$)"))throw new InvalidOperationException("Archive path contains unsupported alias or wildcard syntax."); + if(new DriveInfo(Path.GetPathRoot(path)).DriveType!=DriveType.Fixed)throw new InvalidOperationException("Archive path requires a local fixed drive."); + string current=Path.GetDirectoryName(path); + while(!String.IsNullOrEmpty(current)) { + FileAttributes attributes=File.GetAttributes(current); + if((attributes&FileAttributes.ReparsePoint)!=0)throw new InvalidOperationException("Archive path traverses a reparse point."); + if((attributes&FileAttributes.Directory)==0)throw new InvalidOperationException("Archive parent is not a directory."); + string next=Path.GetDirectoryName(current);if(next==current)break;current=next; + } + } + static FileInfo Info(SafeFileHandle handle,string path) { + FileInfo info;if(!GetFileInformationByHandle(handle,out info))throw new Win32Exception(Marshal.GetLastWin32Error(),"Cannot identify trace handle."); + if((info.Attributes&0x410)!=0)throw new InvalidOperationException("Trace must be a regular non-reparse file."); + StringBuilder final=new StringBuilder(32768);uint size=GetFinalPathNameByHandle(handle,final,(uint)final.Capacity,0); + if(size==0||size>=final.Capacity||!String.Equals(final.ToString(),"\\\\?\\"+path,StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("Final trace handle path differs from the selected local path."); + return info; + } + static string Identity(FileInfo info){return info.Volume+":"+info.IndexHigh+":"+info.IndexLow+":"+info.Created+":"+info.Written;} + public static Archive Inspect(string source,long maximumBytes) { return Read(source,null,maximumBytes); } + public static Archive Read(string source,string destination,long maximumBytes) { + if(maximumBytes<1048576||maximumBytes>4294967296L)throw new InvalidOperationException("Archive cap must be between 1 MiB and 4 GiB."); + PathGuard(source);if(destination!=null)PathGuard(destination); + // No writer/delete sharing. A stopped trace that cannot be exclusively read is unknown, never empty. + SafeFileHandle handle=CreateFile(source,0x80000000,1,IntPtr.Zero,3,0x08200000,IntPtr.Zero); + if(handle.IsInvalid){int error=Marshal.GetLastWin32Error();handle.Dispose();if(error==2)return new Archive{State="ObservedAbsent",SourcePath=source};throw new Win32Exception(error,"Trace absence was not established (only FILE_NOT_FOUND is accepted).");} + using(handle) { + FileInfo info=Info(handle,source);long length=((long)info.SizeHigh<<32)|info.SizeLow; + if(length<=0)throw new InvalidOperationException("Existing trace has no readable ETL bytes; no empty archive is fabricated."); + if(length>maximumBytes)throw new InvalidOperationException("Trace exceeds the explicit archive byte cap; no truncation is allowed."); + using(FileStream input=new FileStream(handle,FileAccess.Read,65536,false)) { + FileStream output=null; + try { + if(destination!=null)output=new FileStream(destination,FileMode.CreateNew,FileAccess.ReadWrite,FileShare.None,65536,FileOptions.SequentialScan); + byte[] buffer=new byte[65536];long total=0;string hash; + using(SHA256 sha=SHA256.Create()) { + int read;while((read=input.Read(buffer,0,buffer.Length))!=0) { + total+=read;if(total>maximumBytes||total>length)throw new InvalidOperationException("Trace size changed while archiving."); + sha.TransformBlock(buffer,0,read,buffer,0);if(output!=null)output.Write(buffer,0,read); + } + sha.TransformFinalBlock(new byte[0],0,0);hash=Hex(sha.Hash); + } + if(total!=length||Identity(Info(handle,source))!=Identity(info))throw new InvalidOperationException("Trace identity or size changed during archive."); + string archived=null; + if(output!=null) { + output.Flush(true);output.Position=0;using(SHA256 sha=SHA256.Create()){archived=Hex(sha.ComputeHash(output));} + Info(output.SafeFileHandle,destination); + if(output.Length!=length||archived!=hash)throw new InvalidOperationException("Archived bytes failed readback verification."); + } + return new Archive {State=destination==null?"ObservedBytes":"ArchivedBytes",SourcePath=source,Identity=Identity(info),Length=length,Sha256=hash,ArchivePath=destination,ArchivedSha256=archived}; + } finally {if(output!=null)output.Dispose();} + } + } + } + } +} diff --git a/tests/DnsAnalytical.Tests.ps1 b/tests/DnsAnalytical.Tests.ps1 new file mode 100644 index 00000000..1b00ec51 --- /dev/null +++ b/tests/DnsAnalytical.Tests.ps1 @@ -0,0 +1,147 @@ +$ErrorActionPreference='Stop' +$root=Split-Path $PSScriptRoot -Parent +. (Join-Path $root 'scripts/NativeProviderPacks.ps1') +. (Join-Path $root 'scripts/DnsAnalytical.ps1') +$count=0 +function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++} +function Throws($Code,$Pattern){$message='';try{& $Code|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern; got $message"} +function Clone($Value){$Value|ConvertTo-Json -Depth 24|ConvertFrom-Json} +function Hash($Bytes){$h=[Security.Cryptography.SHA256]::Create();try{([BitConverter]::ToString($h.ComputeHash([byte[]]$Bytes))).Replace('-','').ToLowerInvariant()}finally{$h.Dispose()}} +$script:definition=Get-WelaDnsAnalyticalDefinition +Assert ($script:definition.Pack.mode -eq 'ManualOnly' -and $script:definition.Rules.Count -eq 3) 'Dedicated adapter preserves existing manual-only ordinary pack and three pinned rules.' +$script:sourceReader=(Get-Command Get-WelaDnsAnalyticalSources).ScriptBlock +function Get-WelaDnsAnalyticalDefinition {Clone $script:definition} +$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-dns-tests-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $temp +function Reset { + $script:backup=Join-Path $temp ([guid]::NewGuid().ToString('N'));$script:scenario='';$script:nativeCalls=@();$script:trace=[Text.Encoding]::UTF8.GetBytes('original ETL fixture');$script:reads=0;$script:afterArchiveContextReads=0 + $script:state=[pscustomobject]@{Context=[pscustomobject]@{Key='actual-server-build-patch'};Channel='Microsoft-Windows-DNSServer/Analytical';Provider='Microsoft-Windows-DNSServer';ProviderGuid='eb79061a-a566-4698-9119-3ed2807060e7';ChannelType='Analytical';ServiceState='Running';Schema=[pscustomobject]@{Events=@([pscustomobject]@{Id=257;Version=0;TemplateSha256='pinned-template'})};IsEnabled=$false;MaximumSizeInBytes=[long]33554432;LogMode='Circular';SecurityDescriptor='original-ACL';LogFilePath='C:\fixture\DNS.etl';RegisteredLogFilePath='C:\fixture\DNS.etl'} +} +function Get-WelaDnsAnalyticalState { + param($Definition) + $script:reads++ + if($script:scenario -eq 'denied-state'){throw 'Denied native metadata'} + if($script:scenario -eq 'final-drift' -and (Test-Path (Join-Path $script:backup '04-applied.json'))){$script:state.SecurityDescriptor='concurrent-ACL'} + if(Test-Path (Join-Path $script:backup '03-archive.json')){$script:afterArchiveContextReads++} + if($script:scenario -eq 'final-context-trace-race' -and $script:afterArchiveContextReads -eq 2){$script:trace=[Text.Encoding]::UTF8.GetBytes('changed during final context observation')} + Clone $script:state +} +function Get-WelaDnsAnalyticalSources { + $sources=@(& $script:sourceReader) + if($script:scenario -eq 'source-race' -and (Test-Path (Join-Path $script:backup '01-before.json'))){$sources[0].Sha256='changed'} + $sources +} +function Read-Host { + if($script:scenario -eq 'prompt-race'){$script:state.MaximumSizeInBytes=67108864} + if($script:scenario -eq 'declined'){return 'n'} + 'y' +} +function Copy-WelaDnsAnalyticalTrace { + param([string]$Source,[string]$Destination,[long]$MaximumBytes) + if($script:scenario -eq 'archive-denied'){throw 'Trace access denied; not empty'} + if($Source -eq $script:state.LogFilePath){ + Assert (-not $script:state.IsEnabled) 'Source archive/verification occurs only while stopped.' + $bytes=$script:trace + if($null -eq $bytes){return [pscustomobject]@{State='ObservedAbsent';SourcePath=$Source;Identity=$null;Length=0;Sha256=$null;ArchivePath=$null;ArchivedSha256=$null}} + }else{$bytes=[IO.File]::ReadAllBytes($Source)} + if($bytes.Length -gt $MaximumBytes){throw 'Trace exceeds explicit cap'} + $hash=Hash $bytes + if($Destination){ + Assert (Test-Path (Join-Path $script:backup '01-before.json')) 'Durable before-state precedes archive.' + [IO.File]::WriteAllBytes($Destination,$bytes) + if($script:scenario -eq 'trace-race'){$script:trace=[Text.Encoding]::UTF8.GetBytes('changed ETL')} + if($script:scenario -eq 'archive-tamper'){[IO.File]::WriteAllText($Destination,'tampered')} + if($script:scenario -eq 'appeared-trace'){$script:trace=[Text.Encoding]::UTF8.GetBytes('new trace')} + } + [pscustomobject]@{State=$(if($Destination){'ArchivedBytes'}else{'ObservedBytes'});SourcePath=$Source;Identity='stable-file-id';Length=$bytes.Length;Sha256=$hash;ArchivePath=$Destination;ArchivedSha256=$(if($Destination){$hash}else{$null})} +} +$script:jsonWriter=(Get-Command Write-WelaDnsAnalyticalJson).ScriptBlock +function Write-WelaDnsAnalyticalJson { + param($Path,$Value) + if($script:scenario -eq 'journal-failure' -and (Split-Path $Path -Leaf) -eq '01-before.json'){throw 'Journal write failed'} + & $script:jsonWriter $Path $Value +} +function Set-WelaDnsAnalyticalNative { + param([string[]]$Arguments) + $script:nativeCalls+=,(@($Arguments)) + Assert (Test-Path (Join-Path $script:backup '01-before.json')) 'Every channel mutation follows a durable before-state.' + if($script:scenario -eq 'native-failure'){throw 'wevtutil failed exit5'} + if($Arguments -contains '/e:true'){ + Assert (($Arguments -contains '/q:true') -and (Test-Path (Join-Path $script:backup '03-archive.json'))) 'Reset-capable enable has explicit quiet consent and persisted verified archive.' + $script:state.IsEnabled=$true;$script:trace=[Text.Encoding]::UTF8.GetBytes('new ETL session') + } + if($Arguments -contains '/e:false'){$script:state.IsEnabled=$false} + foreach($arg in $Arguments){if($arg -like '/ms:*'){$script:state.MaximumSizeInBytes=[long]$arg.Substring(4)};if($arg -eq '/rt:true'){$script:state.LogMode='Retain'};if($arg -eq '/rt:false'){$script:state.LogMode='Circular'}} + if($script:scenario -eq 'write-drift'){$script:state.RegisteredLogFilePath='C:\changed\DNS.etl'} +} +function Configure { + param([switch]$DryRun,[switch]$Auto) + Invoke-WelaDnsAnalytical -Action Configure -State Enabled -AllowTraceReset -BackupPath $script:backup -DryRun:$DryRun -Auto:$Auto +} +try { + $context=[pscustomobject]@{Role='MemberServer';Build=20348} + $service=[pscustomobject]@{State='Running'} + $schema=[pscustomobject]@{State='Observed';ChannelType='Analytical';ProviderGuid='eb79061a-a566-4698-9119-3ed2807060e7';Events=@([pscustomobject]@{Id=257;Version=0;Fields=@([pscustomobject]@{Name='QNAME';InType='win:UnicodeString'})})} + Assert-WelaDnsAnalyticalCapability $script:definition $context $service $schema + foreach($role in @('Client','Unknown')){$bad=Clone $context;$bad.Role=$role;Throws {Assert-WelaDnsAnalyticalCapability $script:definition $bad $service $schema} 'build/role'} + $bad=Clone $context;$bad.Build=99999;Throws {Assert-WelaDnsAnalyticalCapability $script:definition $bad $service $schema} 'build/role' + foreach($state in @('Stopped','Unknown','Not installed')){$bad=Clone $service;$bad.State=$state;Throws {Assert-WelaDnsAnalyticalCapability $script:definition $context $bad $schema} 'service'} + foreach($value in @('Operational','Debug','Unknown')){$bad=Clone $schema;$bad.ChannelType=$value;Throws {Assert-WelaDnsAnalyticalCapability $script:definition $context $service $bad} 'provider/schema'} + $bad=Clone $schema;$bad.ProviderGuid=[guid]::Empty.ToString();Throws {Assert-WelaDnsAnalyticalCapability $script:definition $context $service $bad} 'provider/schema' + $bad=Clone $schema;$bad.Events=@();Throws {Assert-WelaDnsAnalyticalCapability $script:definition $context $service $bad} 'event257' + $bad=Clone $schema;$bad.Events[0].Fields[0].Name='QueryName';Throws {Assert-WelaDnsAnalyticalCapability $script:definition $context $service $bad} 'QNAME' + $bad=Clone $schema;$bad.Events[0].Fields[0].InType='win:UInt32';Throws {Assert-WelaDnsAnalyticalCapability $script:definition $context $service $bad} 'QNAME' + $bad=Clone $schema;$bad.Events+=Clone $bad.Events[0];$bad.Events[1].Version=1;$bad.Events[1].Fields=@();Throws {Assert-WelaDnsAnalyticalCapability $script:definition $context $service $bad} 'QNAME' + Reset;$a=Invoke-WelaDnsAnalytical + Assert ($a.Status -eq 'AlreadyCompliant' -and $a.ReadyRuleCredit -eq 0 -and $script:nativeCalls.Count -eq 0 -and -not(Test-Path $script:backup)) 'Default Audit reads actual state without archives, mutation or readiness credit.' + $p=Invoke-WelaDnsAnalytical -Action Plan -State Enabled + Assert ($p.Status -eq 'ChangeRequired' -and $p.RequiresTraceResetConsent) 'Plan exposes reset consent before writes.' + $r=Configure -DryRun + Assert ($r.Status -eq 'Skipped' -and -not(Test-Path $script:backup) -and $script:nativeCalls.Count -eq 0) 'DryRun makes no directories, archives or channel changes.' + $r=Invoke-WelaDnsAnalytical -Action Configure -State Enabled -BackupPath $script:backup -Auto + Assert ($r.ExitCode -eq 1 -and $r.Diagnostic -match 'AllowDnsTraceReset' -and $script:nativeCalls.Count -eq 0) 'Auto cannot bypass explicit trace-reset consent.' + $r=Configure -Auto + Assert ($r.Status -eq 'Applied' -and $r.After.IsEnabled -and $r.Archive.State -eq 'ArchivedBytes' -and $r.After.SecurityDescriptor -ceq 'original-ACL') 'Reviewed channel enable preserves ACL/path with real archive metadata.' + Assert ([IO.File]::ReadAllText($r.Archive.ArchivePath) -ceq 'original ETL fixture') 'Archived original bytes survive trace reset.' + $calls=$script:nativeCalls.Count;$again=Configure -Auto + Assert ($again.Status -eq 'AlreadyCompliant' -and $script:nativeCalls.Count -eq $calls) 'Idempotent configure performs no stop/archive/reset.' + Reset;$script:state.IsEnabled=$true;$script:state.MaximumSizeInBytes=67108864 + $r=Invoke-WelaDnsAnalytical -Action Configure -State Enabled -Retention Retain -AllowTraceReset -Auto -BackupPath $script:backup + Assert ($r.Status -eq 'Applied' -and $r.After.MaximumSizeInBytes -eq 67108864 -and $r.After.LogMode -eq 'Retain' -and $script:nativeCalls[0] -contains '/e:false') 'Retention changes stop/archive first and preserve larger buffers.' + Reset;$script:state.MaximumSizeInBytes=1048576 + $r=Invoke-WelaDnsAnalytical -Action Configure -State Enabled -MinimumBytes 1048577 -AllowTraceReset -Auto -BackupPath $script:backup + Assert ($r.After.MaximumSizeInBytes -eq 1114112) 'Requested minimum rounds upward by native64KiB units.' + Reset;$script:state.IsEnabled=$true + $r=Invoke-WelaDnsAnalytical -Action Configure -State Disabled -AllowTraceReset -Auto -BackupPath $script:backup + Assert ($r.Status -eq 'Applied' -and -not$r.After.IsEnabled -and $script:nativeCalls.Count -eq 1 -and $r.Archive.State -eq 'ArchivedBytes') 'Explicit disable archives without re-enabling or resizing.' + Reset;$script:trace=$null;$r=Configure -Auto + Assert ($r.Status -eq 'Applied' -and $r.Archive.State -eq 'ObservedAbsent' -and -not$r.Archive.Sha256 -and -not(Test-Path (Join-Path $script:backup 'trace-before.etl'))) 'Absent trace is typed native absence, never fabricated empty archive proof.' + foreach($case in @('prompt-race','source-race','declined','journal-failure')){ + Reset;$script:scenario=$case;$r=Configure + Assert ($script:nativeCalls.Count -eq 0 -and $r.Status -in @('Failed','Skipped')) "No channel mutation after $case." + } + foreach($case in @('archive-denied','trace-race','archive-tamper','final-context-trace-race')){ + Reset;$script:state.IsEnabled=$true;$script:state.MaximumSizeInBytes=1048576;$script:scenario=$case;$r=Configure -Auto + Assert ($r.Status -eq 'Failed' -and $script:nativeCalls.Count -eq 1 -and -not$r.After.IsEnabled -and -not(Test-Path (Join-Path $script:backup '04-applied.json'))) "$case leaves stopped trace explicitly failed without resetting unarchived evidence." + } + foreach($case in @('native-failure','write-drift','final-drift','denied-state')){ + Reset;$script:scenario=$case;$r=Configure -Auto + Assert ($r.Status -eq 'Failed' -and $r.ExitCode -eq 1) "$case cannot claim applied success." + } + Reset;$out=Join-Path $temp 'relative.json';Push-Location $temp + try{$null=Invoke-WelaDnsAnalytical -ResultsPath 'relative.json'}finally{Pop-Location} + Assert (Test-Path $out) 'Report relative path follows PowerShell location.' + Throws {Invoke-WelaDnsAnalytical -ResultsPath $out} 'must be new' + foreach($suffix in @('base.json:stream','bad*.json','bad?.json','bad[1].json',('bad'+[char]10+'.json'),'trailing.','trailing ')){ + Throws {Resolve-WelaDnsAnalyticalOutput (Join-Path $temp $suffix)} 'unsupported' + } + Throws {Invoke-WelaDnsAnalytical -Action Configure} 'explicit DNS state' + Throws {Invoke-WelaDnsAnalytical -DryRun} 'require DNS Configure' + $exe=(Get-Process -Id $PID).Path + foreach($arguments in @(@('configure','-DnsAction','Audit'),@('dns-analytical','-Profile','wela-2.2.0'),@('dns-analytical','-DryRun'))){ + $ErrorActionPreference='Continue';try{$output=& $exe -NoProfile -File (Join-Path $root 'WELA.ps1') @arguments 2>&1;$code=$LASTEXITCODE}finally{$ErrorActionPreference='Stop'} + $plain=($output -join ' ') -replace '\x1b\[[0-9;]*[A-Za-z]','' -replace '[|\r\n]',' ' + Assert ($code -ne 0 -and $plain -match 'No\s+command\s+was\s+run') 'Public guard rejects unrelated/ignored options before any command.' + } + Write-Host "PASS: $script:count DNS analytical mocked assertions. No native channel, feature, service, policy or DNS query mutations." +}finally{Remove-Item -LiteralPath $temp -Recurse -Force} +$global:LASTEXITCODE=0 diff --git a/tests/DnsAnalytical.Windows.Tests.ps1 b/tests/DnsAnalytical.Windows.Tests.ps1 new file mode 100644 index 00000000..3f60877c --- /dev/null +++ b/tests/DnsAnalytical.Windows.Tests.ps1 @@ -0,0 +1,135 @@ +param([switch]$AllowDisposableDns,[ValidateSet('powershell','pwsh')][string]$TestEngine='powershell') +$ErrorActionPreference='Stop' +if(-not $AllowDisposableDns -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted' -or $env:OS -ne 'Windows_NT'){throw 'Explicit DNS mutation opt-in on a disposable GitHub-hosted Windows runner is required.'} +$root=Split-Path $PSScriptRoot -Parent +Import-Module (Join-Path $root 'modules/AuditProfiles.psm1') -Force +Import-Module (Join-Path $root 'modules/NativeProviders.psm1') -Force +. (Join-Path $root 'scripts/Configuration.ps1') +. (Join-Path $root 'scripts/ControlApplicability.ps1') +. (Join-Path $root 'scripts/NativeProviderPacks.ps1') +. (Join-Path $root 'scripts/DnsAnalytical.ps1') +$script:count=0 +function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++} +function Throws($Code,$Pattern){$message='';try{& $Code|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, got $message"} +$hostOs=Get-CimInstance Win32_OperatingSystem;$hostComputer=Get-CimInstance Win32_ComputerSystem +if($hostOs.ProductType -ne 3 -or [int]$hostOs.BuildNumber -notin @(20348,26100) -or $hostComputer.PartOfDomain -or $hostComputer.DomainRole -ne 2 -or (Get-WindowsFeature DNS).Installed){throw 'This disposable test requires an unjoined Server2022/2025 with no pre-existing DNS role.'} +$engine=(Get-Command $TestEngine -ErrorAction Stop).Source +$nonce=[guid]::NewGuid().ToString('N');$zone='wela-'+$nonce+'.test';$query='probe.'+$zone;$zoneFile=$zone+'.dns' +$private=New-WelaDnsAnalyticalBackup (Join-Path $env:TEMP ('wela-dns-native-'+$nonce)) +$beforeFeatures=@(Get-WindowsFeature|Where-Object Installed|ForEach-Object Name);$beforePolicies=Get-WelaEffectiveAuditPolicy +$installed=$false;$zoneCreated=$false;$original=$null;$passed=$false +function Invoke-Cli { + param([string[]]$Arguments,[int]$Expected=0) + $ErrorActionPreference='Continue' + try{$text=@(& $engine -NoProfile -File (Join-Path $root 'WELA.ps1') @Arguments 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference='Stop'} + $text|ForEach-Object{Write-Host $_};$global:LASTEXITCODE=0 + Assert ($code -eq $Expected) "Public DNS CLI exit $code, expected $Expected." +} +try { + # Real file-handle/archive behavior uses only owned temporary bytes. + $source=Join-Path $private 'stream-source.etl';$destination=Join-Path $private 'stream-archive.etl' + $bytes=New-Object byte[] 131073;for($i=0;$i -lt $bytes.Length;$i++){$bytes[$i]=[byte]($i%251)};[IO.File]::WriteAllBytes($source,$bytes) + $archive=Copy-WelaDnsAnalyticalTrace $source $destination 1048576 + Assert ($archive.State -eq 'ArchivedBytes' -and $archive.Length -eq 131073 -and $archive.Sha256 -ceq (Get-FileHash $source).Hash.ToLowerInvariant() -and $archive.Sha256 -ceq $archive.ArchivedSha256) 'Streaming native archive retains exact bytes and hashes.' + Assert-WelaDnsAnalyticalArchive $archive 1048576 + $observed=Copy-WelaDnsAnalyticalTrace -Source $source -MaximumBytes 1048576 + Assert ($observed.State -eq 'ObservedBytes' -and $observed.Sha256 -ceq $archive.Sha256 -and -not$observed.ArchivePath) 'Native read-only overload preserves bytes without a destination or null-string coercion.' + foreach($alias in @(($source+':stream'),($source+'.'),($source+' '),(Join-Path $private 'bad?.etl'))){Throws {Resolve-WelaDnsAnalyticalOutput $alias} 'unsupported'} + Throws {Copy-WelaDnsAnalyticalTrace $source $destination 1048576} 'already exists|exist' + $lock=[IO.File]::Open($source,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None) + try{Throws {Copy-WelaDnsAnalyticalTrace -Source $source -MaximumBytes 1048576} 'absence was not established|another process'}finally{$lock.Dispose()} + $junctionTarget=Join-Path $private 'junction-target';$junction=Join-Path $private 'junction' + $null=New-Item -ItemType Directory -Path $junctionTarget + [IO.File]::WriteAllText((Join-Path $junctionTarget 'trace.etl'),'owned link fixture') + $null=New-Item -ItemType Junction -Path $junction -Value $junctionTarget + try{Throws {Copy-WelaDnsAnalyticalTrace -Source (Join-Path $junction 'trace.etl') -MaximumBytes 1048576} 'reparse'}finally{(Get-Item -LiteralPath $junction -Force).Delete()} + $absent=Copy-WelaDnsAnalyticalTrace -Source (Join-Path $private 'absent.etl') -MaximumBytes 1048576 + Assert ($absent.State -eq 'ObservedAbsent' -and -not $absent.Sha256) 'Native FILE_NOT_FOUND is typed absence without fabricated archive/hash.' + $empty=Join-Path $private 'empty.etl';[IO.File]::WriteAllBytes($empty,[byte[]]@()) + Throws {Copy-WelaDnsAnalyticalTrace -Source $empty -MaximumBytes 1048576} 'empty archive' + $large=Join-Path $private 'large.etl';$s=[IO.File]::Create($large);try{$s.SetLength(1048577)}finally{$s.Dispose()} + Throws {Copy-WelaDnsAnalyticalTrace -Source $large -MaximumBytes 1048576} 'cap' + Throws {Copy-WelaDnsAnalyticalTrace -Source (Join-Path $private 'missing-parent\trace.etl') -MaximumBytes 1048576} 'find|exist' + # The DNS role and zone belong exclusively to this disposable runner. + $installed=$true;$feature=Install-WindowsFeature DNS -IncludeManagementTools -ErrorAction Stop + if(-not $feature.Success -or [string]$feature.RestartNeeded -ne 'No'){throw 'DNS role installation failed or needs a restart; no native acceptance claim.'} + Start-Service DNS -ErrorAction Stop + $definition=Get-WelaDnsAnalyticalDefinition;$original=Get-WelaDnsAnalyticalState $definition + Write-WelaDnsAnalyticalJson (Join-Path $private 'original-channel.json') $original + $zoneCreated=$true + Add-DnsServerPrimaryZone -Name $zone -ZoneFile $zoneFile -DynamicUpdate None -ErrorAction Stop + Add-DnsServerResourceRecordA -ZoneName $zone -Name probe -IPv4Address '127.0.0.42' -TimeToLive ([TimeSpan]::FromMinutes(1)) -ErrorAction Stop|Out-Null + $configuredPath=Join-Path $private 'configured.json' + Invoke-Cli @('dns-analytical','-DnsAction','Configure','-DnsState','Enabled','-DnsRetention','Retain','-AllowDnsTraceReset','-Auto','-BackupPath',(Join-Path $private 'enable-journal'),'-ResultsPath',$configuredPath) + $configured=Get-Content $configuredPath -Raw -Encoding UTF8|ConvertFrom-Json + Assert ($configured.Status -eq 'Applied' -and $configured.After.IsEnabled -and $configured.After.LogMode -eq 'Retain' -and $configured.ReadyRuleCredit -eq 0) 'Actual public configure enables only the reviewed analytical channel without readiness credit.' + Assert ($configured.After.SecurityDescriptor -ceq $original.SecurityDescriptor -and $configured.After.LogFilePath -ceq $original.LogFilePath) 'Native transition preserves ACL and registered local trace path.' + $repeatPath=Join-Path $private 'repeat.json' + Invoke-Cli @('dns-analytical','-DnsAction','Configure','-DnsState','Enabled','-DnsRetention','Retain','-AllowDnsTraceReset','-Auto','-BackupPath',(Join-Path $private 'unused-repeat-journal'),'-ResultsPath',$repeatPath) + $repeat=Get-Content $repeatPath -Raw -Encoding UTF8|ConvertFrom-Json + Assert ($repeat.Status -eq 'AlreadyCompliant' -and -not(Test-Path (Join-Path $private 'unused-repeat-journal'))) 'Actual repeat neither restarts nor creates misleading recovery evidence.' + $started=[DateTime]::UtcNow + $answers=@(Resolve-DnsName -Name $query -Type A -Server '127.0.0.1' -DnsOnly -NoHostsFile -ErrorAction Stop) + Assert (@($answers|Where-Object IPAddress -eq '127.0.0.42').Count -eq 1) 'Unique authoritative .test query is resolved entirely through loopback.' + # Stop and archive through the public lifecycle before reading the ETL. + $disabledPath=Join-Path $private 'disabled.json' + Invoke-Cli @('dns-analytical','-DnsAction','Configure','-DnsState','Disabled','-AllowDnsTraceReset','-Auto','-BackupPath',(Join-Path $private 'disable-journal'),'-ResultsPath',$disabledPath) + $disabled=Get-Content $disabledPath -Raw -Encoding UTF8|ConvertFrom-Json + Assert ($disabled.Status -eq 'Applied' -and -not$disabled.After.IsEnabled -and $disabled.Archive.State -eq 'ArchivedBytes') 'Native disable preserves stopped trace in a hash-verified archive.' + Assert ($disabled.Archive.SourcePath -ceq $original.LogFilePath -and $disabled.Before.Channel -ceq $definition.Pack.channel -and $disabled.After.Channel -ceq $definition.Pack.channel) 'ETL bytes came from the exact registered analytical trace path and preserved channel state.' + $events=@(Get-WinEvent -Path $disabled.Archive.ArchivePath -Oldest -MaxEvents 4096 -ErrorAction Stop) + Assert ($events.Count -lt 4096) 'Archived native event read stays below its explicit completeness cap.' + $candidateXml=New-Object 'System.Collections.Generic.List[string]' + $eventSummary=New-Object 'System.Collections.Generic.List[string]' + $matches=@(foreach($event in $events){ + try{ + $rawXml=$event.ToXml();$xml=[xml]$rawXml;$data=@{};foreach($node in $xml.Event.EventData.Data){$data[[string]$node.Name]=[string]$node.'#text'} + if($eventSummary.Count -lt 20){$eventSummary.Add("Event $($event.Id), provider $($event.ProviderName), channel '$($xml.Event.System.Channel)', QNAME '$($data.QNAME)', time $($event.TimeCreated.ToUniversalTime().ToString('o'))")} + if($event.Id -eq 257 -and $candidateXml.Count -lt 8){$candidateXml.Add($rawXml)} + # Raw ETL rendering can leave System.Channel empty. Do not rewrite it: bind + # provenance to the verified registered trace path above and exact native manifest link. + $manifest=@($disabled.Before.Schema.Events|Where-Object {$_.Id -eq $event.Id -and $_.Version -eq $event.Version -and $_.Channel -ceq $definition.Pack.channel}) + $channel=[string]$xml.Event.System.Channel + if($event.Id -eq 257 -and $event.ProviderName -ceq 'Microsoft-Windows-DNSServer' -and [string]$xml.Event.System.Provider.Guid -ieq '{eb79061a-a566-4698-9119-3ed2807060e7}' -and $manifest.Count -eq 1 -and ($channel -ceq '' -or $channel -ceq $definition.Pack.channel) -and [string]$xml.Event.System.Computer -ieq [Environment]::MachineName -and $event.TimeCreated.ToUniversalTime() -ge $started -and $event.TimeCreated.ToUniversalTime() -le [DateTime]::UtcNow -and [string]$data.QNAME.TrimEnd('.') -ieq $query -and $data.InterfaceIP -ceq '127.0.0.1' -and $data.Destination -ceq '127.0.0.1' -and $data.QTYPE -ceq '1' -and $data.RCODE -ceq '0' -and $data.AA -ceq '1'){$rawXml} + }finally{if($event -is [IDisposable]){$event.Dispose()}} + }) + if($matches.Count -ne 1){Write-Host "Expected one event257 for $query since $($started.ToString('o')); matched $($matches.Count) from $($events.Count) events.";$eventSummary|ForEach-Object{Write-Host $_};$candidateXml|ForEach-Object{Write-Host $_};throw 'Exact bounded native DNS257/QNAME evidence was not established.'} + [IO.File]::WriteAllText((Join-Path $private 'event257.xml'),$matches[0],[Text.UTF8Encoding]::new($false));Write-Host $matches[0] + $proof=[pscustomobject]@{Kind='WelaDnsAnalyticalNativeProbe';Engine=$TestEngine;Computer=[Environment]::MachineName;Query=$query;StartedUtc=$started.ToString('o');VerifiedUtc=[DateTime]::UtcNow.ToString('o');EventId=257;XmlChannel=[string]([xml]$matches[0]).Event.System.Channel;RegisteredChannel=$definition.Pack.channel;RegisteredTracePath=$original.LogFilePath;ChannelEvidence='Verified registered ETL path and provider event/version manifest link; raw XML channel is retained without normalization.';ArchiveSha256=$disabled.Archive.Sha256;ReadyRuleCredit=0} + Write-WelaDnsAnalyticalJson (Join-Path $private 'event257-proof.json') $proof + Write-Host ($proof|ConvertTo-Json -Depth 6) + Assert ((Get-FileHash $disabled.Archive.ArchivePath).Hash.ToLowerInvariant() -ceq $disabled.Archive.Sha256) 'Collected native ETL retains the recorded archive hash.' + $passed=$true + Write-Host "PASS: $script:count DNS native assertions through $TestEngine; exact257 XML observed, no external DNS query or backend/readiness claim." +}catch{Write-Host ('DNS native test failure before cleanup: '+($_|Out-String));Write-Host $_.ScriptStackTrace;throw} +finally { + $errors=@() + if($original){try{ + $current=Get-WelaDnsAnalyticalState $definition + # Fixture cleanup is explicit, confined to the DNS role created above. + if($current.IsEnabled){Set-WelaDnsAnalyticalNative @('sl',$current.Channel,'/e:false')} + $preserved=Copy-WelaDnsAnalyticalTrace -Source $current.LogFilePath -Destination (Join-Path $private 'cleanup-trace.etl') -MaximumBytes 1073741824 + Write-WelaDnsAnalyticalJson (Join-Path $private 'cleanup-archive.json') $preserved + Assert-WelaDnsAnalyticalArchive $preserved 1073741824 + $args=@('sl',$original.Channel,('/ms:'+[string]$original.MaximumSizeInBytes),('/rt:'+([string]($original.LogMode -eq 'Retain')).ToLowerInvariant()),('/e:'+([string]$original.IsEnabled).ToLowerInvariant()),'/q:true') + Set-WelaDnsAnalyticalNative $args + if((Get-WelaDnsAnalyticalStateKey (Get-WelaDnsAnalyticalState $definition)) -cne (Get-WelaDnsAnalyticalStateKey $original)){throw 'Exact DNS channel configuration restoration differs.'} + }catch{$errors+=$_.Exception.Message}} + if($zoneCreated){try{ + $owned=Get-DnsServerZone -Name $zone -ErrorAction SilentlyContinue + if($owned){if($owned.IsDsIntegrated -or $owned.ZoneType -ne 'Primary'){throw 'Owned DNS zone identity changed; cleanup refused.'};Remove-DnsServerZone -Name $zone -Force -ErrorAction Stop} + if(Get-DnsServerZone -Name $zone -ErrorAction SilentlyContinue){throw 'Owned test zone remains.'} + $file=Join-Path $env:SystemRoot ('System32\dns\'+$zoneFile);if(Test-Path -LiteralPath $file){Remove-Item -LiteralPath $file -ErrorAction Stop} + }catch{$errors+=$_.Exception.Message}} + try{$afterPolicies=Get-WelaEffectiveAuditPolicy;foreach($guid in $beforePolicies.Keys){if($afterPolicies[$guid] -ne $beforePolicies[$guid]){throw 'Native audit policy unexpectedly changed.'}}}catch{$errors+=$_.Exception.Message} + $removal=[pscustomobject]@{ChannelAndZoneRestored=($errors.Count -eq 0);Attempted=$false;Features=@();Success=$null;RestartNeeded=$null;Boundary='Feature removal can await disposal of this GitHub-hosted VM; no production restart or complete live feature-restoration claim.'} + if($installed -and $errors.Count -eq 0){try{ + $added=@(Get-WindowsFeature|Where-Object{$_.Installed -and $_.Name -notin $beforeFeatures -and $_.Name -in @('DNS','RSAT-DNS-Server')}|ForEach-Object Name) + if($added.Count){$removal.Attempted=$true;$removal.Features=$added;$removed=Uninstall-WindowsFeature -Name $added -ErrorAction Stop;$removal.Success=[bool]$removed.Success;$removal.RestartNeeded=[string]$removed.RestartNeeded;if(-not $removed.Success -or $removal.RestartNeeded -notin @('No','Yes')){throw 'Created DNS feature removal failed or restart status is unknown.'}} + }catch{$errors+=$_.Exception.Message}} + Write-WelaDnsAnalyticalJson (Join-Path $private 'feature-removal.json') $removal;$removal|ConvertTo-Json -Depth 5|Write-Host + if($errors.Count){throw "Disposable DNS cleanup failed; receipts retained at $private : $($errors -join '; ')"} + if($passed){Remove-Item -LiteralPath $private -Recurse -Force} +} +$global:LASTEXITCODE=0 +Write-Host 'PASS: exact native channel configuration and audit state restored; owned DNS zone/records removed. Feature removal/disposal boundary recorded separately.' diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 561f5aac..faff024b 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,6 +7,7 @@ **改善:** +- 任意実行の`dns-analytical`を追加し、DNS Serverの分析ログを監査・計画・明示選択で設定できるようにしました。トレース再設定への個別同意、永続的な変更前記録、容量を制限したネイティブETLの退避とハッシュ検証に対応し、ACL・パス・既存の大きいバッファを保持します。退避失敗で停止した状態を失敗として報告します。使い捨てDNS環境のループバックイベント257と設定復元のテストを追加し、転送・Sigmaの利用可能性は未検証のままです。 (#425) (@Shirofune-Security) - 読み取り専用の`wec-runtime`を追加し、WEC標準APIの稼働状態、数値エラー、UTC時刻と件数を制限した送信元別の観測結果を取得します。実行者・ホスト・定義の変更、不明な値と上限到達を明示し、既存のWEF・保存状態レポートでは元の文字列も保持します。過去の送信元一覧を現在の接続数とは扱わず、Activeからイベント到着やSigma利用可能性を推定しません。無効な使い捨てサブスクリプションで検証し、サービス状態とテスト対象を復元します。WEF・EVTXの合成テスト資料で時刻の末尾ゼロが失われる問題も修正しました。 (#424) (@Shirofune-Security) - 既存の監査専用ポリシーに対する固定ネイティブ EXE の実行と AppLocker イベントの厳密な照合、保護されたハッシュ付き証拠、変更検出を行うオプトインの `applocker-probe` を追加しました。ポリシー・サービス・チャネルの変更や Sigma の評価加算は行いません。使い捨て Windows CI は一時的な監査専用テストポリシーで実際の 8003 を収集し、管理状態の観測を保持して GP ポリシーとチャネル設定を復元します。 (#423) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index ced6bc27..e6776f76 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,6 +7,7 @@ **Improvements:** +- Added opt-in `dns-analytical` auditing, planning and selective DNS Server channel configuration with explicit trace-reset consent, durable state records and bounded native ETL archives verified before resets. Preserve ACLs, paths and larger buffers; report stopped partial failures honestly. Added disposable standalone-DNS tests for loopback event 257 and exact configuration restoration; forwarding and Sigma readiness remain unverified. (#425) (@Shirofune-Security) - Added read-only `wec-runtime` with typed native WEC activity, numeric errors, UTC timestamps and bounded per-source observations. Actual reader/context and definition checks keep partial reads, caps and drift explicit; existing WEF/retention inventories retain raw text alongside typed fields. Historical source lists are not connection counts and Active grants no arrival or Sigma credit. Disposable disabled-subscription tests restore service state and remove only their owned fixture. Also fixed synthetic WEF/EVTX fixture timestamp roundtrips without weakening bundle validation. (#424) (@Shirofune-Security) - Added opt-in `applocker-probe` planning and fixed native EXE collection against existing audit-only policy, with exact AppLocker event correlation, private hashed evidence and drift checks. No policy/service/channel changes or Sigma credit; disposable Windows CI prepares one temporary audit-only fixture for real 8003 collection, preserving management observations and restoring GP policy/channel settings. (#423) (@Shirofune-Security)