mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-09-30 11:37:14 +02:00
Merge final dev and verify strict transcription recovery CLI
This commit is contained in:
@@ -58,3 +58,8 @@ tests/SelectedSaclFixtureProtection.cs text eol=lf
|
||||
/scripts/WmiNamespaceAuditing.ps1 text eol=lf
|
||||
/scripts/WefArrival.ps1 text eol=lf
|
||||
/tests/WmiProbe*.ps1 text eol=lf
|
||||
|
||||
# Reviewed WEC state plans bind native setter and runtime source bytes.
|
||||
/scripts/WecState* text eol=lf
|
||||
/scripts/WecRuntime* text eol=lf
|
||||
/tests/WecState* text eol=lf
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
Copy-Item -Recurse -Path ./scripts -Destination release-binaries/
|
||||
Copy-Item -Recurse -Path ./modules -Destination release-binaries/
|
||||
New-Item -ItemType Directory -Path release-binaries/docs -Force | Out-Null
|
||||
Copy-Item -Path ./docs/gpo-audit-packages.md, ./docs/gpo-package-deployment.md, ./docs/gpo-creation.md, ./docs/wmi-probe.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/ipsec-prerequisites.md -Destination release-binaries/docs/
|
||||
Copy-Item -Path ./docs/gpo-audit-packages.md, ./docs/gpo-package-deployment.md, ./docs/gpo-creation.md, ./docs/wmi-probe.md, ./docs/smb-runtime-activation.md, ./docs/smb-auditing.md, ./docs/wec-state.md, ./docs/wec-update.md, ./docs/wec-runtime.md, ./docs/wef-deployment.md, ./docs/native-channel-access.md, ./docs/eventlog-settings.md, ./docs/channel-read.md, ./docs/native-rule-eligibility.md, ./docs/native-validation.md, ./docs/wef-arrival.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/ipsec-prerequisites.md, ./docs/wec-ingress.md -Destination release-binaries/docs/
|
||||
|
||||
- name: Set Artifact Name
|
||||
if: contains(matrix.info.os, 'windows') == true
|
||||
@@ -77,4 +77,4 @@ jobs:
|
||||
with:
|
||||
name: wela-documents
|
||||
path: |
|
||||
./*.pdf
|
||||
./*.pdf
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
name: SMB runtime audit activation
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
paths:
|
||||
- 'WELA.ps1'
|
||||
- 'scripts/SmbRuntimeActivation.ps1'
|
||||
- 'scripts/SmbAuditing.ps1'
|
||||
- 'scripts/Configuration.ps1'
|
||||
- 'scripts/WefArrival.ps1'
|
||||
- 'tests/SmbRuntimeActivation*'
|
||||
- '.github/workflows/smb-runtime-activation.yml'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
native-smb-activation:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2022, windows-2025]
|
||||
shell: [powershell, pwsh]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Focused regressions in Windows PowerShell 5.1
|
||||
if: matrix.shell == 'powershell'
|
||||
shell: powershell
|
||||
run: |
|
||||
./tests/SmbRuntimeActivation.Tests.ps1
|
||||
./tests/SmbRuntimeActivation.Cli.Tests.ps1
|
||||
- name: Native activation and restoration in Windows PowerShell 5.1
|
||||
if: matrix.shell == 'powershell'
|
||||
shell: powershell
|
||||
env:
|
||||
WELA_DISPOSABLE_SMB_ACTIVATION: 'true'
|
||||
run: ./tests/SmbRuntimeActivation.Windows.Tests.ps1
|
||||
- name: Focused regressions in PowerShell 7
|
||||
if: matrix.shell == 'pwsh'
|
||||
shell: pwsh
|
||||
run: |
|
||||
./tests/SmbRuntimeActivation.Tests.ps1
|
||||
./tests/SmbRuntimeActivation.Cli.Tests.ps1
|
||||
- name: Native activation and restoration in PowerShell 7
|
||||
if: matrix.shell == 'pwsh'
|
||||
shell: pwsh
|
||||
env:
|
||||
WELA_DISPOSABLE_SMB_ACTIVATION: 'true'
|
||||
run: ./tests/SmbRuntimeActivation.Windows.Tests.ps1
|
||||
- name: Retain native evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: smb-runtime-${{ matrix.os }}-${{ matrix.shell }}
|
||||
path: ${{ runner.temp }}/wela-smb-runtime-*/
|
||||
if-no-files-found: warn
|
||||
@@ -29,12 +29,14 @@ jobs:
|
||||
shell: powershell
|
||||
run: |
|
||||
./tests/TranscriptionRecovery.Tests.ps1
|
||||
./tests/TranscriptionRecovery.Cli.Tests.ps1
|
||||
./tests/TranscriptionRecovery.Windows.Tests.ps1 -AllowDisposablePolicyWrite
|
||||
- name: Focused and native public CLI recovery in PowerShell 7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: |
|
||||
./tests/TranscriptionRecovery.Tests.ps1
|
||||
./tests/TranscriptionRecovery.Cli.Tests.ps1
|
||||
./tests/TranscriptionRecovery.Windows.Tests.ps1 -AllowDisposablePolicyWrite
|
||||
- name: Retain native policy and cleanup evidence
|
||||
if: always()
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
name: Reviewed collector firewall ingress
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
wec-ingress:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2022, windows-2025]
|
||||
engine: [powershell, pwsh]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
- name: Fixtures and public guards in Windows PowerShell5.1
|
||||
if: matrix.engine == 'powershell'
|
||||
shell: powershell
|
||||
run: |
|
||||
./tests/WefFirewallAddress.Tests.ps1
|
||||
./tests/WecIngress.Tests.ps1
|
||||
./tests/WecIngress.Cli.Tests.ps1
|
||||
- name: Native scoped firewall rule in Windows PowerShell5.1
|
||||
if: matrix.engine == 'powershell'
|
||||
shell: powershell
|
||||
run: ./tests/WecIngress.Windows.Tests.ps1 -AllowDisposableFirewallRule
|
||||
- name: Fixtures and public guards in PowerShell7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: |
|
||||
./tests/WefFirewallAddress.Tests.ps1
|
||||
./tests/WecIngress.Tests.ps1
|
||||
./tests/WecIngress.Cli.Tests.ps1
|
||||
- name: Native scoped firewall rule in PowerShell7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: ./tests/WecIngress.Windows.Tests.ps1 -AllowDisposableFirewallRule
|
||||
- name: Retain owned fixture evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: wec-ingress-${{ matrix.os }}-${{ matrix.engine }}
|
||||
path: ${{ runner.temp }}/wela-ingress-*/
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Reviewed existing WEC subscription state
|
||||
on:
|
||||
push:
|
||||
paths: ['WELA.ps1', 'scripts/WecState*', 'tests/WecState*', '.github/workflows/wec-state.yml']
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
wec-state:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-2022, windows-2025]
|
||||
engine: [powershell, pwsh]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
||||
- name: Portable guards in Windows PowerShell 5.1
|
||||
if: matrix.engine == 'powershell'
|
||||
shell: powershell
|
||||
run: |
|
||||
./tests/WecState.Tests.ps1
|
||||
./tests/WecState.Cli.Tests.ps1
|
||||
./tests/WecSubscriptionXml.Tests.ps1
|
||||
- name: Actual owned Enabled transitions in Windows PowerShell 5.1
|
||||
if: matrix.engine == 'powershell'
|
||||
shell: powershell
|
||||
run: ./tests/WecState.Windows.Tests.ps1 -AllowDisposableSubscription
|
||||
- name: Portable guards in PowerShell 7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: |
|
||||
./tests/WecState.Tests.ps1
|
||||
./tests/WecState.Cli.Tests.ps1
|
||||
./tests/WecSubscriptionXml.Tests.ps1
|
||||
- name: Actual owned Enabled transitions in PowerShell 7
|
||||
if: matrix.engine == 'pwsh'
|
||||
shell: pwsh
|
||||
run: ./tests/WecState.Windows.Tests.ps1 -AllowDisposableSubscription
|
||||
- name: Retain native state evidence and cleanup receipt
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: wec-state-${{ matrix.os }}-${{ matrix.engine }}
|
||||
path: ${{ runner.temp }}/wela-wec-state-*
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
@@ -15,7 +15,9 @@ jobs:
|
||||
- name: Safe public command fixtures in Windows PowerShell 5.1
|
||||
shell: powershell
|
||||
timeout-minutes: 3
|
||||
run: ./tests/WefDeployment.Tests.ps1
|
||||
run: |
|
||||
./tests/WefFirewallAddress.Tests.ps1
|
||||
./tests/WefDeployment.Tests.ps1
|
||||
- name: Public CLI rejection checks in Windows PowerShell 5.1
|
||||
shell: powershell
|
||||
timeout-minutes: 3
|
||||
@@ -23,7 +25,9 @@ jobs:
|
||||
- name: Safe public command fixtures in PowerShell 7
|
||||
shell: pwsh
|
||||
timeout-minutes: 3
|
||||
run: ./tests/WefDeployment.Tests.ps1
|
||||
run: |
|
||||
./tests/WefFirewallAddress.Tests.ps1
|
||||
./tests/WefDeployment.Tests.ps1
|
||||
- name: Public CLI rejection checks in PowerShell 7
|
||||
shell: pwsh
|
||||
timeout-minutes: 3
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
- `transcription-recovery` を追加し、完了した Windows PowerShell 文字起こし設定を、再構築したハッシュ付き計画から型を保持して復元できるようにしました。ローカル保存先、ユーザー・ヘッダー・他のポリシーを確認し、一時停止は明示的な同意を求め、変更順序と途中結果を永続記録します。使い捨て環境の実 CLI テストで復元とドリフト拒否を検証し、本番セッション・集中管理先の権限と収集・Sigma 対応は未検証とします。 (#376) (@Shirofune-Security)
|
||||
|
||||
- `wec-ingress` の Plan/Apply を追加し、明示した IPv4 範囲から Domain プロファイルの TCP5985 を許可する新規ルールを作成します。実ホスト・ログオン・プロファイル・コードと計画ハッシュ、変更前の永続記録、両ストアとフィルターの読戻しで変更や既存名を拒否します。既存の収集サーバー前提条件も、範囲を広げずに同等のIPv4ネットマスク表記・IPv6表記を照合します。使い捨て Windows テストは作成・名前衝突・再実行拒否・既存前提条件との連携・削除を検証し、リスナー・配送・Sigma の証明は加算しません。 (@Shirofune-Security)
|
||||
|
||||
- `smb-runtime` を追加し、Windows 標準の SMB 監査スイッチ6個を明示的に有効化します。モジュール・ビルド・ADMX、型付きポリシーの競合、設定全体の変化を確認し、各変更の意図と確認結果を永続的に記録します。署名・暗号化・ゲスト接続・サービス設定を保持し、現在の有効化とイベント・永続性・Sigma の証明を区別します。使い捨て Server 2025 の有効化と復元、Server 2022 の拒否を PowerShell 5.1/7 で検証します。 (#441) (@Shirofune-Security)
|
||||
|
||||
- 既存のWindows標準ソース開始型購読1件のEnabledだけをレビュー後に変更する`wec-state`のPlan/Applyを追加しました。送信元認可と完全な定義、実行者・ログオン・トークン、永続的な変更前記録とネイティブ読戻しを確認して他の設定を保持し、既に一致する状態では保存・再有効化を行いません。稼働状況の観測と配送・ブックマークの継続性を区別し、使い捨てWindowsテストで実際の有効/無効切替と所有リソース・サービス状態の復元を確認します。 (関連 #368) (@Shirofune-Security)
|
||||
|
||||
- 強化プロファイルのオプション IPsec Main Mode 監査に、Windows ネイティブの前提条件確認を追加しました。有効なポリシーストアのルールと現在の関連付けを読み取り、適用可能・確認範囲内で未観測・不明を区別します。共有設定処理は書き込み直前に再確認し、明示的な選択とカスタムプロファイルの指定を保持します。Server 2022/2025・PowerShell 5.1/7 の一時ルールを使ったテストで監査ポリシーの復元を確認します。ネゴシエーション、イベント生成、Sigma の対応は保証しません。 (#370) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
- Added explicit `transcription-recovery` for one completed Windows PowerShell transcription configuration, with independently rebuilt hashed plans, typed local-directory restoration, preserved user/header/other policy, explicit temporary-suspension consent and durable ordered receipts. Disposable native public-CLI tests verify restoration and drift refusal; production sessions, central authorization/collection and Sigma readiness remain unverified. (#376) (@Shirofune-Security)
|
||||
|
||||
- Added explicit `wec-ingress` Plan/Apply for one new, narrowly scoped Domain TCP5985 collector firewall rule. Actual host/logon/profile/source guards, reviewed hashes, flushed pending evidence and both-store/filter readback refuse drift and existing names; partial creation remains explicit. The existing collector prerequisite recognizes equivalent native dotted netmasks and IPv6 spellings without broadening accepted scopes. Disposable native tests cover creation, collision, replay, collector integration and cleanup without listener, delivery or Sigma claims. (@Shirofune-Security)
|
||||
|
||||
- Added explicit `smb-runtime` activation of six native SMB audit switches, with reviewed module/build/ADMX capabilities, typed policy conflicts, complete configuration drift guards and durable per-switch receipts. Signing, encryption, guest access and service settings are preserved; runtime verification stays separate from events, persistence and Sigma credit. Disposable Server 2025 activation/restoration and Server 2022 refusal tests cover PowerShell 5.1/7. (#441) (@Shirofune-Security)
|
||||
|
||||
- Added reviewed `wec-state` Plan/Apply for the Enabled flag of one existing native source-initiated subscription. Exact authorization and complete definition checks, operator/logon/token guards, durable pending evidence and native readback preserve other settings; matching states never save or reactivate. Runtime remains separate, and interrupted delivery/bookmark continuity require multi-host validation. Disposable Windows lifecycle tests restore owned resources and service state. (Related #368) (@Shirofune-Security)
|
||||
|
||||
- Added native prerequisite evidence for the stronger profile's optional IPsec Main Mode auditing. Effective-store rule and current association observations distinguish scoped applicability, absence and unknown results; both shared configuration paths recheck before writing and preserve explicit selection/custom-profile intent. Native disposable-rule tests cover Server 2022/2025 and PowerShell 5.1/7 with exact audit-policy restoration, without negotiation/event or Sigma claims. (#370) (@Shirofune-Security)
|
||||
|
||||
- Fixed `wmi-probe` operation timing to use the native precise UTC clock consistently in the parent and worker. Explicit clock receipts and launch/completion bounds preserve exact event correlation; sub-millisecond boundary tests and repeated native public-CLI runs cover timing failures without widening the accepted interval. (@Shirofune-Security)
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
[ValidateRange(16384, 32767)][int]$FirewallMinimumSizeKiB = 16384,
|
||||
[string]$HtmlPath,
|
||||
[ValidateSet('Audit', 'Plan', 'Configure')][string]$SmbAction = 'Audit',
|
||||
[ValidateSet('Plan','Activate')][string]$SmbRuntimeAction = 'Plan',
|
||||
[string]$SmbRuntimeOutputPath,
|
||||
[ValidateSet('Audit', 'Plan', 'Configure', 'Rollback')][string]$AdSaclAction = 'Audit',
|
||||
[string]$AdServer,
|
||||
[ValidateSet('MdiDomain', 'MdiConfiguration', 'PkiObjects')][string[]]$AdSaclProfile,
|
||||
@@ -119,6 +121,13 @@
|
||||
[string]$RecoveryOutputPath,
|
||||
[string]$ArrivalProbePath,
|
||||
[string]$ArrivalOutputPath,
|
||||
[ValidateSet('Plan','Apply')][string]$WecIngressAction = 'Plan',
|
||||
[string]$WecIngressName,
|
||||
[string[]]$WecIngressLocalAddress,
|
||||
[string[]]$WecIngressRemoteAddress,
|
||||
[string]$WecIngressPlanPath,
|
||||
[string]$WecIngressPlanHash,
|
||||
[string]$WecIngressOutputPath,
|
||||
[ValidateSet('Plan','Apply')][string]$WecUpdateAction = 'Plan',
|
||||
[string]$WecUpdateId,
|
||||
[string[]]$WecUpdateSourceSid,
|
||||
@@ -127,6 +136,13 @@
|
||||
[string]$WecUpdatePlanPath,
|
||||
[string]$WecUpdatePlanHash,
|
||||
[string]$WecUpdateOutputPath,
|
||||
[ValidateSet('Plan','Apply')][string]$WecStateAction = 'Plan',
|
||||
[string]$WecStateId,
|
||||
[string[]]$WecStateSourceSid,
|
||||
[ValidateSet('Enabled','Disabled')][string]$WecStateDesired,
|
||||
[string]$WecStatePlanPath,
|
||||
[string]$WecStatePlanHash,
|
||||
[string]$WecStateOutputPath,
|
||||
[ValidateSet('Audit','Plan','Configure')][string]$DnsAction = 'Audit',
|
||||
[ValidateSet('Enabled','Disabled')][string]$DnsState,
|
||||
[ValidateSet('Preserve','Circular','Retain')][string]$DnsRetention = 'Preserve',
|
||||
@@ -163,6 +179,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
|
||||
. (Join-Path $ScriptRoot "scripts/AuditIntegrity.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/SmbRuntimeActivation.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/LdapDiagnostics.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/ControlApplicability.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/NativeValidation.ps1")
|
||||
@@ -189,6 +206,8 @@ Import-Module (Join-Path $ScriptRoot "modules/NativeChannelAccess.psm1") -ErrorA
|
||||
Import-Module (Join-Path $ScriptRoot "modules/WefSubscriptions.psm1") -ErrorAction Stop
|
||||
. (Join-Path $ScriptRoot "scripts/WefDeployment.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/WecUpdate.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/WecIngress.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/WecState.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/RetentionHealth.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/AuditScoring.ps1")
|
||||
. (Join-Path $ScriptRoot "scripts/TargetedSaclPlanning.ps1")
|
||||
@@ -1934,6 +1953,8 @@ Usage:
|
||||
# Firewall text logging is opt-in; it does not change firewall enforcement or rules.
|
||||
./WELA.ps1 smb-auditing -SmbAction Audit -ResultsPath smb-audit.json
|
||||
./WELA.ps1 smb-auditing -SmbAction Plan
|
||||
./WELA.ps1 smb-runtime -SmbRuntimeAction Plan
|
||||
./WELA.ps1 smb-runtime -SmbRuntimeAction Activate -SmbRuntimeOutputPath C:\Evidence\new-smb -Auto
|
||||
./WELA.ps1 rule-eligibility -ResultsPath eligibility.json -HtmlPath eligibility.html
|
||||
./WELA.ps1 event-measurement -MeasurementChannel Security
|
||||
./WELA.ps1 event-measurement -MeasurementChannel Security -MeasurementAction Run -MeasurementOutputPath C:\Evidence\new-sample -MeasurementExportEvtx
|
||||
@@ -1973,6 +1994,8 @@ Usage:
|
||||
./WELA.ps1 score -Help # Separate configuration compliance and evidence-qualified readiness
|
||||
./WELA.ps1 intune-export -Help # Offline native audit OMA-URI/Graph artifacts; no tenant changes
|
||||
./WELA.ps1 adcs-resume -Help # Review a pending CA auditing restart
|
||||
./WELA.ps1 wec-ingress -Help # Review scoped collector firewall rule creation
|
||||
./WELA.ps1 wec-state -Help # Review enable/disable of one existing subscription
|
||||
./WELA.ps1 wec-update -Help # Review query/description updates on a disabled subscription
|
||||
./WELA.ps1 wmi-probe -Help # Fixed local read and matched namespace Security4662 evidence
|
||||
./WELA.ps1 applocker-probe -Help # Collect a fixed native AppLocker EXE event
|
||||
@@ -1990,6 +2013,8 @@ Write-Host "WELA v$WELAVersion - $WELAReleaseName"
|
||||
Write-Host ""
|
||||
|
||||
if ($Cmd -ne 'channel-read' -and @($PSBoundParameters.Keys | Where-Object { $_ -like 'ChannelRead*' }).Count) { throw 'ChannelRead options require channel-read. No command was run.' }
|
||||
if ($Cmd -ne 'smb-runtime' -and @($PSBoundParameters.Keys | Where-Object { $_ -like 'SmbRuntime*' }).Count) {throw 'SmbRuntime options require smb-runtime. No command was run.'}
|
||||
if ($Cmd -eq 'smb-runtime' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','SmbRuntimeAction','SmbRuntimeOutputPath','Auto','DryRun','Help') }).Count) {throw 'smb-runtime accepts only its dedicated options, Auto and DryRun. No command was run.'}
|
||||
if ($Cmd -eq 'channel-read' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','ChannelReadName','ChannelReadOutputPath','Help') }).Count) { throw 'channel-read accepts only dedicated channel/output options. No command was run.' }
|
||||
|
||||
if ($Cmd -ne 'event-measurement' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'Measurement*'}).Count) {throw 'Measurement options require event-measurement. No command was run.'}
|
||||
@@ -2039,7 +2064,7 @@ if ($Cmd -eq 'intune-export' -and @($PSBoundParameters.Keys | Where-Object { $_
|
||||
if ($Cmd -ne 'evtx-recovery' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'Evtx*'}).Count) {throw 'EVTX options require evtx-recovery. No command was run.'}
|
||||
if ($Cmd -eq 'evtx-recovery' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','EvtxAction','EvtxProbePath','EvtxArchivePath','EvtxOutputPath','Help')}).Count) {throw 'evtx-recovery accepts only its dedicated options. No command was run.'}
|
||||
if ($Cmd -ne 'transcription-recovery' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'TranscriptRecovery*'}).Count) {throw 'TranscriptRecovery options require transcription-recovery.'}
|
||||
if ($Cmd -eq 'transcription-recovery' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','TranscriptRecoveryAction','TranscriptRecoveryJournalPath','TranscriptRecoveryOriginalResultsPath','TranscriptRecoveryPlanPath','TranscriptRecoveryPlanHash','TranscriptRecoveryOutputPath','TranscriptRecoveryAllowTemporarySuspension','Auto','DryRun','Help')}).Count) {throw 'transcription-recovery accepts only its dedicated options, Auto and DryRun.'}
|
||||
if ($Cmd -eq 'transcription-recovery' -and ($args.Count -gt 0 -or @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','TranscriptRecoveryAction','TranscriptRecoveryJournalPath','TranscriptRecoveryOriginalResultsPath','TranscriptRecoveryPlanPath','TranscriptRecoveryPlanHash','TranscriptRecoveryOutputPath','TranscriptRecoveryAllowTemporarySuspension','Auto','DryRun','Help')}).Count)) {throw 'transcription-recovery accepts only its dedicated options, Auto and DryRun.'}
|
||||
if ($Cmd -ne 'audit-recovery' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'Recovery*'}).Count) {throw 'Recovery options require audit-recovery. No command was run.'}
|
||||
if ($Cmd -eq 'audit-recovery' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','RecoveryAction','RecoveryJournalPath','RecoveryOriginalResultsPath','RecoveryControlId','RecoveryPlanPath','RecoveryOutputPath','Auto','DryRun','Help')}).Count) {throw 'audit-recovery accepts only dedicated recovery options, Auto and DryRun. No command was run.'}
|
||||
|
||||
@@ -2051,6 +2076,10 @@ if ($PSBoundParameters.ContainsKey('ProfileFile')) {
|
||||
if ($Cmd -eq 'profiles' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','ProfileFile','Help') }).Count) { throw 'profiles -ProfileFile lists the selected file and accepts no assessment/configuration options.' }
|
||||
}
|
||||
|
||||
if ($Cmd -ne 'wec-ingress' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WecIngress*'}).Count) {throw 'WecIngress options require wec-ingress.'}
|
||||
if ($Cmd -eq 'wec-ingress' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WecIngressAction','WecIngressName','WecIngressLocalAddress','WecIngressRemoteAddress','WecIngressPlanPath','WecIngressPlanHash','WecIngressOutputPath','Help')}).Count) {throw 'wec-ingress accepts only dedicated options.'}
|
||||
if ($Cmd -ne 'wec-state' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WecState*'}).Count) {throw 'WecState options require wec-state.'}
|
||||
if ($Cmd -eq 'wec-state' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WecStateAction','WecStateId','WecStateSourceSid','WecStateDesired','WecStatePlanPath','WecStatePlanHash','WecStateOutputPath','Help')}).Count) {throw 'wec-state accepts only dedicated options.'}
|
||||
if ($Cmd -ne 'wec-update' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WecUpdate*'}).Count) {throw 'WecUpdate options require wec-update.'}
|
||||
if ($Cmd -eq 'wec-update' -and @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WecUpdateAction','WecUpdateId','WecUpdateSourceSid','WecUpdateQueryPath','WecUpdateDescription','WecUpdatePlanPath','WecUpdatePlanHash','WecUpdateOutputPath','Help')}).Count) {throw 'wec-update accepts only dedicated options.'}
|
||||
if ($Cmd -ne 'wec-runtime' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WecRuntime*'}).Count) {
|
||||
@@ -2133,6 +2162,7 @@ if ($DryRun -and -not ($Cmd -eq 'transcription-recovery' -and $TranscriptRecover
|
||||
-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
|
||||
-not ($Cmd -eq 'smb-runtime' -and $SmbRuntimeAction -eq 'Activate') -and
|
||||
-not ($Cmd -eq 'powershell-transcription' -and $TranscriptionAction -eq 'Configure') -and
|
||||
-not ($Cmd -eq 'channel-settings' -and $ChannelAction -eq 'Configure') -and
|
||||
-not ($Cmd -in @('wef-source','wec-collector') -and $WefAction -eq 'Configure') -and
|
||||
@@ -2257,6 +2287,23 @@ switch ($Cmd.ToLower()) {
|
||||
$report
|
||||
if ($report.ExitCode) {exit $report.ExitCode}
|
||||
}
|
||||
'wec-ingress' {
|
||||
if ($Help) {Write-Host 'Usage: wec-ingress [-WecIngressAction Plan] -WecIngressName WELA-WEC-name -WecIngressLocalAddress IPv4 -WecIngressRemoteAddress IPv4/CIDR -WecIngressOutputPath new-directory; then Apply with -WecIngressPlanPath plan.json -WecIngressPlanHash SHA256 -WecIngressOutputPath new-directory. Creates one new Domain TCP5985 rule. See docs/wec-ingress.md.';return}
|
||||
$arguments=@{Action=$WecIngressAction;OutputPath=$WecIngressOutputPath}
|
||||
$map=@{WecIngressName='Name';WecIngressLocalAddress='LocalAddress';WecIngressRemoteAddress='RemoteAddress';WecIngressPlanPath='PlanPath';WecIngressPlanHash='PlanHash'}
|
||||
foreach($name in $map.Keys){if($PSBoundParameters.ContainsKey($name)){$arguments[$map[$name]]=$PSBoundParameters[$name]}}
|
||||
$report=Invoke-WelaWecIngress @arguments;$report
|
||||
if($report.ExitCode){exit $report.ExitCode}
|
||||
}
|
||||
'wec-state' {
|
||||
if ($Help) {Write-Host 'Usage: wec-state [-WecStateAction Plan] -WecStateId ID -WecStateSourceSid SID -WecStateDesired Enabled|Disabled -WecStateOutputPath new-directory; then Apply with -WecStatePlanPath reviewed-plan.json -WecStatePlanHash SHA256 -WecStateOutputPath new-directory. Only Enabled on an existing subscription. Disable interrupts collection; enable/save activates it. See docs/wec-state.md.';return}
|
||||
$arguments=@{Action=$WecStateAction;OutputPath=$WecStateOutputPath}
|
||||
$map=@{WecStateId='Id';WecStateSourceSid='SourceSids';WecStateDesired='State';WecStatePlanPath='PlanPath';WecStatePlanHash='PlanHash'}
|
||||
foreach($name in $map.Keys){if($PSBoundParameters.ContainsKey($name)){$arguments[$map[$name]]=$PSBoundParameters[$name]}}
|
||||
$report=Invoke-WelaWecState @arguments
|
||||
$report
|
||||
if($report.ExitCode){exit $report.ExitCode}
|
||||
}
|
||||
'wec-update' {
|
||||
if ($Help) {Write-Host 'Usage: wec-update [-WecUpdateAction Plan] -WecUpdateId ID -WecUpdateSourceSid SID -WecUpdateQueryPath query.xml -WecUpdateDescription text -WecUpdateOutputPath new-directory; then Apply with -WecUpdatePlanPath reviewed-plan.json -WecUpdatePlanHash SHA256 -WecUpdateOutputPath new-directory. Only query/description on already disabled subscriptions. See docs/wec-update.md.';return}
|
||||
$arguments=@{Action=$WecUpdateAction;OutputPath=$WecUpdateOutputPath}
|
||||
@@ -2437,6 +2484,14 @@ switch ($Cmd.ToLower()) {
|
||||
if ($report.ExitCode) { exit $report.ExitCode }
|
||||
} catch { Write-Host "[Failed] Firewall logging: $_" -ForegroundColor Red; exit 1 }
|
||||
}
|
||||
'smb-runtime' {
|
||||
if ($Help) {Write-Host 'Usage: ./WELA.ps1 smb-runtime [-SmbRuntimeAction Plan|Activate] [-SmbRuntimeOutputPath new-local-directory] [-Auto] [-DryRun]. Activates only six native SMB audit switches; policy and security settings are preserved. See docs/smb-runtime-activation.md.';return}
|
||||
try {
|
||||
$report=Invoke-WelaSmbRuntimeActivation -Action $SmbRuntimeAction -OutputPath $SmbRuntimeOutputPath -Auto:$Auto -DryRun:$DryRun
|
||||
$report
|
||||
if($report.ExitCode){exit $report.ExitCode}
|
||||
}catch{Write-Host "[Failed] SMB runtime activation: $_" -ForegroundColor Red;exit 1}
|
||||
}
|
||||
'smb-auditing' {
|
||||
if ($Help) {
|
||||
Write-Host 'Usage: ./WELA.ps1 smb-auditing [-SmbAction Audit|Plan|Configure] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath file.json]'
|
||||
|
||||
@@ -30,6 +30,8 @@ Microsoft's Policy CSP pages list **26100.3613** as the availability floor for t
|
||||
|
||||
## Policy registry versus effective runtime
|
||||
|
||||
The separate explicit [`smb-runtime` activation command](smb-runtime-activation.md) can activate the six native audit Booleans through reviewed SMB setters, with policy-conflict and complete configuration guards. This policy command does not invoke it automatically. Both operations keep event generation and policy persistence separate from current configuration observations.
|
||||
|
||||
Reports keep `Policy` (the actual policy-registry value/type) separate from `Runtime` (the corresponding property of `Get-SmbServerConfiguration` or `Get-SmbClientConfiguration`). WELA never substitutes the policy DWORD for a runtime observation:
|
||||
|
||||
- `Observed`: the getter exposes an actual Boolean. `RuntimeState=Active` means that Boolean was True, not that representative events were generated. False is `NotActive` before the desired policy exists, or `PendingVerification` when the policy registry contains DWORD 1. A correctly written/read-back policy therefore succeeds even when the runtime Boolean remains False. Pending verification does **not** assert propagation delay, a future activation deadline, or that a policy refresh/restart will fix the discrepancy. Its cause and activation timing are unknown; investigate and repeat Audit independently. WELA performs no refresh/restart and never weakens security to make a Boolean change.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Explicit native SMB audit activation
|
||||
|
||||
Related to #377. `smb-runtime` explicitly activates the six reviewed native SMB audit switches when their actual runtime Booleans are False. It complements `smb-auditing`, which configures policy DWORDs and reports runtime state separately. Sysmon is excluded.
|
||||
|
||||
```powershell
|
||||
.\WELA.ps1 smb-runtime
|
||||
.\WELA.ps1 smb-runtime -SmbRuntimeAction Activate -DryRun
|
||||
.\WELA.ps1 smb-runtime -SmbRuntimeAction Activate -SmbRuntimeOutputPath C:\Evidence\new-smb-activation -Auto
|
||||
```
|
||||
|
||||
The default Plan and Activate dry-run only read. Activate requires a new evidence directory outside the source tree on a local fixed drive with an existing parent. It protects that directory for the actual user, Administrators and SYSTEM. Without `-Auto`, each required change asks for explicit consent. Existing True flags are checked without invoking their setters. Activation requires permissions to use the native SMB configuration cmdlets.
|
||||
|
||||
Only native 64-bit Windows 11 24H2/25H2 (builds 26100/26200) and Server 2025 (26100, including DC product type) are reviewed. Each switch also requires the exact local machine ADMX mapping, genuine Windows `SmbShare` module location, an actual Boolean setter parameter and a native CIM Boolean getter property. Missing definitions, properties, unsupported builds, unreadable values and unexpected configuration types stop the operation. Windows 11 and DC deployment acceptance remain separate from hosted member-server testing.
|
||||
|
||||
| Native command | Only permitted parameters |
|
||||
| --- | --- |
|
||||
| `Set-SmbServerConfiguration` | `AuditClientDoesNotSupportEncryption`, `AuditClientDoesNotSupportSigning`, `AuditInsecureGuestLogon` |
|
||||
| `Set-SmbClientConfiguration` | `AuditServerDoesNotSupportEncryption`, `AuditServerDoesNotSupportSigning`, `AuditInsecureGuestLogon` |
|
||||
|
||||
Every selected value is set to Boolean True, one at a time. The command does not set signing/encryption requirements, enable guest access, modify shares, change services, restart Windows, refresh policy, change channels or generate traffic. It changes no registry-policy value. A current absent policy value is compatible and stays absent; a present policy must be DWORD 1. Any conflicting or malformed policy blocks the entire activation before writes. Absence does not establish local ownership or rule out future GPO/MDM changes. This is an explicit local runtime configuration operation, not a GPO edit or a promise of persistence.
|
||||
|
||||
The plan captures all six typed policy tuples, local ADMX hashes, host/build identity, native module/source fingerprints and every supported property exposed by both native configuration getters. Before each setter, WELA compares the complete current snapshot, writes and flushes a Pending receipt to disk, then checks the snapshot again after any prompt. The only permitted readback difference is that single audit Boolean becoming True. Every other native configuration property and policy tuple must remain unchanged before a Confirmed receipt is written. A final complete readback is required for `RuntimeAuditingActive`.
|
||||
|
||||
The evidence directory retains `plan.json`, numbered Pending/Confirmed receipts and `result.json`. Failure, drift, declined changes or incomplete readback produce a nonzero result. After a failed operation, remaining flags are skipped; earlier successful changes stay recorded. A setter may have changed its flag before throwing or before a receipt failure, so Pending alone is not proof of either success or no change. There is no automatic rollback. Reports and hashes establish observed consistency, not historic authenticity or protection against an administrator replacing the evidence. No atomic lock against concurrent Windows policy/configuration writers is claimed.
|
||||
|
||||
For manual recovery, select one original flag and compare its Pending/Confirmed receipts with fresh native configuration and policy. Restore only that flag's original Boolean through the matching native setter after reviewing concurrent changes and policy authority. Do not replay the entire configuration object or copy getter values into arbitrary setter parameters. Retain the recovery readback separately. Restoring a getter value does not prove the exact historical registry representation or future policy persistence.
|
||||
|
||||
**Runtime activation grants zero Sigma readiness credit.** The command neither generates nor verifies representative SMB events, forwarding, a backend query, guest behavior or persistence after policy refresh. Keep #377 open until its remaining secure-peer event and ingestion acceptance is completed; never weaken signing/encryption or enable guest access solely to manufacture test evidence.
|
||||
|
||||
Focused tests exercise typed configuration, policy conflicts, idempotence, durable-receipt failure, prompt/prewrite/final drift and partial native failures. The explicitly gated disposable GitHub VM fixture prepares only these audit flags as False on Server 2025, invokes the public CLI to activate all six, checks dry-run/idempotence and restores their original native values. It compares every other exposed native configuration property, all policy tuples and source context before/after. Server 2022 tests actual unsupported refusal. Both run under Windows PowerShell 5.1 and PowerShell 7. The fixture performs no SMB traffic or policy changes, and must never run on production.
|
||||
|
||||
Microsoft sources: [SMB client audit parameters](https://learn.microsoft.com/en-us/powershell/module/smbshare/set-smbclientconfiguration?view=windowsserver2025-ps), [SMB server audit parameters](https://learn.microsoft.com/en-us/powershell/module/smbshare/set-smbserverconfiguration?view=windowsserver2025-ps), [signing and encryption audit events](https://learn.microsoft.com/en-us/windows-server/storage/file-server/smb-signing-overview), [LanmanServer policy mappings](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-lanmanserver), [LanmanWorkstation policy mappings](https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-lanmanworkstation).
|
||||
@@ -0,0 +1,44 @@
|
||||
# Reviewed collector firewall ingress
|
||||
|
||||
`wec-ingress` creates one new local Windows Firewall rule for a prepared Windows Event Collector. It addresses the collector ingress portion of #368. It is optional and separate from source configuration, subscription installation and `wec-update`.
|
||||
|
||||
The command supports elevated native 64-bit Windows Server 2022/2025 standalone or member servers. The Domain firewall must already be enabled, permit inbound/local rules, and have running BFE/MpsSvc services. WinRM and Wecsvc must be installed; the command does not start them. Domain membership and an active Domain network are not required for preparation, but an inactive Domain profile means the new rule does not currently allow traffic. Domain controllers and Windows clients are outside this command's initial support.
|
||||
|
||||
## Review and apply
|
||||
|
||||
Run in the same elevated operator logon on the collector, from the same WELA checkout. Replace the example local address with an address actually assigned to the collector and choose remote source scopes appropriate for your network:
|
||||
|
||||
```powershell
|
||||
./WELA.ps1 wec-ingress -WecIngressName WELA-WEC-BranchSources `
|
||||
-WecIngressLocalAddress 10.20.30.40 -WecIngressRemoteAddress 10.20.40.0/24 `
|
||||
-WecIngressOutputPath C:\WelaEvidence\ingress-plan
|
||||
|
||||
# Inspect plan.json, including every address and the actual host/profile context.
|
||||
# Supply the PlanHash printed by Plan after reviewing that exact file.
|
||||
./WELA.ps1 wec-ingress -WecIngressAction Apply `
|
||||
-WecIngressPlanPath C:\WelaEvidence\ingress-plan\plan.json `
|
||||
-WecIngressPlanHash '<reviewed SHA256>' `
|
||||
-WecIngressOutputPath C:\WelaEvidence\ingress-apply
|
||||
```
|
||||
|
||||
The parent evidence directory must exist; each output directory must be new on a local fixed drive. Output is protected for the operator, Administrators and SYSTEM. Plans are strict, size-bounded JSON and SHA256 binds their exact bytes. SHA256 is an integrity comparison, not a signature or independent authorization.
|
||||
|
||||
Select 1–8 exact local IPv4 addresses currently in Preferred state and 1–16 remote IPv4 literals or aligned `/24`–`/32` CIDRs. The initial implementation deliberately restricts scope size. It rejects wildcard/DNS/range/IPv6 addresses, host bits in networks, duplicate canonical addresses, loopback, unspecified and multicast/reserved destinations. Expand future address support with native validation rather than editing a generated plan.
|
||||
|
||||
The fixed rule is enabled, inbound Allow, Domain profile only, TCP local port 5985, remote port Any, with exactly the reviewed local/remote addresses. Edge traversal and block-rule override are disabled. The rule has no application, service, user or machine filter, so it also permits other HTTP/WinRM uses of port 5985 within that scope. `Authentication=NotRequired` and `Encryption=NotRequired` describe the new firewall rule's IPsec criteria; they do not modify WinRM authentication, transport or encryption settings.
|
||||
|
||||
The name must begin `WELA-WEC-`. It must be absent from both PersistentStore and ActiveStore, checked again immediately before native creation. The command never updates an existing rule. Windows duplicate-name rejection protects against a competing local creation. A Group Policy refresh or a later policy change can still supersede a local rule; configuration verification is a point-in-time observation, not a lock or persistence guarantee.
|
||||
|
||||
## Evidence and failure handling
|
||||
|
||||
Plan reads actual host/build/patch, MachineGuid, elevated operator SID/logon/groups, firewall profiles, assigned IPv4 addresses and service state, plus implementation hashes. Apply requires the same context, writes and flushes a Pending receipt before mutation, rechecks inputs, then uses `New-NetFirewallRule` once. It verifies PersistentStore and ActiveStore rule properties and all associated native filter classes. Native dotted netmasks are canonicalized for comparison. The existing `wec-collector` ingress prerequisite also compares explicit IP/network identities, so the same reviewed `/24` configuration recognizes Windows' dotted-netmask readback. Its existing broader IPv4/IPv6 CIDR support is preserved; this does not expand the narrower address selection of `wec-ingress`. Different networks, prefixes, IPv6 scope IDs, extra addresses, dynamic aliases and malformed masks remain mismatches or unreadable evidence. Raw native address strings remain in the reports.
|
||||
|
||||
`CreatedAndVerified` means the new rule's selected properties and filters matched during readback. `Refused` means no create attempt was made. `CreateAttemptedUnverified` means a rule may have been created: retain the Pending receipt and any after-state artifacts, inspect the named rule and correct or remove it explicitly. There is no automatic rollback or reuse of an existing rule, and replay of an applied plan is refused. Evidence filesystem failures are fatal and may leave only the already-flushed Pending receipt.
|
||||
|
||||
Other existing rules may allow broader access. This command does not claim that the collector's overall exposure is restricted to these addresses. It creates no WinRM listener, subscription, source GPO or service configuration and performs no network probe. Use the existing collector/source prerequisite and arrival checks separately. It grants zero Sigma readiness credit. Sysmon is excluded.
|
||||
|
||||
## Validation
|
||||
|
||||
Portable tests cover strict address/plan validation, source/context/hash drift, duplicate names, durable-before-write ordering, broader readback and partial failures. Public CLI guards reject unrelated options. The disposable Windows matrix uses Server 2022/2025 and PowerShell 5.1/7, an actual assigned local address and documentation remote subnet `192.0.2.0/24`; it exercises public Plan/Apply, native collision rejection, both policy stores and replay, checks that the real existing collector prerequisite accepts the reviewed `/24` and rejects `/25`, then removes only its uniquely named test rule and checks original rule properties, profiles and services. This is configuration evidence, not packet, listener or WEF delivery evidence.
|
||||
|
||||
References: [Microsoft New-NetFirewallRule](https://learn.microsoft.com/en-us/powershell/module/netsecurity/new-netfirewallrule?view=windowsserver2025-ps), [Get-NetFirewallRule and associated filters](https://learn.microsoft.com/en-us/powershell/module/netsecurity/get-netfirewallrule?view=windowsserver2025-ps), [firewall security filters](https://learn.microsoft.com/en-us/powershell/module/netsecurity/get-netfirewallsecurityfilter?view=windowsserver2025-ps).
|
||||
@@ -0,0 +1,39 @@
|
||||
# Reviewed enable/disable of an existing WEC subscription
|
||||
|
||||
`wec-state` reviews and changes only the **Enabled** Boolean of one existing native source-initiated HTTP subscription to ForwardedEvents. It completes the local pause/resume configuration step around [disabled query updates](wec-update.md). Disabling interrupts collection; enabling and saving activates the subscription. Review the source authorization, query, ReadExistingEvents setting and collection impact before Apply. No subscription is created, replaced or deleted, and no listener, firewall, service, channel, source authorization or query is changed.
|
||||
|
||||
```powershell
|
||||
# Native 64-bit Windows PowerShell 5.1 or PowerShell 7 on the collector.
|
||||
./WELA.ps1 wec-state -WecStateId 'Reviewed native subscription' `
|
||||
-WecStateSourceSid 'S-1-5-21-111111111-222222222-333333333-1234' `
|
||||
-WecStateDesired Disabled -WecStateOutputPath C:\Evidence\disable-plan
|
||||
|
||||
# Review plan.json and record its PlanHash from the planning result.
|
||||
./WELA.ps1 wec-state -WecStateAction Apply `
|
||||
-WecStatePlanPath C:\Evidence\disable-plan\plan.json `
|
||||
-WecStatePlanHash '<reviewed 64-character lowercase SHA256>' `
|
||||
-WecStateOutputPath C:\Evidence\disable-apply
|
||||
|
||||
# Resuming requires a fresh plan against the current definition:
|
||||
./WELA.ps1 wec-state -WecStateId 'Reviewed native subscription' `
|
||||
-WecStateSourceSid 'S-1-5-21-111111111-222222222-333333333-1234' `
|
||||
-WecStateDesired Enabled -WecStateOutputPath C:\Evidence\enable-plan
|
||||
```
|
||||
|
||||
Plan is the default and performs read-only native observations plus new evidence files. State is always explicit. Apply requires the reviewed file and separately supplied SHA256. `-Auto`, `-DryRun`, hypothetical host/role overrides and unrelated configuration options are rejected. An already matching state performs no native save: saving an enabled subscription could otherwise reactivate/retry it.
|
||||
|
||||
The actual collector must be a standalone or member Server 2022/2025 with Wecsvc already running. Enabling additionally requires ForwardedEvents already enabled; its observed configuration is included in the review/context guards. Explicit domain source SIDs must match its existing narrow authorization exactly; this does not prove those sources exist or can connect. Supported definitions use the existing strict native subscription parser: exact built-in channel filters, source-initiated HTTP5985, ForwardedEvents, a standard delivery preset, explicit content format/locale and ReadExistingEvents. Certificate/non-domain sources, arbitrary delivery properties and Sysmon/EMET are excluded. Dedicated domain/Kerberos deployment remains a separate [WEF configuration](wef-deployment.md) operation.
|
||||
|
||||
The reviewed plan binds complete original subscription XML, desired Boolean, actual host/build/role and operator identity/logon, service state and implementation hashes. Plan and Apply may run in separate processes in the same Windows logon; a different logon needs a fresh plan. Each operation also compares full native token statistics, including token/modification identifiers, to reject token or privilege changes during that operation. Hashes establish consistency, not authenticated approval or an untrusted evidence author's identity.
|
||||
|
||||
Apply uses `EC_OPEN_EXISTING` and requires the complete current definition to match its reviewed pre-state. A private Pending receipt is flushed and verified before mutation. Immediately before saving it rechecks evidence, source files, host/reader/token/service and full XML; a freshly opened native view also checks Enabled, query, description and authorization. The only property passed to `EcSetSubscriptionProperty` is `EcSubscriptionEnabled`. Readback requires the desired state and every other observed XML element to remain semantically identical, including native Delivery/EventSources expansion. Raw original/after XML is retained without rewriting it. A changing source inventory can therefore leave the configuration result unverified even when the requested Enabled value is observed.
|
||||
|
||||
Windows exposes no subscription lock, generation identity or atomic compare-and-swap. Concurrent administrators, source updates or an identical delete/recreate cannot all be excluded by these observations. Coordinate the operation on a quiescent subscription. The command makes no automatic rollback: reversing a state change requires another reviewed plan against the current definition. Failed saves or differing readback return `SaveAttemptedUnverified`, retaining the native error code and a best-effort post-failure definition/runtime observation. An activation failure can still persist Enabled; failure never implies rollback; retain the pending receipt and inspect actual Windows state before deciding what to do next. `NativeSaveAttempted` records whether the native save call was reached, including its failures. Pre-save refusals do not receive that flag.
|
||||
|
||||
Output must be a new directory under an existing local fixed-drive parent. UNC/device paths, streams and observed reparse points are rejected through the shared evidence-path helper. The new directory is restricted to the operator, SYSTEM and Administrators; existing paths and ACLs remain unchanged. Files use exclusive creation, flushed readback and SHA256 checks before the final manifest. These are sequential observations, not protection against a competing administrator. Reports contain sensitive source/host/account metadata. A missing final manifest means the evidence is incomplete.
|
||||
|
||||
`ReviewRequired`, `AlreadyMatches` and `StateChangedAndVerified` are configuration results. Separate bounded `RuntimeBefore`/`RuntimeAfter` objects reuse [typed native runtime observations](wec-runtime.md), capped at 32 sources; their Unknown/Partial statuses remain visible and do not become healthy-delivery claims. Active, heartbeat or an enabled setting proves neither event arrival nor uninterrupted collection. Bookmark continuity, backlog, transmission latency, source authorization effectiveness, retention and Sigma readiness remain unverified; `ReadyRuleCredit` is always zero. The command does not create an event or refresh a source.
|
||||
|
||||
Portable tests exercise stale plans, wrong hashes/types/authorization, duplicate JSON, unsupported queries, host/token/source drift, false native success, preservation/evidence failures, and idempotence. The gated disposable Server 2022/2025 × Windows PowerShell 5.1/PowerShell 7 fixture creates one uniquely owned subscription authorized to a fictional SID, uses the public CLI for actual enable/disable and idempotent transitions, checks complete preservation and stale-plan refusal, temporarily enables ForwardedEvents as a fixture prerequisite, then removes only the owned subscription and restores exact channel settings plus service state/startup. It creates no listener or real source. Native CI validates local state transitions only; connected Windows 11/member/DC/ADCS sources, actual event arrival, disable/resume gaps and bookmarks remain isolated multi-host acceptance for issue #368.
|
||||
|
||||
References: Microsoft [subscription property types](https://learn.microsoft.com/en-us/windows/win32/api/evcoll/ne-evcoll-ec_subscription_property_id), [existing-only open](https://learn.microsoft.com/en-us/windows/win32/api/evcoll/nf-evcoll-ecopensubscription), [access/open constants](https://learn.microsoft.com/en-us/windows/win32/wec/windows-event-collector-constants), [save activation/retry semantics](https://learn.microsoft.com/en-us/windows/win32/api/evcoll/nf-evcoll-ecsavesubscription) and [token statistics](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_statistics).
|
||||
+1
-1
@@ -21,7 +21,7 @@ The reviewed plan binds the complete original XML, explicit desired values, actu
|
||||
|
||||
Concurrent changes, enabled subscriptions, unsupported definitions, denied reads and changed plans fail rather than broadening scope. If save is attempted but fails or readback differs, the manifest says `SaveAttemptedUnverified`; no automatic rollback can overwrite an intervening administrator change. Preserve the receipt and inspect the actual subscription. Restoring original values requires a fresh plan against its current state using the original recorded query/description. Windows exposes no compare-and-swap or subscription lock here: the pre-save checks narrow but cannot eliminate a concurrent administrative write between observation and save. Coordinate a maintenance window; hashes are consistency checks, not signatures or authenticated approval.
|
||||
|
||||
The subscription remains disabled, and authorization, destination, delivery, locale, transport, ReadExistingEvents and other observed settings must remain unchanged. This first version deliberately requires disabled state: Microsoft documents that saving an enabled subscription activates it. Active-source delivery and bookmark continuity require separate lab acceptance before extending that scope. A successful disabled update grants **zero Sigma readiness credit** and proves neither delivery nor retention.
|
||||
The subscription remains disabled, and authorization, destination, delivery, locale, transport, ReadExistingEvents and other observed settings must remain unchanged. This first version deliberately requires disabled state: Microsoft documents that saving an enabled subscription activates it. Use the separate [reviewed Enabled transition](wec-state.md) command to disable or enable an existing subscription. Active-source delivery and bookmark continuity require separate lab acceptance. A successful disabled update grants **zero Sigma readiness credit** and proves neither delivery nor retention.
|
||||
|
||||
Tests include malformed/duplicate JSON, stale plans, changed context, unexpected enablement, preservation failure, native error, false success, idempotence and pending receipt ordering. Disposable Server 2022/2025 × Windows PowerShell 5.1/PowerShell 7 CI creates one unique disabled subscription with no real source, changes and restores query/description through the public command, rejects the stale plan, verifies other properties and restores subscription inventory plus original Wecsvc state/startup. It does not validate active sources or bookmarks.
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ On a domain controller, BUILTIN group membership has domain/AD authority rather
|
||||
|
||||
The local host must be a domain member server whose observed DNS name equals `CollectorFqdn`. WinRM and Wecsvc can be set to Automatic and started. WELA never runs `winrm quickconfig`, `wecutil qc` or `Enable-PSRemoting`, and never creates or broadens listeners/firewall rules.
|
||||
|
||||
Before enabling ForwardedEvents or creating a subscription, WELA requires one existing listener matching `ListenerAddress`, HTTP, enabled state, port 5985 and URL prefix `wsman`; one named effective ActiveStore ingress rule matching inbound Allow, Domain profile, TCP 5985 and the exact `IngressLocalAddresses`/`IngressRemoteAddresses`; running Automatic services; enabled collector Kerberos; and the two assessed ASD hardening settings below. Supply explicit IP/CIDR address lists, not `Any` or `/0`. The check verifies the selected definitions, not actual packet acceptance, reachability, profile activation or the absence of other broad rules. Listener/rule evidence is retained in JSON.
|
||||
Before enabling ForwardedEvents or creating a subscription, WELA requires one existing listener matching `ListenerAddress`, HTTP, enabled state, port 5985 and URL prefix `wsman`; one named effective ActiveStore ingress rule matching inbound Allow, Domain profile, TCP 5985 and the exact `IngressLocalAddresses`/`IngressRemoteAddresses`; running Automatic services; enabled collector Kerberos; and the two assessed ASD hardening settings below. Supply explicit IP/CIDR address lists, not `Any` or `/0`. The check verifies the selected definitions, not actual packet acceptance, reachability, profile activation or the absence of other broad rules. Listener/rule evidence is retained in JSON. Explicit address scopes are compared by IP/network identity, recognizing native IPv4 dotted-netmask spelling and equivalent IPv6 compression while preserving network size, address family and scope ID. Raw native strings remain visible; dynamic aliases, malformed masks and different scopes never become an ingress match.
|
||||
|
||||
`Hardening: "ApplyASD"` explicitly permits setting `WSMan:\localhost\Service\Auth\CbtHardeningLevel` to `Strict` and `WSMan:\localhost\Shell\AllowRemoteShellAccess` to `false`. Disabling remote shells prevents new remote-shell sessions; review this on a dedicated collector using local/out-of-band administration. `AssessOnly` records unmet hardening and blocks subscription creation. Policy-owned mismatches are refused; WELA does not rewrite their controlling GPO. No Basic, CredSSP, TrustedHosts, authentication fallback or firewall access setting is changed.
|
||||
|
||||
|
||||
@@ -119,6 +119,41 @@ function ConvertFrom-WelaWefSubscription {
|
||||
[pscustomobject]@{ Id=$id; Xml=$doc.OuterXml; Definition=[pscustomobject]$definition; Key=($definition | ConvertTo-Json -Depth 30 -Compress); Query=$query; SourceSids=@($SourceSids | Sort-Object -Unique) }
|
||||
}
|
||||
|
||||
# Compare explicit firewall address scopes by network identity, retaining family
|
||||
# and IPv6 scope ID. Windows may report IPv4 CIDR as a dotted netmask.
|
||||
function ConvertTo-WelaWefFirewallAddressKey {
|
||||
param([string]$Value,[switch]$Observed)
|
||||
$parts=$Value -split '/';$address=$null
|
||||
if($parts.Count -gt 2 -or -not [Net.IPAddress]::TryParse($parts[0],[ref]$address)){throw 'Expected an explicit firewall IP address or CIDR network.'}
|
||||
$bytes=$address.GetAddressBytes();$bits=$bytes.Length*8;$prefix=$bits
|
||||
if($parts.Count -eq 2){
|
||||
if($Observed -and $bytes.Length -eq 4 -and $parts[1].Contains('.')){
|
||||
if($parts[1] -notmatch '^[0-9]{1,3}(\.[0-9]{1,3}){3}$'){throw 'Invalid observed IPv4 netmask.'}
|
||||
$mask=@($parts[1].Split('.')|ForEach-Object {if([int]$_ -gt 255){throw 'Invalid observed IPv4 netmask.'};[int]$_})
|
||||
$prefix=0;$zeroSeen=$false
|
||||
foreach($octet in $mask){for($bit=7;$bit -ge 0;$bit--){if(($octet -band (1 -shl $bit)) -ne 0){if($zeroSeen){throw 'Observed IPv4 netmask is not contiguous.'};$prefix++}else{$zeroSeen=$true}}}
|
||||
}else{
|
||||
if($parts[1] -notmatch '^\d+$'){throw 'Expected a numeric firewall CIDR prefix.'}
|
||||
$prefix=[int]$parts[1]
|
||||
}
|
||||
if($prefix -lt 1 -or $prefix -gt $bits){throw 'Zero or out-of-range firewall CIDR prefix is unsupported.'}
|
||||
}
|
||||
# Network host bits are immaterial to an explicit CIDR scope.
|
||||
for($i=0;$i -lt $bytes.Length;$i++){
|
||||
$remaining=$prefix-8*$i
|
||||
if($remaining -le 0){$bytes[$i]=0}elseif($remaining -lt 8){$bytes[$i]=[byte]($bytes[$i] -band (256-(1 -shl (8-$remaining))))}
|
||||
}
|
||||
$scope=if($bits -eq 128){'%'+$address.ScopeId}else{''}
|
||||
[string]$bits+':'+([BitConverter]::ToString($bytes)).Replace('-','')+$scope+'/'+$prefix
|
||||
}
|
||||
function Test-WelaWefFirewallAddressSet {
|
||||
param([object[]]$Expected,[object[]]$Observed)
|
||||
if(-not $Expected.Count -or -not $Observed.Count){return $false}
|
||||
$expectedKeys=@(foreach($value in $Expected){if($value -isnot [string]){throw 'Expected firewall address must be a string.'};ConvertTo-WelaWefFirewallAddressKey $value})
|
||||
$observedKeys=@(foreach($value in $Observed){if($value -isnot [string]){throw 'Observed firewall address must be a string.'};ConvertTo-WelaWefFirewallAddressKey $value -Observed})
|
||||
return @((Compare-Object @($expectedKeys|Sort-Object -Unique) @($observedKeys|Sort-Object -Unique))).Count -eq 0
|
||||
}
|
||||
|
||||
function Import-WelaWefConfig {
|
||||
param([string]$Path, [ValidateSet('Source','Collector')][string]$Role)
|
||||
$full = (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
|
||||
@@ -172,4 +207,4 @@ function Read-WelaWecSubscriptionXml {
|
||||
[Wela.WecXml.Reader]::ReadXml($Id)
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Read-WelaWecSubscriptionXml, Read-WelaWefXml, Get-WelaWefXmlKey, ConvertFrom-WelaWefQuery, Get-WelaWefAuthorization, ConvertFrom-WelaWefSubscription, Import-WelaWefConfig
|
||||
Export-ModuleMember -Function ConvertTo-WelaWefFirewallAddressKey, Test-WelaWefFirewallAddressSet, Read-WelaWecSubscriptionXml, Read-WelaWefXml, Get-WelaWefXmlKey, ConvertFrom-WelaWefQuery, Get-WelaWefAuthorization, ConvertFrom-WelaWefSubscription, Import-WelaWefConfig
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# Explicit native audit-switch activation. No registry policy, security, share or service writes.
|
||||
function Get-WelaSmbRuntimeKey { param($Value) ConvertTo-Json -InputObject $Value -Depth 24 -Compress }
|
||||
|
||||
function Get-WelaSmbRuntimeSources {
|
||||
$result=[ordered]@{}
|
||||
foreach($name in @('WELA.ps1','scripts/SmbRuntimeActivation.ps1','scripts/SmbAuditing.ps1','scripts/Configuration.ps1','scripts/WefArrival.ps1')) {
|
||||
$result[$name]=(Get-FileHash -LiteralPath (Join-Path $script:ScriptRoot $name) -Algorithm SHA256 -ErrorAction Stop).Hash
|
||||
}
|
||||
[pscustomobject]$result
|
||||
}
|
||||
|
||||
function Assert-WelaSmbRuntimeCommand {
|
||||
param($Command,[ValidateSet('Server','Client')][string]$Side,[ValidateSet('Get','Set')][string]$Verb,[string]$ModuleBase)
|
||||
# SmbShare exports functions from these native nested CDXML modules.
|
||||
if($Command.Name -cne "$Verb-Smb${Side}Configuration" -or $Command.ModuleName -cne "Smb${Side}Configuration" -or
|
||||
[string]$Command.CommandType -cne 'Function' -or [IO.Path]::GetFullPath($Command.Module.ModuleBase) -ine $ModuleBase){
|
||||
$observed=[pscustomobject]@{Name=$Command.Name;ModuleName=$Command.ModuleName;ModuleBase=$Command.Module.ModuleBase;Type=[string]$Command.CommandType}
|
||||
throw "SMB commands must resolve to the reviewed native SmbShare CDXML module. Expected $ModuleBase; observed $(Get-WelaSmbRuntimeKey $observed)"
|
||||
}
|
||||
if($Verb -eq 'Set') {
|
||||
$component=if($Side -eq 'Server'){'LanmanServer'}else{'LanmanWorkstation'}
|
||||
foreach($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $component)) {
|
||||
if(-not $Command.Parameters.ContainsKey($definition.Name) -or $Command.Parameters[$definition.Name].ParameterType -ne [bool]) {
|
||||
throw "Native setter lacks the exact Boolean parameter $($definition.Name)."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-WelaSmbRuntimeCommands {
|
||||
$base=[IO.Path]::GetFullPath((Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell/v1.0/Modules/SmbShare'))
|
||||
$commands=[ordered]@{}
|
||||
foreach($side in @('Server','Client')) {
|
||||
foreach($verb in @('Get','Set')) {
|
||||
$name="SmbShare\$verb-Smb${side}Configuration"
|
||||
$found=@(Get-Command -Name $name -ErrorAction Stop)
|
||||
if($found.Count -ne 1){throw 'Expected exactly one native module-qualified SMB command.'}
|
||||
Assert-WelaSmbRuntimeCommand -Command $found[0] -Side $side -Verb $verb -ModuleBase $base
|
||||
$commands[$name]=[pscustomobject]@{ModuleName=$found[0].ModuleName;ModuleBase=$base;ModuleVersion=$found[0].Module.Version.ToString();CommandType=$found[0].CommandType.ToString()}
|
||||
}
|
||||
}
|
||||
$files=@(Get-ChildItem -LiteralPath $base -File -Recurse -ErrorAction Stop | Where-Object Extension -in @('.psd1','.psm1','.cdxml','.dll','.ps1xml') | Sort-Object FullName)
|
||||
if($files.Count -lt 1 -or $files.Count -gt 100){throw 'Unexpected native SMB module inventory.'}
|
||||
$hashes=[ordered]@{}
|
||||
foreach($file in $files){
|
||||
if($file.Length -gt 16MB -or ($file.Attributes -band [IO.FileAttributes]::ReparsePoint)){throw 'Unsupported SMB module source.'}
|
||||
$hashes[$file.FullName]=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256 -ErrorAction Stop).Hash
|
||||
}
|
||||
[pscustomobject]@{Commands=[pscustomobject]$commands;Files=[pscustomobject]$hashes}
|
||||
}
|
||||
|
||||
function ConvertTo-WelaSmbRuntimeConfiguration {
|
||||
param($Configuration,[ValidateSet('Server','Client')][string]$Side)
|
||||
if($Configuration.CimClass.CimClassName -cne "MSFT_Smb${Side}Configuration"){throw 'Expected one actual native SMB configuration CIM instance.'}
|
||||
$properties=@($Configuration.CimInstanceProperties | Sort-Object Name)
|
||||
if($properties.Count -lt 3 -or $properties.Count -gt 160){throw 'Unexpected SMB configuration property count.'}
|
||||
$result=[ordered]@{}
|
||||
foreach($property in $properties) {
|
||||
if($result.Contains($property.Name)){throw 'Duplicate SMB configuration property.'}
|
||||
$value=$property.Value
|
||||
foreach($item in @($value)) {
|
||||
if($null -ne $item -and $item -isnot [bool] -and $item -isnot [string] -and
|
||||
$item -isnot [byte] -and $item -isnot [uint16] -and $item -isnot [uint32] -and $item -isnot [uint64] -and
|
||||
$item -isnot [int16] -and $item -isnot [int32] -and $item -isnot [int64]){throw "Unsupported native configuration value: $($property.Name)"}
|
||||
if($item -is [string] -and $item.Length -gt 8192){throw 'Native configuration string exceeds bound.'}
|
||||
}
|
||||
if(@($value).Count -gt 128){throw 'Native configuration array exceeds bound.'}
|
||||
$result[$property.Name]=[pscustomobject]@{CimType=$property.CimType.ToString();Value=$value}
|
||||
}
|
||||
$component=if($Side -eq 'Server'){'LanmanServer'}else{'LanmanWorkstation'}
|
||||
foreach($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $component)) {
|
||||
if(-not $result.Contains($definition.Name) -or $result[$definition.Name].Value -isnot [bool] -or $result[$definition.Name].CimType -cne 'Boolean') {
|
||||
throw "Native getter lacks the exact Boolean property $($definition.Name)."
|
||||
}
|
||||
}
|
||||
[pscustomobject]$result
|
||||
}
|
||||
|
||||
function Get-WelaSmbRuntimeState {
|
||||
$hostState=Get-WelaSmbAuditHost
|
||||
if($hostState.Status -ne 'Candidate'){throw "SMB runtime activation is $($hostState.Status): $($hostState.Diagnostic)"}
|
||||
$commands=Get-WelaSmbRuntimeCommands
|
||||
$policies=[ordered]@{}
|
||||
foreach($definition in Get-WelaSmbAuditDefinitions) {
|
||||
$capability=Get-WelaSmbAuditCapability -Definition $definition -HostState $hostState
|
||||
if($capability.Status -ne 'Supported'){throw "Unverified $($definition.Component)/$($definition.Name): $($capability.Diagnostic)"}
|
||||
$policies["$($definition.Component)/$($definition.Name)"]=[pscustomobject]@{
|
||||
Path=$definition.Path;Name=$definition.Name;AdmxSha256=$capability.AdmxSha256
|
||||
Policy=Get-WelaRegistryState -Path $definition.Path -Name $definition.Name
|
||||
}
|
||||
}
|
||||
$configurations=[ordered]@{}
|
||||
foreach($side in @('Server','Client')) {
|
||||
$command="SmbShare\Get-Smb${side}Configuration"
|
||||
$native=@(& $command -ErrorAction Stop)
|
||||
if($native.Count -ne 1){throw 'Expected exactly one native SMB configuration.'}
|
||||
$configurations[$side]=ConvertTo-WelaSmbRuntimeConfiguration -Configuration $native[0] -Side $side
|
||||
}
|
||||
[pscustomobject][ordered]@{Computer=[Environment]::MachineName;Host=$hostState;Commands=$commands;Sources=Get-WelaSmbRuntimeSources;Policies=[pscustomobject]$policies;Configurations=[pscustomobject]$configurations}
|
||||
}
|
||||
|
||||
function Get-WelaSmbRuntimePlan {
|
||||
param($State)
|
||||
foreach($definition in Get-WelaSmbAuditDefinitions) {
|
||||
$id="$($definition.Component)/$($definition.Name)"
|
||||
$policy=$State.Policies.$id.Policy
|
||||
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
|
||||
$value=$State.Configurations.$side.($definition.Name).Value
|
||||
$compatible=($policy.ValueExists -is [bool] -and -not $policy.ValueExists) -or
|
||||
($policy.ValueExists -eq $true -and $policy.Type -ceq 'DWord' -and
|
||||
($policy.Value -is [int] -or $policy.Value -is [long] -or $policy.Value -is [uint32]) -and $policy.Value -eq 1)
|
||||
[pscustomobject][ordered]@{Id=$id;Side=$side;Name=$definition.Name;Before=$value;Desired=$true;Policy=$policy
|
||||
Status=$(if(-not $compatible){'BlockedPolicy'}elseif($value){'AlreadyActive'}else{'ActivationRequired'})
|
||||
Diagnostic=$(if(-not $compatible){'Existing policy is not absent or DWORD 1; review its authority. It will not be overwritten.'}elseif($policy.ValueExists){'Policy DWORD 1 and runtime Boolean are separate observations.'}else{'Policy value is absent; explicit activation changes native local configuration only.'})}
|
||||
}
|
||||
}
|
||||
|
||||
function Set-WelaSmbRuntimeFlag {
|
||||
param([string]$Id)
|
||||
$matches=@(Get-WelaSmbAuditDefinitions | Where-Object {"$($_.Component)/$($_.Name)" -ceq $Id})
|
||||
if($matches.Count -ne 1){throw 'Unknown SMB audit switch.'}
|
||||
$definition=$matches[0]
|
||||
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
|
||||
$command="SmbShare\Set-Smb${side}Configuration"
|
||||
$parameters=@{Confirm=$false;Force=$true;ErrorAction='Stop'}
|
||||
$parameters[$definition.Name]=$true
|
||||
$null=& $command @parameters
|
||||
}
|
||||
|
||||
function Write-WelaSmbRuntimeReceipt {
|
||||
param([string]$Root,[string]$Name,$Value)
|
||||
if($Name -notmatch '^(plan|result|[1-6]-(pending|confirmed))\.json$'){throw 'Unexpected receipt filename.'}
|
||||
$null=Resolve-WelaArrivalPath $Root
|
||||
$path=Join-Path $Root $Name
|
||||
$bytes=[Text.UTF8Encoding]::new($false).GetBytes((Get-WelaSmbRuntimeKey $Value))
|
||||
if($bytes.Length -gt 4MB){throw 'SMB activation receipt exceeds bound.'}
|
||||
$stream=[IO.File]::Open($path,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
|
||||
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)}finally{$stream.Dispose()}
|
||||
$expected=Get-WelaArrivalHash $bytes
|
||||
if((Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant() -cne $expected){throw 'SMB receipt readback differs.'}
|
||||
[pscustomobject]@{Name=$Name;Bytes=$bytes.Length;Sha256=$expected}
|
||||
}
|
||||
|
||||
function Invoke-WelaSmbRuntimeActivation {
|
||||
param([ValidateSet('Plan','Activate')][string]$Action='Plan',[string]$OutputPath,[switch]$Auto,[switch]$DryRun)
|
||||
if($DryRun -and $Action -ne 'Activate'){throw 'DryRun requires SmbRuntimeAction Activate.'}
|
||||
if($Action -eq 'Plan' -and ($Auto -or $OutputPath)){throw 'Plan reads only; Auto and OutputPath apply to Activate.'}
|
||||
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaSmbRuntimeActivation';Action=$Action;DryRun=[bool]$DryRun;RecordedUtc=[DateTime]::UtcNow.ToString('o')
|
||||
Status='Unverified';ExitCode=1;Before=$null;After=$null;Controls=@();Results=@();Artifacts=@();OutputPath=$null;Diagnostic=''
|
||||
VerificationScope='Native local audit switches at the recorded observations; policy authority and persistence are unknown';ReadyRuleCredit=0;EventGeneration='Not tested';Forwarding='Not tested'}
|
||||
try {
|
||||
$state=Get-WelaSmbRuntimeState;$report.Before=$state
|
||||
$report.Controls=@(Get-WelaSmbRuntimePlan $state)
|
||||
if(@($report.Controls | Where-Object Status -eq BlockedPolicy).Count){throw 'One or more policy values conflict or are malformed. No audit flags were changed.'}
|
||||
if($Action -eq 'Plan' -or $DryRun){$report.Status=if($DryRun){'DryRun'}else{'Planned'};$report.ExitCode=0;return $report}
|
||||
if(-not $OutputPath){throw 'Activate requires a new SmbRuntimeOutputPath on a local fixed drive.'}
|
||||
$output=New-WelaArrivalOutput -Path $OutputPath -SourcePath $script:ScriptRoot;$report.OutputPath=$output
|
||||
$report.Artifacts+=Write-WelaSmbRuntimeReceipt $output 'plan.json' ([pscustomobject]@{State=$state;Controls=$report.Controls})
|
||||
$expectedKey=Get-WelaSmbRuntimeKey $state
|
||||
$index=0;$stopped=$false
|
||||
foreach($control in $report.Controls) {
|
||||
$index++
|
||||
$row=[pscustomobject][ordered]@{Id=$control.Id;Before=$control.Before;After=$null;Status='Skipped';Diagnostic='';PendingReceipt=$null;ConfirmedReceipt=$null}
|
||||
$report.Results+= $row
|
||||
if($stopped){$row.Diagnostic='A prior activation failed; no further changes were attempted.';continue}
|
||||
try {
|
||||
$fresh=Get-WelaSmbRuntimeState
|
||||
if((Get-WelaSmbRuntimeKey $fresh) -cne $expectedKey){throw 'Host, source, policy or native configuration drifted after the snapshot.'}
|
||||
if($control.Before){$row.After=$true;$row.Status='AlreadyActive';continue}
|
||||
if(-not $Auto -and (Read-Host "Activate only SMB audit flag $($control.Id)? (y/N)") -cnotin @('y','Y')){$row.Diagnostic='Declined by operator.';continue}
|
||||
$row.PendingReceipt=Write-WelaSmbRuntimeReceipt $output "$index-pending.json" ([pscustomobject]@{Kind='Pending';Id=$control.Id;Before=$fresh;Desired=$true;RecordedUtc=[DateTime]::UtcNow.ToString('o')})
|
||||
# Re-read after interaction and durable intent, immediately before the setter.
|
||||
if((Get-WelaSmbRuntimeKey (Get-WelaSmbRuntimeState)) -cne $expectedKey){throw 'Context drifted before the native setter; activation refused.'}
|
||||
Set-WelaSmbRuntimeFlag -Id $control.Id
|
||||
$after=Get-WelaSmbRuntimeState;$row.After=$after.Configurations.($control.Side).($control.Name).Value
|
||||
# The only permitted delta is this one Boolean. All policies and every
|
||||
# other native configuration property (including security) must match.
|
||||
$next=Get-WelaSmbRuntimeKey $fresh | ConvertFrom-Json
|
||||
$next.Configurations.($control.Side).($control.Name).Value=$true
|
||||
if((Get-WelaSmbRuntimeKey $after) -cne (Get-WelaSmbRuntimeKey $next)){throw 'Native readback did not show exactly the requested audit-only delta.'}
|
||||
$row.ConfirmedReceipt=Write-WelaSmbRuntimeReceipt $output "$index-confirmed.json" ([pscustomobject]@{Kind='Confirmed';Id=$control.Id;Pending=$row.PendingReceipt;After=$after;RecordedUtc=[DateTime]::UtcNow.ToString('o')})
|
||||
$state=$after;$expectedKey=Get-WelaSmbRuntimeKey $state
|
||||
$row.Status='Activated';$row.Diagnostic='Native Boolean True observed; policy tuple and all other configuration properties preserved.'
|
||||
}catch{$row.Status='Failed';$row.Diagnostic=$_.Exception.Message;$stopped=$true}
|
||||
}
|
||||
$report.After=Get-WelaSmbRuntimeState
|
||||
if((Get-WelaSmbRuntimeKey $report.After) -cne $expectedKey){throw 'Final context differs from the last verified configuration. Review partial receipts; no automatic rollback is attempted.'}
|
||||
if(@($report.Results | Where-Object Status -notin @('Activated','AlreadyActive')).Count){throw 'Some flags were not activated. Inspect per-control results and receipts.'}
|
||||
$report.Status='RuntimeAuditingActive';$report.ExitCode=0
|
||||
}catch{$report.Diagnostic=$_.Exception.Message}
|
||||
if($report.OutputPath){$null=Write-WelaSmbRuntimeReceipt $report.OutputPath 'result.json' $report}
|
||||
$report
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
# Explicit creation of one new, narrowly scoped collector firewall rule.
|
||||
function ConvertTo-WelaIngressAddress {
|
||||
param([string]$Value,[switch]$Remote,[switch]$Observed)
|
||||
if($Value -cnotmatch '^([0-9]{1,3}\.){3}[0-9]{1,3}(/([0-9]{1,2}|([0-9]{1,3}\.){3}[0-9]{1,3}))?$'){throw 'An exact canonical IPv4 address or remote /24-/32 network is required.'}
|
||||
$parts=$Value.Split('/');$octets=$parts[0].Split('.');$number=0L
|
||||
foreach($octet in $octets){if([int]$octet -gt 255 -or ([int]$octet).ToString() -cne $octet){throw 'Noncanonical IPv4 address.'};$number=($number -shl 8)+[int]$octet}
|
||||
if([int]$octets[0] -in @(0,127) -or [int]$octets[0] -ge 224 -or $parts[0] -eq '169.254.0.0'){throw 'Unspecified, loopback or multicast/reserved addresses are unsupported.'}
|
||||
$prefix=32
|
||||
if($parts.Count -eq 2){
|
||||
if(-not $Remote){throw 'Local addresses must be exact assigned IPv4 addresses.'}
|
||||
if($parts[1].Contains('.')){
|
||||
if(-not $Observed){throw 'Use a numeric CIDR prefix.'}
|
||||
$mask=0L;foreach($piece in $parts[1].Split('.')){if([int]$piece -gt 255){throw 'Invalid netmask.'};$mask=($mask -shl 8)+[int]$piece}
|
||||
$prefix=0;while($prefix -lt 32 -and ($mask -band (1L -shl (31-$prefix)))){$prefix++}
|
||||
$expected=if($prefix -eq 0){0L}else{(0xffffffffL -shl (32-$prefix)) -band 0xffffffffL}
|
||||
if($mask -ne $expected){throw 'Noncontiguous netmask.'}
|
||||
}else{$prefix=[int]$parts[1];if($prefix.ToString() -cne $parts[1]){throw 'Noncanonical prefix.'}}
|
||||
if($prefix -lt 24 -or $prefix -gt 32 -or ($number -band ((1L -shl (32-$prefix))-1)) -ne 0){throw 'Remote scopes require aligned /24-/32 networks.'}
|
||||
}
|
||||
if($prefix -eq 32){$parts[0]}else{$parts[0]+'/'+$prefix}
|
||||
}
|
||||
function Get-WelaIngressSelection {
|
||||
param([string]$Name,[object[]]$LocalAddresses,[object[]]$RemoteAddresses)
|
||||
if($Name -cnotmatch '^WELA-WEC-[A-Za-z0-9][A-Za-z0-9-]{0,63}$'){throw 'Rule name must start WELA-WEC- and contain only letters, digits and hyphens.'}
|
||||
if($LocalAddresses.Count -lt 1 -or $LocalAddresses.Count -gt 8 -or $RemoteAddresses.Count -lt 1 -or $RemoteAddresses.Count -gt 16){throw 'Select 1-8 local addresses and 1-16 remote scopes.'}
|
||||
$local=@();$remote=@()
|
||||
foreach($value in $LocalAddresses){if($value -isnot [string]){throw 'Address must be a string.'};$local+=ConvertTo-WelaIngressAddress $value}
|
||||
foreach($value in $RemoteAddresses){if($value -isnot [string]){throw 'Address must be a string.'};$remote+=ConvertTo-WelaIngressAddress $value -Remote}
|
||||
if(@($local|Select-Object -Unique).Count -ne $local.Count -or @($remote|Select-Object -Unique).Count -ne $remote.Count){throw 'Duplicate address scopes are unsupported.'}
|
||||
[pscustomobject][ordered]@{Name=$Name;LocalAddresses=@($local|Sort-Object);RemoteAddresses=@($remote|Sort-Object)}
|
||||
}
|
||||
function Get-WelaIngressSources {
|
||||
$root=Split-Path $PSScriptRoot -Parent;$sources=[ordered]@{}
|
||||
foreach($name in @('WELA.ps1','scripts/WecIngress.ps1','scripts/WecUpdate.ps1','scripts/WefArrival.ps1','scripts/ChannelRead.ps1','scripts/ChannelReadNative.cs','modules/AuditProfiles.psm1','scripts/CustomAuditProfiles.ps1')){
|
||||
$sources[$name]=(Get-FileHash -LiteralPath (Join-Path $root $name) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
|
||||
}
|
||||
$sources|ConvertTo-Json -Compress
|
||||
}
|
||||
function Get-WelaIngressContext {
|
||||
$reader=Get-WelaChannelReader;$hostState=Get-WelaChannelReadHost
|
||||
if(-not $reader.ElevatedAdministrator -or $hostState.ProductType -ne 3 -or $hostState.DomainRole -notin @(2,3) -or $hostState.Build -notin @(20348,26100)){throw 'An elevated native Server 2022/2025 member or standalone collector is required.'}
|
||||
$services=@();foreach($name in @('BFE','MpsSvc','WinRM','Wecsvc')){
|
||||
$found=@(Get-CimInstance Win32_Service -Filter "Name='$name'" -ErrorAction Stop)
|
||||
if($found.Count -ne 1 -or ($name -in @('BFE','MpsSvc') -and $found[0].State -ne 'Running')){throw 'Firewall services must run and WinRM/Wecsvc must be installed.'}
|
||||
$services+=[ordered]@{Name=$name;State=[string]$found[0].State;StartMode=[string]$found[0].StartMode}
|
||||
}
|
||||
$profiles=@(NetSecurity\Get-NetFirewallProfile -PolicyStore ActiveStore -ErrorAction Stop|Sort-Object Name|Select-Object Name,Enabled,DefaultInboundAction,DefaultOutboundAction,AllowInboundRules,AllowLocalFirewallRules)
|
||||
$domain=@($profiles|Where-Object Name -eq 'Domain')
|
||||
if($profiles.Count -ne 3 -or $domain.Count -ne 1 -or [string]$domain[0].Enabled -ne 'True' -or [string]$domain[0].AllowLocalFirewallRules -eq 'False' -or [string]$domain[0].AllowInboundRules -eq 'False'){throw 'Domain firewall must be enabled and permit local inbound rules.'}
|
||||
$addresses=@(NetTCPIP\Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop|Where-Object AddressState -eq 'Preferred'|ForEach-Object IPAddress|Sort-Object -Unique)
|
||||
[pscustomobject][ordered]@{Host=$hostState;MachineGuid=(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid;Reader=[ordered]@{Sid=$reader.UserSid;Logon=$reader.AuthenticationId;Groups=$reader.GroupSids};Services=$services;Profiles=$profiles;Addresses=$addresses}
|
||||
}
|
||||
function Read-WelaIngressRules {
|
||||
param([string]$Store)
|
||||
$rules=@(NetSecurity\Get-NetFirewallRule -PolicyStore $Store -ErrorAction Stop|Select-Object -First 4097)
|
||||
if($rules.Count -gt 4096){throw 'Firewall inventory exceeds the 4096-rule bound.'}
|
||||
$rules
|
||||
}
|
||||
function Assert-WelaIngressAbsent {
|
||||
param([string]$Name)
|
||||
foreach($store in @('PersistentStore','ActiveStore')){if(@(Read-WelaIngressRules $store|Where-Object Name -eq $Name).Count){throw 'The selected rule name already exists; existing rules are never replaced.'}}
|
||||
}
|
||||
function New-WelaIngressNativeRule {
|
||||
param($Selection)
|
||||
$null=NetSecurity\New-NetFirewallRule -PolicyStore PersistentStore -Name $Selection.Name -DisplayName $Selection.Name -Description 'WELA reviewed collector ingress; TCP 5985, Domain profile, explicit IPv4 scopes.' -Group 'WELA reviewed collector ingress' -Enabled True -Profile Domain -Direction Inbound -Action Allow -Protocol TCP -LocalPort 5985 -RemotePort Any -LocalAddress $Selection.LocalAddresses -RemoteAddress $Selection.RemoteAddresses -EdgeTraversalPolicy Block -LooseSourceMapping $false -LocalOnlyMapping $false -Authentication NotRequired -Encryption NotRequired -OverrideBlockRules $false -ErrorAction Stop
|
||||
}
|
||||
function Read-WelaIngressRule {
|
||||
param([string]$Store,[string]$Name)
|
||||
$rule=@(Read-WelaIngressRules $Store|Where-Object Name -eq $Name)
|
||||
if($rule.Count -ne 1){throw 'Exactly one selected rule must be observed.'}
|
||||
$r=$rule[0];$filters=[ordered]@{}
|
||||
foreach($kind in @('Port','Address','Application','Service','Interface','InterfaceType','Security')){
|
||||
$command='NetSecurity\Get-NetFirewall'+$kind+'Filter';$items=@(&$command -AssociatedNetFirewallRule $r -ErrorAction Stop)
|
||||
if($items.Count -ne 1){throw "Ambiguous $kind filter."};$filters[$kind]=$items[0]
|
||||
}
|
||||
[pscustomobject]@{Store=$Store;Rule=$r;Filters=$filters}
|
||||
}
|
||||
function ConvertTo-WelaIngressEvidence {
|
||||
param($Observed)
|
||||
# Project the inspected fields, not recursive CIM class/session metadata.
|
||||
$fields=[ordered]@{
|
||||
Rule=@('Name','DisplayName','Description','Group','Enabled','Profile','Direction','Action','EdgeTraversalPolicy','LooseSourceMapping','LocalOnlyMapping','PolicyStoreSourceType','Owner','Platform','PrimaryStatus','EnforcementStatus')
|
||||
Port=@('Protocol','LocalPort','RemotePort','IcmpType','DynamicTarget')
|
||||
Address=@('LocalAddress','RemoteAddress')
|
||||
Application=@('Program','Package')
|
||||
Service=@('Service');Interface=@('InterfaceAlias');InterfaceType=@('InterfaceType')
|
||||
Security=@('Authentication','Encryption','OverrideBlockRules','LocalUser','RemoteUser','RemoteMachine')
|
||||
}
|
||||
$result=[ordered]@{Store=$Observed.Store}
|
||||
foreach($kind in $fields.Keys){
|
||||
$item=if($kind -eq 'Rule'){$Observed.Rule}else{$Observed.Filters[$kind]};$values=[ordered]@{}
|
||||
foreach($name in $fields[$kind]){
|
||||
$property=$item.PSObject.Properties[$name]
|
||||
$values[$name]=[ordered]@{Present=($null -ne $property);Value=$(if($null -eq $property -or $null -eq $property.Value){$null}else{@($property.Value|ForEach-Object {[string]$_})})}
|
||||
}
|
||||
$result[$kind]=$values
|
||||
}
|
||||
[pscustomobject]$result
|
||||
}
|
||||
function Assert-WelaIngressReadback {
|
||||
param($Observed,$Selection)
|
||||
$r=$Observed.Rule;$f=$Observed.Filters
|
||||
foreach($field in @('Name','DisplayName')){if([string]$r.$field -cne $Selection.Name){throw "Rule $field differs."}}
|
||||
$fixed=@{Description='WELA reviewed collector ingress; TCP 5985, Domain profile, explicit IPv4 scopes.';Group='WELA reviewed collector ingress';Enabled='True';Profile='Domain';Direction='Inbound';Action='Allow';EdgeTraversalPolicy='Block';LooseSourceMapping='False';LocalOnlyMapping='False';PolicyStoreSourceType='Local'}
|
||||
foreach($field in $fixed.Keys){if([string]$r.$field -cne $fixed[$field]){throw "Rule $field differs."}}
|
||||
if($r.Owner -or @($r.Platform|Where-Object {$_}).Count){throw 'Unexpected rule owner or platform restriction.'}
|
||||
if([string]$f.Port.Protocol -notin @('TCP','6') -or [string]$f.Port.LocalPort -ne '5985' -or [string]$f.Port.RemotePort -ne 'Any' -or [string]$f.Port.IcmpType -ne 'Any' -or [string]$f.Port.DynamicTarget -ne 'Any'){throw 'Port filter differs.'}
|
||||
$local=@($f.Address.LocalAddress|ForEach-Object {ConvertTo-WelaIngressAddress $_}|Sort-Object)
|
||||
$remote=@($f.Address.RemoteAddress|ForEach-Object {ConvertTo-WelaIngressAddress $_ -Remote -Observed}|Sort-Object)
|
||||
if(($local -join '|') -cne ($Selection.LocalAddresses -join '|') -or ($remote -join '|') -cne ($Selection.RemoteAddresses -join '|')){throw 'Address filters differ.'}
|
||||
foreach($pair in @(@('Application','Program'),@('Service','Service'),@('Interface','InterfaceAlias'),@('InterfaceType','InterfaceType'),@('Security','LocalUser'),@('Security','RemoteUser'),@('Security','RemoteMachine'))){if([string]$f[$pair[0]].($pair[1]) -ne 'Any'){throw "Unexpected $($pair -join '/') filter: '$($f[$pair[0]].($pair[1]))'."}}
|
||||
# Native Package is a nullable SID, unlike the Program 'Any' alias. A
|
||||
# present empty/null Package means no package restriction; missing is unknown.
|
||||
$package=$f.Application.PSObject.Properties['Package']
|
||||
if($null -eq $package -or ($null -ne $package.Value -and ($package.Value -isnot [string] -or $package.Value -cnotin @('','Any')))){throw 'Unexpected or missing Application/Package filter.'}
|
||||
if([string]$f.Security.Authentication -ne 'NotRequired' -or [string]$f.Security.Encryption -ne 'NotRequired' -or [string]$f.Security.OverrideBlockRules -ne 'False'){throw 'Security filter differs.'}
|
||||
}
|
||||
function Assert-WelaIngressPlan {
|
||||
param($Plan)
|
||||
Assert-WelaArrivalObject $Plan @('SchemaVersion','Kind','Selection','ContextKey','Sources','RecordedUtc')
|
||||
if(($Plan.SchemaVersion -isnot [int] -and $Plan.SchemaVersion -isnot [long]) -or $Plan.SchemaVersion -ne 1 -or $Plan.Kind -cne 'WelaCollectorIngressPlan' -or $Plan.ContextKey -isnot [string] -or $Plan.Sources -isnot [string]){throw 'Unknown ingress plan.'}
|
||||
Assert-WelaArrivalObject $Plan.Selection @('Name','LocalAddresses','RemoteAddresses')
|
||||
if($Plan.Selection.Name -isnot [string] -or $Plan.Selection.LocalAddresses -isnot [array] -or $Plan.Selection.RemoteAddresses -isnot [array]){throw 'Mistyped ingress selection.'}
|
||||
$null=Get-WelaIngressSelection $Plan.Selection.Name $Plan.Selection.LocalAddresses $Plan.Selection.RemoteAddresses
|
||||
$null=ConvertTo-WelaArrivalUtc $Plan.RecordedUtc
|
||||
}
|
||||
function Invoke-WelaWecIngress {
|
||||
param([ValidateSet('Plan','Apply')][string]$Action='Plan',[string]$Name,[string[]]$LocalAddress,[string[]]$RemoteAddress,[string]$PlanPath,[string]$PlanHash,[Parameter(Mandatory)][string]$OutputPath)
|
||||
if($Action -eq 'Plan'){
|
||||
if(-not $Name -or -not $LocalAddress -or -not $RemoteAddress -or $PlanPath -or $PlanHash){throw 'Plan requires a new rule name and explicit local/remote IPv4 scopes, without a prior plan.'}
|
||||
$selection=Get-WelaIngressSelection $Name $LocalAddress $RemoteAddress;$inputFile=$null
|
||||
}else{
|
||||
if(-not $PlanPath -or $PlanHash -cnotmatch '^[a-f0-9]{64}$' -or $Name -or $LocalAddress -or $RemoteAddress){throw 'Apply accepts only a reviewed plan path, SHA256 and new output.'}
|
||||
$inputFile=Read-WelaWecUpdateFile $PlanPath
|
||||
}
|
||||
$ErrorActionPreference='Stop'
|
||||
$sourcePath=if($inputFile){$inputFile.Path}else{Join-Path (Split-Path $PSScriptRoot -Parent) 'WELA.ps1'}
|
||||
$output=New-WelaArrivalOutput $OutputPath $sourcePath
|
||||
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaCollectorIngress';Action=$Action;Status='Refused';ExitCode=1;OutputPath=$output;PlanHash=$null;NativeCreateAttempted=$false;Artifacts=@();Diagnostic='';ReadyRuleCredit=0;Scope='One local Domain-profile TCP5985 IPv4 allow rule only. Other rules may allow broader access; no listener, service, authentication, subscription, packet delivery or Sigma proof. Sysmon excluded.'}
|
||||
try {
|
||||
$context=Get-WelaIngressContext;$key=$context|ConvertTo-Json -Depth 16 -Compress;$sources=Get-WelaIngressSources
|
||||
if($Action -eq 'Apply'){
|
||||
if($inputFile.Hash -cne $PlanHash){throw 'Reviewed plan hash differs.'}
|
||||
$plan=ConvertFrom-WelaArrivalJson $inputFile.Text;Assert-WelaIngressPlan $plan
|
||||
if($plan.ContextKey -cne $key -or $plan.Sources -cne $sources){throw 'Host, reader, firewall context or source code differs from reviewed plan.'}
|
||||
$selection=Get-WelaIngressSelection $plan.Selection.Name $plan.Selection.LocalAddresses $plan.Selection.RemoteAddresses;$report.PlanHash=$inputFile.Hash
|
||||
}
|
||||
foreach($address in $selection.LocalAddresses){if($address -cnotin $context.Addresses){throw 'Every local address must currently be assigned and Preferred.'}}
|
||||
Assert-WelaIngressAbsent $selection.Name
|
||||
if($Action -eq 'Plan'){
|
||||
$plan=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaCollectorIngressPlan';Selection=$selection;ContextKey=$key;Sources=$sources;RecordedUtc=[DateTime]::UtcNow.ToString('o')};Assert-WelaIngressPlan $plan
|
||||
if((Get-WelaIngressContext|ConvertTo-Json -Depth 16 -Compress) -cne $key -or (Get-WelaIngressSources) -cne $sources){throw 'Context or code drift during planning.'}
|
||||
Assert-WelaIngressAbsent $selection.Name
|
||||
$artifact=Write-WelaWecUpdateArtifact $output 'plan.json' ($plan|ConvertTo-Json -Depth 20);$report.Artifacts+=$artifact;$report.PlanHash=$artifact.Sha256;$report.Status='ReviewRequired';$report.ExitCode=0
|
||||
}else{
|
||||
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'reviewed-plan.json' $inputFile.Text
|
||||
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'before-create.json' ([ordered]@{Status='Pending';Selection=$selection;Context=$context;PlanHash=$PlanHash;RecordedUtc=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json -Depth 20)
|
||||
if((Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash -or (Get-WelaIngressSources) -cne $sources -or (Get-WelaIngressContext|ConvertTo-Json -Depth 16 -Compress) -cne $key){throw 'Plan, context or source drift immediately before creation.'}
|
||||
Assert-WelaIngressAbsent $selection.Name
|
||||
$report.NativeCreateAttempted=$true;New-WelaIngressNativeRule $selection
|
||||
foreach($store in @('PersistentStore','ActiveStore')){
|
||||
$observed=Read-WelaIngressRule $store $selection.Name
|
||||
$report.Artifacts+=Write-WelaWecUpdateArtifact $output ($store+'-after.json') ((ConvertTo-WelaIngressEvidence $observed)|ConvertTo-Json -Depth 8)
|
||||
Assert-WelaIngressReadback $observed $selection
|
||||
}
|
||||
if((Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash -or (Get-WelaIngressSources) -cne $sources -or (Get-WelaIngressContext|ConvertTo-Json -Depth 16 -Compress) -cne $key){throw 'Context, code or plan changed after creation.'}
|
||||
$report.Status='CreatedAndVerified';$report.ExitCode=0
|
||||
}
|
||||
}catch{$report.Status=if($report.NativeCreateAttempted){'CreateAttemptedUnverified'}else{'Refused'};$report.Diagnostic=$_.Exception.Message}
|
||||
$null=Write-WelaWecUpdateArtifact $output 'manifest.json' ($report|ConvertTo-Json -Depth 20)
|
||||
$report
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
# Reviewed, existing-only Enabled changes; runtime observations are separate evidence.
|
||||
function Initialize-WelaWecStateNative {
|
||||
$path=Join-Path $PSScriptRoot 'WecStateNative.cs';$hash=(Get-FileHash -LiteralPath $path -Algorithm SHA256 -ErrorAction Stop).Hash
|
||||
if(-not('Wela.WecState.Edit' -as [type])){Add-Type -Path $path -ErrorAction Stop;$script:WelaWecStateNativeHash=$hash}
|
||||
if($script:WelaWecStateNativeHash -cne $hash){throw 'Loaded native state setter differs from source; start a fresh process.'}
|
||||
}
|
||||
function Get-WelaWecStateContext {
|
||||
$context=Get-WelaWecUpdateContext
|
||||
Initialize-WelaWecStateNative
|
||||
$identity=[Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
try {$context.Reader=[ordered]@{Name=$identity.Name;Sid=$identity.User.Value;AuthenticationType=$identity.AuthenticationType;ImpersonationLevel=[string]$identity.ImpersonationLevel;Groups=@($identity.Groups.Value|Sort-Object);TokenStatistics=[Wela.WecState.Edit]::TokenKey($identity.Token)}}finally{$identity.Dispose()}
|
||||
$channel=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('ForwardedEvents')
|
||||
try {$context|Add-Member NoteProperty DestinationLog ([ordered]@{Name=$channel.LogName;Enabled=$channel.IsEnabled;Mode=[string]$channel.LogMode;MaximumBytes=$channel.MaximumSizeInBytes;Path=$channel.LogFilePath;SecurityDescriptor=$channel.SecurityDescriptor})}finally{$channel.Dispose()}
|
||||
$context
|
||||
}
|
||||
function Get-WelaWecStateReviewKey {
|
||||
param($Context)
|
||||
# Separate CLI invocations can hold different token objects in the same logon.
|
||||
# Bind plan/apply to the actual logon, and compare complete token statistics
|
||||
# within each operation to reject privilege or token changes during writes.
|
||||
$copy=$Context|ConvertTo-Json -Depth 16 -Compress|ConvertFrom-Json
|
||||
$copy.Reader.TokenStatistics=$Context.Reader.TokenStatistics.Substring(16,16)
|
||||
$copy|ConvertTo-Json -Depth 16 -Compress
|
||||
}
|
||||
function Get-WelaWecStateSources {
|
||||
$root=Split-Path $PSScriptRoot -Parent;$sources=[ordered]@{}
|
||||
foreach($name in @('scripts/WecState.ps1','scripts/WecStateNative.cs','scripts/WecUpdate.ps1','modules/WefSubscriptions.psm1','modules/WecSubscriptionXml.cs','scripts/Configuration.ps1','scripts/ControlApplicability.ps1','scripts/WefArrival.ps1','modules/AuditProfiles.psm1','scripts/WecRuntime.ps1','scripts/WecRuntimeNative.cs')){$sources[$name]=(Get-FileHash -LiteralPath (Join-Path $root $name) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
|
||||
$sources|ConvertTo-Json -Compress
|
||||
}
|
||||
function Get-WelaWecStateDefinition {
|
||||
param([string]$Xml,[string[]]$SourceSids)
|
||||
$model=ConvertFrom-WelaWefSubscription -Xml $Xml -SourceSids $SourceSids -Observed
|
||||
$doc=Read-WelaWefXml $Xml;$root=$doc.DocumentElement;$whole=Get-WelaWefXmlKey $root
|
||||
$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('s',$root.NamespaceURI)
|
||||
$null=$root.RemoveChild($root.SelectSingleNode('s:Enabled',$ns))
|
||||
[pscustomobject]@{Id=$model.Id;Xml=$Xml;WholeKey=$whole;PreservedKey=(Get-WelaWefXmlKey $root);Enabled=$model.Definition.Enabled;QueryKey=$model.Query.Key;Description=$model.Definition.Description;SourceAuthorization=$model.Definition.SourceAuthorization}
|
||||
}
|
||||
function Read-WelaWecStateDefinition {
|
||||
param([string]$Id,[string[]]$SourceSids)
|
||||
if($Id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$'){throw 'Invalid exact subscription ID.'}
|
||||
$definition=Get-WelaWecStateDefinition (Read-WelaWecSubscriptionXml $Id) $SourceSids
|
||||
if($definition.Id -cne $Id){throw 'Native subscription identity differs from the selected ID.'}
|
||||
$definition
|
||||
}
|
||||
function New-WelaWecStateEdit {
|
||||
param($Before)
|
||||
Initialize-WelaWecStateNative
|
||||
$edit=[Wela.WecState.Edit]::new($Before.Id)
|
||||
try {
|
||||
if($edit.OriginalEnabled -ne $Before.Enabled -or (ConvertFrom-WelaWefQuery $edit.OriginalQuery).Key -cne $Before.QueryKey -or $edit.OriginalDescription -cne $Before.Description -or $edit.OriginalAuthorization -cne $Before.SourceAuthorization){throw 'Native handle state differs from the reviewed definition.'}
|
||||
$edit
|
||||
}catch{$edit.Dispose();throw}
|
||||
}
|
||||
function Assert-WelaWecStatePlan {
|
||||
param($Plan)
|
||||
Assert-WelaArrivalObject $Plan @('SchemaVersion','Kind','Id','SourceSids','ContextKey','Sources','BeforeXml','DesiredEnabled','RecordedUtc')
|
||||
if(($Plan.SchemaVersion -isnot [int] -and $Plan.SchemaVersion -isnot [long]) -or $Plan.SchemaVersion -ne 1 -or $Plan.Kind -cne 'WelaWecStatePlan' -or $Plan.Id -isnot [string] -or $Plan.Id -cnotmatch '^[A-Za-z0-9][A-Za-z0-9 ._-]{0,127}$' -or $Plan.SourceSids -isnot [array] -or $Plan.ContextKey -isnot [string] -or $Plan.Sources -isnot [string] -or $Plan.BeforeXml -isnot [string] -or $Plan.DesiredEnabled -isnot [bool]){throw 'Unknown or mistyped state plan.'}
|
||||
$null=ConvertTo-WelaArrivalUtc $Plan.RecordedUtc
|
||||
foreach($sid in $Plan.SourceSids){if($sid -isnot [string]){throw 'Source SID must be a string.'}}
|
||||
$null=Get-WelaWefAuthorization $Plan.SourceSids
|
||||
$before=Get-WelaWecStateDefinition $Plan.BeforeXml $Plan.SourceSids
|
||||
if($before.Id -cne $Plan.Id){throw 'Plan identity contradicts its original subscription.'}
|
||||
}
|
||||
function Read-WelaWecStateRuntime {
|
||||
param([string]$Id)
|
||||
try {Get-WelaWecRuntime -Id $Id -MaximumSources 32}
|
||||
catch {[pscustomobject]@{Status='Unknown';Diagnostic=$_.Exception.Message;ReadyRuleCredit=0}}
|
||||
}
|
||||
function Assert-WelaWecStateArtifacts {
|
||||
param([string]$Root,$Artifacts)
|
||||
foreach($artifact in $Artifacts){if((Get-FileHash -LiteralPath (Join-Path $Root $artifact.Name) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant() -cne $artifact.Sha256){throw 'Saved state evidence changed before completion.'}}
|
||||
}
|
||||
function Invoke-WelaWecState {
|
||||
param([ValidateSet('Plan','Apply')][string]$Action='Plan',[string]$Id,[string[]]$SourceSids,[ValidateSet('Enabled','Disabled')][string]$State,[string]$PlanPath,[string]$PlanHash,[Parameter(Mandatory)][string]$OutputPath)
|
||||
$ErrorActionPreference='Stop'
|
||||
if($Action -eq 'Plan'){
|
||||
if(-not $Id -or -not $SourceSids -or -not $State -or $PlanPath -or $PlanHash){throw 'Plan requires exact ID, explicit source SIDs, Enabled or Disabled state and new output; no prior plan.'}
|
||||
$sourceInput=$null;$sourcePath=Join-Path (Split-Path $PSScriptRoot -Parent) 'scripts'
|
||||
}else{
|
||||
if(-not $PlanPath -or $PlanHash -cnotmatch '^[a-f0-9]{64}$' -or $Id -or $SourceSids -or $State){throw 'Apply accepts only a reviewed plan path, its SHA256 and new output.'}
|
||||
$sourceInput=Read-WelaWecUpdateFile $PlanPath;$sourcePath=$sourceInput.Path
|
||||
}
|
||||
$output=New-WelaArrivalOutput $OutputPath $sourcePath
|
||||
$report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaWecState';Action=$Action;Status='Refused';ExitCode=1;RecordedUtc=[DateTime]::UtcNow.ToString('o');OutputPath=$output;PlanHash=$null;BeforeEnabled=$null;DesiredEnabled=$null;NativeSaveAttempted=$false;NativeErrorCode=$null;After=$null;RuntimeBefore=$null;RuntimeAfter=$null;Artifacts=@();Diagnostic='';ReadyRuleCredit=0;Delivery='Not established';BookmarkContinuity='Not established';Scope='Only Enabled on one existing native source-initiated subscription. Disable interrupts collection; enable/save activates it. No listener, firewall, service or authorization changes. Sysmon excluded.'}
|
||||
$edit=$null;$plan=$null;$before=$null
|
||||
try {
|
||||
$context=Get-WelaWecStateContext;$contextKey=$context|ConvertTo-Json -Depth 16 -Compress;$sources=Get-WelaWecStateSources
|
||||
if($Action -eq 'Plan'){
|
||||
$before=Read-WelaWecStateDefinition $Id $SourceSids
|
||||
$plan=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaWecStatePlan';Id=$Id;SourceSids=@($SourceSids);ContextKey=(Get-WelaWecStateReviewKey $context);Sources=$sources;BeforeXml=$before.Xml;DesiredEnabled=($State -eq 'Enabled');RecordedUtc=[DateTime]::UtcNow.ToString('o')}
|
||||
Assert-WelaWecStatePlan $plan
|
||||
if($plan.DesiredEnabled -and -not $context.DestinationLog.Enabled){throw 'ForwardedEvents must already be enabled before planning activation; no channel changes are made.'}
|
||||
$report.RuntimeBefore=Read-WelaWecStateRuntime $Id
|
||||
if((Read-WelaWecStateDefinition $Id $SourceSids).WholeKey -cne $before.WholeKey -or ((Get-WelaWecStateContext|ConvertTo-Json -Depth 16 -Compress) -cne $contextKey) -or (Get-WelaWecStateSources) -cne $sources){throw 'Host, reader, implementation or subscription drift during planning.'}
|
||||
$planText=$plan|ConvertTo-Json -Depth 20
|
||||
if([Text.Encoding]::UTF8.GetByteCount($planText) -gt 4194304){throw 'Reviewed plan exceeds the four-MiB apply limit.'}
|
||||
$artifact=Write-WelaWecUpdateArtifact $output 'plan.json' $planText;$report.Artifacts+=$artifact;$report.PlanHash=$artifact.Sha256;$report.Status='ReviewRequired'
|
||||
}else{
|
||||
if($sourceInput.Hash -cne $PlanHash){throw 'Reviewed plan hash differs from the selected file bytes.'}
|
||||
$plan=ConvertFrom-WelaArrivalJson $sourceInput.Text;Assert-WelaWecStatePlan $plan;$report.PlanHash=$sourceInput.Hash
|
||||
if($plan.DesiredEnabled -and -not $context.DestinationLog.Enabled){throw 'ForwardedEvents must already be enabled before activation; no channel changes are made.'}
|
||||
if($plan.ContextKey -cne (Get-WelaWecStateReviewKey $context) -or $plan.Sources -cne $sources){throw 'Actual host/reader/token/service or implementation sources differ from the reviewed plan.'}
|
||||
$before=Get-WelaWecStateDefinition $plan.BeforeXml $plan.SourceSids
|
||||
$report.BeforeEnabled=$before.Enabled;$report.DesiredEnabled=$plan.DesiredEnabled
|
||||
$report.RuntimeBefore=Read-WelaWecStateRuntime $plan.Id
|
||||
if((Read-WelaWecStateDefinition $plan.Id $plan.SourceSids).WholeKey -cne $before.WholeKey){throw 'Current subscription differs from the reviewed complete definition.'}
|
||||
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'reviewed-plan.json' $sourceInput.Text
|
||||
if($before.Enabled -eq $plan.DesiredEnabled){$report.Status='AlreadyMatches'}else{
|
||||
$edit=New-WelaWecStateEdit $before
|
||||
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'before-save.json' ([ordered]@{Status='Pending';RecordedUtc=[DateTime]::UtcNow.ToString('o');Context=$context;BeforeXml=$before.Xml;DesiredEnabled=$plan.DesiredEnabled;PlanHash=$sourceInput.Hash}|ConvertTo-Json -Depth 20)
|
||||
Assert-WelaWecStateArtifacts $output $report.Artifacts
|
||||
if((Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash -or (Get-WelaWecStateSources) -cne $sources -or ((Get-WelaWecStateContext|ConvertTo-Json -Depth 16 -Compress) -cne $contextKey) -or (Read-WelaWecStateDefinition $plan.Id $plan.SourceSids).WholeKey -cne $before.WholeKey){throw 'Plan, code, context or complete subscription changed immediately before save.'}
|
||||
try {$edit.Save($plan.DesiredEnabled)}finally{$report.NativeSaveAttempted=[bool]$edit.SaveAttempted}
|
||||
$report.Status='SavedAwaitingReadback'
|
||||
}
|
||||
$report.RuntimeAfter=Read-WelaWecStateRuntime $plan.Id
|
||||
$after=Read-WelaWecStateDefinition $plan.Id $plan.SourceSids;$report.After=$after
|
||||
$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'after.xml' $after.Xml
|
||||
if($after.Enabled -ne $plan.DesiredEnabled -or $after.PreservedKey -cne $before.PreservedKey -or ((Get-WelaWecStateContext|ConvertTo-Json -Depth 16 -Compress) -cne $contextKey) -or (Get-WelaWecStateSources) -cne $sources -or (Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash){throw 'Readback, preserved configuration, context, plan or implementation differs after operation.'}
|
||||
if($report.NativeSaveAttempted){$report.Status='StateChangedAndVerified'}
|
||||
}
|
||||
$report.BeforeEnabled=$before.Enabled;$report.DesiredEnabled=$plan.DesiredEnabled
|
||||
Assert-WelaWecStateArtifacts $output $report.Artifacts
|
||||
$report.ExitCode=0
|
||||
}catch{
|
||||
$report.Status=if($report.NativeSaveAttempted){'SaveAttemptedUnverified'}else{'Refused'};$report.ExitCode=1;$report.Diagnostic=$_.Exception.Message
|
||||
$errorObject=$_.Exception
|
||||
while($errorObject){if($errorObject -is [ComponentModel.Win32Exception]){$report.NativeErrorCode=$errorObject.NativeErrorCode;break};$errorObject=$errorObject.InnerException}
|
||||
if($report.NativeSaveAttempted -and $plan){
|
||||
# A failed activation can still persist Enabled. Never imply rollback.
|
||||
$report.RuntimeAfter=Read-WelaWecStateRuntime $plan.Id
|
||||
try {$report.After=Read-WelaWecStateDefinition $plan.Id $plan.SourceSids;$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'failed-after.xml' $report.After.Xml}
|
||||
catch {$report.Diagnostic+=' Final definition unavailable: '+$_.Exception.Message}
|
||||
}
|
||||
}
|
||||
finally{if($edit){$edit.Dispose()}}
|
||||
$null=Write-WelaWecUpdateArtifact $output 'manifest.json' ($report|ConvertTo-Json -Depth 32)
|
||||
$report
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Existing-only native WEC Enabled setter. No create/delete or other setters.
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
namespace Wela.WecState {
|
||||
public sealed class Edit : IDisposable {
|
||||
[StructLayout(LayoutKind.Explicit, Size=16)] struct Variant {
|
||||
[FieldOffset(0)] public int Boolean; [FieldOffset(8)] public uint Count; [FieldOffset(12)] public uint Type;
|
||||
}
|
||||
[DllImport("wecapi.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern IntPtr EcOpenSubscription(string name,uint access,uint flags);
|
||||
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcGetSubscriptionProperty(IntPtr handle,int property,uint flags,uint size,IntPtr value,out uint used);
|
||||
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcSetSubscriptionProperty(IntPtr handle,int property,uint flags,ref Variant value);
|
||||
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcSaveSubscription(IntPtr handle,uint flags);
|
||||
[DllImport("wecapi.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool EcClose(IntPtr handle);
|
||||
[DllImport("advapi32.dll",SetLastError=true)] [return:MarshalAs(UnmanagedType.Bool)] static extern bool GetTokenInformation(IntPtr token,int information,IntPtr buffer,int size,out int used);
|
||||
IntPtr handle; readonly string name,oldQuery,oldDescription,oldAuthorization; readonly bool oldEnabled;
|
||||
public bool OriginalEnabled {get{return oldEnabled;}}
|
||||
public string OriginalQuery {get{return oldQuery;}}
|
||||
public string OriginalDescription {get{return oldDescription;}}
|
||||
public string OriginalAuthorization {get{return oldAuthorization;}}
|
||||
public bool SaveAttempted {get;private set;}
|
||||
// TOKEN_STATISTICS: TokenId, AuthenticationId and ModifiedId, plus token type.
|
||||
public static string TokenKey(IntPtr token) {
|
||||
IntPtr buffer=Marshal.AllocHGlobal(56);
|
||||
try {int used;if(!GetTokenInformation(token,10,buffer,56,out used))throw new Win32Exception(Marshal.GetLastWin32Error());if(used!=56)throw new InvalidOperationException("Unexpected TOKEN_STATISTICS size.");
|
||||
byte[] bytes=new byte[56];Marshal.Copy(buffer,bytes,0,bytes.Length);return BitConverter.ToString(bytes).Replace("-","");
|
||||
}finally{Marshal.FreeHGlobal(buffer);}
|
||||
}
|
||||
static object Read(IntPtr h,int property) {
|
||||
uint size=16;
|
||||
for(int attempt=0;attempt<3;attempt++) {
|
||||
IntPtr buffer=Marshal.AllocHGlobal((int)size);
|
||||
try {
|
||||
uint used;bool ok=EcGetSubscriptionProperty(h,property,0,size,buffer,out used);int error=Marshal.GetLastWin32Error();
|
||||
if(!ok){if(error!=122)throw new Win32Exception(error);if(used<=size||used>1048576)throw new InvalidOperationException("Invalid native property buffer size.");size=used;continue;}
|
||||
if(used<16||used>size)throw new InvalidOperationException("Invalid native property length.");
|
||||
int type=Marshal.ReadInt32(buffer,12);
|
||||
if(property==0){if(type!=1)throw new InvalidOperationException("Enabled is not a scalar Boolean.");int value=Marshal.ReadInt32(buffer);if(value!=0&&value!=1)throw new InvalidOperationException("Invalid native Boolean.");return value==1;}
|
||||
if(type==0&&property==6)return "";
|
||||
if(type!=4)throw new InvalidOperationException("Expected scalar native string.");
|
||||
IntPtr pointer=Marshal.ReadIntPtr(buffer);long offset=pointer.ToInt64()-buffer.ToInt64();
|
||||
if(pointer==IntPtr.Zero||offset<16||offset>used-2)throw new InvalidOperationException("Native string pointer is outside its buffer.");
|
||||
StringBuilder text=new StringBuilder();
|
||||
for(int i=0;i<524288&&offset+2L*i+2<=used;i++){char c=(char)(ushort)Marshal.ReadInt16(pointer,2*i);if(c==0)return text.ToString();text.Append(c);}
|
||||
throw new InvalidOperationException("Unterminated native string.");
|
||||
}finally{Marshal.FreeHGlobal(buffer);}
|
||||
}
|
||||
throw new InvalidOperationException("Native property changed repeatedly.");
|
||||
}
|
||||
void Check(IntPtr h) {
|
||||
if((bool)Read(h,0)!=oldEnabled||!String.Equals((string)Read(h,10),oldQuery,StringComparison.Ordinal)||!String.Equals((string)Read(h,6),oldDescription,StringComparison.Ordinal)||!String.Equals((string)Read(h,31),oldAuthorization,StringComparison.Ordinal))throw new InvalidOperationException("Native enabled/query/description/authorization changed since review.");
|
||||
}
|
||||
public Edit(string id) {
|
||||
if(String.IsNullOrWhiteSpace(id)||id.Length>128||id.IndexOf('\0')>=0)throw new ArgumentException("Invalid subscription ID.");
|
||||
name=id;handle=EcOpenSubscription(name,3,2);if(handle==IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try{oldEnabled=(bool)Read(handle,0);oldQuery=(string)Read(handle,10);oldDescription=(string)Read(handle,6);oldAuthorization=(string)Read(handle,31);}catch{Dispose();throw;}
|
||||
}
|
||||
public void Save(bool enabled) {
|
||||
if(handle==IntPtr.Zero)throw new ObjectDisposedException("Edit");
|
||||
if(SaveAttempted)throw new InvalidOperationException("A native edit may be saved only once.");
|
||||
if(enabled==oldEnabled)throw new InvalidOperationException("Idempotent state must not save or reactivate a subscription.");
|
||||
IntPtr fresh=EcOpenSubscription(name,1,2);if(fresh==IntPtr.Zero)throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
try{Check(fresh);}finally{EcClose(fresh);}
|
||||
Variant value=new Variant{Boolean=enabled?1:0,Count=0,Type=1};
|
||||
if(!EcSetSubscriptionProperty(handle,0,0,ref value))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
SaveAttempted=true;
|
||||
if(!EcSaveSubscription(handle,0))throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
public void Dispose(){if(handle!=IntPtr.Zero){EcClose(handle);handle=IntPtr.Zero;}}
|
||||
}
|
||||
}
|
||||
@@ -125,8 +125,8 @@ function Get-WelaWefCollectorPrerequisites {
|
||||
if ($rules.Count -ne 1) { throw 'Expected exactly one existing effective firewall rule.' }
|
||||
$rule=$rules[0]; $ports=@($rule | Get-NetFirewallPortFilter -ErrorAction Stop); $addresses=@($rule | Get-NetFirewallAddressFilter -ErrorAction Stop)
|
||||
$scopeMatches=$addresses.Count -eq 1 -and
|
||||
(@(Compare-Object @($Config.IngressLocalAddresses | Sort-Object -Unique) @($addresses[0].LocalAddress | Sort-Object -Unique)).Count -eq 0) -and
|
||||
(@(Compare-Object @($Config.IngressRemoteAddresses | Sort-Object -Unique) @($addresses[0].RemoteAddress | Sort-Object -Unique)).Count -eq 0)
|
||||
(Test-WelaWefFirewallAddressSet $Config.IngressLocalAddresses @($addresses[0].LocalAddress)) -and
|
||||
(Test-WelaWefFirewallAddressSet $Config.IngressRemoteAddresses @($addresses[0].RemoteAddress))
|
||||
$ok=[string]$rule.Enabled -eq 'True' -and [string]$rule.Direction -eq 'Inbound' -and [string]$rule.Action -eq 'Allow' -and [string]$rule.Profile -eq 'Domain' -and
|
||||
$ports.Count -eq 1 -and [string]$ports[0].Protocol -in @('TCP','6') -and [string]$ports[0].LocalPort -eq '5985' -and $scopeMatches
|
||||
$checks += [pscustomobject]@{ Name='Existing scoped domain ingress rule'; Verified=[bool]$ok; Evidence=@{ Rule=($rule | Select-Object Name,Enabled,Direction,Action,Profile,PolicyStoreSourceType,EnforcementStatus); Ports=$ports | Select-Object Protocol,LocalPort,RemotePort; Addresses=$addresses | Select-Object LocalAddress,RemoteAddress }; Diagnostic='Exact selected rule definition only; other rules, network reachability and effective packet acceptance are not established.' }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
$engine=(Get-Process -Id $PID).Path
|
||||
$script:checks=0
|
||||
function Check-Cli {
|
||||
param([string[]]$Arguments,[bool]$Success,[string]$Match)
|
||||
$old=$ErrorActionPreference;$ErrorActionPreference='Continue'
|
||||
try{$output=(& $engine -NoProfile -File (Join-Path $repo 'WELA.ps1') @Arguments 2>&1 | Out-String);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$old}
|
||||
if(($Success -and $code -ne 0) -or (-not $Success -and $code -eq 0) -or $output -notmatch $Match){throw "CLI guard failed: $($Arguments -join ' '), exit $code : $output"}
|
||||
$script:checks++
|
||||
}
|
||||
Check-Cli @('smb-runtime','-Help') $true 'smb-runtime'
|
||||
Check-Cli @('smb-runtime','-Profile','test','-Help') $false 'dedicated options'
|
||||
Check-Cli @('help','-SmbRuntimeAction','Activate') $false 'SmbRuntime options require'
|
||||
Check-Cli @('smb-runtime','-SmbAction','Configure','-Help') $false 'dedicated options'
|
||||
Check-Cli @('smb-runtime','-DryRun') $false 'DryRun is supported only'
|
||||
Check-Cli @('smb-runtime','-SmbRuntimeAction','Activate','-DryRun','-Help') $true 'smb-runtime'
|
||||
Check-Cli @('smb-runtime','-BackupPath','unused','-Help') $false 'dedicated options'
|
||||
Write-Host "PASS: $script:checks public SMB runtime CLI guards"
|
||||
# Expected child failures are assertions, not the enclosing Actions step result.
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,130 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$script:ScriptRoot=Split-Path $PSScriptRoot -Parent
|
||||
. (Join-Path $script:ScriptRoot 'scripts/Configuration.ps1')
|
||||
. (Join-Path $script:ScriptRoot 'scripts/SmbAuditing.ps1')
|
||||
. (Join-Path $script:ScriptRoot 'scripts/WefArrival.ps1')
|
||||
. (Join-Path $script:ScriptRoot 'scripts/SmbRuntimeActivation.ps1')
|
||||
$script:checks=0
|
||||
function Assert($Condition,[string]$Message){if(-not $Condition){throw "FAIL: $Message"};$script:checks++}
|
||||
function Reject([scriptblock]$Code,[string]$Message){$failed=$false;try{& $Code}catch{$failed=$true};Assert $failed $Message}
|
||||
Reject {Set-WelaSmbRuntimeFlag 'LanmanWorkstation/EnableInsecureGuestLogons'} 'security parameter refused by actual setter adapter'
|
||||
Reject {Set-WelaSmbRuntimeFlag 'LanmanServer/auditinsecureguestlogon'} 'mis-cased control refused'
|
||||
$nativeModuleBase=[IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
||||
$command=[pscustomobject]@{Name='Set-SmbServerConfiguration';ModuleName='SmbServerConfiguration';CommandType='Function';Module=[pscustomobject]@{ModuleBase=$nativeModuleBase};Parameters=@{}}
|
||||
foreach($definition in @(Get-WelaSmbAuditDefinitions | Where-Object Component -eq LanmanServer)){$command.Parameters[$definition.Name]=[pscustomobject]@{ParameterType=[bool]}}
|
||||
Assert-WelaSmbRuntimeCommand $command Server Set $nativeModuleBase
|
||||
Assert $true 'actual nested native CDXML module metadata accepted'
|
||||
$command.ModuleName='Other'
|
||||
Reject {Assert-WelaSmbRuntimeCommand $command Server Set $nativeModuleBase} 'foreign module refused'
|
||||
$command.ModuleName='SmbServerConfiguration'
|
||||
Reject {Assert-WelaSmbRuntimeCommand $command Server Set ($nativeModuleBase+'other')} 'unexpected module directory refused'
|
||||
$command.Parameters.AuditInsecureGuestLogon.ParameterType=[string]
|
||||
Reject {Assert-WelaSmbRuntimeCommand $command Server Set $nativeModuleBase} 'mistyped native parameter refused'
|
||||
$command.Parameters.Remove('AuditInsecureGuestLogon')
|
||||
Reject {Assert-WelaSmbRuntimeCommand $command Server Set $nativeModuleBase} 'missing native parameter refused'
|
||||
function FixtureConfiguration {
|
||||
param([string]$Side='Server')
|
||||
$component=if($Side -eq 'Server'){'LanmanServer'}else{'LanmanWorkstation'}
|
||||
$properties=@(Get-WelaSmbAuditDefinitions | Where-Object Component -eq $component | ForEach-Object {[pscustomobject]@{Name=$_.Name;Value=$false;CimType='Boolean'}})
|
||||
$properties+=[pscustomobject]@{Name='RequireSecuritySignature';Value=$true;CimType='Boolean'}
|
||||
[pscustomobject]@{CimClass=[pscustomobject]@{CimClassName="MSFT_Smb${Side}Configuration"};CimInstanceProperties=$properties}
|
||||
}
|
||||
$native=FixtureConfiguration
|
||||
$config=ConvertTo-WelaSmbRuntimeConfiguration $native Server
|
||||
Assert ($config.RequireSecuritySignature.Value -eq $true -and $config.AuditInsecureGuestLogon.Value -eq $false) 'native typed security and audit properties retained'
|
||||
$native.CimInstanceProperties[0].Value='False'
|
||||
Reject {ConvertTo-WelaSmbRuntimeConfiguration $native Server} 'string audit Boolean rejected'
|
||||
$native=FixtureConfiguration;$native.CimInstanceProperties[0].CimType='String'
|
||||
Reject {ConvertTo-WelaSmbRuntimeConfiguration $native Server} 'wrong native CIM type rejected'
|
||||
$native=FixtureConfiguration;$native.CimClass.CimClassName='MSFT_AnotherConfiguration'
|
||||
Reject {ConvertTo-WelaSmbRuntimeConfiguration $native Server} 'wrong native class rejected'
|
||||
$native=FixtureConfiguration;$native.CimInstanceProperties+=[pscustomobject]@{Name='Mystery';Value=[pscustomobject]@{a=1};CimType='Instance'}
|
||||
Reject {ConvertTo-WelaSmbRuntimeConfiguration $native Server} 'unknown unrelated configuration remains unverified'
|
||||
|
||||
$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-smb-activation-'+[guid]::NewGuid().ToString('N'))
|
||||
$null=New-Item -ItemType Directory -Path $root
|
||||
$script:receiptWriter=${function:Write-WelaSmbRuntimeReceipt}
|
||||
function Reset-Fixture {
|
||||
$policies=[ordered]@{}
|
||||
foreach($definition in Get-WelaSmbAuditDefinitions){$policies["$($definition.Component)/$($definition.Name)"]=[pscustomobject]@{Policy=[pscustomobject]@{KeyExists=$false;ValueExists=$false;Type=$null;Value=$null}}}
|
||||
$script:fixture=[pscustomobject][ordered]@{Computer='fixture';Host=[pscustomobject]@{Build=26100};Commands='native';Sources='hash';Policies=[pscustomobject]$policies;Configurations=[pscustomobject]@{Server=(ConvertTo-WelaSmbRuntimeConfiguration (FixtureConfiguration Server) Server);Client=(ConvertTo-WelaSmbRuntimeConfiguration (FixtureConfiguration Client) Client)}}
|
||||
$script:writes=0;$script:reads=0;$script:driftRead=0;$script:failWrite=0;$script:securityDrift=$false;$script:receiptFail=$false;$script:promptDrift=$false
|
||||
$script:out=Join-Path $root ([guid]::NewGuid().ToString('N'))
|
||||
}
|
||||
function Get-WelaSmbRuntimeState {
|
||||
$script:reads++
|
||||
if($script:reads -eq $script:driftRead){$script:fixture.Sources='changed'}
|
||||
Get-WelaSmbRuntimeKey $script:fixture | ConvertFrom-Json
|
||||
}
|
||||
function Write-WelaSmbRuntimeReceipt {
|
||||
param($Root,$Name,$Value)
|
||||
if($script:receiptFail -and $Name -eq '1-pending.json'){throw 'Injected durable-write failure'}
|
||||
& $script:receiptWriter $Root $Name $Value
|
||||
}
|
||||
function Set-WelaSmbRuntimeFlag {
|
||||
param($Id)
|
||||
$script:writes++
|
||||
Assert (Test-Path (Join-Path $script:out "$($script:writes)-pending.json")) 'pending receipt exists before setter'
|
||||
if($script:writes -eq $script:failWrite){throw 'Injected native setter failure'}
|
||||
$parts=$Id.Split('/');$side=if($parts[0] -eq 'LanmanServer'){'Server'}else{'Client'}
|
||||
$script:fixture.Configurations.$side.($parts[1]).Value=$true
|
||||
if($script:securityDrift){$script:fixture.Configurations.Server.RequireSecuritySignature.Value=$false}
|
||||
}
|
||||
function Read-Host {param($Prompt) if($script:promptDrift){$script:fixture.Sources='changed at prompt'};'y'}
|
||||
try {
|
||||
Reset-Fixture
|
||||
$plan=Invoke-WelaSmbRuntimeActivation
|
||||
Assert ($plan.Status -eq 'Planned' -and $plan.Controls.Count -eq 6 -and $script:writes -eq 0) 'default Plan is six read-only audit controls'
|
||||
Assert (-not (Test-Path $script:out)) 'Plan creates no evidence directory'
|
||||
$dry=Invoke-WelaSmbRuntimeActivation -Action Activate -DryRun -OutputPath $script:out
|
||||
Assert ($dry.Status -eq 'DryRun' -and $script:writes -eq 0 -and -not (Test-Path $script:out)) 'DryRun does not write'
|
||||
Reject {Invoke-WelaSmbRuntimeActivation -Action Plan -Auto} 'irrelevant Plan consent rejected'
|
||||
Reject {Invoke-WelaSmbRuntimeActivation -Action Plan -DryRun} 'invalid dry run action rejected'
|
||||
$id='LanmanServer/AuditInsecureGuestLogon'
|
||||
foreach($value in @(0,'1',2)) {
|
||||
Reset-Fixture;$script:fixture.Policies.$id.Policy=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Type='DWord';Value=$value}
|
||||
$report=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($report.ExitCode -eq 1 -and $script:writes -eq 0 -and -not (Test-Path $script:out)) 'conflicting or mistyped policy stops all mutations'
|
||||
}
|
||||
Reset-Fixture;$script:fixture.Policies.$id.Policy=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Type='String';Value=1}
|
||||
Assert ((Invoke-WelaSmbRuntimeActivation).ExitCode -eq 1) 'wrong registry kind blocks'
|
||||
Reset-Fixture;$script:fixture.Policies.$id.Policy=[pscustomobject]@{KeyExists=$true;ValueExists=$true;Type='DWord';Value=1}
|
||||
Assert ((Invoke-WelaSmbRuntimeActivation).ExitCode -eq 0) 'existing enabled policy is compatible'
|
||||
|
||||
Reset-Fixture
|
||||
$report=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($report.ExitCode -eq 0 -and $report.Status -eq 'RuntimeAuditingActive' -and $script:writes -eq 6) "six native activations succeed: $($report.Diagnostic)"
|
||||
Assert (@($report.Results | Where-Object Status -eq Activated).Count -eq 6) 'all six report confirmed activation'
|
||||
Assert ((Get-ChildItem -LiteralPath $script:out -File).Count -eq 14) 'plan, six pending, six confirmed, final result retained'
|
||||
Assert ($report.After.Configurations.Server.RequireSecuritySignature.Value -eq $true) 'security property preserved'
|
||||
Assert ($report.ReadyRuleCredit -eq 0 -and $report.EventGeneration -eq 'Not tested') 'activation grants no event or rule proof'
|
||||
$prior=Get-Content -Raw -LiteralPath (Join-Path $script:out 'result.json')
|
||||
$second=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($second.ExitCode -eq 1 -and (Get-Content -Raw -LiteralPath (Join-Path $script:out 'result.json')) -ceq $prior) 'existing evidence is never overwritten'
|
||||
$script:out=Join-Path $root ([guid]::NewGuid().ToString('N'));$script:writes=0
|
||||
$repeat=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($repeat.ExitCode -eq 0 -and $script:writes -eq 0 -and @($repeat.Results | Where-Object Status -eq AlreadyActive).Count -eq 6) 'idempotence requires no setters'
|
||||
|
||||
Reset-Fixture;$script:failWrite=2
|
||||
$partial=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($partial.ExitCode -eq 1 -and $script:writes -eq 2) 'partial native failure stops remaining writes'
|
||||
Assert ($partial.Results[0].Status -eq 'Activated' -and $partial.Results[1].Status -eq 'Failed' -and $partial.Results[2].Status -eq 'Skipped') 'partial outcomes preserved'
|
||||
Assert ((Test-Path (Join-Path $script:out '1-confirmed.json')) -and -not (Test-Path (Join-Path $script:out '2-confirmed.json'))) 'failed operation is never confirmed'
|
||||
foreach($read in @(2,3,20)) {
|
||||
Reset-Fixture;$script:driftRead=$read
|
||||
$drift=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($drift.ExitCode -eq 1) 'fresh/prewrite/final source drift fails closed'
|
||||
if($read -lt 4){Assert ($script:writes -eq 0) 'prewrite drift performs no setter'}
|
||||
}
|
||||
Reset-Fixture;$script:promptDrift=$true
|
||||
$drift=Invoke-WelaSmbRuntimeActivation -Action Activate -OutputPath $script:out
|
||||
Assert ($drift.ExitCode -eq 1 -and $script:writes -eq 0) 'prompt-time drift refused'
|
||||
Reset-Fixture;$script:securityDrift=$true
|
||||
$drift=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($drift.ExitCode -eq 1 -and $script:writes -eq 1 -and -not (Test-Path (Join-Path $script:out '1-confirmed.json'))) 'unrelated security delta prevents confirmation'
|
||||
Reset-Fixture;$script:receiptFail=$true
|
||||
$failed=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $script:out
|
||||
Assert ($failed.ExitCode -eq 1 -and $script:writes -eq 0) 'failed durable intent blocks setter'
|
||||
Assert (Test-Path (Join-Path $script:out 'result.json')) 'partial diagnostic survives pending-write failure'
|
||||
Write-Host "PASS: $script:checks SMB runtime activation assertions"
|
||||
}finally{Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Mutates only six audit flags on disposable GitHub-hosted Windows VMs. Never run on production.
|
||||
$ErrorActionPreference='Stop'
|
||||
if($env:OS -ne 'Windows_NT' -or $env:GITHUB_ACTIONS -ne 'true' -or $env:WELA_DISPOSABLE_SMB_ACTIVATION -ne 'true') {throw 'Explicit disposable GitHub Windows test opt-in is required.'}
|
||||
$script:ScriptRoot=Split-Path $PSScriptRoot -Parent
|
||||
. (Join-Path $script:ScriptRoot 'scripts/Configuration.ps1')
|
||||
. (Join-Path $script:ScriptRoot 'scripts/SmbAuditing.ps1')
|
||||
. (Join-Path $script:ScriptRoot 'scripts/WefArrival.ps1')
|
||||
. (Join-Path $script:ScriptRoot 'scripts/SmbRuntimeActivation.ps1')
|
||||
$computer=Get-CimInstance Win32_ComputerSystem
|
||||
$os=Get-CimInstance Win32_OperatingSystem
|
||||
if($computer.PartOfDomain -or $computer.DomainRole -ne 2 -or $os.ProductType -ne 3 -or [int]$os.BuildNumber -notin @(20348,26100)){throw 'Fixture requires an isolated member-class Server 2022/2025 host.'}
|
||||
$evidence=Join-Path $env:RUNNER_TEMP ('wela-smb-runtime-'+[guid]::NewGuid().ToString('N'))
|
||||
$null=New-Item -ItemType Directory -Path $evidence
|
||||
$reportPath=Join-Path $evidence 'activation'
|
||||
$cleanup=[ordered]@{Build=[int]$os.BuildNumber;Engine=$PSVersionTable.PSVersion.ToString();OriginalCaptured=$false;AuditFlagsRestored=$false;FullContextRestored=$false;NativeActivation=$false;UnsupportedRefusal=$false}
|
||||
$original=$null
|
||||
try {
|
||||
if([int]$os.BuildNumber -eq 20348) {
|
||||
$report=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath $reportPath
|
||||
if($report.ExitCode -ne 1 -or $report.Diagnostic -notlike '*NotApplicable*' -or (Test-Path $reportPath)){throw 'Server 2022 activation was not refused before writes.'}
|
||||
$report | ConvertTo-Json -Depth 24 | Set-Content -LiteralPath (Join-Path $evidence 'refusal.json') -Encoding UTF8
|
||||
$global:LASTEXITCODE=0
|
||||
$null=& (Join-Path $script:ScriptRoot 'WELA.ps1') smb-runtime -SmbRuntimeAction Activate -SmbRuntimeOutputPath $reportPath -Auto
|
||||
if($LASTEXITCODE -ne 1 -or (Test-Path $reportPath)){throw 'Public CLI did not refuse unsupported Server 2022.'}
|
||||
$cleanup.UnsupportedRefusal=$true
|
||||
Write-Host 'PASS: actual Server 2022 native and public-CLI refusal, no output or setters.'
|
||||
}else{
|
||||
$original=Get-WelaSmbRuntimeState
|
||||
if(@(Get-WelaSmbRuntimePlan $original | Where-Object Status -eq BlockedPolicy).Count){throw 'Fixture will not overwrite a conflicting policy.'}
|
||||
$cleanup.OriginalCaptured=$true
|
||||
$original | ConvertTo-Json -Depth 24 | Set-Content -LiteralPath (Join-Path $evidence 'original.json') -Encoding UTF8
|
||||
foreach($definition in Get-WelaSmbAuditDefinitions) {
|
||||
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
|
||||
$command="SmbShare\Set-Smb${side}Configuration"
|
||||
$parameters=@{Force=$true;Confirm=$false;ErrorAction='Stop'};$parameters[$definition.Name]=$false
|
||||
$null=& $command @parameters
|
||||
}
|
||||
$prepared=Get-WelaSmbRuntimeState
|
||||
$expected=Get-WelaSmbRuntimeKey $original | ConvertFrom-Json
|
||||
foreach($definition in Get-WelaSmbAuditDefinitions) {
|
||||
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
|
||||
$expected.Configurations.$side.($definition.Name).Value=$false
|
||||
}
|
||||
if((Get-WelaSmbRuntimeKey $prepared) -cne (Get-WelaSmbRuntimeKey $expected)){throw 'Fixture preparation changed other settings or did not make audit flags False.'}
|
||||
$dry=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -DryRun -OutputPath $reportPath
|
||||
if($dry.ExitCode -ne 0 -or (Test-Path $reportPath) -or (Get-WelaSmbRuntimeKey (Get-WelaSmbRuntimeState)) -cne (Get-WelaSmbRuntimeKey $prepared)){throw 'Native dry-run changed context or wrote output.'}
|
||||
$global:LASTEXITCODE=0
|
||||
$cli=@(& (Join-Path $script:ScriptRoot 'WELA.ps1') smb-runtime -SmbRuntimeAction Activate -SmbRuntimeOutputPath $reportPath -Auto)
|
||||
if($LASTEXITCODE -ne 0){throw "Public CLI exited $LASTEXITCODE"}
|
||||
$report=Get-Content -Raw -LiteralPath (Join-Path $reportPath 'result.json') | ConvertFrom-Json
|
||||
if($report.ExitCode -ne 0 -or $report.Status -ne 'RuntimeAuditingActive' -or @($report.Results | Where-Object Status -eq Activated).Count -ne 6){throw "Native six-flag activation failed: $($report.Diagnostic)"}
|
||||
$active=Get-WelaSmbRuntimeState
|
||||
foreach($definition in Get-WelaSmbAuditDefinitions) {
|
||||
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
|
||||
$expected.Configurations.$side.($definition.Name).Value=$true
|
||||
}
|
||||
if((Get-WelaSmbRuntimeKey $active) -cne (Get-WelaSmbRuntimeKey $expected)){throw 'Activation did not preserve every unrelated configuration field and policy tuple.'}
|
||||
$repeat=Invoke-WelaSmbRuntimeActivation -Action Activate -Auto -OutputPath (Join-Path $evidence 'idempotent')
|
||||
if($repeat.ExitCode -ne 0 -or @($repeat.Results | Where-Object Status -eq AlreadyActive).Count -ne 6){throw 'Native idempotence failed.'}
|
||||
if(@(Get-ChildItem -LiteralPath $repeat.OutputPath -Filter '*-pending.json').Count){throw 'Idempotent run unexpectedly journaled a setter.'}
|
||||
$cleanup.NativeActivation=$true
|
||||
Write-Host 'PASS: actual Server 2025 public-CLI activation of all six native Boolean audit flags, dry-run, idempotence and preservation of all unrelated native configuration.'
|
||||
}
|
||||
}finally{
|
||||
if($original) {
|
||||
$failures=@()
|
||||
foreach($definition in Get-WelaSmbAuditDefinitions) {
|
||||
try {
|
||||
$side=if($definition.Component -eq 'LanmanServer'){'Server'}else{'Client'}
|
||||
$command="SmbShare\Set-Smb${side}Configuration"
|
||||
$parameters=@{Force=$true;Confirm=$false;ErrorAction='Stop'};$parameters[$definition.Name]=[bool]$original.Configurations.$side.($definition.Name).Value
|
||||
$null=& $command @parameters
|
||||
}catch{$failures+=$_.Exception.Message}
|
||||
}
|
||||
$restored=Get-WelaSmbRuntimeState
|
||||
$restored | ConvertTo-Json -Depth 24 | Set-Content -LiteralPath (Join-Path $evidence 'restored.json') -Encoding UTF8
|
||||
$cleanup.AuditFlagsRestored=$failures.Count -eq 0
|
||||
$cleanup.FullContextRestored=(Get-WelaSmbRuntimeKey $restored) -ceq (Get-WelaSmbRuntimeKey $original)
|
||||
$cleanup | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $evidence 'acceptance.json') -Encoding UTF8
|
||||
if(-not $cleanup.AuditFlagsRestored -or -not $cleanup.FullContextRestored){throw "Native SMB fixture cleanup mismatch: $($failures -join '; ')"}
|
||||
Write-Host 'PASS: exact native audit flags and full configuration/policy/source context restored.'
|
||||
}else{$cleanup | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $evidence 'acceptance.json') -Encoding UTF8}
|
||||
Write-Host "Native SMB evidence: $evidence"
|
||||
}
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,23 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
$engine=(Get-Process -Id $PID).Path
|
||||
$cases=@(
|
||||
@{Args=@('transcription-recovery','-Help');Code=0;Pattern='Usage: transcription-recovery'},
|
||||
@{Args=@('transcription-recovery','-TranscriptRecoveryAction','Restore','-Help');Code=0;Pattern='TranscriptRecoveryPlanHash'},
|
||||
@{Args=@('transcription-recovery','-Profile','CisV4L2');Code=1;Pattern='dedicated options'},
|
||||
@{Args=@('help','-TranscriptRecoveryAction','Plan');Code=1;Pattern='require transcription-recovery'},
|
||||
@{Args=@('transcription-recovery','-TranscriptRecoveryAction','Restore','-TranscriptRecoveryPlanHash','bad');Code=1;Pattern='Restore consumes'},
|
||||
@{Args=@('transcription-recovery','-Auto');Code=1;Pattern='Plan takes'},
|
||||
@{Args=@('transcription-recovery','-TranscriptRecoveryAllowTemporarySuspension');Code=1;Pattern='Plan takes'},
|
||||
@{Args=@('transcription-recovery','-TranscriptRecoveryAction','Restore','-WhatIf');Code=1;Pattern='dedicated options'},
|
||||
@{Args=@('transcription-recovery','-TranscriptRecoveryAction','Restore','-DryRnu');Code=1;Pattern='dedicated options'}
|
||||
)
|
||||
foreach($case in $cases) {
|
||||
$prior=$ErrorActionPreference
|
||||
try{$ErrorActionPreference='Continue';$output=@(& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') @($case.Args) 2>&1);$code=$LASTEXITCODE}
|
||||
finally{$ErrorActionPreference=$prior}
|
||||
if($code -ne $case.Code -or ($output -join "`n") -notmatch $case.Pattern){throw "CLI failure: $($case.Args -join ' ') -> $code / $($output -join ' ')"}
|
||||
}
|
||||
Write-Host "Passed $($cases.Count) public transcription recovery CLI checks."
|
||||
# Expected child refusals must not become the enclosing Actions step result.
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,15 @@
|
||||
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent
|
||||
$engine=(Get-Process -Id $PID).Path;$count=0
|
||||
$cases=@(
|
||||
@{Args=@('wec-ingress','-Help');Code=0;Pattern='TCP5985'},
|
||||
@{Args=@('configure','-WecIngressAction','Apply','-Auto');Code=1;Pattern='require wec-ingress'},
|
||||
@{Args=@('wec-ingress','-Help','-Profile','wela-2.2.0');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-ingress','-Help','-WefAction','Configure');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-ingress','-Help','-Auto');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-ingress','-Help','-DryRun');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-ingress','-WecIngressAction','Apply','-WecIngressOutputPath','not-created');Code=1;Pattern='reviewed plan'},
|
||||
@{Args=@('wec-ingress','-WecIngressOutputPath','not-created');Code=1;Pattern='Plan requires'}
|
||||
)
|
||||
foreach($case in $cases){$prior=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$output=@(&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" @($case.Args) 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior};if(($case.Code -eq 0 -and $code -ne 0) -or ($case.Code -ne 0 -and $code -eq 0) -or ($output -join "`n") -notmatch $case.Pattern){throw "CLI failure: $($case.Args -join ' ') -> $code / $($output -join ' ')"};$count++}
|
||||
Write-Host "WEC ingress CLI: $count checks passed."
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,62 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module "$repo/modules/AuditProfiles.psm1" -Force
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/WecUpdate.ps1"
|
||||
. "$repo/scripts/WecIngress.ps1"
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Reject([scriptblock]$Action,[string]$Pattern){$message='';try{&$Action|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern; got $message"}
|
||||
$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-ingress-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
|
||||
$script:mode='ok';$script:reads=0;$script:creates=0;$script:exists=$false
|
||||
function Get-WelaIngressContext {[pscustomobject][ordered]@{Computer='TEST';Addresses=@('10.10.10.10');Reader='LOGON'}}
|
||||
function Assert-WelaIngressAbsent {param($Name);$script:reads++;if($script:exists -or ($script:mode -eq 'race' -and $script:reads -eq 2)){throw 'already exists'}}
|
||||
function New-WelaIngressNativeRule {param($Selection);Assert (Test-Path $script:journal) 'Pending receipt precedes creation';$script:creates++;if($script:mode -eq 'failure'){throw 'native failure'};$script:exists=$true}
|
||||
function Read-WelaIngressRule {
|
||||
param($Store,$Name)
|
||||
$r=[pscustomobject]@{Name=$Name;DisplayName=$Name;Description='WELA reviewed collector ingress; TCP 5985, Domain profile, explicit IPv4 scopes.';Group='WELA reviewed collector ingress';Enabled='True';Profile='Domain';Direction='Inbound';Action='Allow';EdgeTraversalPolicy='Block';LooseSourceMapping=$false;LocalOnlyMapping=$false;PolicyStoreSourceType='Local';Owner='';Platform=@()}
|
||||
$f=[ordered]@{Port=[pscustomobject]@{Protocol='TCP';LocalPort='5985';RemotePort='Any';IcmpType='Any';DynamicTarget='Any'};Address=[pscustomobject]@{LocalAddress=@('10.10.10.10');RemoteAddress=@('192.0.2.0/255.255.255.0')};Application=[pscustomobject]@{Program='Any';Package='Any'};Service=[pscustomobject]@{Service='Any'};Interface=[pscustomobject]@{InterfaceAlias='Any'};InterfaceType=[pscustomobject]@{InterfaceType='Any'};Security=[pscustomobject]@{Authentication='NotRequired';Encryption='NotRequired';OverrideBlockRules=$false;LocalUser='Any';RemoteUser='Any';RemoteMachine='Any'}}
|
||||
if($script:mode -eq 'broader'){$f.Address.RemoteAddress=@('Any')}
|
||||
if($script:mode -eq 'wrong-port'){$f.Port.LocalPort='Any'}
|
||||
if($script:mode -eq 'wrong-store' -and $Store -eq 'ActiveStore'){$r.PolicyStoreSourceType='GroupPolicy'}
|
||||
[pscustomobject]@{Store=$Store;Rule=$r;Filters=$f}
|
||||
}
|
||||
try {
|
||||
foreach($bad in @('Any','10.1','010.0.0.1','127.0.0.1','0.0.0.0','224.0.0.1','255.255.255.255','10.0.0.1/24','10.0.0.0/16','192.0.2.0/33','192.0.2.0/024','192.0.2.0/255.255.255.0','example.org','192.0.2.1-192.0.2.4','::1')){Reject {ConvertTo-WelaIngressAddress $bad -Remote} '.'}
|
||||
Assert ((ConvertTo-WelaIngressAddress '192.0.2.0/255.255.255.0' -Remote -Observed) -eq '192.0.2.0/24') 'Observed mask canonicalized'
|
||||
Reject {ConvertTo-WelaIngressAddress '192.0.2.0/255.0.255.0' -Remote -Observed} 'Noncontiguous'
|
||||
Reject {Get-WelaIngressSelection 'WELA-WEC-Test' @('10.10.10.10') @('192.0.2.1','192.0.2.1/32')} 'Duplicate'
|
||||
Reject {Get-WelaIngressSelection '*' @('10.10.10.10') @('192.0.2.1')} 'name'
|
||||
$selection=Get-WelaIngressSelection 'WELA-WEC-Test' @('10.10.10.10') @('192.0.2.0/24')
|
||||
$observation=Read-WelaIngressRule PersistentStore $selection.Name;Assert-WelaIngressReadback $observation $selection
|
||||
foreach($package in @($null,'')){$observation.Filters.Application.Package=$package;Assert-WelaIngressReadback $observation $selection;$count++}
|
||||
$observation.Filters.Application.Package='S-1-15-2-1';Reject {Assert-WelaIngressReadback $observation $selection} 'Package'
|
||||
$observation.Filters.Application.PSObject.Properties.Remove('Package');Reject {Assert-WelaIngressReadback $observation $selection} 'Package'
|
||||
$observation.Filters.Application|Add-Member NoteProperty Package 'Any'
|
||||
$evidence=ConvertTo-WelaIngressEvidence $observation;Assert ($evidence.Application.Package.Present -and $evidence.Application.Package.Value -eq 'Any') 'Evidence preserves explicit inspected fields'
|
||||
foreach($field in @('Enabled','Direction','Profile','Action','EdgeTraversalPolicy','LooseSourceMapping','LocalOnlyMapping','PolicyStoreSourceType','Description','Group','DisplayName','Name')){
|
||||
$saved=$observation.Rule.$field;$observation.Rule.$field='unexpected';Reject {Assert-WelaIngressReadback $observation $selection} 'differs';$observation.Rule.$field=$saved
|
||||
}
|
||||
foreach($scenario in @('ok','hash','context','duplicate','race','failure','broader','wrong-port','wrong-store','unassigned','replay')){
|
||||
$script:mode='ok';$script:reads=0;$script:creates=0;$script:exists=$false
|
||||
$result=Invoke-WelaWecIngress Plan -Name $selection.Name -LocalAddress $selection.LocalAddresses -RemoteAddress $selection.RemoteAddresses -OutputPath (Join-Path $root ($scenario+'-plan'))
|
||||
Assert ($result.Status -eq 'ReviewRequired' -and $script:creates -eq 0) "Read-only plan: $($result.Diagnostic)"
|
||||
$path=Join-Path $result.OutputPath 'plan.json';$hash=$result.PlanHash
|
||||
if($scenario -eq 'hash'){$hash='b'*64}
|
||||
if($scenario -in @('context','duplicate','unassigned')){
|
||||
$text=[IO.File]::ReadAllText($path)
|
||||
if($scenario -eq 'context'){$text=$text.Replace('TEST','OTHER')}
|
||||
if($scenario -eq 'duplicate'){$text=$text.Replace('"SchemaVersion":','"SchemaVersion":1,"SchemaVersion":')}
|
||||
if($scenario -eq 'unassigned'){$text=$text.Replace('"10.10.10.10"','"10.10.10.11"')}
|
||||
[IO.File]::WriteAllText($path,$text);$hash=(Get-FileHash $path).Hash.ToLowerInvariant()
|
||||
}
|
||||
$script:mode=$scenario;$script:reads=0;$out=Join-Path $root ($scenario+'-apply');$script:journal=Join-Path $out 'before-create.json'
|
||||
$applied=Invoke-WelaWecIngress Apply -PlanPath $path -PlanHash $hash -OutputPath $out
|
||||
Assert (($applied.ExitCode -eq 0) -eq ($scenario -in @('ok','replay'))) "Scenario $scenario : $($applied.Diagnostic)"
|
||||
Assert ($applied.ReadyRuleCredit -eq 0 -and (Test-Path (Join-Path $out 'manifest.json'))) 'No detection credit; durable result'
|
||||
if($scenario -in @('hash','context','duplicate','race','unassigned')){Assert ($script:creates -eq 0) 'Refused before mutation'}
|
||||
if($scenario -in @('failure','broader','wrong-port','wrong-store')){Assert ($applied.Status -eq 'CreateAttemptedUnverified' -and $script:creates -eq 1) 'Unverified possible creation retained'}
|
||||
if($scenario -eq 'replay'){$again=Invoke-WelaWecIngress Apply -PlanPath $path -PlanHash $hash -OutputPath (Join-Path $root 'replay-again');Assert ($again.Status -eq 'Refused' -and $script:creates -eq 1) 'Existing rule never overwritten'}
|
||||
}
|
||||
}finally{Remove-Item $root -Recurse -Force}
|
||||
Write-Host "WEC ingress tests passed: $count assertions."
|
||||
@@ -0,0 +1,75 @@
|
||||
param([switch]$AllowDisposableFirewallRule)
|
||||
$ErrorActionPreference='Stop'
|
||||
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not $AllowDisposableFirewallRule -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit disposable GitHub-hosted Windows opt-in required.'}
|
||||
$repo=Split-Path $PSScriptRoot -Parent;$script:ScriptRoot=$repo
|
||||
Import-Module "$repo/modules/AuditProfiles.psm1" -Force
|
||||
Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
|
||||
. "$repo/scripts/WefDeployment.ps1"
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/WecUpdate.ps1"
|
||||
. "$repo/scripts/ChannelRead.ps1"
|
||||
. "$repo/scripts/WecIngress.ps1"
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Key($Value){ConvertTo-Json -InputObject $Value -Depth 16 -Compress}
|
||||
function RulesKey {param([string]$Exclude);Key @(Read-WelaIngressRules PersistentStore|Where-Object Name -ne $Exclude|Sort-Object Name|Select-Object Name,DisplayName,Description,Group,Enabled,Profile,Direction,Action,EdgeTraversalPolicy,LooseSourceMapping,LocalOnlyMapping,Owner)}
|
||||
$engine=(Get-Process -Id $PID).Path
|
||||
function Invoke-IngressFixtureCli {param([string[]]$Arguments,[int]$Expected=0)
|
||||
$old=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$lines=@(&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" @Arguments 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$old}
|
||||
if(($Expected -eq 0 -and $code -ne 0) -or ($Expected -ne 0 -and $code -eq 0)){throw "CLI $code : $($lines -join ' ')"}
|
||||
}
|
||||
$context=Get-WelaIngressContext;$contextKey=Key $context;$beforeRules=RulesKey ''
|
||||
$local=@($context.Addresses|Where-Object {$_ -notlike '127.*' -and $_ -notlike '169.254.*'})[0]
|
||||
if(-not $local){throw 'An assigned nonloopback IPv4 address is required.'}
|
||||
$name='WELA-WEC-Test-'+[guid]::NewGuid().ToString('N');$selection=Get-WelaIngressSelection $name @($local) @('192.0.2.0/24')
|
||||
$root=Join-Path $env:RUNNER_TEMP ('wela-ingress-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
|
||||
$primary=$null;$attempted=$false
|
||||
try {
|
||||
Assert-WelaIngressAbsent $name
|
||||
Invoke-IngressFixtureCli @('wec-ingress','-WecIngressName',$name,'-WecIngressLocalAddress',$local,'-WecIngressRemoteAddress','192.0.2.0/24','-WecIngressOutputPath',"$root/plan")
|
||||
$plan=Get-Content "$root/plan/manifest.json" -Raw|ConvertFrom-Json
|
||||
Assert ($plan.Status -eq 'ReviewRequired' -and -not $plan.NativeCreateAttempted) 'Public Plan is read only'
|
||||
Assert ((RulesKey '') -ceq $beforeRules) 'Planning preserves persistent rule inventory and properties'
|
||||
$attempted=$true
|
||||
Invoke-IngressFixtureCli @('wec-ingress','-WecIngressAction','Apply','-WecIngressPlanPath',"$root/plan/plan.json",'-WecIngressPlanHash',$plan.PlanHash,'-WecIngressOutputPath',"$root/apply")
|
||||
$apply=Get-Content "$root/apply/manifest.json" -Raw|ConvertFrom-Json
|
||||
Assert ($apply.Status -eq 'CreatedAndVerified' -and $apply.NativeCreateAttempted -and $apply.ReadyRuleCredit -eq 0) 'Actual public creation and readback'
|
||||
foreach($store in @('PersistentStore','ActiveStore')){Assert-WelaIngressReadback (Read-WelaIngressRule $store $name) $selection;$count++}
|
||||
Assert ((RulesKey $name) -ceq $beforeRules) 'Other persistent rule properties preserved'
|
||||
# Exercise the real existing collector prerequisite against the new native rule.
|
||||
# Other prerequisites may be unmet on this standalone fixture; inspect only ingress.
|
||||
$collectorConfig=[pscustomobject]@{CollectorFqdn=(Get-WelaWefHost).Fqdn;ListenerAddress='*';IngressRuleName=$name;IngressLocalAddresses=@($local);IngressRemoteAddresses=@('192.0.2.0/24')}
|
||||
$collectorChecks=@(Get-WelaWefCollectorPrerequisites $collectorConfig)
|
||||
$ingress=@($collectorChecks|Where-Object Name -eq 'Existing scoped domain ingress rule')
|
||||
Assert ($ingress.Count -eq 1 -and $ingress[0].Verified) 'Existing collector prerequisite accepts the same reviewed CIDR after native dotted-netmask readback'
|
||||
$null=Write-WelaWecUpdateArtifact $root 'collector-ingress-check.json' ($ingress[0]|ConvertTo-Json -Depth 8)
|
||||
$collectorConfig.IngressRemoteAddresses=@('192.0.2.0/25')
|
||||
$mismatch=@(Get-WelaWefCollectorPrerequisites $collectorConfig|Where-Object Name -eq 'Existing scoped domain ingress rule')
|
||||
Assert ($mismatch.Count -eq 1 -and -not $mismatch[0].Verified) 'Collector prerequisite refuses a genuinely different approved network'
|
||||
$null=Write-WelaWecUpdateArtifact $root 'collector-ingress-mismatch.json' ($mismatch[0]|ConvertTo-Json -Depth 8)
|
||||
# Native New must not replace an existing name, even if a creator races our last absence check.
|
||||
$collision=$false;try{New-WelaIngressNativeRule $selection}catch{$collision=$true}
|
||||
Assert $collision 'Native duplicate-name creation refuses replacement'
|
||||
Assert-WelaIngressReadback (Read-WelaIngressRule PersistentStore $name) $selection
|
||||
Invoke-IngressFixtureCli @('wec-ingress','-WecIngressAction','Apply','-WecIngressPlanPath',"$root/plan/plan.json",'-WecIngressPlanHash',$plan.PlanHash,'-WecIngressOutputPath',"$root/replay") 1
|
||||
$replay=Get-Content "$root/replay/manifest.json" -Raw|ConvertFrom-Json
|
||||
Assert ($replay.Status -eq 'Refused' -and -not $replay.NativeCreateAttempted) 'Plan replay refuses an existing rule'
|
||||
Assert ((Key (Get-WelaIngressContext)) -ceq $contextKey) 'Profiles, services, host and address context unchanged'
|
||||
Write-Host "Native WEC ingress passed $count assertions on $([Environment]::OSVersion.Version), PowerShell $($PSVersionTable.PSVersion). No listener or traffic created."
|
||||
}catch{
|
||||
$primary=$_
|
||||
foreach($store in @('PersistentStore','ActiveStore')){try{$snapshot=ConvertTo-WelaIngressEvidence (Read-WelaIngressRule $store $name);Write-Host ($snapshot|ConvertTo-Json -Depth 8)}catch{Write-Host "Diagnostic read $store : $($_.Exception.Message)"}}
|
||||
}
|
||||
finally {
|
||||
$errors=@()
|
||||
try {
|
||||
$owned=@(Read-WelaIngressRules PersistentStore|Where-Object Name -eq $name)
|
||||
if($owned.Count){if(-not $attempted -or $owned.Count -ne 1 -or $owned[0].Group -cne 'WELA reviewed collector ingress' -or $owned[0].DisplayName -cne $name){throw 'Fixture ownership is ambiguous; refusing removal.'};$owned[0]|NetSecurity\Remove-NetFirewallRule -ErrorAction Stop}
|
||||
Assert-WelaIngressAbsent $name
|
||||
if((RulesKey '') -cne $beforeRules -or (Key (Get-WelaIngressContext)) -cne $contextKey){throw 'Original rule inventory, service or profile state differs after cleanup.'}
|
||||
}catch{$errors+=$_.Exception.Message}
|
||||
if($errors.Count){throw "Fixture cleanup failed: $($errors -join '; '); primary: $primary; evidence: $root"}
|
||||
Write-Host 'Owned rule removed; original persistent rules, firewall profiles and services preserved.'
|
||||
}
|
||||
if($primary){throw $primary}
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,17 @@
|
||||
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent
|
||||
$engine=(Get-Process -Id $PID).Path;$count=0
|
||||
$cases=@(
|
||||
@{Args=@('wec-state','-Help');Code=0;Pattern='Disable interrupts'},
|
||||
@{Args=@('configure','-WecStateAction','Apply','-Auto');Code=1;Pattern='require wec-state'},
|
||||
@{Args=@('wec-state','-Help','-Profile','wela-2.2.0');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-state','-Help','-WefAction','Configure');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-state','-Help','-WecUpdateAction','Apply');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-state','-Help','-Auto');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-state','-Help','-DryRun');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-state','-Help','-ResultsPath','not-created');Code=1;Pattern='only dedicated'},
|
||||
@{Args=@('wec-state','-WecStateAction','Apply','-WecStateOutputPath','not-created');Code=1;Pattern='reviewed plan'},
|
||||
@{Args=@('wec-state','-WecStateOutputPath','not-created');Code=1;Pattern='Plan requires'}
|
||||
)
|
||||
foreach($case in $cases){$prior=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$output=@(&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" @($case.Args) 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior};if(($case.Code -eq 0 -and $code -ne 0) -or ($case.Code -ne 0 -and $code -eq 0) -or ($output -join "`n") -notmatch $case.Pattern){throw "CLI failure: $($case.Args -join ' ') -> $code / $($output -join ' ')"};$count++}
|
||||
Write-Host "WEC state CLI: $count checks passed."
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,114 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
|
||||
Import-Module "$repo/modules/AuditProfiles.psm1" -Force
|
||||
. "$repo/scripts/Configuration.ps1"
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/WecUpdate.ps1"
|
||||
. "$repo/scripts/WecState.ps1"
|
||||
Initialize-WelaWecStateNative
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Reject([scriptblock]$Action,[string]$Pattern){$message='';try{&$Action|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern; got $message"}
|
||||
$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-wec-state-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
|
||||
$sid='S-1-5-21-11-22-33-1001';$id='WELA Native Security Example'
|
||||
$base=[IO.File]::ReadAllText("$repo/config/wef-examples/native-security.xml").Replace('<Enabled>true</Enabled>','<Enabled>false</Enabled>').Replace('<AllowedSourceDomainComputers></AllowedSourceDomainComputers>',('<AllowedSourceDomainComputers>'+(Get-WelaWefAuthorization @($sid))+'</AllowedSourceDomainComputers>'))
|
||||
$script:xml=$base;$script:saves=0;$script:reads=0;$script:contextReads=0;$script:mode='ok';$script:journal=''
|
||||
function Get-WelaWecStateContext {
|
||||
$script:contextReads++;$token='11'*56
|
||||
if($script:mode -eq 'token-drift' -and $script:contextReads -gt 1){$token='22'*56}
|
||||
[pscustomobject][ordered]@{Computer='TEST';HostKey='20348';Reader=[pscustomobject]@{Sid='S-1-5-21-1-2-3-1000';TokenStatistics=$token};Service='Running';DestinationLog=[pscustomobject]@{Enabled=($script:mode -ne 'disabled-destination')}}
|
||||
}
|
||||
function Read-WelaWecStateDefinition {
|
||||
param($Id,$SourceSids)
|
||||
$script:reads++
|
||||
if($script:mode -eq 'drift' -and $script:reads -eq 2){$script:xml=$script:xml.Replace('MinLatency','Normal')}
|
||||
if($script:mode -eq 'denied'){throw 'Native access denied'}
|
||||
Get-WelaWecStateDefinition $script:xml $SourceSids
|
||||
}
|
||||
function Read-WelaWecStateRuntime {param($Id);[pscustomobject]@{Status='Unknown';Diagnostic='Runtime unavailable';ReadyRuleCredit=0}}
|
||||
function New-WelaWecStateEdit {
|
||||
param($Before)
|
||||
$edit=[pscustomobject]@{SaveAttempted=$false}
|
||||
$edit|Add-Member ScriptMethod Save {param($Enabled)
|
||||
Assert (Test-Path -LiteralPath $script:journal) 'Durable pending record precedes native save'
|
||||
$pending=ConvertFrom-WelaArrivalJson ([IO.File]::ReadAllText($script:journal))
|
||||
Assert ($pending.Status -ceq 'Pending' -and $pending.DesiredEnabled -eq $Enabled) 'Pending receipt names exact desired state'
|
||||
if($script:mode -eq 'native-refusal'){throw 'Native view changed before save'}
|
||||
$this.SaveAttempted=$true;$script:saves++
|
||||
if($script:mode -eq 'failure'){throw 'native save failed'}
|
||||
if($script:mode -eq 'false-success'){return}
|
||||
$doc=Read-WelaWefXml $script:xml;$doc.Subscription.Enabled=$Enabled.ToString().ToLowerInvariant()
|
||||
if($script:mode -eq 'preservation'){$doc.Subscription.ReadExistingEvents='true'}
|
||||
$script:xml=$doc.OuterXml
|
||||
if($script:mode -eq 'evidence-tamper'){[IO.File]::AppendAllText($script:journal,' ')}
|
||||
}
|
||||
$edit|Add-Member ScriptMethod Dispose {}
|
||||
$edit
|
||||
}
|
||||
try {
|
||||
$before=Get-WelaWecStateDefinition $base @($sid)
|
||||
Assert (-not $before.Enabled -and $before.Id -ceq $id) 'Disabled original parsed'
|
||||
$enabled=Get-WelaWecStateDefinition ($base.Replace('<Enabled>false','<Enabled>true')) @($sid)
|
||||
Assert ($enabled.Enabled -and $enabled.PreservedKey -ceq $before.PreservedKey -and $enabled.WholeKey -cne $before.WholeKey) 'Only Enabled excluded from preservation comparison'
|
||||
Reject {Get-WelaWecStateDefinition $base @('S-1-1-0')} 'SID'
|
||||
Reject {Get-WelaWecStateDefinition ($base.Replace('Path="Security"','Path="Microsoft-Windows-Sysmon/Operational"')) @($sid)} 'Sysmon'
|
||||
Reject {Get-WelaWecStateDefinition ($base.Replace('SourceInitiated','CollectorInitiated')) @($sid)} 'source-initiated'
|
||||
Reject {Get-WelaWecStateDefinition ($base.Replace('<Enabled>false</Enabled>','<Enabled>false</Enabled><Enabled>true</Enabled>')) @($sid)} 'duplicate'
|
||||
Reject {Invoke-WelaWecState -Id $id -SourceSids @($sid) -OutputPath (Join-Path $root 'invalid')} 'Plan requires'
|
||||
Reject {Invoke-WelaWecState -Action Apply -PlanPath missing -PlanHash ('a'*64) -State Enabled -OutputPath (Join-Path $root 'invalid')} 'only'
|
||||
foreach($scenario in @('ok','drift','token-drift','failure','false-success','preservation','hash','stale','context','duplicate-key','wrong-type','source-hash','denied','native-refusal','evidence-tamper')){
|
||||
$script:xml=$base;$script:mode='ok';$script:reads=0;$script:contextReads=0;$script:saves=0
|
||||
$planResult=Invoke-WelaWecState -Id $id -SourceSids @($sid) -State Enabled -OutputPath (Join-Path $root ($scenario+'-plan'))
|
||||
Assert ($planResult.ExitCode -eq 0 -and $planResult.Status -eq 'ReviewRequired') "Plan created: $($planResult.Diagnostic)"
|
||||
Assert ($script:saves -eq 0 -and -not $planResult.BeforeEnabled -and $planResult.DesiredEnabled) 'Plan is read only and states exact transition'
|
||||
$planPath=Join-Path $planResult.OutputPath 'plan.json';$hash=$planResult.PlanHash
|
||||
$script:mode=$scenario;$script:reads=0;$script:contextReads=0
|
||||
if($scenario -eq 'hash'){$hash='b'*64}
|
||||
if($scenario -eq 'stale'){$script:xml=$base.Replace('MinLatency','Normal')}
|
||||
if($scenario -in @('context','duplicate-key','wrong-type','source-hash')){
|
||||
$text=[IO.File]::ReadAllText($planPath)
|
||||
switch($scenario){
|
||||
context {$text=$text.Replace('TEST','OTHER')}
|
||||
duplicate-key {$text=$text.Replace('"SchemaVersion":','"SchemaVersion": 1, "SchemaVersion":')}
|
||||
wrong-type {$text=$text -replace '"DesiredEnabled":\s*true','"DesiredEnabled": "true"'}
|
||||
source-hash {$text=$text.Replace('scripts/WecState.ps1','scripts/Untrusted.ps1')}
|
||||
}
|
||||
[IO.File]::WriteAllText($planPath,$text);$hash=(Get-FileHash $planPath).Hash.ToLowerInvariant()
|
||||
}
|
||||
$out=Join-Path $root ($scenario+'-apply');$script:journal=Join-Path $out 'before-save.json'
|
||||
$result=Invoke-WelaWecState Apply -PlanPath $planPath -PlanHash $hash -OutputPath $out
|
||||
Assert (($result.ExitCode -eq 0) -eq ($scenario -eq 'ok')) "Scenario $scenario : $($result.Diagnostic)"
|
||||
Assert ($result.ReadyRuleCredit -eq 0 -and $result.BookmarkContinuity -eq 'Not established') 'No delivery/bookmark/Sigma credit'
|
||||
Assert (Test-Path (Join-Path $out 'manifest.json')) 'Result retained'
|
||||
if($scenario -in @('drift','token-drift','hash','stale','context','duplicate-key','wrong-type','source-hash','denied','native-refusal')){Assert ($script:saves -eq 0 -and -not $result.NativeSaveAttempted) 'Rejected before native save'}
|
||||
if($scenario -in @('failure','false-success','preservation','evidence-tamper')){Assert ($result.Status -eq 'SaveAttemptedUnverified' -and $result.NativeSaveAttempted) 'Partial failure remains explicit'}
|
||||
if($scenario -eq 'ok'){
|
||||
$after=Get-WelaWecStateDefinition $script:xml @($sid)
|
||||
Assert ($after.PreservedKey -ceq $before.PreservedKey -and $after.Enabled -and $result.Status -eq 'StateChangedAndVerified') 'Only Enabled changed'
|
||||
Assert ($result.RuntimeAfter.Status -eq 'Unknown') 'Unknown runtime does not become healthy or invalidate observed configuration'
|
||||
}
|
||||
}
|
||||
$script:mode='disabled-destination';$script:xml=$base;$script:saves=0
|
||||
$blocked=Invoke-WelaWecState -Id $id -SourceSids @($sid) -State Enabled -OutputPath (Join-Path $root 'disabled-destination-plan')
|
||||
Assert ($blocked.Status -eq 'Refused' -and $blocked.Diagnostic -match 'ForwardedEvents' -and $script:saves -eq 0) 'Disabled destination is rejected before planning activation'
|
||||
$disabled=Invoke-WelaWecState -Id $id -SourceSids @($sid) -State Disabled -OutputPath (Join-Path $root 'disabled-destination-disable')
|
||||
Assert ($disabled.ExitCode -eq 0) 'Disabled destination does not block a reviewed disable plan'
|
||||
foreach($desired in @('Enabled','Disabled')){
|
||||
$script:mode='ok';$script:xml=if($desired -eq 'Disabled'){$base}else{$base.Replace('<Enabled>false','<Enabled>true')};$script:saves=0
|
||||
$planResult=Invoke-WelaWecState -Id $id -SourceSids @($sid) -State $desired -OutputPath (Join-Path $root ($desired+'-same-plan'))
|
||||
$result=Invoke-WelaWecState Apply -PlanPath (Join-Path $planResult.OutputPath 'plan.json') -PlanHash $planResult.PlanHash -OutputPath (Join-Path $root ($desired+'-same-apply'))
|
||||
Assert ($result.Status -eq 'AlreadyMatches' -and $result.ExitCode -eq 0 -and $script:saves -eq 0) 'Idempotent enabled/disabled state never saves/reactivates'
|
||||
}
|
||||
$script:xml=$base.Replace('<Enabled>false','<Enabled>true');$script:saves=0
|
||||
$planResult=Invoke-WelaWecState -Id $id -SourceSids @($sid) -State Disabled -OutputPath (Join-Path $root 'disable-plan')
|
||||
$out=Join-Path $root 'disable-apply';$script:journal=Join-Path $out 'before-save.json'
|
||||
$result=Invoke-WelaWecState Apply -PlanPath (Join-Path $planResult.OutputPath 'plan.json') -PlanHash $planResult.PlanHash -OutputPath $out
|
||||
Assert ($result.ExitCode -eq 0 -and $result.BeforeEnabled -and -not $result.DesiredEnabled -and (Get-WelaWecStateDefinition $script:xml @($sid)).WholeKey -ceq $before.WholeKey) 'Explicit disable restores exact original XML semantics'
|
||||
Reject {Invoke-WelaWecState -Id $id -SourceSids @($sid) -State Disabled -OutputPath $root} 'new directory'
|
||||
$one=Get-WelaWecStateContext;$two=Get-WelaWecStateContext;$two.Reader.TokenStatistics=('22'*8)+$two.Reader.TokenStatistics.Substring(16)
|
||||
Assert ((Get-WelaWecStateReviewKey $one) -ceq (Get-WelaWecStateReviewKey $two)) 'Different token objects in the same logon can use a reviewed plan'
|
||||
$two.Reader.TokenStatistics='22'*56
|
||||
Assert ((Get-WelaWecStateReviewKey $one) -cne (Get-WelaWecStateReviewKey $two)) 'Different actual logon cannot reuse a reviewed plan'
|
||||
}finally{Remove-Item -LiteralPath $root -Recurse -Force}
|
||||
Write-Host "WEC state tests passed: $count assertions."
|
||||
@@ -0,0 +1,112 @@
|
||||
param([switch]$AllowDisposableSubscription)
|
||||
$ErrorActionPreference='Stop'
|
||||
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not $AllowDisposableSubscription -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit disposable GitHub-hosted Windows subscription opt-in required.'}
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
|
||||
Import-Module "$repo/modules/AuditProfiles.psm1" -Force
|
||||
. "$repo/scripts/Configuration.ps1"
|
||||
. "$repo/scripts/ControlApplicability.ps1"
|
||||
. "$repo/scripts/WefArrival.ps1"
|
||||
. "$repo/scripts/WecUpdate.ps1"
|
||||
. "$repo/scripts/WecRuntime.ps1"
|
||||
. "$repo/scripts/WecState.ps1"
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Key($Value){ConvertTo-Json -InputObject $Value -Depth 16 -Compress}
|
||||
function ServiceState {Get-CimInstance Win32_Service -Filter "Name='Wecsvc'"|Select-Object Name,State,StartMode}
|
||||
function ChannelState {
|
||||
$c=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('ForwardedEvents')
|
||||
try {[pscustomobject]@{Enabled=$c.IsEnabled;Mode=[string]$c.LogMode;MaximumBytes=$c.MaximumSizeInBytes;Path=$c.LogFilePath;SecurityDescriptor=$c.SecurityDescriptor}}finally{$c.Dispose()}
|
||||
}
|
||||
function Set-ChannelEnabled([bool]$Enabled){$c=[Diagnostics.Eventing.Reader.EventLogConfiguration]::new('ForwardedEvents');try{$c.IsEnabled=$Enabled;$c.SaveChanges()}finally{$c.Dispose()}}
|
||||
function Subscriptions {@((Invoke-WelaNative 'wecutil.exe' @('es')).Output|ForEach-Object {$_.ToString().Trim()}|Where-Object {$_})}
|
||||
function Invoke-Cli {
|
||||
param([string[]]$Arguments,[string]$Output,[bool]$Success=$true)
|
||||
$engine=(Get-Process -Id $PID).Path
|
||||
$prior=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$text=@(&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" wec-state @Arguments -WecStateOutputPath $Output 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior}
|
||||
Assert (($code -eq 0) -eq $Success) "Public CLI exit $code : $($text -join ' ')"
|
||||
$manifest=Join-Path $Output 'manifest.json';Assert (Test-Path $manifest) 'Public command emitted actual durable result'
|
||||
Get-Content -LiteralPath $manifest -Raw|ConvertFrom-Json
|
||||
}
|
||||
$beforeService=ServiceState;$serviceKey='HKLM:\SYSTEM\CurrentControlSet\Services\Wecsvc';$beforeDelayed=Get-WelaRegistryState $serviceKey DelayedAutoStart
|
||||
if($beforeService.State -notin @('Running','Stopped') -or $beforeService.StartMode -notin @('Auto','Manual','Disabled')){throw 'Stable Wecsvc state required.'}
|
||||
$nonce=[guid]::NewGuid().ToString('N');$id='WELA-State-Test-'+$nonce;$description='Owned state '+([string][char]0x65e5)+([string][char]0x672c)+([string][char]0x8a9e)+' '+$nonce;$changedDescription=$description
|
||||
$sid='S-1-5-21-111111111-222222222-333333333-1234'
|
||||
$root=Join-Path $env:RUNNER_TEMP ('wela-wec-state-'+$nonce);$null=New-Item -ItemType Directory $root
|
||||
$created=$false;$beforeIds=$null;$primary=$null;$beforeChannel=ChannelState
|
||||
try {
|
||||
if($beforeService.StartMode -eq 'Disabled'){Set-Service Wecsvc -StartupType Manual}
|
||||
if($beforeService.State -eq 'Stopped'){Start-Service Wecsvc}
|
||||
if(-not $beforeChannel.Enabled){Set-ChannelEnabled $true}
|
||||
$duringChannel=ChannelState
|
||||
Assert ($duringChannel.Enabled) 'Disposable fixture enabled only destination channel prerequisite'
|
||||
[pscustomobject]@{Destination=$duringChannel;WinRM=(Get-CimInstance Win32_Service -Filter "Name='WinRM'"|Select-Object Name,State,StartMode);Wecsvc=(ServiceState)}|ConvertTo-Json -Depth 8|Set-Content -LiteralPath (Join-Path $root 'fixture-prerequisites.json') -Encoding UTF8
|
||||
$beforeIds=@(Subscriptions);if($beforeIds -contains $id){throw 'Unique ID already exists.'}
|
||||
$query='<QueryList><Query Id="0" Path="Application"><Select Path="Application">*[System[(EventID=1)]]</Select></Query></QueryList>'
|
||||
$xml=@"
|
||||
<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription"><SubscriptionId>$id</SubscriptionId><SubscriptionType>SourceInitiated</SubscriptionType><Description>$description</Description><Enabled>false</Enabled><Uri>http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog</Uri><ConfigurationMode>Normal</ConfigurationMode><Query><![CDATA[$query]]></Query><ReadExistingEvents>false</ReadExistingEvents><TransportName>HTTP</TransportName><ContentFormat>Events</ContentFormat><Locale Language="en-US"/><LogFile>ForwardedEvents</LogFile><AllowedSourceDomainComputers>$(Get-WelaWefAuthorization @($sid))</AllowedSourceDomainComputers></Subscription>
|
||||
"@
|
||||
$xmlPath=Join-Path $root 'owned.xml';[IO.File]::WriteAllText($xmlPath,$xml);$created=$true;$null=Invoke-WelaNative 'wecutil.exe' @('cs',$xmlPath)
|
||||
$before=Read-WelaWecStateDefinition $id @($sid);$duringService=ServiceState
|
||||
Assert (-not $before.Enabled -and $before.Description -ceq $description) 'Real owned disabled subscription preserves Unicode description'
|
||||
Initialize-WelaWecStateNative
|
||||
$missing=$false;try{$unexpected=[Wela.WecState.Edit]::new($id+'-absent');$unexpected.Dispose()}catch{$missing=$true}
|
||||
Assert ($missing -and @(Subscriptions) -notcontains ($id+'-absent')) 'Native existing-only open never creates missing subscription'
|
||||
$enablePlan=$null
|
||||
foreach($state in @('Disabled','Enabled','Enabled','Disabled','Disabled')){
|
||||
$index=$count;$out=Join-Path $root ("plan-$index")
|
||||
$prior=Read-WelaWecStateDefinition $id @($sid)
|
||||
$plan=Invoke-Cli -Arguments @('-WecStateId',$id,'-WecStateSourceSid',$sid,'-WecStateDesired',$state) -Output $out
|
||||
Assert ($plan.Status -eq 'ReviewRequired' -and -not $plan.NativeSaveAttempted) 'Public plan never changes Enabled'
|
||||
Assert ((Read-WelaWecStateDefinition $id @($sid)).WholeKey -ceq $prior.WholeKey) 'Plan preserved complete native subscription'
|
||||
$planPath=Join-Path $out 'plan.json'
|
||||
$apply=Invoke-Cli -Arguments @('-WecStateAction','Apply','-WecStatePlanPath',$planPath,'-WecStatePlanHash',$plan.PlanHash) -Output (Join-Path $root ("apply-$index"))
|
||||
$expected=($state -eq 'Enabled');$changed=($prior.Enabled -ne $expected)
|
||||
Assert ($apply.NativeSaveAttempted -eq $changed -and $apply.Status -eq $(if($changed){'StateChangedAndVerified'}else{'AlreadyMatches'})) 'Only an actual state transition invokes EcSaveSubscription'
|
||||
$after=Read-WelaWecStateDefinition $id @($sid)
|
||||
Assert ($after.Enabled -eq $expected -and $after.PreservedKey -ceq $before.PreservedKey) 'Native readback differs only in Enabled'
|
||||
Assert ($apply.ReadyRuleCredit -eq 0 -and $apply.BookmarkContinuity -eq 'Not established' -and $null -ne $apply.RuntimeAfter) 'Separate native runtime observation supplies no delivery or bookmark claim'
|
||||
foreach($artifact in $apply.Artifacts){Assert ((Get-FileHash -LiteralPath (Join-Path $apply.OutputPath $artifact.Name)).Hash.ToLowerInvariant() -ceq $artifact.Sha256) 'Saved native artifacts match hashes'}
|
||||
if($changed -and $expected){
|
||||
$enablePlan=$plan
|
||||
$stale=Invoke-Cli -Arguments @('-WecStateAction','Apply','-WecStatePlanPath',$planPath,'-WecStatePlanHash',$plan.PlanHash) -Output (Join-Path $root 'stale-enabled-plan') -Success $false
|
||||
Assert ($stale.Status -eq 'Refused' -and -not $stale.NativeSaveAttempted -and $stale.Diagnostic -match 'differs') 'A completed transition cannot replay its stale pre-state'
|
||||
Assert ((Read-WelaWecStateDefinition $id @($sid)).WholeKey -ceq $after.WholeKey) 'Stale plan refusal preserved enabled definition'
|
||||
}
|
||||
}
|
||||
Assert ((Read-WelaWecStateDefinition $id @($sid)).WholeKey -ceq $before.WholeKey) 'Explicit disable restored entire original native definition'
|
||||
# A separately opened native handle sees a changed description and refuses save.
|
||||
$edit=New-WelaWecStateEdit $before
|
||||
try {
|
||||
$null=Invoke-WelaNative 'wecutil.exe' @('ss',$id,('/d:'+($description+' drift')))
|
||||
$refused=$false;try{$edit.Save($true)}catch{$refused=$true}
|
||||
Assert ($refused -and -not $edit.SaveAttempted) 'Fresh native handle guards description drift before saving'
|
||||
Assert (-not(Read-WelaWecStateDefinition $id @($sid)).Enabled) 'Native drift refusal did not enable subscription'
|
||||
}finally{$edit.Dispose();$null=Invoke-WelaNative 'wecutil.exe' @('ss',$id,('/d:'+$description))}
|
||||
Assert ((Read-WelaWecStateDefinition $id @($sid)).WholeKey -ceq $before.WholeKey) 'Native drift fixture restored original description'
|
||||
Assert ((Key (ChannelState)) -ceq (Key $duringChannel)) 'Product command preserved complete channel configuration'
|
||||
Assert ((Key (ServiceState)) -ceq (Key $duringService)) 'Product command preserved service state/startup'
|
||||
Write-Host "Native WEC state passed $count assertions on $([Environment]::OSVersion.Version), PowerShell $($PSVersionTable.PSVersion). No real source, listener or bookmark claim."
|
||||
}catch{$primary=$_}
|
||||
finally {
|
||||
$errors=@()
|
||||
try {
|
||||
if($created -and @(Subscriptions) -contains $id){$raw=Read-WelaWecSubscriptionXml $id;$doc=Read-WelaWefXml $raw;$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('s',$doc.DocumentElement.NamespaceURI);$observed=$doc.SelectSingleNode('/s:Subscription/s:Description',$ns).InnerText;if($observed -cnotin @($description,$changedDescription)){throw 'Fixture ownership changed; refusing deletion.'};$null=Invoke-WelaNative 'wecutil.exe' @('ds',$id)}
|
||||
if($null -ne $beforeIds -and (Key @($beforeIds|Sort-Object)) -cne (Key @(Subscriptions|Sort-Object))){throw 'Subscription inventory differs after cleanup.'}
|
||||
}catch{$errors+=$_.Exception.Message}
|
||||
try {
|
||||
if((ChannelState).Enabled -ne $beforeChannel.Enabled){Set-ChannelEnabled $beforeChannel.Enabled}
|
||||
if((Key (ChannelState)) -cne (Key $beforeChannel)){throw 'Original destination channel configuration differs.'}
|
||||
}catch{$errors+=$_.Exception.Message}
|
||||
try {
|
||||
if($beforeService.State -eq 'Stopped' -and (Get-Service Wecsvc).Status -ne 'Stopped'){Stop-Service Wecsvc}
|
||||
if($beforeService.StartMode -eq 'Disabled'){Set-Service Wecsvc -StartupType Disabled}
|
||||
if((Key (Get-WelaRegistryState $serviceKey DelayedAutoStart)) -cne (Key $beforeDelayed)){if($beforeDelayed.ValueExists){$null=New-ItemProperty -LiteralPath $serviceKey -Name DelayedAutoStart -Value $beforeDelayed.Value -PropertyType $beforeDelayed.Type -Force}else{Remove-ItemProperty -LiteralPath $serviceKey -Name DelayedAutoStart -ErrorAction Stop}}
|
||||
if((Key (ServiceState)) -cne (Key $beforeService) -or (Key (Get-WelaRegistryState $serviceKey DelayedAutoStart)) -cne (Key $beforeDelayed)){throw 'Original Wecsvc state/startup differs.'}
|
||||
}catch{$errors+=$_.Exception.Message}
|
||||
if($errors.Count){throw "Fixture cleanup failed; retained $root : $($errors -join '; '); primary failure: $primary"}
|
||||
[pscustomobject]@{Passed=($null -eq $primary);Assertions=$count;OriginalSubscriptionsRestored=$true;OriginalServiceRestored=$true;OriginalChannelRestored=$true;Computer=[Environment]::MachineName;Engine=$PSVersionTable.PSVersion.ToString();Scope='Owned native Enabled transitions only; no real source, listener, forwarding or bookmark proof'}|ConvertTo-Json|Set-Content -LiteralPath (Join-Path $root 'acceptance.json') -Encoding UTF8
|
||||
Write-Host 'Original subscription inventory and Wecsvc state/startup restored.'
|
||||
}
|
||||
if($primary){throw $primary}
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -78,7 +78,7 @@ function Get-WSManInstance {
|
||||
}
|
||||
function Get-NetFirewallRule { param($Name,$PolicyStore) Assert ($PolicyStore -eq 'ActiveStore') 'Ingress is read from effective ActiveStore'; [pscustomobject]@{ Name=$Name; Enabled=$global:WelaWefFixture.Ingress; Direction='Inbound'; Action='Allow'; Profile='Domain'; PolicyStoreSourceType='Local'; EnforcementStatus='Full' } }
|
||||
function Get-NetFirewallPortFilter { [CmdletBinding()]param([Parameter(ValueFromPipeline)]$Rule) process { [pscustomobject]@{ Protocol='TCP'; LocalPort='5985'; RemotePort='Any' } } }
|
||||
function Get-NetFirewallAddressFilter { [CmdletBinding()]param([Parameter(ValueFromPipeline)]$Rule) process { [pscustomobject]@{ LocalAddress=@('192.0.2.10'); RemoteAddress=@('192.0.2.0/24') } } }
|
||||
function Get-NetFirewallAddressFilter { [CmdletBinding()]param([Parameter(ValueFromPipeline)]$Rule) process { [pscustomobject]@{ LocalAddress=@('192.0.2.10/255.255.255.255'); RemoteAddress=@('192.0.2.0/255.255.255.0') } } }
|
||||
function Read-Host { param($Prompt) return $global:WelaWefFixture.Prompt }
|
||||
function Invoke-WelaNative {
|
||||
param($FilePath,$Arguments)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Scope comparison only; no firewall or Windows setting mutation.
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
|
||||
$count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Reject([scriptblock]$Code){$caught=$false;try{&$Code|Out-Null}catch{$caught=$true};Assert $caught 'Malformed, broad or non-IP observed scope must be refused.'}
|
||||
foreach($pair in @(
|
||||
@('192.0.2.0/24','192.0.2.0/255.255.255.0'),
|
||||
@('10.0.0.0/8','10.0.0.0/255.0.0.0'),
|
||||
@('192.0.2.1','192.0.2.1/255.255.255.255'),
|
||||
@('192.0.2.1/32','192.0.2.1'),
|
||||
@('192.0.2.128/25','192.0.2.128/255.255.255.128'),
|
||||
@('192.0.2.129/25','192.0.2.128/255.255.255.128'),
|
||||
@('2001:0DB8:0000:0000::/64','2001:db8::/64'),
|
||||
@('2001:db8::1/128','2001:0db8::1'),
|
||||
@('2001:db8::1/64','2001:db8::/64'),
|
||||
@('fe80::1%3','fe80:0:0:0:0:0:0:1%3')
|
||||
)){
|
||||
Assert (Test-WelaWefFirewallAddressSet @($pair[0]) @($pair[1])) ('Equivalent scopes compare equal: '+($pair -join ' / '))
|
||||
}
|
||||
foreach($pair in @(
|
||||
@('192.0.2.0/24','192.0.2.0/255.255.254.0'),
|
||||
@('192.0.2.0/24','192.0.2.0/255.255.255.128'),
|
||||
@('192.0.2.0/24','198.51.100.0/255.255.255.0'),
|
||||
@('192.0.2.1','192.0.2.2'),
|
||||
@('2001:db8::/64','2001:db8::/63'),
|
||||
@('2001:db8::/64','2001:db8:0:1::/64'),
|
||||
@('192.0.2.1','::ffff:192.0.2.1'),
|
||||
@('fe80::1%3','fe80::1%4')
|
||||
)){
|
||||
Assert (-not(Test-WelaWefFirewallAddressSet @($pair[0]) @($pair[1]))) ('Different scope is refused: '+($pair -join ' / '))
|
||||
}
|
||||
Assert (Test-WelaWefFirewallAddressSet @('2001:db8::/64','192.0.2.0/24') @('192.0.2.0/255.255.255.0','2001:0db8::/64')) 'Unordered mixed IPv4/IPv6 scopes remain equivalent.'
|
||||
Assert (-not(Test-WelaWefFirewallAddressSet @('192.0.2.1') @('192.0.2.1','192.0.2.2'))) 'Extra native scope is not a match.'
|
||||
Assert (-not(Test-WelaWefFirewallAddressSet @('192.0.2.1','192.0.2.2') @('192.0.2.1'))) 'Missing native scope is not a match.'
|
||||
Assert (-not(Test-WelaWefFirewallAddressSet @('192.0.2.1') @())) 'Empty native scope is unknown, never Any.'
|
||||
foreach($value in @('Any','LocalSubnet','example.org','192.0.2.1-192.0.2.10','192.0.2.0/0','192.0.2.0/0.0.0.0','192.0.2.0/255.0.255.0','192.0.2.0/255.255.999.0','192.0.2.0/255.255.0','192.0.2.0/33','::/0','::1/129','2001:db8::/255.255.255.0','192.0.2.1/24/32','')){Reject {Test-WelaWefFirewallAddressSet @('192.0.2.0/24') @($value)}}
|
||||
Reject {Test-WelaWefFirewallAddressSet @('192.0.2.0/255.255.255.0') @('192.0.2.0/24')}
|
||||
Reject {Test-WelaWefFirewallAddressSet @(1) @('192.0.2.1')}
|
||||
Reject {Test-WelaWefFirewallAddressSet @('192.0.2.1') @(1)}
|
||||
Write-Host "WEF firewall address comparison: $count assertions passed."
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
- `transcription-recovery` を追加し、完了した Windows PowerShell 文字起こし設定を、再構築したハッシュ付き計画から型を保持して復元できるようにしました。ローカル保存先、ユーザー・ヘッダー・他のポリシーを確認し、一時停止は明示的な同意を求め、変更順序と途中結果を永続記録します。使い捨て環境の実 CLI テストで復元とドリフト拒否を検証し、本番セッション・集中管理先の権限と収集・Sigma 対応は未検証とします。 (#376) (@Shirofune-Security)
|
||||
|
||||
- `wec-ingress` の Plan/Apply を追加し、明示した IPv4 範囲から Domain プロファイルの TCP5985 を許可する新規ルールを作成します。実ホスト・ログオン・プロファイル・コードと計画ハッシュ、変更前の永続記録、両ストアとフィルターの読戻しで変更や既存名を拒否します。既存の収集サーバー前提条件も、範囲を広げずに同等のIPv4ネットマスク表記・IPv6表記を照合します。使い捨て Windows テストは作成・名前衝突・再実行拒否・既存前提条件との連携・削除を検証し、リスナー・配送・Sigma の証明は加算しません。 (@Shirofune-Security)
|
||||
|
||||
- `smb-runtime` を追加し、Windows 標準の SMB 監査スイッチ6個を明示的に有効化します。モジュール・ビルド・ADMX、型付きポリシーの競合、設定全体の変化を確認し、各変更の意図と確認結果を永続的に記録します。署名・暗号化・ゲスト接続・サービス設定を保持し、現在の有効化とイベント・永続性・Sigma の証明を区別します。使い捨て Server 2025 の有効化と復元、Server 2022 の拒否を PowerShell 5.1/7 で検証します。 (#441) (@Shirofune-Security)
|
||||
|
||||
- 既存のWindows標準ソース開始型購読1件のEnabledだけをレビュー後に変更する`wec-state`のPlan/Applyを追加しました。送信元認可と完全な定義、実行者・ログオン・トークン、永続的な変更前記録とネイティブ読戻しを確認して他の設定を保持し、既に一致する状態では保存・再有効化を行いません。稼働状況の観測と配送・ブックマークの継続性を区別し、使い捨てWindowsテストで実際の有効/無効切替と所有リソース・サービス状態の復元を確認します。 (関連 #368) (@Shirofune-Security)
|
||||
|
||||
- 強化プロファイルのオプション IPsec Main Mode 監査に、Windows ネイティブの前提条件確認を追加しました。有効なポリシーストアのルールと現在の関連付けを読み取り、適用可能・確認範囲内で未観測・不明を区別します。共有設定処理は書き込み直前に再確認し、明示的な選択とカスタムプロファイルの指定を保持します。Server 2022/2025・PowerShell 5.1/7 の一時ルールを使ったテストで監査ポリシーの復元を確認します。ネゴシエーション、イベント生成、Sigma の対応は保証しません。 (#370) (@Shirofune-Security)
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
|
||||
- Added explicit `transcription-recovery` for one completed Windows PowerShell transcription configuration, with independently rebuilt hashed plans, typed local-directory restoration, preserved user/header/other policy, explicit temporary-suspension consent and durable ordered receipts. Disposable native public-CLI tests verify restoration and drift refusal; production sessions, central authorization/collection and Sigma readiness remain unverified. (#376) (@Shirofune-Security)
|
||||
|
||||
- Added explicit `wec-ingress` Plan/Apply for one new, narrowly scoped Domain TCP5985 collector firewall rule. Actual host/logon/profile/source guards, reviewed hashes, flushed pending evidence and both-store/filter readback refuse drift and existing names; partial creation remains explicit. The existing collector prerequisite recognizes equivalent native dotted netmasks and IPv6 spellings without broadening accepted scopes. Disposable native tests cover creation, collision, replay, collector integration and cleanup without listener, delivery or Sigma claims. (@Shirofune-Security)
|
||||
|
||||
- Added explicit `smb-runtime` activation of six native SMB audit switches, with reviewed module/build/ADMX capabilities, typed policy conflicts, complete configuration drift guards and durable per-switch receipts. Signing, encryption, guest access and service settings are preserved; runtime verification stays separate from events, persistence and Sigma credit. Disposable Server 2025 activation/restoration and Server 2022 refusal tests cover PowerShell 5.1/7. (#441) (@Shirofune-Security)
|
||||
|
||||
- Added reviewed `wec-state` Plan/Apply for the Enabled flag of one existing native source-initiated subscription. Exact authorization and complete definition checks, operator/logon/token guards, durable pending evidence and native readback preserve other settings; matching states never save or reactivate. Runtime remains separate, and interrupted delivery/bookmark continuity require multi-host validation. Disposable Windows lifecycle tests restore owned resources and service state. (Related #368) (@Shirofune-Security)
|
||||
|
||||
- Added native prerequisite evidence for the stronger profile's optional IPsec Main Mode auditing. Effective-store rule and current association observations distinguish scoped applicability, absence and unknown results; both shared configuration paths recheck before writing and preserve explicit selection/custom-profile intent. Native disposable-rule tests cover Server 2022/2025 and PowerShell 5.1/7 with exact audit-policy restoration, without negotiation/event or Sigma claims. (#370) (@Shirofune-Security)
|
||||
|
||||
- Fixed `wmi-probe` operation timing to use the native precise UTC clock consistently in the parent and worker. Explicit clock receipts and launch/completion bounds preserve exact event correlation; sub-millisecond boundary tests and repeated native public-CLI runs cover timing failures without widening the accepted interval. (@Shirofune-Security)
|
||||
|
||||
Reference in New Issue
Block a user