Merge pull request #460 from Shirofune-Security/feat/362-scoped-outgoing-ntlm

Add scoped outgoing NTLM audit configuration
This commit is contained in:
田中ザック Isaac Mathis authored and GitHub committed 2026-09-22 12:03:49 +09:00
commit 9720ad7886
13 files changed
+325 -3

No files matched your search

+1 -1
View File
@@ -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/firewall-logging.md, ./docs/firewall-logging-recovery.md, ./docs/ipsec-prerequisites.md, ./docs/wec-state.md, ./docs/wec-update.md, ./docs/wec-runtime.md, ./docs/wef-deployment.md, ./docs/wec-collector-observation.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/smb-runtime-activation.md, ./docs/smb-auditing.md, ./docs/wec-ingress.md, ./docs/capi2-probe.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/eventlog-recovery.md, ./docs/failed-logon-probe.md, ./docs/wec-listener.md, ./docs/file-access-probe.md, ./docs/applocker-script-probe.md, ./docs/applocker-probe.md, ./docs/selected-sacl-configuration.md, ./docs/targeted-sacl-planning.md, ./docs/native-registry-sacl-validation.md, ./docs/wec-authorization.md, ./docs/channel-recovery.md, ./docs/native-provider-packs.md, ./docs/native-provider-acceptance.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/firewall-logging.md, ./docs/firewall-logging-recovery.md, ./docs/ipsec-prerequisites.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/smb-runtime-activation.md, ./docs/smb-auditing.md, ./docs/wec-ingress.md, ./docs/capi2-probe.md, ./docs/powershell-transcription.md, ./docs/transcription-recovery.md, ./docs/eventlog-recovery.md, ./docs/failed-logon-probe.md, ./docs/wec-listener.md, ./docs/file-access-probe.md, ./docs/applocker-script-probe.md, ./docs/applocker-probe.md, ./docs/selected-sacl-configuration.md, ./docs/targeted-sacl-planning.md, ./docs/native-registry-sacl-validation.md, ./docs/wec-authorization.md, ./docs/channel-recovery.md, ./docs/outgoing-ntlm.md, ./docs/wec-collector-observation.md, ./docs/native-provider-packs.md, ./docs/native-provider-acceptance.md -Destination release-binaries/docs/
- name: Set Artifact Name
if: contains(matrix.info.os, 'windows') == true
@@ -0,0 +1,47 @@
name: Scoped outgoing NTLM auditing
on:
push:
branches: ['**']
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
scoped-outgoing-ntlm:
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/OutgoingNtlmAudit.Tests.ps1
./tests/OutgoingNtlmAudit.Cli.Tests.ps1
- name: Scoped outgoing NTLM auditing in Windows PowerShell5.1
if: matrix.engine == 'powershell'
shell: powershell
run: ./tests/OutgoingNtlmAudit.Windows.Tests.ps1 -AllowDisposableAuditWrite
- name: Fixtures and public guards in PowerShell7
if: matrix.engine == 'pwsh'
shell: pwsh
run: |
./tests/OutgoingNtlmAudit.Tests.ps1
./tests/OutgoingNtlmAudit.Cli.Tests.ps1
- name: Scoped outgoing NTLM auditing in PowerShell7
if: matrix.engine == 'pwsh'
shell: pwsh
run: ./tests/OutgoingNtlmAudit.Windows.Tests.ps1 -AllowDisposableAuditWrite
- name: Retain owned fixture evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: scoped-outgoing-ntlm-${{ matrix.os }}-${{ matrix.engine }}
path: ${{ runner.temp }}/wela-outgoing-audit-*/
if-no-files-found: warn
retention-days: 7
+2
View File
@@ -4,6 +4,8 @@
**改善:**
- `outgoing-ntlm` のAudit/Plan/Configureを追加し、送信NTLM監査DWORDを個別に設定できます。既存の拒否設定は既定で維持し、置換には明示的なAuditを要求します。不明な型・値や書込直前の変化を拒否し、元の型付き記録と再読取を保持します。Server 2022/2025のテストで範囲と復元を確認し、認証・イベント生成は未検証として報告します。(関連 #362) (@Shirofune-Security)
- コレクターのサブスクリプション観測で、コンソール経由の文字変換を、上限付きの完全なネイティブ名前列挙と厳密なUnicode XML読取に置き換えました。空の一覧・読取失敗・実際の無効状態を区別し、Unicodeの説明とXPathを保持します。Server 2022/2025と両PowerShellで公開Audit/Planおよび正確な後処理を検証し、ドメイン展開・転送・Sigma対応は主張しません。(関連 #368) (@Shirofune-Security)
- Server 2022/2025 の Windows PowerShell 5.1/PowerShell 7 で、公開 provider-packs コマンドの Plan、DryRun、Configure、冪等性、前提不足・手動対象の拒否、部分適用を実機検証する使い捨て CI を追加。DNS Client、CAPI2、WinRM、RDP Client の設定と復元記録・ハッシュを確認し、完全な ACL、保持モード、大きい既存バッファ、他チャネル、サービス、全監査マスクの保持とテスト後の正確な復元を検証。イベント生成や Sigma 対応の証明は含みません。 WinRM のマニフェスト ID をネイティブの Int64 として比較し、対象外の大きい ID により必要なイベントの確認が失敗する不具合も修正。 (@Shirofune-Security)
+2
View File
@@ -4,6 +4,8 @@
**Improvements:**
- Add `outgoing-ntlm` Audit/Plan/Configure to manage the outgoing audit DWORD independently of broad configuration. Preserve existing deny by default, require explicit Audit to replace it, refuse unknown types/values and pre-write drift, and retain typed original journals plus native readback. Native Server 2022/2025 tests verify narrow scope and exact cleanup; authentication/event generation remain unverified. (Related #362) (@Shirofune-Security)
- Fixed collector subscription observations to use complete bounded native name enumeration and strict Unicode XML reads instead of console decoding. Empty inventories, failed reads and actual disabled state remain distinct; Unicode descriptions and XPath are preserved. Disposable public Audit/Plan tests cover both Server 2022/2025 and PowerShell engines with exact cleanup, without domain deployment, forwarding or Sigma claims. (Related #368) (@Shirofune-Security)
- Added disposable native acceptance for public provider-pack Plan, DryRun, Configure, idempotence, missing/manual refusal and partial outcomes on Server 2022/2025 under Windows PowerShell 5.1/PowerShell 7. Actual DNS Client, CAPI2, WinRM and RDP Client configuration preserves descriptors, retention, larger buffers, unrelated channels, services and all audit masks, with journal/hash evidence and exact fixture cleanup; no event or Sigma credit. Fixed WinRM manifest inspection to preserve native Int64 event IDs, so unrelated large IDs no longer block the selected event schema. (@Shirofune-Security)
+17 -1
View File
@@ -16,6 +16,7 @@
[switch]$Auto,
[ValidateSet("PreserveOrAudit", "Audit", "Deny")]
[string]$OutgoingNtlmMode = "PreserveOrAudit",
[ValidateSet("Audit","Plan","Configure")][string]$NtlmAction = "Audit",
[switch]$DryRun,
[string]$BackupPath,
[string]$ResultsPath,
@@ -225,6 +226,7 @@ $EidMappingPath = Join-Path $ScriptRoot "config/eid_subcategory_mapping.csv"
$AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt"
$SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
. (Join-Path $ScriptRoot "scripts/Configuration.ps1")
. (Join-Path $ScriptRoot "scripts/OutgoingNtlmAudit.ps1")
. (Join-Path $ScriptRoot "scripts/AdcsAuditing.ps1")
. (Join-Path $ScriptRoot "scripts/AdcsRestartResume.ps1")
. (Join-Path $ScriptRoot "scripts/AuditIntegrity.ps1")
@@ -2064,6 +2066,7 @@ Usage:
./WELA.ps1 eventlog-recovery -Help # Review restoration of one completed log size/mode write
./WELA.ps1 wec-listener -Help # Review one fixed-address native HTTP5985 listener
./WELA.ps1 wec-ingress -Help # Review scoped collector firewall rule creation
./WELA.ps1 outgoing-ntlm -Help # Configure outgoing NTLM auditing independently
./WELA.ps1 wec-authorization -Help # Review source SID authorization on a disabled subscription
./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
@@ -2161,6 +2164,12 @@ if ($Cmd -ne 'wec-listener' -and @($PSBoundParameters.Keys | Where-Object {$_ -l
if ($Cmd -eq 'wec-listener' -and ($args.Count -or @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WecListenerAction','WecListenerComputerName','WecListenerLocalAddress','WecListenerPlanPath','WecListenerPlanHash','WecListenerOutputPath','Help')}).Count)) {throw 'wec-listener accepts only dedicated 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 'outgoing-ntlm' -and $PSBoundParameters.ContainsKey('NtlmAction')) {throw 'NtlmAction requires outgoing-ntlm.'}
if ($Cmd -eq 'outgoing-ntlm') {
if (@($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','NtlmAction','OutgoingNtlmMode','Auto','DryRun','BackupPath','ResultsPath','Help')}).Count) {throw 'outgoing-ntlm accepts only its dedicated options.'}
if ($OutgoingNtlmMode -eq 'Deny') {throw 'outgoing-ntlm configures auditing only; Deny enforcement is not accepted.'}
if ($NtlmAction -ne 'Configure' -and ($Auto -or $DryRun -or $BackupPath)) {throw 'Consent, dry-run and backup options require NtlmAction Configure.'}
}
if ($Cmd -ne 'wec-authorization' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WecAuthorization*'}).Count) {throw 'WecAuthorization options require wec-authorization.'}
if ($Cmd -eq 'wec-authorization' -and ($args.Count -or @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WecAuthorizationAction','WecAuthorizationId','WecAuthorizationSourceSid','WecAuthorizationPlanPath','WecAuthorizationPlanHash','WecAuthorizationOutputPath','Help')}).Count)) {throw 'wec-authorization accepts only dedicated options.'}
if ($Cmd -ne 'wec-state' -and @($PSBoundParameters.Keys | Where-Object {$_ -like 'WecState*'}).Count) {throw 'WecState options require wec-state.'}
@@ -2249,7 +2258,7 @@ if ($Cmd -ne 'ad-object-sacl' -and @($PSBoundParameters.Keys | Where-Object {
}).Count) {
throw 'AD object SACL options require the dedicated ad-object-sacl command. No command was run.'
}
if ($DryRun -and -not ($Cmd -eq 'transcription-recovery' -and $TranscriptRecoveryAction -eq 'Restore') -and -not ($Cmd -eq 'adcs-auditing' -and $AdcsAction -eq 'Configure') -and -not ($Cmd -eq 'adcs-resume' -and $AdcsResumeAction -eq 'Resume') -and -not ($Cmd -eq 'gpo-create' -and $GpoCreateAction -eq 'Create') -and -not ($Cmd -eq 'dns-analytical' -and $DnsAction -eq 'Configure') -and -not ($Cmd -eq 'targeted-sacl' -and $TargetSaclAction -eq 'Configure') -and -not ($Cmd -eq 'audit-recovery' -and $RecoveryAction -eq 'Restore') -and -not ($Cmd -eq 'gpo-package' -and $GpoAction -eq 'Export') -and -not ($Cmd -eq 'audit-integrity' -and $IntegrityAction -eq 'Configure') -and -not ($Cmd -eq 'audit-notifications' -and $NotificationAction -eq 'Configure') -and -not ($Cmd -eq 'ldap-diagnostics' -and $LdapAction -eq 'Configure') -and -not ($Cmd -eq 'applocker-readiness' -and $AppLockerAction -eq 'Import') -and $Cmd -notin @('configure', 'configure-eventlogs') -and
if ($DryRun -and -not ($Cmd -eq 'outgoing-ntlm' -and $NtlmAction -eq 'Configure') -and -not ($Cmd -eq 'transcription-recovery' -and $TranscriptRecoveryAction -eq 'Restore') -and -not ($Cmd -eq 'adcs-auditing' -and $AdcsAction -eq 'Configure') -and -not ($Cmd -eq 'adcs-resume' -and $AdcsResumeAction -eq 'Resume') -and -not ($Cmd -eq 'gpo-create' -and $GpoCreateAction -eq 'Create') -and -not ($Cmd -eq 'dns-analytical' -and $DnsAction -eq 'Configure') -and -not ($Cmd -eq 'targeted-sacl' -and $TargetSaclAction -eq 'Configure') -and -not ($Cmd -eq 'audit-recovery' -and $RecoveryAction -eq 'Restore') -and -not ($Cmd -eq 'gpo-package' -and $GpoAction -eq 'Export') -and -not ($Cmd -eq 'audit-integrity' -and $IntegrityAction -eq 'Configure') -and -not ($Cmd -eq 'audit-notifications' -and $NotificationAction -eq 'Configure') -and -not ($Cmd -eq 'ldap-diagnostics' -and $LdapAction -eq 'Configure') -and -not ($Cmd -eq 'applocker-readiness' -and $AppLockerAction -eq 'Import') -and $Cmd -notin @('configure', 'configure-eventlogs') -and
-not ($Cmd -eq 'provider-packs' -and $ProviderAction -eq 'Configure') -and
-not ($Cmd -eq 'firewall-logging' -and $FirewallAction -eq 'Configure') -and
-not ($Cmd -eq 'firewall-recovery' -and $FirewallRecoveryAction -eq 'Restore') -and
@@ -2428,6 +2437,13 @@ switch ($Cmd.ToLower()) {
$report=Invoke-WelaWecIngress @arguments;$report
if($report.ExitCode){exit $report.ExitCode}
}
'outgoing-ntlm' {
if ($Help) {Write-Host 'Usage: outgoing-ntlm [-NtlmAction Audit|Plan|Configure] [-OutgoingNtlmMode PreserveOrAudit|Audit] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath report.json]. Changes only the outgoing audit DWORD. Existing deny is preserved by default; explicit Audit authorizes replacing it. See docs/outgoing-ntlm.md.';return}
if ($NtlmAction -eq 'Configure' -and -not (TestAdministrator)) {throw 'Outgoing NTLM configuration requires Administrator privileges.'}
$report=Invoke-WelaOutgoingAuditCommand -Action $NtlmAction -Mode $OutgoingNtlmMode -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath
$report
if ($report.ExitCode) {exit $report.ExitCode}
}
'wec-authorization' {
if ($Help) {Write-Host 'Usage: wec-authorization [-WecAuthorizationAction Plan] -WecAuthorizationId ID -WecAuthorizationSourceSid desired-SID1,desired-SID2 -WecAuthorizationOutputPath new-directory; then Apply with -WecAuthorizationPlanPath plan.json -WecAuthorizationPlanHash SHA256 -WecAuthorizationOutputPath new-directory. Only the explicit source SID authorization of one already disabled subscription. No SID resolution or forwarding proof. See docs/wec-authorization.md.';return}
$arguments=@{Action=$WecAuthorizationAction;OutputPath=$WecAuthorizationOutputPath}
+22
View File
@@ -0,0 +1,22 @@
# Scoped outgoing NTLM auditing
`outgoing-ntlm` audits or configures only `HKLM\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0\RestrictSendingNTLMTraffic`. The separate broad `configure` workflow retains its existing behavior. Use elevated 64-bit PowerShell for Configure on reviewed Windows 11 builds (22000/22621/22631/26100/26200) or Server 2022/2025 (20348/26100); observed role/build and the existing key are required. Role overrides are refused.
```powershell
./WELA.ps1 outgoing-ntlm -NtlmAction Audit -ResultsPath audit.json
./WELA.ps1 outgoing-ntlm -NtlmAction Plan -ResultsPath plan.json
./WELA.ps1 outgoing-ntlm -NtlmAction Configure -DryRun -ResultsPath preview.json
./WELA.ps1 outgoing-ntlm -NtlmAction Configure -Auto -BackupPath ./before -ResultsPath result.json
# Explicitly replace a previously reviewed Deny all value with auditing:
./WELA.ps1 outgoing-ntlm -NtlmAction Configure -OutgoingNtlmMode Audit -BackupPath ./before-reviewed -ResultsPath reviewed.json
```
The default `PreserveOrAudit` mode sets only DWORD **1 (Audit all)** when absent or DWORD0. Existing DWORD1 is already compliant. Existing DWORD **2 (Deny all)** is reported as `PreservedEnforcement` and skipped; exit0 for this preserved case does not mean auditing was enabled. Explicit `Audit` authorizes replacing a known DWORD2 with1. Unknown types/values fail without writes in either mode. `Deny` is refused by this scoped command. It never changes incoming/domain NTLM policy, exceptions, audit subcategories, channel settings, services, or authentication restrictions other than the explicit conversion of a known outgoing deny to audit.
Plan is a live read-only assessment, not an importable authorization file. Audit/Plan reject mutation options. Configure re-reads the actual host and typed value, journals before mutation, refuses pre-write drift, and verifies immediate/final readback. A race after the final pre-write read remains possible; these observations are not atomic with GPO or another administrator. RSoP is explicitly last-applied and potentially stale, never proof of the current registry writer. Skipped, Failed and Overridden results remain distinct. No automatic rollback occurs.
For manual recovery, inspect the selected successful result and its original `before.jsonl` entry. The original typed registry state is `Before.Policy`; preserve current policy ownership and review drift before restoring that one value/type or removing that value if it was originally absent. Never remove the parent MSV1_0 key or replay another journal kind. Failed/partial attempts require individual inspection. Keep the original journal and result together.
Native acceptance uses disposable unjoined Server2022/2025 hosts under PowerShell5.1/7, exercises actual absence/allow→audit, original journals, dry run, repeat, readback and exact cleanup. Existing enforcement and malformed values are never installed on a native runner merely for testing; portable regressions verify those preservation/refusal paths, prompt-time drift and failures. Native tests preserve incoming/domain policy, siblings/access descriptor, channels, service and all59 audit masks. Windows11/DC/ADCS acceptance, authentication behavior, representative NTLM events, GPO persistence and collector delivery remain separate work for #362. No Sigma credit is inferred. Built-in Windows only; Sysmon is excluded.
Microsoft distinguishes outgoing audit from deny, describes GPO precedence and identifies the NTLM Operational log for validation: [outgoing NTLM policy](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/network-security-restrict-ntlm-outgoing-ntlm-traffic-to-remote-servers).
+1 -1
View File
@@ -108,7 +108,7 @@ function Invoke-WelaConfigurationControl {
function Complete-WelaConfiguration {
param($Context, [string]$ResultsPath, $Plan,
[ValidateSet("native-windows-configuration", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only", "smb-audit-policies-only", "native-channel-settings-only", "wmi-namespace-sacl-only", "ad-object-sacl-only", "windows-powershell-transcription-policy-only", "wef-source-configuration-only", "wec-collector-subscriptions-only", "audit-integrity-local-policy-only", "adcs-audit-settings-only", "disabled-unlinked-gpo-creation-only")]
[ValidateSet("native-windows-configuration", "outgoing-ntlm-audit-policy-only", "advanced-audit-policy-only", "advanced-audit-policy-and-precedence", "firewall-text-logging-only", "event-log-size-and-mode-only", "smb-audit-policies-only", "native-channel-settings-only", "wmi-namespace-sacl-only", "ad-object-sacl-only", "windows-powershell-transcription-policy-only", "wef-source-configuration-only", "wec-collector-subscriptions-only", "audit-integrity-local-policy-only", "adcs-audit-settings-only", "disabled-unlinked-gpo-creation-only")]
[string]$Scope = "native-windows-configuration",
[string]$SuccessMessage = 'Configuration completed; all requested controls verified.')
if ($Context.PSObject.Properties['CustomProfileGuard']) {
+77
View File
@@ -0,0 +1,77 @@
# Explicit outgoing audit policy; does not invoke the broad configure workflow.
function Get-WelaOutgoingAuditSnapshot {
if ($env:OS -ne 'Windows_NT' -or -not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell on Windows.' }
$os=Get-CimInstance Win32_OperatingSystem -Property BuildNumber,ProductType -ErrorAction Stop
$computer=Get-CimInstance Win32_ComputerSystem -Property DomainRole,PartOfDomain -ErrorAction Stop
$build=[int]$os.BuildNumber;$product=[int]$os.ProductType
if (-not (($product -eq 1 -and $build -in @(22000,22621,22631,26100,26200)) -or ($product -in @(2,3) -and $build -in @(20348,26100)))) { throw 'This Windows role/build has not been reviewed for the scoped command.' }
if ($computer.DomainRole -notin @(0,1,2,3,4,5) -or $computer.PartOfDomain -isnot [bool]) {throw 'Computer role/join context is unavailable.'}
$policy=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' RestrictSendingNTLMTraffic
if (-not $policy.KeyExists) {throw 'The existing MSV1_0 policy key is required; no parent key will be created.'}
[pscustomobject][ordered]@{Host=[pscustomobject][ordered]@{Build=$build;ProductType=$product;DomainRole=[int]$computer.DomainRole;PartOfDomain=$computer.PartOfDomain};Policy=$policy}
}
function Get-WelaOutgoingAuditDisposition {
param($Snapshot,[ValidateSet('PreserveOrAudit','Audit')][string]$Mode)
$p=$Snapshot.Policy
if ($p.ValueExists -and ($p.Type -cne 'DWord' -or $p.Value -notin @(0,1,2))) {return 'Unknown'}
if ($p.ValueExists -and $p.Value -eq 1) {return 'AlreadyCompliant'}
if ($p.ValueExists -and $p.Value -eq 2 -and $Mode -eq 'PreserveOrAudit') {return 'PreservedEnforcement'}
return 'ChangeRequired'
}
function Get-WelaOutgoingAuditPlan {
param([ValidateSet('PreserveOrAudit','Audit')][string]$Mode='PreserveOrAudit')
try {
$snapshot=Get-WelaOutgoingAuditSnapshot
$status=Get-WelaOutgoingAuditDisposition $snapshot $Mode
$diagnostic=switch($status){
Unknown {'Unknown registry type/value is preserved; investigate it before configuration.'}
PreservedEnforcement {'Deny all (2) is authentication enforcement, preserved by default. Explicit -OutgoingNtlmMode Audit authorizes replacing it with Audit all (1).'}
AlreadyCompliant {'Audit all (1) is configured; authentication, events and policy persistence are unverified.'}
default {'Set only outgoing NTLM Audit all (DWORD 1).'}
}
[pscustomobject]@{Status=$status;Mode=$Mode;Desired=1;Before=$snapshot;Diagnostic=$diagnostic;PolicySource=Get-WelaOutgoingNtlmPolicySource}
}catch{[pscustomobject]@{Status='Unknown';Mode=$Mode;Desired=1;Before=$null;Diagnostic=$_.ToString();PolicySource='Unknown'}}
}
function Invoke-WelaOutgoingAuditCommand {
param([ValidateSet('Audit','Plan','Configure')][string]$Action='Audit',[ValidateSet('PreserveOrAudit','Audit')][string]$Mode='PreserveOrAudit',[switch]$Auto,[switch]$DryRun,[string]$BackupPath,[string]$ResultsPath)
if ($Action -ne 'Configure' -and ($Auto -or $DryRun -or $BackupPath)) {throw 'Consent, dry-run and backup options require Configure.'}
$plan=Get-WelaOutgoingAuditPlan $Mode
if ($Action -eq 'Configure') {
$context=New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath
$path='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0';$name='RestrictSendingNTLMTraffic'
if ($plan.Status -eq 'Unknown') {
$context.Results.Add([pscustomobject]@{Id="Registry/$path/$name";Kind='Registry';Target=@{Path=$path;Name=$name};Desired=@{Value=1;Type='DWord'};Before=$plan.Before;After=$null;Status='Failed';Diagnostic=$plan.Diagnostic})
}else{
$state=@{Observed=$null;PlannedHost=($plan.Before.Host|ConvertTo-Json -Compress);Mode=$Mode;Path=$path;Name=$name}
$read={param($s)
$snapshot=Get-WelaOutgoingAuditSnapshot
if (($snapshot.Host|ConvertTo-Json -Compress) -cne $s.PlannedHost) {throw 'Observed host context changed; review a new plan.'}
if ((Get-WelaOutgoingAuditDisposition $snapshot $s.Mode) -eq 'Unknown') {throw 'Unknown registry type/value is preserved.'}
$s.Observed=$snapshot
return $snapshot
}
$test={param($snapshot) $snapshot.Policy.ValueExists -and $snapshot.Policy.Type -ceq 'DWord' -and $snapshot.Policy.Value -eq 1}
$preserve=if($Mode -eq 'PreserveOrAudit'){{param($snapshot) if($snapshot.Policy.ValueExists -and $snapshot.Policy.Type -ceq 'DWord' -and $snapshot.Policy.Value -eq 2){'Preserved Deny all enforcement; explicit Audit mode is required to replace it.'}}}else{$null}
$apply={param($s)
$fresh=Get-WelaOutgoingAuditSnapshot
if (($fresh|ConvertTo-Json -Depth 8 -Compress) -cne ($s.Observed|ConvertTo-Json -Depth 8 -Compress)) {throw 'Outgoing NTLM state changed after the original journal snapshot; no write was attempted.'}
if ((Get-WelaOutgoingAuditDisposition $fresh $s.Mode) -ne 'ChangeRequired') {throw 'The current state no longer authorizes this write.'}
Set-ItemProperty -LiteralPath $s.Path -Name $s.Name -Value 1 -Type DWord -ErrorAction Stop
'Only outgoing NTLM Audit all (1) was requested; no authentication or event-generation test was performed.'
}
Invoke-WelaConfigurationControl -Context $context -Id "Registry/$path/$name" -Kind Registry -Target @{Path=$path;Name=$name} -Desired @{Value=1;Type='DWord'} -Read $read -Compliant $test -PreserveWhen $preserve -Apply $apply -CallbackState $state -Description $plan.Diagnostic
}
$report=Complete-WelaConfiguration -Context $context -Scope 'outgoing-ntlm-audit-policy-only' -SuccessMessage 'Outgoing NTLM configuration results recorded; inspect preserved/skipped controls separately.'
$report|Add-Member NoteProperty Plan $plan
}else{$report=[pscustomobject]@{ExitCode=$(if($plan.Status -eq 'Unknown'){1}else{0});Scope='outgoing-ntlm-audit-policy-only';Action=$Action;Plan=$plan}}
$report|Add-Member NoteProperty EventGeneration 'Not verified; registry compliance does not establish authentication, NTLM events, forwarding, GPO persistence or Sigma readiness.'
$report|Add-Member NoteProperty ReadyRuleCredit 0
if ($ResultsPath) {
try {$report|ConvertTo-Json -Depth 16|Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop}
catch {$report.ExitCode=1;Write-Host "[Failed] Writing outgoing NTLM results: $_" -ForegroundColor Red}
}
return $report
}
+18
View File
@@ -0,0 +1,18 @@
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent;$engine=(Get-Process -Id $PID).Path;$count=0
$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-ntlm-cli-'+[guid]::NewGuid().ToString('N'))
$cases=@(
@{Args=@('outgoing-ntlm','-Help');Code=0;Pattern='Changes only'},
@{Args=@('configure','-NtlmAction','Configure');Code=1;Pattern='requires outgoing-ntlm'},
@{Args=@('outgoing-ntlm','-OutgoingNtlmMode','Deny');Code=1;Pattern='enforcement is not accepted'},
@{Args=@('outgoing-ntlm','-Role','Client');Code=1;Pattern='dedicated options'},
@{Args=@('outgoing-ntlm','-Profile','wela-2.2.0');Code=1;Pattern='dedicated options'},
@{Args=@('outgoing-ntlm','-Auto');Code=1;Pattern='require NtlmAction Configure'},
@{Args=@('outgoing-ntlm','-DryRun');Code=1;Pattern='require NtlmAction Configure'},
@{Args=@('outgoing-ntlm','-BackupPath',$root);Code=1;Pattern='require NtlmAction Configure'},
@{Args=@('outgoing-ntlm','-Help','-ProviderAction','Configure');Code=1;Pattern='dedicated options'},
@{Args=@('outgoing-ntlm','-NtlmAction','Configure','-Typo');Code=1;Pattern='Unsupported trailing arguments'}
)
foreach($case in $cases){$prior=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$text=@(&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" @($case.Args) 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior};if(($code -eq 0) -ne ($case.Code -eq 0) -or ($text -join "`n") -notmatch $case.Pattern){throw "CLI failed: $($case.Args -join ' ') -> $code / $($text -join ' ')"};$count++}
if(Test-Path $root){throw 'Refused CLI input created unexpected output.'}
Write-Host "PASS: $count scoped outgoing NTLM CLI guards."
exit 0
+58
View File
@@ -0,0 +1,58 @@
$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/OutgoingNtlmAudit.ps1')
$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-ntlm-test-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
$count=0;$sequence=0
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
function Key($Value){ConvertTo-Json -InputObject $Value -Depth 12 -Compress}
function Reset($Value,$Type='DWord'){
$script:policy=[pscustomobject][ordered]@{KeyExists=$true;ValueExists=($null -ne $Value);Value=$Value;Type=$(if($null -ne $Value){$Type}else{$null})}
$script:writes=0;$script:reads=0;$script:failRead=$false;$script:failWrite=$false;$script:ignoreWrite=$false;$script:promptChange=$null;$script:onRead=$null
}
function Get-WelaOutgoingAuditSnapshot {
$script:reads++;if($script:onRead){& $script:onRead};if($script:failRead){throw 'Access denied'}
[pscustomobject][ordered]@{Host=[pscustomobject][ordered]@{Build=26100;ProductType=3;DomainRole=2;PartOfDomain=$false};Policy=($script:policy|ConvertTo-Json|ConvertFrom-Json)}
}
function Get-WelaOutgoingNtlmPolicySource {'Unknown (fixture has no RSoP ownership evidence)'}
function Set-ItemProperty {param($LiteralPath,$Name,$Value,$Type,$ErrorAction)
Assert ($LiteralPath -ceq 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' -and $Name -ceq 'RestrictSendingNTLMTraffic' -and $Value -eq 1 -and $Type -ceq 'DWord') 'Only the one exact audit-only target may be written.'
$script:writes++;if($script:failWrite){throw 'Write denied'};if(-not $script:ignoreWrite){$script:policy.ValueExists=$true;$script:policy.Value=1;$script:policy.Type='DWord'}
}
function Read-Host {param($Prompt) if($script:promptChange){& $script:promptChange};return 'Y'}
function Configure([string]$Mode='PreserveOrAudit',[switch]$DryRun,[switch]$Prompt){
$script:sequence++;$script:backup=Join-Path $root ('case-'+$script:sequence)
Invoke-WelaOutgoingAuditCommand -Action Configure -Mode $Mode -Auto:(-not $Prompt) -DryRun:$DryRun -BackupPath $script:backup
}
try{
foreach($initial in @($null,0,1)){
Reset $initial;$old=Key $script:policy;$r=Configure
Assert ($r.ExitCode -eq 0 -and $r.Scope -ceq 'outgoing-ntlm-audit-policy-only' -and $r.ReadyRuleCredit -eq 0) 'Public report scopes success to one policy, without detection credit.'
Assert ($script:policy.Value -eq 1 -and $script:writes -eq $(if($initial -eq 1){0}else{1})) 'Absent/allow are audited; existing audit is idempotent.'
if($initial -ne 1){$j=@(Get-Content (Join-Path $backup 'before.jsonl')|ConvertFrom-Json);Assert ($j.Count -eq 1 -and (Key $j[0].Before.Policy) -ceq $old) 'Typed original snapshot is durable before the one write.'}
else{Assert (-not(Test-Path (Join-Path $backup 'before.jsonl'))) 'Already configured mode does not journal a write.'}
}
Reset 2;$r=Configure;Assert ($script:writes -eq 0 -and $r.Results[0].Status -ceq 'Skipped' -and $r.Plan.Status -ceq 'PreservedEnforcement' -and $script:policy.Value -eq 2) 'Default mode preserves and identifies authentication enforcement.'
Reset 2;$r=Configure Audit;Assert ($script:writes -eq 1 -and $script:policy.Value -eq 1 -and $r.Results[0].Status -ceq 'Applied') 'Explicit audit mode authorizes replacing deny with auditing.'
foreach($value in @(42,'1')){foreach($mode in @('PreserveOrAudit','Audit')){
Reset $value $(if($value -is [string]){'String'}else{'DWord'});$r=Configure $mode
Assert ($r.ExitCode -eq 1 -and $script:writes -eq 0 -and $r.Results[0].Status -ceq 'Failed') 'Unknown values/types remain untouched even in explicit Audit mode.'
}}
Reset 0;$r=Configure -DryRun;Assert ($script:writes -eq 0 -and $r.DryRun -and -not(Test-Path $backup)) 'Dry run has no policy or journal-directory mutation.'
Reset 0;$script:failRead=$true;$r=Configure;Assert ($r.ExitCode -eq 1 -and $script:writes -eq 0) 'An unreadable policy fails closed.'
foreach($kind in @('failWrite','ignoreWrite')){
Reset 0;Set-Variable -Scope Script -Name $kind -Value $true;$r=Configure
Assert ($r.ExitCode -eq 1 -and $r.Results[0].Status -ceq 'Failed') 'Native failure and ignored-write readback cannot report success.'
}
foreach($changed in @(1,2,42)){
Reset 0;$script:changed=$changed;$script:promptChange={$script:policy.Value=$script:changed};$r=Configure -Prompt
Assert ($r.ExitCode -eq 1 -and $script:writes -eq 0 -and $script:policy.Value -eq $changed) 'Prompt-time drift refuses writes after preserving the exact original receipt.'
}
Reset 0;$script:onRead={if($script:reads -eq 5){$script:policy.Value=0}};$r=Configure
Assert ($script:writes -eq 1 -and $r.ExitCode -eq 1 -and $r.Results[0].Status -ceq 'Overridden') 'A later policy change fails final verification.'
Reset 0;$r=Invoke-WelaOutgoingAuditCommand -Action Plan;Assert ($r.Plan.Status -ceq 'ChangeRequired' -and $script:writes -eq 0) 'Plan is current-host assessment and does not mutate policy.'
foreach($action in @('Audit','Plan')){foreach($option in @('Auto','DryRun','BackupPath')){
$a=@{Action=$action};$a[$option]=$(if($option -eq 'BackupPath'){'unused'}else{$true});$threw=$false;try{Invoke-WelaOutgoingAuditCommand @a}catch{$threw=$true};Assert $threw 'Read-only actions reject mutation-only options.'
}}
}finally{Remove-Item -LiteralPath $root -Recurse -Force}
Write-Host "PASS: $count scoped outgoing NTLM assertions."
exit 0
+76
View File
@@ -0,0 +1,76 @@
param([switch]$AllowDisposableAuditWrite)
$ErrorActionPreference='Stop'
if(-not $AllowDisposableAuditWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit opt-in on a disposable GitHub-hosted Windows runner is required.'}
$repo=Split-Path $PSScriptRoot -Parent
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/OutgoingNtlmAudit.ps1')
Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force
Import-Module (Join-Path $repo 'modules/NativeProviders.psm1') -Force
$engine=(Get-Process -Id $PID).Path;$count=0;$failure=$null;$errors=@()
$root=Join-Path $env:RUNNER_TEMP ('wela-outgoing-audit-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
$path='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0';$name='RestrictSendingNTLMTraffic'
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
function Key($Value){ConvertTo-Json -InputObject $Value -Depth 24 -Compress}
function Save($Name,$Value){ConvertTo-Json -InputObject $Value -Depth 24|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8}
function Masks {$m=Get-WelaEffectiveAuditPolicy;@($m.Keys|Sort-Object|ForEach-Object{"$_=$($m[$_])"}) -join ';'}
function Other {
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64);$k=$null
try{
$k=$base.OpenSubKey('SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0');if(-not $k){throw 'Existing MSV1_0 key required.'}
$values=@($k.GetValueNames()|Sort-Object|Where-Object {$_ -ine $name}|ForEach-Object{[pscustomobject][ordered]@{Name=$_;Type=$k.GetValueKind($_).ToString();Value=$k.GetValue($_,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)}})
$children=@($k.GetSubKeyNames()|Sort-Object)
$security=if($PSVersionTable.PSVersion.Major -ge 6){[Microsoft.Win32.RegistryAclExtensions]::GetAccessControl($k)}else{$k.GetAccessControl()}
$acl=$security.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access -bor [Security.AccessControl.AccessControlSections]::Owner -bor [Security.AccessControl.AccessControlSections]::Group)
}finally{if($k){$k.Dispose()};$base.Dispose()}
[pscustomobject][ordered]@{Values=$values;Children=$children;Access=$acl;DomainPolicy=Get-WelaRegistryState 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' AuditNTLMInDomain;NtlmChannel=Get-WelaNativeChannel 'Microsoft-Windows-NTLM/Operational';SecurityChannel=Get-WelaNativeChannel Security;NetlogonService=[string](Get-Service Netlogon).Status}
}
function Public([string]$Label,[string[]]$Arguments){
$prior=$ErrorActionPreference
try{$ErrorActionPreference='Continue';$output=& $engine -NoLogo -NoProfile -NonInteractive -File (Join-Path $repo 'WELA.ps1') outgoing-ntlm @Arguments 2>&1|Out-String;$code=$LASTEXITCODE}finally{$ErrorActionPreference=$prior}
$output|Set-Content -LiteralPath (Join-Path $root ($Label+'.txt')) -Encoding UTF8
Assert ($code -eq 0) "Public $Label exited $code : $output"
Get-Content -Raw -LiteralPath (Join-Path $root ($Label+'.json'))|ConvertFrom-Json
}
$original=Get-WelaOutgoingAuditSnapshot
Assert ($original.Host.ProductType -eq 3 -and $original.Host.DomainRole -eq 2 -and -not $original.Host.PartOfDomain) 'Actual unjoined disposable Server is required.'
Assert (-not $original.Policy.ValueExists -or ($original.Policy.Type -ceq 'DWord' -and $original.Policy.Value -in @(0,1))) 'Fixture never replaces pre-existing enforcement or an unknown policy.'
$other=Other;$masks=Masks
Save 'original.json' @{Snapshot=$original;Unselected=$other;Masks=$masks;UBR=(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR;Engine=$PSVersionTable.PSVersion.ToString();Commit=$env:GITHUB_SHA}
try{
foreach($case in @('absent','allow')){
if((Get-WelaRegistryState $path $name).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop}
if($case -eq 'allow'){$null=New-ItemProperty -LiteralPath $path -Name $name -PropertyType DWord -Value 0}
$prepared=Get-WelaOutgoingAuditSnapshot
$plan=Public ($case+'-plan') @('-NtlmAction','Plan','-ResultsPath',(Join-Path $root ($case+'-plan.json')))
Assert ($plan.Plan.Status -ceq 'ChangeRequired' -and (Key $plan.Plan.Before) -ceq (Key $prepared)) 'Public plan retains the exact native absence/allow state and actual host.'
$dryBackup=Join-Path $root ($case+'-dry-backup')
$dry=Public ($case+'-dry') @('-NtlmAction','Configure','-Auto','-DryRun','-BackupPath',$dryBackup,'-ResultsPath',(Join-Path $root ($case+'-dry.json')))
Assert ($dry.DryRun -and $dry.Results[0].Status -ceq 'Skipped' -and -not(Test-Path $dryBackup) -and (Key (Get-WelaOutgoingAuditSnapshot)) -ceq (Key $prepared)) 'Dry run preserves policy and creates no journal directory.'
$backup=Join-Path $root ($case+'-backup')
$report=Public $case @('-NtlmAction','Configure','-Auto','-BackupPath',$backup,'-ResultsPath',(Join-Path $root ($case+'.json')))
$after=Get-WelaOutgoingAuditSnapshot
Assert ($report.Scope -ceq 'outgoing-ntlm-audit-policy-only' -and $report.Results.Count -eq 1 -and $report.Results[0].Status -ceq 'Applied') 'Exactly one native outgoing policy is applied through public CLI.'
Assert ($after.Policy.Type -ceq 'DWord' -and $after.Policy.Value -eq 1 -and (Key $report.Results[0].After) -ceq (Key $after)) 'Native audit-only readback matches the public result.'
$journal=@(Get-Content (Join-Path $backup 'before.jsonl')|ConvertFrom-Json)
Assert ($journal.Count -eq 1 -and (Key $journal[0].Before) -ceq (Key $prepared) -and $journal[0].Target.Path -ceq $path -and $journal[0].Target.Name -ceq $name) 'One original journal retains the actual typed policy and native context.'
$repeatBackup=Join-Path $root ($case+'-repeat-backup')
$repeat=Public ($case+'-repeat') @('-NtlmAction','Configure','-Auto','-BackupPath',$repeatBackup,'-ResultsPath',(Join-Path $root ($case+'-repeat.json')))
Assert ($repeat.Results[0].Status -ceq 'AlreadyCompliant' -and -not(Test-Path (Join-Path $repeatBackup 'before.jsonl'))) 'Repeated configuration is idempotent without another original journal.'
$audit=Public ($case+'-audit') @('-NtlmAction','Audit','-ResultsPath',(Join-Path $root ($case+'-audit.json')))
Assert ($audit.Plan.Status -ceq 'AlreadyCompliant' -and $audit.ReadyRuleCredit -eq 0 -and $audit.EventGeneration -like 'Not verified*') 'Audit distinguishes registry compliance from event or authentication proof.'
Assert ((Key (Other)) -ceq (Key $other) -and (Masks) -ceq $masks) 'Incoming/domain policies, siblings, access descriptor, channels, service and all59 masks remain unchanged.'
}
Save 'completed.json' @{Status='Passed';Assertions=$count;NativeWrites=2;Scope='Only outgoing audit DWORD1. No network authentication attempt, enforcement, event generation, GPO refresh or Sigma proof.'}
}catch{$failure=$_.ToString();throw}finally{
try{
if((Get-WelaRegistryState $path $name).ValueExists){Remove-ItemProperty -LiteralPath $path -Name $name -ErrorAction Stop}
if($original.Policy.ValueExists){$null=New-ItemProperty -LiteralPath $path -Name $name -Value $original.Policy.Value -PropertyType $original.Policy.Type}
}catch{$errors+=$_.ToString()}
$checks=[ordered]@{}
foreach($pair in @(@('Policy',{(Key (Get-WelaOutgoingAuditSnapshot)) -ceq (Key $original)}),@('Unselected',{(Key (Other)) -ceq (Key $other)}),@('All59Masks',{(Masks) -ceq $masks}))){try{$checks[$pair[0]]=& $pair[1]}catch{$checks[$pair[0]]=$false;$errors+=$_.ToString()}}
$complete=$errors.Count -eq 0 -and @($checks.Values|Where-Object {-not $_}).Count -eq 0
Save 'cleanup.json' @{Complete=$complete;Checks=$checks;Errors=$errors;Failure=$failure;Assertions=$count}
if(-not $complete){throw 'Outgoing NTLM native fixture cleanup failed.'}
}
Write-Host "PASS: $count native public outgoing NTLM assertions and exact cleanup."
exit 0
+2
View File
@@ -7,6 +7,8 @@
**改善:**
- `outgoing-ntlm` のAudit/Plan/Configureを追加し、送信NTLM監査DWORDを個別に設定できます。既存の拒否設定は既定で維持し、置換には明示的なAuditを要求します。不明な型・値や書込直前の変化を拒否し、元の型付き記録と再読取を保持します。Server 2022/2025のテストで範囲と復元を確認し、認証・イベント生成は未検証として報告します。(関連 #362) (@Shirofune-Security)
- コレクターのサブスクリプション観測で、コンソール経由の文字変換を、上限付きの完全なネイティブ名前列挙と厳密なUnicode XML読取に置き換えました。空の一覧・読取失敗・実際の無効状態を区別し、Unicodeの説明とXPathを保持します。Server 2022/2025と両PowerShellで公開Audit/Planおよび正確な後処理を検証し、ドメイン展開・転送・Sigma対応は主張しません。(関連 #368) (@Shirofune-Security)
- Server 2022/2025 の Windows PowerShell 5.1/PowerShell 7 で、公開 provider-packs コマンドの Plan、DryRun、Configure、冪等性、前提不足・手動対象の拒否、部分適用を実機検証する使い捨て CI を追加。DNS Client、CAPI2、WinRM、RDP Client の設定と復元記録・ハッシュを確認し、完全な ACL、保持モード、大きい既存バッファ、他チャネル、サービス、全監査マスクの保持とテスト後の正確な復元を検証。イベント生成や Sigma 対応の証明は含みません。 WinRM のマニフェスト ID をネイティブの Int64 として比較し、対象外の大きい ID により必要なイベントの確認が失敗する不具合も修正。 (@Shirofune-Security)
+2
View File
@@ -7,6 +7,8 @@
**Improvements:**
- Add `outgoing-ntlm` Audit/Plan/Configure to manage the outgoing audit DWORD independently of broad configuration. Preserve existing deny by default, require explicit Audit to replace it, refuse unknown types/values and pre-write drift, and retain typed original journals plus native readback. Native Server 2022/2025 tests verify narrow scope and exact cleanup; authentication/event generation remain unverified. (Related #362) (@Shirofune-Security)
- Fixed collector subscription observations to use complete bounded native name enumeration and strict Unicode XML reads instead of console decoding. Empty inventories, failed reads and actual disabled state remain distinct; Unicode descriptions and XPath are preserved. Disposable public Audit/Plan tests cover both Server 2022/2025 and PowerShell engines with exact cleanup, without domain deployment, forwarding or Sigma claims. (Related #368) (@Shirofune-Security)
- Added disposable native acceptance for public provider-pack Plan, DryRun, Configure, idempotence, missing/manual refusal and partial outcomes on Server 2022/2025 under Windows PowerShell 5.1/PowerShell 7. Actual DNS Client, CAPI2, WinRM and RDP Client configuration preserves descriptors, retention, larger buffers, unrelated channels, services and all audit masks, with journal/hash evidence and exact fixture cleanup; no event or Sigma credit. Fixed WinRM manifest inspection to preserve native Int64 event IDs, so unrelated large IDs no longer block the selected event schema. (@Shirofune-Security)