Merge commit '966aa21' into HEAD

# Conflicts:
#	CHANGELOG-Japanese.md
#	CHANGELOG.md
#	WELA.ps1
#	website/docs/resources/changelog.ja.md
#	website/docs/resources/changelog.md
This commit is contained in:
Shirofune-Security committed 2026-09-19 04:48:54 +09:00
commit e43a43bf34
11 files changed
+716 -75

No files matched your search

@@ -0,0 +1,36 @@
name: Native provider assessment regressions
on:
push:
branches: ['**']
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Test public output in Windows PowerShell 5.1
shell: powershell
run: ./tests/NativeProviders.Tests.ps1
- name: Test public output in PowerShell 7
shell: pwsh
run: ./tests/NativeProviders.Tests.ps1
- name: Read-only native smoke in Windows PowerShell 5.1
shell: powershell
run: ./tests/NativeProviders.Windows.Tests.ps1 -OutputDirectory "$env:RUNNER_TEMP/native-provider-ps51"
- name: Read-only native smoke in PowerShell 7
shell: pwsh
run: ./tests/NativeProviders.Windows.Tests.ps1 -OutputDirectory "$env:RUNNER_TEMP/native-provider-ps7"
- name: Preserve read-only native evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-provider-observations
path: |
${{ runner.temp }}/native-provider-ps51/native-assessment.json
${{ runner.temp }}/native-provider-ps51/native-assessment.html
${{ runner.temp }}/native-provider-ps7/native-assessment.json
${{ runner.temp }}/native-provider-ps7/native-assessment.html
if-no-files-found: warn
+1
View File
@@ -16,6 +16,7 @@
**バグ修正:**
- 通常の`configure`と`configure -Profile`で、詳細監査サブカテゴリを適用する前に`SCENoApplyLegacyAuditPolicy=1` (DWORD)の変更前の状態を記録し、設定後の値を検証するようにした。前提設定の変更に失敗した場合や変更を拒否した場合は、依存する書き込みを行わない。書き込み直前と最終確認で設定の変化を検出し、プロファイルの計画には現在の状態と取得可能な最終適用RSoP情報を含める。ポリシー更新後の永続性は保証しない。 (#393) (@Shirofune-Security)
- Windows標準チャネルを一律に`Enabled`と表示していた処理を、実際の有効状態・ログモード・ACLの読み取りとプロバイダーの前提条件の確認に置き換えた。AppLocker、NTLM、Defenderなどのイベント生成は検証されるまで条件付きとし、チャネルが有効なだけではルールを利用可能と判定しない。アクセス拒否・未登録の状態とソースの確認結果を保持するJSON/HTML監査レポート出力を追加した。ルールのチャネルパターンを具体的なカタログのチャネル名と照合する処理を、ルールの絞り込みとソースの対応付けで統一した。 (#395) (@Shirofune-Security)
- `configure`が既定で送信NTLM認証をブロックしていた問題を修正した。未設定またはAllow allの場合はAudit all (`RestrictSendingNTLMTraffic=1`)を設定し、既存のDeny all (`2`)や不明な値・型は維持する。拒否設定を明示的に監査へ変更するには`-OutgoingNtlmMode Audit`、ブロックを有効にするには`Deny`を指定する。書き込み前にポリシーを再確認し、変更後の値の検証、失敗の報告、確認できたポリシー状態と取得可能な最終適用RSoP情報の表示に対応した。 (#388) (@Shirofune-Security)
- `audit-settings`でホストの役割に適用されない監査ポリシーを`Not applicable`と表示し、カテゴリの有効・無効の集計から除外するようにした。NTLMポリシーの値は、DWORD型で保存されている場合にのみ有効な設定値として解釈・検証する。 (#392) (@Shirofune-Security)
- 設定時に外部コマンドの終了コードと変更後の設定値を確認し、処理の終了前にも再確認するようにした。書き込み失敗、設定の未反映、CAサービスの再起動失敗、最終確認時の設定の不一致を明示的に報告し、一律に成功とせず、0以外の終了コードを返すようにした。 (#392) (@Shirofune-Security)
+1
View File
@@ -18,6 +18,7 @@
**Bug Fixes:**
- Both `configure` paths now journal and verify `SCENoApplyLegacyAuditPolicy=1` (DWORD) before applying advanced audit subcategories. Failed or declined precedence changes block dependent writes; pre-write and final checks detect drift. Profile plans report precedence state and available last-applied RSoP evidence without claiming persistence through policy refresh. (#393) (@Shirofune-Security)
- Replaced static native-channel `Enabled` claims with actual channel state, mode and ACL reads plus provider prerequisite observations. AppLocker, NTLM, Defender and other native sources remain conditional until event generation is validated; channel enablement alone grants no usable-rule credit. Added JSON/HTML audit assessment exports preserving denied/absent states and source evidence. Rule channel patterns now match concrete catalog channels consistently during filtering and source mapping. (#395) (@Shirofune-Security)
- Fixed `configure` enabling outgoing NTLM blocking by default. It now sets Audit all (`RestrictSendingNTLMTraffic=1`) for unset or Allow policies while preserving existing Deny all (`2`) and unknown values/types. Use `-OutgoingNtlmMode Audit` to explicitly replace a deny policy, or `Deny` to enable blocking. Configuration rechecks policy before writing, verifies changes, reports failures, and displays the observed policy and available last-applied RSoP information. (#388) (@Shirofune-Security)
- `audit-settings` now reports role-inapplicable audit policies as `Not applicable` and excludes them from category enablement totals. NTLM policy values are interpreted and verified only when stored as DWORDs. (#392) (@Shirofune-Security)
- Configuration now checks native command exit codes, verifies settings after applying changes, and checks them again before finishing. Failed writes, ineffective changes, CA restart failures and settings that no longer match at the final check produce explicit results and a nonzero exit code instead of unconditional success. (#392) (@Shirofune-Security)
+71 -35
View File
@@ -17,6 +17,7 @@
[ValidateSet('Audit', 'Plan', 'Configure')][string]$FirewallAction = 'Audit',
[ValidateSet('Preserve', 'CisV4')][string]$FirewallPathMode = 'Preserve',
[ValidateRange(16384, 32767)][int]$FirewallMinimumSizeKiB = 16384,
[string]$HtmlPath,
[switch]$Help
)
@@ -33,6 +34,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
. (Join-Path $ScriptRoot "scripts/Configuration.ps1")
. (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1")
Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop
Import-Module (Join-Path $ScriptRoot "modules/NativeProviders.psm1") -ErrorAction Stop
# 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。
$PowerShellPolicyRoots = @(
@@ -46,6 +48,9 @@ class WELA {
[string] $SubCategory
[string] $CurrentSetting = ""
[string] $AuditPolicyGuid = ""
[string] $ChannelState = ""
[string] $GenerationReadiness = ""
[array] $NativeSources = @()
[array] $Rules
[hashtable] $RulesCount
[string] $DefaultSetting = ""
@@ -86,7 +91,7 @@ class WELA {
"std" {
# -contains は文字列に対しては完全一致なので、部分一致には -like を使う
$color = if ($this.CurrentSetting -eq "Enabled" -or $this.CurrentSetting -like "*Success*" -or $this.CurrentSetting -like "*Failure*") { "Green" }
elseif ($this.CurrentSetting -eq "Unknown") { "DarkYellow" }
elseif ($this.CurrentSetting -in @("Unknown", "Conditional", "Not installed", "Not applicable")) { "DarkYellow" }
else { "Red" }
$ruleCounts = ""
$logEnabled = $this.CurrentSetting
@@ -120,6 +125,12 @@ class WELA {
if ($this.CurrentSetting) {
Write-Host " - Current Setting: $($this.CurrentSetting)"
}
foreach ($source in $this.NativeSources) {
Write-Host " - Channel: $($source.Channel.Name); state: $($source.Channel.State); mode: $($source.Channel.LogMode)"
Write-Host " Provider readiness: $($source.Provider.Readiness); rule coverage: $($source.RuleCoverage)"
if ($source.Channel.Error) { Write-Host " Channel read: $($source.Channel.Error.Category): $($source.Channel.Error.Message)" }
if ($source.Provider.Error) { Write-Host " Provider read: $($source.Provider.Error.Category): $($source.Provider.Error.Message)" }
}
if ($this.RecommendedSetting) {
Write-Host " - Recommended Setting: $($this.RecommendedSetting)"
}
@@ -163,7 +174,12 @@ function RuleFilter {
if ($category_channels.Count -gt 0) {
$hasCriteria = $true
if (-not ($rule.channel | Where-Object { $category_channels -contains $_ })) {
if (-not ($rule.channel | Where-Object {
$ruleChannel = $_
# Catalog channels are concrete names/aliases; rule channels are patterns,
# matching the convention used by Get-WelaNativeSources.
$category_channels | Where-Object { $_ -like $ruleChannel }
})) {
return $false
}
}
@@ -368,6 +384,7 @@ function BuildAuditResult {
$auditpol = GetAuditpol
$auditResult = @()
$nativeCache = @{}
$sharedPlan = $null
if ($baselineName -eq 'YamatoSecurity') {
$context = Get-WelaSelectedContext
@@ -384,35 +401,18 @@ function BuildAuditResult {
}
# 現在の設定と、そのサブカテゴリ/チャネルが有効かどうかを決める
$nativeSources = @()
switch ($item.currentSetting.type) {
"static" {
$enabled = $true
$current = $item.currentSetting.value
"native-channel" {
# Channel availability is observed separately from event generation.
# Rule-specific source mappings are attached after filtering below.
$enabled = $false
$current = 'Unknown'
}
"auditpol" {
$enabled = $enabledguid -contains $item.select.guid
$current = $auditpol[$item.select.guid]
}
"channel" {
# レジストリの Enabled 値はマニフェストの既定値のままだと存在しないことがあり、
# 「値が無い」を無効と解釈すると既定で有効なチャネルを誤判定する。
# また役割未導入でチャネル自体が無い場合と無効化されている場合も区別できないため、
# 実際のチャネル状態を Get-WinEvent から取得する。
$logInfo = $null
try {
# Windows 以外や役割未導入の環境では取得できないので、その場合は判定不能とする
$logInfo = Get-WinEvent -ListLog $item.currentSetting.channel -ErrorAction Stop
} catch {
$logInfo = $null
}
if ($null -eq $logInfo) {
$enabled = $false
$current = "Unknown"
} else {
$enabled = [bool]$logInfo.IsEnabled
$current = if ($enabled) { "Enabled" } else { "Disabled" }
}
}
"registry" {
# 64bit/32bit でレジストリビューが分かれる設定があるため、いずれかで有効なら有効とみなす
$enabled = $false
@@ -437,20 +437,29 @@ function BuildAuditResult {
} else {
$eids = AsArray $item.select.eventIds
$channels = AsArray $item.select.channels
if ($item.currentSetting.type -eq 'native-channel') {
$channels = @($channels) + @($item.currentSetting.channels)
}
$guid = $item.select.guid
$rules = $all_rules | Where-Object { RuleFilter $_ $eids $channels $guid }
}
if ($item.currentSetting.type -eq 'native-channel') {
$nativeSources = @(Get-WelaNativeSources -Definition $item.currentSetting -Rules @($rules) -Cache $nativeCache)
$sourceRuleIds = @($nativeSources | ForEach-Object { $_.MappedRuleIds })
$rules = @($rules | Where-Object { $sourceRuleIds -contains $_.id })
$current = Get-WelaNativeSourceState -Sources $nativeSources
}
# 1つのルールは複数カテゴリに属しうるので、有効なカテゴリが1つでもあれば
# 利用可能とする(OR)。カテゴリごとに上書きすると最後のカテゴリで結果が決まってしまう。
if ($enabled) {
$rules | ForEach-Object { $_.applicable = $true }
}
if ($setting.ideal) {
if ($setting.ideal -and $item.currentSetting.type -ne 'native-channel') {
$rules | ForEach-Object { $_.ideal = $true }
}
$auditResult += [WELA]::New(
$entry = [WELA]::New(
$item.category,
$item.subCategory,
$current,
@@ -460,6 +469,13 @@ function BuildAuditResult {
$setting.volume,
$setting.note
)
$entry.NativeSources = $nativeSources
if ($nativeSources.Count) {
$entry.ChannelState = (@($nativeSources | ForEach-Object { $_.Channel.State } | Select-Object -Unique)) -join '; '
$entry.GenerationReadiness = (@($nativeSources | ForEach-Object { $_.Provider.Readiness } | Select-Object -Unique)) -join '; '
$entry.Note = ($entry.Note + ' Channel availability does not establish event generation. Native provider rule coverage remains unconfirmed; inspect source evidence and validate representative events.').Trim()
}
$auditResult += $entry
}
if ($sharedPlan) {
@@ -517,7 +533,9 @@ function AuditLogSetting {
param (
[string] $outType,
[string] $Baseline,
[switch] $debug
[switch] $debug,
[string] $ResultsPath,
[string] $HtmlPath
)
if (-not $debug -and -not (TestAdministrator)) {
@@ -597,7 +615,7 @@ function AuditLogSetting {
if ($outType -eq "std") {
$auditResult | Group-Object -Property Category | ForEach-Object {
$notEnabled = @("No Auditing", "Disabled", "Unknown")
$notEnabled = @("No Auditing", "Disabled", "Unknown", "Conditional", "Not installed")
$summaryRows = @($_.Group | Where-Object { $_.CurrentSetting -ne 'Not applicable' })
$enabledCount = ($summaryRows | Where-Object { $notEnabled -notcontains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum
$disabledCount = ($summaryRows | Where-Object { $notEnabled -contains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum
@@ -607,6 +625,10 @@ function AuditLogSetting {
$out = 'Not applicable'
$color = 'DarkYellow'
}
elseif (@($summaryRows | Where-Object { $_.NativeSources.Count -eq 0 }).Count -eq 0) {
$out = ($summaryRows | Select-Object -ExpandProperty CurrentSetting -Unique) -join '; '
$color = 'DarkYellow'
}
elseif (@($summaryRows | Where-Object { $_.Rules.Count -gt 0 }).Count -eq 0) {
# Configuration-only rows have no rule coverage to aggregate.
# Preserve their observed state, including applicability and errors.
@@ -637,7 +659,8 @@ function AuditLogSetting {
if ($enabledCount + $disabledCount -ne 0) {
$enabledPercentage = "({0:N2}%)" -f (($enabledCount / ($enabledCount + $disabledCount)) * 100)
}
if ($_.Name -notmatch "Powershell" -and $_.Name -notmatch "Security Advanced") {
if (($_.Name -notmatch "Powershell" -and $_.Name -notmatch "Security Advanced") -or
@($summaryRows | Where-Object { $_.NativeSources.Count -gt 0 }).Count -gt 0) {
$enabledPercentage = ""
}
Write-Host "$( $_.Name ): $out$($enabledPercentage)" -ForegroundColor $color
@@ -647,7 +670,7 @@ function AuditLogSetting {
Write-Host ""
}
} elseif ($outType -eq "table") {
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, DefaultSetting, CurrentSetting, RecommendedSetting, Volume | Format-Table
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, DefaultSetting, CurrentSetting, ChannelState, GenerationReadiness, RecommendedSetting, Volume | Format-Table
}
# 1つのルールが複数カテゴリに属するため、集計とCSVはルールID単位で重複排除する
@@ -661,19 +684,29 @@ function AuditLogSetting {
$currentJson = Join-Path $script:ScriptRoot "mitre-ttp-navigator-current.json"
$idealJson = Join-Path $script:ScriptRoot "mitre-ttp-navigator-ideal.json"
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, CurrentSetting, RecommendedSetting, Volume, Note | Export-Csv -Path $auditCsv -NoTypeInformation
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, CurrentSetting, ChannelState, GenerationReadiness, RecommendedSetting, Volume, Note,
@{ Name = 'NativeSourceEvidence'; Expression = { if ($_.NativeSources.Count) { ConvertTo-Json -InputObject $_.NativeSources -Depth 12 -Compress } else { '' } } } |
Export-Csv -Path $auditCsv -NoTypeInformation
$usableRules | Select-Object title, level, service, category, description, id | Export-Csv -Path $usableCsv -NoTypeInformation
$unUsableRules | Select-Object title, level, service, category, description, id | Export-Csv -Path $unusableCsv -NoTypeInformation
if ($ResultsPath -or $HtmlPath) {
Export-WelaAuditAssessment -Rows $auditResult -Rules @($uniqueRules) -Baseline $Baseline -ResultsPath $ResultsPath -HtmlPath $HtmlPath
}
if ($outType -eq "gui") {
$usableRules | Select-Object title, level, service, category, description, id | Out-GridView -Title "Usable Detection Rules"
$unUsableRules | Select-Object title, level, service, category, description, id | Out-GridView -Title "Unusable Detection Rules"
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, CurrentSetting, RecommendedSetting, Volume, Note | Out-GridView -Title "WELA Audit Result"
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, CurrentSetting, ChannelState, GenerationReadiness, RecommendedSetting, Volume, Note | Out-GridView -Title "WELA Audit Result"
}
Write-Output "Audit check result saved to: $auditCsv"
Write-Output "Usable detection rules list saved to: $usableCsv"
Write-Output "Unusable detection rules list saved to: $unusableCsv"
if ($ResultsPath) { Write-Output "Audit assessment JSON saved to: $ResultsPath" }
if ($HtmlPath) { Write-Output "Audit assessment HTML saved to: $HtmlPath" }
if (@($auditResult | Where-Object { $_.NativeSources.Count -gt 0 }).Count) {
Write-Host 'Native provider rules remain unconfirmed until event-specific generation is validated; enabled channels alone receive no usable-rule credit.' -ForegroundColor DarkYellow
}
Export-MitreHeatmap -sigmaRules $uniqueRules -OutputPath $currentJson
Write-Output "MITRE ATT&CK Navigator data(based on current settings) saved to: $currentJson"
@@ -1770,6 +1803,7 @@ Usage:
# -Profile changes advanced audit policy plus its precedence prerequisite. Optional controls need -IncludeOptional.
./WELA.ps1 audit-settings -Baseline YamatoSecurity # Audit current setting and show in stdout, save to csv
./WELA.ps1 audit-settings -Baseline ASD -OutType gui # Audit current setting and show in gui, save to csv
./WELA.ps1 audit-settings -Baseline YamatoSecurity -ResultsPath audit.json -HtmlPath audit.html
./WELA.ps1 audit-filesize -Baseline YamatoSecurity # Audit current file size and show in stdout, save to csv
./WELA.ps1 configure -Baseline YamatoSecurity # Configure audit settings based on the specified baseline
./WELA.ps1 configure -Baseline YamatoSecurity -Auto # Configure audit settings automatically without prompts
@@ -1820,11 +1854,13 @@ switch ($Cmd.ToLower()) {
if ($Help -or [string]::IsNullOrEmpty($Baseline)){
Write-Host "Audit current Windows Event Log settings and compare with baseline"
Write-Host ""
Write-Host "Usage: ./WELA.ps1 audit-settings -Baseline <YamatoSecurity|ASD|Microsoft_Client|Microsoft_Server> [-OutType <std|gui|table>]"
Write-Host "Usage: ./WELA.ps1 audit-settings -Baseline <YamatoSecurity|ASD|Microsoft_Client|Microsoft_Server> [-OutType <std|gui|table>] [-ResultsPath <json-file>] [-HtmlPath <html-file>]"
Write-Host ""
Write-Host "Options:"
Write-Host " -Baseline Specify the baseline (YamatoSecurity, ASD, Microsoft_Client, Microsoft_Server)"
Write-Host " -OutType Output type: std (default) or gui or table"
Write-Host " -ResultsPath Save JSON assessment including native channel/provider evidence"
Write-Host " -HtmlPath Save a self-contained HTML assessment with the same evidence"
Write-Host ""
return
}
@@ -1833,7 +1869,7 @@ switch ($Cmd.ToLower()) {
Write-Host "Invalid Guide specified. Valid options are: $($validGuides -join ', ')."
break
}
AuditLogSetting -outType $OutType -Baseline $Baseline -debug:$Debug
AuditLogSetting -outType $OutType -Baseline $Baseline -debug:$Debug -ResultsPath $ResultsPath -HtmlPath $HtmlPath
}
"audit-filesize" {
if ($Help){
+100 -40
View File
@@ -14,8 +14,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Application"
],
"provider": "Application"
}
},
{
@@ -34,8 +37,14 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-AppLocker/MSI and Script",
"Microsoft-Windows-AppLocker/EXE and DLL",
"Microsoft-Windows-AppLocker/Packaged app-Deployment",
"Microsoft-Windows-AppLocker/Packaged app-Execution"
],
"provider": "AppLocker"
}
},
{
@@ -51,8 +60,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-Bits-Client/Operational"
],
"provider": "BITS"
}
},
{
@@ -68,8 +80,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-CodeIntegrity/Operational"
],
"provider": "CodeIntegrity"
}
},
{
@@ -106,8 +121,11 @@
"guid": ""
},
"currentSetting": {
"type": "channel",
"channel": "Microsoft-Windows-DFSN-Server/Admin"
"type": "native-channel",
"channels": [
"Microsoft-Windows-DFSN-Server/Admin"
],
"provider": "DFSN"
}
},
{
@@ -123,8 +141,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-Diagnosis-Scripted/Operational"
],
"provider": "Diagnosis"
}
},
{
@@ -140,8 +161,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-DriverFrameworks-UserMode/Operational"
],
"provider": "DriverFrameworks"
}
},
{
@@ -157,8 +181,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-Windows Firewall With Advanced Security/Firewall"
],
"provider": "Firewall"
}
},
{
@@ -174,8 +201,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-NTLM/Operational"
],
"provider": "NTLM"
}
},
{
@@ -193,8 +223,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Windows PowerShell"
],
"provider": "PowerShellClassic"
}
},
{
@@ -258,8 +291,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-PrintService/Admin"
],
"provider": "PrintService"
}
},
{
@@ -275,8 +311,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-PrintService/Operational"
],
"provider": "PrintService"
}
},
{
@@ -815,13 +854,16 @@
"type": "filter",
"eventIds": [],
"channels": [
"Microsoft-Windows-Security-Mitigations*"
"Microsoft-Windows-Security-Mitigations/KernelMode"
],
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-Security-Mitigations/KernelMode"
],
"provider": "ExploitProtection"
}
},
{
@@ -832,13 +874,16 @@
"type": "filter",
"eventIds": [],
"channels": [
"Microsoft-Windows-Security-Mitigations*"
"Microsoft-Windows-Security-Mitigations/UserMode"
],
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-Security-Mitigations/UserMode"
],
"provider": "ExploitProtection"
}
},
{
@@ -854,8 +899,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-SmbClient/Security"
],
"provider": "SMBClient"
}
},
{
@@ -871,8 +919,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"System"
],
"provider": "System"
}
},
{
@@ -909,8 +960,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-TerminalServices-LocalSessionManager/Operational"
],
"provider": "TerminalServices"
}
},
{
@@ -926,8 +980,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-WMI-Activity/Operational"
],
"provider": "WMI"
}
},
{
@@ -943,8 +1000,11 @@
"guid": ""
},
"currentSetting": {
"type": "static",
"value": "Enabled"
"type": "native-channel",
"channels": [
"Microsoft-Windows-Windows Defender/Operational"
],
"provider": "Defender"
}
}
],
+53
View File
@@ -0,0 +1,53 @@
# Native provider assessment
`audit-settings -Baseline` observes native Windows channels and provider prerequisites instead of assuming that every registered provider is enabled. Sysmon and external telemetry are outside this assessment.
```powershell
./WELA.ps1 audit-settings -Baseline YamatoSecurity -ResultsPath audit.json -HtmlPath audit.html
```
The JSON and self-contained HTML reports preserve channel state, log mode, security descriptor (SDDL), maximum size, registered provider names, metadata read errors, provider observations and source-to-rule mappings. The existing audit CSV includes the same native evidence as a JSON field. Standard output shows channel and provider states; table/grid output includes channel state and generation readiness. No channel, ACL, service or provider policy is modified by these checks. The existing advanced-audit checks and report file writes still run.
`-ResultsPath` and `-HtmlPath` here apply to the ordinary `audit-settings -Baseline` assessment. `audit-settings -Profile` remains an advanced-audit-only view with its existing `-PlanPath` export.
## What the states mean
| Observation | Meaning |
| --- | --- |
| Channel `Enabled` / `Disabled` | Actual `Get-WinEvent -ListLog` `IsEnabled` value. |
| Channel `Not installed` | Windows reports no registration for that exact channel. This does not prove that every part of the associated product is absent. |
| Channel `Unknown` | A read failed, was denied, or did not return the required state. Error category, ID and message are retained. |
| Current setting `Conditional` | At least one channel is enabled, but provider and event-specific generation have not been validated. This is not an enabled-rule claim. |
| Provider `Unknown` | A required provider/status query was unavailable or failed. The channel observation is still retained separately. |
| Domain NTLM `Not applicable` | The host is confirmed not to be a domain controller. An unreadable role remains unknown. |
Each exact channel is read independently. An enabled AppLocker EXE/DLL channel does not hide a disabled MSI/Script channel. PowerShell's `pwsh` metadata alias for classic event 400 maps to `Windows PowerShell`; it does not prove PowerShell 7 logging. Catalog selectors use concrete channel names or metadata aliases. In both filtering and source mapping, rule channels are patterns matched against these concrete names. The Security-Mitigations catalog reads separate KernelMode and UserMode channels, mapping the existing wildcard rules to both without issuing broad wildcard channel queries or mapping a channel-specific rule to its sibling.
## Provider evidence and coverage limits
- AppLocker: Application Identity service state and GP effective rule collection types, modes and rule counts. Microsoft documents that `Get-AppLockerPolicy` cannot see policies deployed through the AppLocker CSP. Empty GP output therefore does not prove the absence of a policy. Shared channels can also contain App Control events. No AppLocker rules are created or modified.
- NTLM: separately observed outgoing and incoming registry values/types, host ProductType, and domain policy only on a DC. Audit modes, restrictions, exception lists and traffic directions are not interchangeable. A DWORD observation alone does not establish which rule events will occur.
- Defender: WinDefend service state, runtime mode, antivirus/real-time/behavior/network inspection flags. Passive mode, ASR, network protection and controlled folder access have different prerequisites. An enabled channel is not proof of active protection or of all Defender events.
- Other native providers: exact channel registration and available service state, including BITS, printing, WMI, Terminal Services, DFSN, Firewall and SMB client. Provider policy, activity and required event fields remain conditional. Application and System channels can receive events from many independent providers.
No rule receives current or ideal usable-rule credit merely because one of these channels is enabled. Their mapped rules remain in the complete, deduplicated corpus denominator and in `UnusableRules.csv` (meaning **not confirmed usable**, including conditional/unknown sources). A rule can still qualify through a separately assessed applicable source. The assessment does not claim a Sigma uplift or prove central ingestion. Existing advanced Security policy estimates retain their own SACL and other prerequisites.
This conservative change can lower the reported percentage. A future event-specific readiness model can promote individual native provider rules when all required policies, event types and fields have been validated; broad channel-level promotion would recreate the original problem.
## Validation and outstanding integration evidence
The regression suite uses the real catalog and public audit/CSV/JSON/HTML path with injected Windows reads. It exercises disabled, absent and denied channels, unreadable ACL metadata, AppLocker policy and sibling-channel states, NTLM role applicability, Defender passive mode, provider query failures, HTML escaping and stable rule denominators. Windows PowerShell 5.1 and PowerShell 7 CI also run read-only native smoke checks and retain their JSON/HTML artifacts. The release workflow already packages the complete `modules` directory, including `NativeProviders.psm1`.
Keep issue #366 open until isolated Windows 11, member server, DC and AD CS tests provide the requested evidence. On snapshots, record OS/build, WELA commit and effective provider policy; compare enabled and deliberately disabled test channels; capture absent-feature and access-denied reports; generate benign, representative provider events and inspect their XML fields. For forwarding deployments, separately confirm central ingestion. Restore the snapshots afterward. The hosted-runner read-only checks do not replace these event-generation and forwarding tests.
## Microsoft sources
- [Get-WinEvent channel metadata](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.diagnostics/get-winevent?view=powershell-7.6)
- [Get-AppLockerPolicy effective policy and CSP limitation](https://learn.microsoft.com/en-us/powershell/module/applocker/get-applockerpolicy?view=windowsserver2025-ps)
- [Application Identity service](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/app-control-for-business/applocker/configure-the-application-identity-service)
- [NTLM auditing prerequisites](https://learn.microsoft.com/en-us/defender-for-identity/deploy/event-collection-overview)
- [Incoming NTLM audit policy](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/network-security-restrict-ntlm-audit-incoming-ntlm-traffic)
- [Get-MpComputerStatus](https://learn.microsoft.com/en-us/powershell/module/defender/get-mpcomputerstatus?view=windowsserver2025-ps)
- [Defender active/passive modes](https://learn.microsoft.com/en-us/defender-endpoint/microsoft-defender-antivirus-compatibility)
- [Exploit protection and other protection event channels](https://learn.microsoft.com/en-us/defender-endpoint/attack-surface-reduction-windows-events)
- [Windows PowerShell classic event logging](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_eventlogs?view=powershell-5.1)
+206
View File
@@ -0,0 +1,206 @@
# Native Windows observations only. This module never changes policy, services or logs.
function Get-WelaNativeReadError {
param([System.Management.Automation.ErrorRecord]$Record)
[pscustomobject]@{
Id = $Record.FullyQualifiedErrorId
Category = [string]$Record.CategoryInfo.Category
Message = $Record.Exception.Message
}
}
function Get-WelaNativeChannel {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Name)
$result = [ordered]@{
Name = $Name; State = 'Unknown'; IsEnabled = $null
LogMode = $null; SecurityDescriptor = $null; MaximumSizeInBytes = $null
ProviderNames = @(); MetadataErrors = @{}; Error = $null
}
try {
# Catalog entries use exact names: an accessible sibling must not hide an error.
$logs = @(Get-WinEvent -ListLog $Name -ErrorAction Stop | Where-Object LogName -eq $Name)
if ($logs.Count -eq 0) {
$result.State = 'Not installed'
$result.Error = [pscustomobject]@{ Id = 'ChannelNotRegistered'; Category = 'ObjectNotFound'; Message = 'No matching channel registration was returned.' }
} elseif ($logs.Count -ne 1) {
throw "Multiple channel registrations returned for '$Name'."
} else {
$log = $logs[0]
foreach ($property in @('IsEnabled', 'LogMode', 'SecurityDescriptor', 'MaximumSizeInBytes', 'ProviderNames')) {
try {
$value = $log.$property
if ($null -eq $value) { throw "Channel metadata '$property' was not returned." }
$result[$property] = if ($property -in @('LogMode', 'SecurityDescriptor')) { [string]$value } else { $value }
} catch { $result.MetadataErrors[$property] = Get-WelaNativeReadError $_ }
}
if ($result.IsEnabled -is [bool]) { $result.State = if ($result.IsEnabled) { 'Enabled' } else { 'Disabled' } }
}
} catch {
$result.Error = Get-WelaNativeReadError $_
# Missing registration is different from denied access or an unavailable cmdlet.
if ($_.FullyQualifiedErrorId -match '^NoMatchingLogsFound(,|$)' -or
$_.Exception.GetType().FullName -eq 'System.Diagnostics.Eventing.Reader.EventLogNotFoundException') {
$result.State = 'Not installed'
}
}
[pscustomobject]$result
}
function Get-WelaNativeService {
param([string]$Name)
try {
$service = Get-Service -Name $Name -ErrorAction Stop
[pscustomobject]@{ Name = $Name; State = [string]$service.Status; Error = $null }
} catch {
$state = if ($_.FullyQualifiedErrorId -match '^NoServiceFoundForGivenName(,|$)') { 'Not installed' } else { 'Unknown' }
[pscustomobject]@{ Name = $Name; State = $state; Error = Get-WelaNativeReadError $_ }
}
}
function Get-WelaNativeRegistryValue {
param([string]$Path, [string]$Name)
$result = [ordered]@{ Path = $Path; Name = $Name; State = 'Unknown'; Type = $null; Value = $null; Error = $null }
try {
$key = Get-Item -LiteralPath $Path -ErrorAction Stop
if ($key.GetValueNames() -notcontains $Name) { $result.State = 'Not configured' }
else {
$result.Type = $key.GetValueKind($Name).ToString()
$result.Value = $key.GetValue($Name)
$result.State = if ($result.Type -eq 'DWord') { 'Observed' } else { 'Unknown' }
}
} catch {
$result.Error = Get-WelaNativeReadError $_
if ($_.CategoryInfo.Category -eq 'ObjectNotFound' -and $_.Exception -is [System.Management.Automation.ItemNotFoundException]) {
$result.State = 'Not configured'
}
}
[pscustomobject]$result
}
function Get-WelaNativeProvider {
[CmdletBinding()]
param([Parameter(Mandatory)][string]$Provider)
$result = [ordered]@{
Name = $Provider; Readiness = 'Conditional'; Evidence = @(); Observations = @{}
Error = $null; EventGenerationVerified = $false
}
switch ($Provider) {
'AppLocker' {
$result.Observations.Service = Get-WelaNativeService AppIDSvc
$result.Evidence += 'Application Identity service and GP effective rule collections are observed; AppLocker CSP policies are not visible to Get-AppLockerPolicy.'
try {
$xml = New-Object System.Xml.XmlDocument
$xml.XmlResolver = $null
$xml.LoadXml([string](Get-AppLockerPolicy -Effective -Xml -ErrorAction Stop))
if ($xml.DocumentElement.Name -ne 'AppLockerPolicy') { throw 'Unexpected AppLocker policy document.' }
$result.Observations.Collections = @($xml.AppLockerPolicy.RuleCollection | ForEach-Object {
[pscustomobject]@{
Type = [string]$_.Type; EnforcementMode = [string]$_.EnforcementMode
RuleCount = @($_.ChildNodes | Where-Object { $_.Name -in @('FilePathRule', 'FileHashRule', 'FilePublisherRule') }).Count
}
})
$result.Evidence += 'GP collection mode/rules do not establish per-rule event generation, effective CSP policy, or every collection in a shared channel.'
} catch { $result.Readiness = 'Unknown'; $result.Error = Get-WelaNativeReadError $_ }
}
'NTLM' {
$lsa = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0'
$result.Observations.Outgoing = Get-WelaNativeRegistryValue $lsa 'RestrictSendingNTLMTraffic'
$result.Observations.Incoming = Get-WelaNativeRegistryValue $lsa 'AuditReceivingNTLMTraffic'
try {
$productType = (Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop).ProductType
if ($productType -notin @(1, 2, 3)) { throw 'Windows ProductType could not be determined.' }
$result.Observations.ProductType = $productType
$result.Observations.Domain = if ($productType -eq 2) {
Get-WelaNativeRegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' 'AuditNTLMInDomain'
} else { [pscustomobject]@{ State = 'Not applicable'; Evidence = 'Domain NTLM auditing applies only to domain controllers.' } }
} catch { $result.Readiness = 'Unknown'; $result.Error = Get-WelaNativeReadError $_ }
$result.Evidence += 'Outgoing, incoming and domain policies cover different traffic. DWORD observations are not interchangeable; exceptions, authentication activity and event-specific prerequisites require validation.'
if (@($result.Observations.Values | Where-Object { $_.State -eq 'Unknown' }).Count) { $result.Readiness = 'Unknown' }
}
'Defender' {
$result.Observations.Service = Get-WelaNativeService WinDefend
try {
$status = Get-MpComputerStatus -ErrorAction Stop
if ($null -eq $status) { throw 'Defender status was not returned.' }
$result.Observations.Status = $status | Select-Object AMRunningMode, AMServiceEnabled, AntivirusEnabled, RealTimeProtectionEnabled, BehaviorMonitorEnabled, NISEnabled
} catch { $result.Readiness = 'Unknown'; $result.Error = Get-WelaNativeReadError $_ }
$result.Evidence += 'Active/passive mode and protection flags are observations, not proof of ASR, network protection, controlled folder access, or every Defender event prerequisite.'
}
'PowerShellClassic' {
$result.Evidence += 'Windows PowerShell classic channel only; PowerShell 7 and session-specific logging preferences are not inferred from this registration.'
}
default {
$serviceNames = @{
BITS = 'BITS'; PrintService = 'Spooler'; WMI = 'Winmgmt'; TerminalServices = 'TermService'
DFSN = 'Dfs'; Firewall = 'MpsSvc'; SMBClient = 'LanmanWorkstation'; TaskScheduler = 'Schedule'
}
if ($serviceNames.ContainsKey($Provider)) { $result.Observations.Service = Get-WelaNativeService $serviceNames[$Provider] }
$result.Evidence += 'Channel registration and service state do not prove provider policy, event-specific prerequisites, activity, or detection-rule fields.'
}
}
if ($result.Observations.Service -and $result.Observations.Service.State -eq 'Unknown') { $result.Readiness = 'Unknown' }
[pscustomobject]$result
}
function Get-WelaNativeSources {
[CmdletBinding()]
param([Parameter(Mandatory)]$Definition, [array]$Rules = @(), [hashtable]$Cache = @{})
$providerKey = 'provider:' + $Definition.provider
if (-not $Cache.ContainsKey($providerKey)) { $Cache[$providerKey] = Get-WelaNativeProvider $Definition.provider }
foreach ($channel in $Definition.channels) {
$channelKey = 'channel:' + $channel
if (-not $Cache.ContainsKey($channelKey)) { $Cache[$channelKey] = Get-WelaNativeChannel $channel }
$matchedRules = @($Rules | Where-Object {
$matchesSource = $false
foreach ($ruleChannel in $_.channel) {
if ($channel -like $ruleChannel -or ($ruleChannel -eq 'pwsh' -and $channel -eq 'Windows PowerShell')) { $matchesSource = $true }
}
$matchesSource
})
[pscustomobject]@{
Channel = $Cache[$channelKey]; Provider = $Cache[$providerKey]
MappedRuleIds = @($matchedRules | Select-Object -ExpandProperty id -Unique)
RuleCoverage = 'Unconfirmed'
Evidence = 'A mapped source is a candidate, not proof that the rule event and required fields will be generated. No usable-rule credit is assigned from channel enablement alone.'
}
}
}
function Get-WelaNativeSourceState {
param([array]$Sources)
$states = @($Sources | ForEach-Object { $_.Channel.State } | Select-Object -Unique)
if ($states.Count -eq 1 -and $states[0] -ne 'Enabled') { return $states[0] }
if ($states -contains 'Unknown' -or @($Sources | Where-Object { $_.Provider.Readiness -eq 'Unknown' }).Count) { return 'Unknown' }
if ($states -contains 'Enabled') { return 'Conditional' }
return 'Unknown'
}
function Export-WelaAuditAssessment {
[CmdletBinding()]
param([array]$Rows, [array]$Rules, [string]$Baseline, [string]$ResultsPath, [string]$HtmlPath)
$report = [pscustomobject][ordered]@{
SchemaVersion = 1; AssessedAtUtc = [DateTime]::UtcNow.ToString('o'); Baseline = $Baseline
Scope = 'Built-in Windows functionality; Sysmon and external telemetry are excluded.'
Coverage = [pscustomobject]@{
TotalRules = @($Rules).Count
UsableRules = @($Rules | Where-Object applicable -eq $true).Count
Note = 'Native channel/provider observations do not prove event generation or forwarding. Conditional and unknown sources receive no usable-rule credit. Other audit-policy estimates retain their documented prerequisites.'
}
Results = @($Rows | Select-Object Category, SubCategory, CurrentSetting, DefaultSetting, RecommendedSetting, RuleCount, ChannelState, GenerationReadiness, NativeSources, Note)
}
if ($ResultsPath) { $report | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
if ($HtmlPath) {
$parts = @('<!doctype html><html lang="en"><head><meta charset="utf-8"><title>WELA audit assessment</title><style>body{font:16px sans-serif;max-width:1100px;margin:2rem auto;padding:1rem}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#f4f4f4;padding:1rem}section{border-top:1px solid #ccc;margin-top:2rem}</style></head><body><h1>WELA audit assessment</h1>')
$parts += '<p>' + [System.Net.WebUtility]::HtmlEncode($report.Scope) + '</p>'
$parts += '<p>' + [System.Net.WebUtility]::HtmlEncode($report.Coverage.Note) + '</p>'
foreach ($row in $report.Results) {
$parts += '<section><h2>' + [System.Net.WebUtility]::HtmlEncode(($row.Category + ' / ' + $row.SubCategory)) + '</h2><pre>'
$parts += [System.Net.WebUtility]::HtmlEncode(($row | ConvertTo-Json -Depth 14))
$parts += '</pre></section>'
}
$parts += '</body></html>'
$parts -join "`n" | Set-Content -LiteralPath $HtmlPath -Encoding UTF8 -ErrorAction Stop
}
}
Export-ModuleMember -Function Get-WelaNativeChannel, Get-WelaNativeProvider, Get-WelaNativeSources, Get-WelaNativeSourceState, Export-WelaAuditAssessment
+217
View File
@@ -0,0 +1,217 @@
# Real catalog + public audit renderer/exports; Windows reads are injected at the OS boundary.
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot '../modules/NativeProviders.psm1') -Force
$module = Get-Module NativeProviders
& $module {
$script:channelReads = @()
$script:appLockerError = $false
$script:defenderError = $false
$script:channelStates = @{
'Microsoft-Windows-PrintService/Operational' = 'Disabled'
'Microsoft-Windows-Bits-Client/Operational' = 'Denied'
'Microsoft-Windows-DFSN-Server/Admin' = 'Absent'
'Microsoft-Windows-AppLocker/MSI and Script' = 'Disabled'
'Microsoft-Windows-Security-Mitigations/UserMode' = 'Disabled'
'System' = 'AclDenied'
}
function script:Get-WinEvent {
[CmdletBinding()]param($ListLog)
$script:channelReads += $ListLog
$state = $script:channelStates[$ListLog]
if ($state -eq 'Denied') {
$exception = [UnauthorizedAccessException]::new('Access denied <provider-test>')
$PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new($exception, 'UnauthorizedAccess', 'PermissionDenied', $ListLog))
}
if ($state -eq 'Absent') {
$PSCmdlet.ThrowTerminatingError([System.Management.Automation.ErrorRecord]::new([Exception]::new('No matching channel'), 'NoMatchingLogsFound', 'ObjectNotFound', $ListLog))
}
$log = [pscustomobject]@{
LogName = $ListLog; IsEnabled = ($state -ne 'Disabled'); LogMode = 'Circular'
SecurityDescriptor = 'O:BAG:SYD:(A;;0x1;;;SY)'; MaximumSizeInBytes = 1048576; ProviderNames = @('Fixture provider')
}
if ($state -eq 'AclDenied') {
$log.PSObject.Properties.Remove('SecurityDescriptor')
$log | Add-Member -MemberType ScriptProperty -Name SecurityDescriptor -Value { throw [UnauthorizedAccessException]::new('ACL denied') }
}
$log
}
function script:Get-Service { [CmdletBinding()]param($Name) [pscustomobject]@{ Name = $Name; Status = 'Running' } }
function script:Get-AppLockerPolicy {
[CmdletBinding()]param([switch]$Effective, [switch]$Xml)
if ($script:appLockerError) { throw [UnauthorizedAccessException]::new('AppLocker policy denied') }
'<AppLockerPolicy Version="1"><RuleCollection Type="Exe" EnforcementMode="AuditOnly"><FilePathRule Id="test" /></RuleCollection><RuleCollection Type="Script" EnforcementMode="NotConfigured" /></AppLockerPolicy>'
}
function script:Get-MpComputerStatus {
[CmdletBinding()]param()
if ($script:defenderError) { throw 'Defender cmdlet unavailable' }
[pscustomobject]@{ AMRunningMode = 'Passive'; AMServiceEnabled = $true; AntivirusEnabled = $false; RealTimeProtectionEnabled = $false; BehaviorMonitorEnabled = $false; NISEnabled = $false }
}
function script:Get-CimInstance { [CmdletBinding()]param($ClassName) [pscustomobject]@{ ProductType = 3 } }
function script:Get-Item {
[CmdletBinding()]param($LiteralPath)
$key = [pscustomobject]@{}
$key | Add-Member ScriptMethod GetValueNames { @('RestrictSendingNTLMTraffic', 'AuditReceivingNTLMTraffic', 'AuditNTLMInDomain') }
$key | Add-Member ScriptMethod GetValueKind { param($Name) 'DWord' }
$key | Add-Member ScriptMethod GetValue { param($Name) if ($Name -eq 'AuditNTLMInDomain') { 7 } elseif ($Name -eq 'AuditReceivingNTLMTraffic') { 2 } else { 1 } }
$key
}
}
$tokens = $null; $parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot '../WELA.ps1'), [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors | Out-String) }
$class = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.TypeDefinitionAst] -and $node.Name -eq 'WELA' }, $true)
. ([scriptblock]::Create($class.Extent.Text))
foreach ($name in @('AsArray', 'RuleFilter', 'ApplyRules', 'GetBaselineConfig', 'BuildAuditResult', 'AuditLogSetting')) {
$definition = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name }, $true)
. ([scriptblock]::Create($definition.Extent.Text))
}
$script:assertions = 0
function Assert-Equal($Actual, $Expected, [string]$Message) {
if ($Actual -cne $Expected) { throw "$Message. Expected '$Expected', got '$Actual'." }
$script:assertions++
}
function TestAdministrator { $true }
function CollectAuditpol { param([switch]$UseCached) $true }
function GetAuditpol { @{ '0CCE9215-69AE-11D9-BED3-505054503030' = 'Success' } }
function CheckRegistryValue { param($registryPath, $valueName, $expectedValue) $false }
function Get-WelaOutgoingNtlmState { [pscustomobject]@{ Description = 'Audit all (1)'; PolicySource = 'Injected observation' } }
function Get-WelaDomainNtlmState { [pscustomobject]@{ Description = 'Not applicable (member server)' } }
function Export-MitreHeatmap { param($sigmaRules, $OutputPath, $UseIdealCount) $script:heatmapRules = @($sigmaRules) }
$script:BaselineConfigPath = Join-Path $PSScriptRoot '../config/baselines.json'
$script:ScriptRoot = Join-Path ([IO.Path]::GetTempPath()) ('wela-native-provider-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $script:ScriptRoot
$script:SecurityRulesPath = Join-Path $script:ScriptRoot 'rules.json'
try {
# Exercise the real corpus's rule-side wildcard against the exact catalog
# selectors, through the public CSV/JSON/HTML assessment path.
$corpus = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/security_rules.json') -Raw | ConvertFrom-Json
# Explicitly enumerate the parsed array on Windows PowerShell 5.1 as well.
$wildcardRules = @($corpus | Where-Object { $_.channel -contains 'Microsoft-Windows-Security-Mitigations*' })
Assert-Equal ($wildcardRules.Count -gt 0) $true 'Real Security-Mitigations wildcard rules are present'
$fixtures = @(
@{ id = 'application'; channel = @('Application') }
@{ id = 'print'; channel = @('Microsoft-Windows-PrintService/Operational') }
@{ id = 'denied'; channel = @('Microsoft-Windows-Bits-Client/Operational') }
@{ id = 'absent'; channel = @('Microsoft-Windows-DFSN-Server/Admin') }
@{ id = 'app-exe'; channel = @('Microsoft-Windows-AppLocker/EXE and DLL') }
@{ id = 'app-script'; channel = @('Microsoft-Windows-AppLocker/MSI and Script') }
@{ id = 'ntlm'; channel = @('Microsoft-Windows-NTLM/Operational') }
@{ id = 'defender'; channel = @('Microsoft-Windows-Windows Defender/Operational') }
@{ id = 'classic'; channel = @('pwsh'); event_ids = @('400') }
@{ id = 'mitigation-kernel'; channel = @('Microsoft-Windows-Security-Mitigations/KernelMode') }
@{ id = 'mitigation-user'; channel = @('Microsoft-Windows-Security-Mitigations/UserMode') }
@{ id = 'security'; channel = @('sec'); subcategory_guids = @('0CCE9215-69AE-11D9-BED3-505054503030') }
)
foreach ($rule in $wildcardRules) {
$fixtures += @{ id = $rule.id; channel = @($rule.channel); event_ids = @($rule.event_ids) }
}
foreach ($rule in $fixtures) {
$rule.level = 'medium'; $rule.title = $rule.id
if (-not $rule.ContainsKey('event_ids')) { $rule.event_ids = @() }
if (-not $rule.ContainsKey('subcategory_guids')) { $rule.subcategory_guids = @() }
}
$fixtures | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $script:SecurityRulesPath -Encoding UTF8
$jsonPath = Join-Path $script:ScriptRoot 'assessment.json'
$htmlPath = Join-Path $script:ScriptRoot 'assessment.html'
$output = AuditLogSetting -outType std -Baseline Microsoft_Server -ResultsPath $jsonPath -HtmlPath $htmlPath 6>&1 | Out-String
$report = Get-Content -LiteralPath $jsonPath -Raw | ConvertFrom-Json
$rows = @($report.Results | Where-Object { $_.NativeSources.Count -gt 0 })
$sources = @($rows | ForEach-Object { $_.NativeSources })
$expectedDefinitions = @((GetBaselineConfig).catalog | Where-Object { $_.currentSetting.type -eq 'native-channel' })
Assert-Equal @((GetBaselineConfig).catalog | Where-Object { $_.currentSetting.type -eq 'static' }).Count 0 'No catalog entry retains a static Enabled claim'
Assert-Equal $rows.Count $expectedDefinitions.Count 'Every native catalog entry appears in public JSON output'
foreach ($definition in $expectedDefinitions) {
foreach ($name in $definition.currentSetting.channels) {
Assert-Equal ($sources.Channel.Name -contains $name) $true "Actual source $name is observed"
}
}
$print = $sources | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-PrintService/Operational' }
Assert-Equal $print.Channel.State 'Disabled' 'Former static PrintService channel is disabled'
Assert-Equal (($rows | Where-Object { $_.NativeSources.Channel.Name -contains 'Microsoft-Windows-PrintService/Operational' }).CurrentSetting) 'Disabled' 'Disabled state survives public report'
$denied = $sources | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-Bits-Client/Operational' }
Assert-Equal $denied.Channel.State 'Unknown' 'Access denied is unknown, never absent or disabled'
Assert-Equal $denied.Channel.Error.Category 'PermissionDenied' 'Access denied retains its reason'
$absent = $sources | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-DFSN-Server/Admin' }
Assert-Equal $absent.Channel.State 'Not installed' 'Missing registration is distinguished from denied access'
$app = $sources | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-AppLocker/EXE and DLL' }
Assert-Equal $app.Channel.State 'Enabled' 'AppLocker channel availability is separately visible'
Assert-Equal $app.Provider.Readiness 'Conditional' 'AppLocker channel and GP rules do not prove all event prerequisites'
Assert-Equal $app.Provider.Observations.Collections[0].EnforcementMode 'AuditOnly' 'Effective GP collection mode is retained'
Assert-Equal $app.Provider.Observations.Collections[0].RuleCount 1 'Effective GP rule count is retained'
Assert-Equal $app.Provider.Observations.Service.State 'Running' 'Application Identity service state is retained'
Assert-Equal ($app.Provider.Evidence -join ' ' -match 'CSP') $true 'CSP blind spot is explicit'
Assert-Equal ($app.MappedRuleIds -contains 'app-exe') $true 'EXE rule maps to its actual channel'
Assert-Equal ($app.MappedRuleIds -contains 'app-script') $false 'Enabled EXE channel does not inherit MSI/Script rules'
$scriptSource = $sources | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-AppLocker/MSI and Script' }
Assert-Equal $scriptSource.Channel.State 'Disabled' 'Disabled sibling channel remains disabled'
Assert-Equal ($scriptSource.MappedRuleIds -contains 'app-script') $true 'Disabled sibling retains its source mapping'
$ntlm = $sources | Where-Object { $_.Provider.Name -eq 'NTLM' }
Assert-Equal $ntlm.Provider.Observations.Outgoing.Type 'DWord' 'NTLM switch type retained'
Assert-Equal $ntlm.Provider.Observations.Outgoing.Value 1 'Outgoing NTLM switch observed separately'
Assert-Equal $ntlm.Provider.Observations.Incoming.Value 2 'Incoming NTLM switch observed separately'
Assert-Equal $ntlm.Provider.Observations.Domain.State 'Not applicable' 'Domain NTLM applies only to DCs'
$defender = $sources | Where-Object { $_.Provider.Name -eq 'Defender' }
Assert-Equal $defender.Provider.Observations.Status.AMRunningMode 'Passive' 'Defender passive mode is preserved'
Assert-Equal $defender.Provider.Observations.Status.RealTimeProtectionEnabled $false 'Disabled Defender protection remains explicit'
Assert-Equal $defender.Provider.Readiness 'Conditional' 'Defender event generation is not inferred from a channel'
Assert-Equal $app.Channel.LogMode 'Circular' 'Channel log mode survives JSON'
Assert-Equal $app.Channel.SecurityDescriptor 'O:BAG:SYD:(A;;0x1;;;SY)' 'Channel ACL survives JSON'
$system = $sources | Where-Object { $_.Channel.Name -eq 'System' }
Assert-Equal $system.Channel.State 'Enabled' 'Unreadable ACL does not erase separately observed channel state'
Assert-Equal ($null -ne $system.Channel.MetadataErrors.SecurityDescriptor) $true 'Unreadable ACL metadata is preserved'
$classic = $sources | Where-Object { $_.Channel.Name -eq 'Windows PowerShell' }
Assert-Equal ($classic.MappedRuleIds -contains 'classic') $true 'Classic PowerShell alias maps to its real channel'
$kernel = $sources | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-Security-Mitigations/KernelMode' }
$user = $sources | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-Security-Mitigations/UserMode' }
Assert-Equal ($kernel.MappedRuleIds -contains 'mitigation-kernel') $true 'Exact mitigation channel matches its concrete catalog selector'
Assert-Equal ($kernel.MappedRuleIds -contains 'mitigation-user') $false 'Kernel mitigation source does not inherit user-mode rules'
Assert-Equal ($user.MappedRuleIds -contains 'mitigation-user') $true 'User mitigation source retains its exact rule'
Assert-Equal ($user.MappedRuleIds -contains 'mitigation-kernel') $false 'User mitigation source does not inherit kernel rules'
foreach ($rule in $wildcardRules) {
Assert-Equal ($kernel.MappedRuleIds -contains $rule.id) $true 'Rule-side wildcard maps to the concrete KernelMode source'
Assert-Equal ($user.MappedRuleIds -contains $rule.id) $true 'Rule-side wildcard maps to the concrete UserMode source'
Assert-Equal (RuleFilter $rule @('999') @($kernel.Channel.Name) '') $false 'A wildcard channel match still requires the selected event ID'
Assert-Equal (RuleFilter $rule @($rule.event_ids[0]) @($kernel.Channel.Name) '') $true 'Matching channel and event ID pass together'
}
foreach ($source in @($kernel, $user)) {
Assert-Equal (($rows | Where-Object { $_.NativeSources.Channel.Name -contains $source.Channel.Name }).RuleCount) (1 + $wildcardRules.Count) 'Each category includes exact and wildcard rules without unrelated sibling rules'
}
Assert-Equal @(& $module { $script:channelReads | Where-Object { $_ -match '[*?]' } }).Count 0 'Wildcard rules never trigger wildcard native channel reads'
Assert-Equal $report.Coverage.TotalRules $fixtures.Count 'Rules mapped to both native sources appear once in the full denominator'
Assert-Equal $report.Coverage.UsableRules 1 'Only the independently enabled Security source receives usable credit'
Assert-Equal @(Import-Csv (Join-Path $script:ScriptRoot 'UsableRules.csv')).Count 1 'Usable CSV matches conservative JSON coverage'
Assert-Equal @(Import-Csv (Join-Path $script:ScriptRoot 'UnusableRules.csv')).Count ($fixtures.Count - 1) 'Unconfirmed native rules are retained once in CSV'
Assert-Equal @($script:heatmapRules | Where-Object { $_.id -ne 'security' -and ($_.applicable -or $_.ideal) }).Count 0 'No current or ideal native provider uplift is fabricated'
$html = Get-Content -LiteralPath $htmlPath -Raw
Assert-Equal ($html -match 'Not installed') $true 'HTML retains absent-feature state'
Assert-Equal ($html -match 'PermissionDenied') $true 'HTML retains denied-access reason'
# Windows PowerShell JSON escapes angle brackets as Unicode; PowerShell 7 may
# leave them for HtmlEncode. Both must safely preserve the original evidence.
$htmlRows = @([regex]::Matches($html, '(?s)<pre>(.*?)</pre>') | ForEach-Object {
[System.Net.WebUtility]::HtmlDecode($_.Groups[1].Value) | ConvertFrom-Json
})
$htmlErrorText = ($htmlRows | ForEach-Object { $_.NativeSources.Channel.Error.Message }) -join ' '
Assert-Equal ($htmlErrorText -match '<provider-test>') $true 'HTML encoding preserves untrusted error text'
Assert-Equal ($html -match '<provider-test>') $false 'HTML never interprets error text as markup'
Assert-Equal ($output -match '(?m)^Applocker: Conditional') $true 'Console does not summarize AppLocker as enabled'
Assert-Equal ($output -match 'Native provider rules remain unconfirmed') $true 'Console explains conservative coverage'
$csv = @(Import-Csv (Join-Path $script:ScriptRoot 'WELA-Audit-Result.csv'))
Assert-Equal (($csv | Where-Object Category -eq 'Applocker').NativeSourceEvidence -match 'SecurityDescriptor') $true 'CSV preserves structured native evidence'
& $module { $script:appLockerError = $true; $script:defenderError = $true }
$null = AuditLogSetting -outType table -Baseline Microsoft_Server -ResultsPath $jsonPath -HtmlPath $htmlPath
$failed = Get-Content -LiteralPath $jsonPath -Raw | ConvertFrom-Json
$failedSources = @($failed.Results | ForEach-Object { $_.NativeSources })
foreach ($name in @('AppLocker', 'Defender')) {
$provider = @($failedSources | Where-Object { $_.Provider.Name -eq $name })[0].Provider
Assert-Equal $provider.Readiness 'Unknown' "$name provider read failures are not confused with ready state"
Assert-Equal ([bool]$provider.Error.Message) $true "$name provider read failure retains evidence"
Assert-Equal @($failed.Results | Where-Object { $_.NativeSources.Provider.Name -contains $name -and $_.CurrentSetting -ne 'Unknown' }).Count 0 "$name read failure is visible in the row state"
}
Assert-Equal $failed.Coverage.UsableRules 1 'Provider read failures do not inflate rule coverage'
Write-Host "PASS: $script:assertions native provider/output assertions; no Windows settings changed."
} finally {
Remove-Item -LiteralPath $script:ScriptRoot -Recurse -Force
Remove-Module NativeProviders
}
+29
View File
@@ -0,0 +1,29 @@
param([string]$OutputDirectory = (Join-Path ([IO.Path]::GetTempPath()) ('wela-native-smoke-' + [guid]::NewGuid().ToString('N'))))
$ErrorActionPreference = 'Stop'
if ($env:OS -ne 'Windows_NT') { throw 'This read-only smoke test requires Windows.' }
$null = New-Item -ItemType Directory -Path $OutputDirectory -Force
# Loading version defines the actual public audit path without configuring Windows.
. (Join-Path $PSScriptRoot '../WELA.ps1') -Cmd version
$script:ScriptRoot = $OutputDirectory
$script:AuditpolTxtPath = Join-Path $OutputDirectory 'auditpol.txt'
$json = Join-Path $OutputDirectory 'native-assessment.json'
$html = Join-Path $OutputDirectory 'native-assessment.html'
$null = AuditLogSetting -outType table -Baseline Microsoft_Server -ResultsPath $json -HtmlPath $html
if (-not (Test-Path -LiteralPath $json)) { throw 'The public audit command did not export an assessment.' }
$report = Get-Content -LiteralPath $json -Raw | ConvertFrom-Json
$sources = @($report.Results | ForEach-Object { $_.NativeSources })
$application = @($sources | Where-Object { $_.Channel.Name -eq 'Application' })
if ($application.Count -ne 1) { throw 'Expected exactly one Application channel observation.' }
$actual = Get-WinEvent -ListLog Application -ErrorAction Stop
if ($application[0].Channel.IsEnabled -ne $actual.IsEnabled -or
$application[0].Channel.LogMode -ne [string]$actual.LogMode -or
$application[0].Channel.SecurityDescriptor -ne $actual.SecurityDescriptor) {
throw 'Application channel evidence does not match the native read-only API.'
}
if (@($sources | Where-Object { $_.Provider.EventGenerationVerified -ne $false -or $_.RuleCoverage -ne 'Unconfirmed' }).Count) {
throw 'Read-only channel observations must not claim validated event generation.'
}
$missing = Get-WelaNativeChannel -Name ('WELA-Not-Registered-' + [guid]::NewGuid().ToString('N'))
if ($missing.State -ne 'Not installed' -or -not $missing.Error) { throw 'A missing real Windows channel must retain its absent-registration evidence.' }
Write-Host "PASS: real Windows channel metadata, absent-channel classification and public JSON/HTML export. Evidence: $OutputDirectory"
Write-Host 'This smoke test does not validate event generation on Windows 11, domain controllers or AD CS, nor central ingestion.'
+1
View File
@@ -19,6 +19,7 @@
**バグ修正:**
- 通常の`configure`と`configure -Profile`で、詳細監査サブカテゴリを適用する前に`SCENoApplyLegacyAuditPolicy=1` (DWORD)の変更前の状態を記録し、設定後の値を検証するようにした。前提設定の変更に失敗した場合や変更を拒否した場合は、依存する書き込みを行わない。書き込み直前と最終確認で設定の変化を検出し、プロファイルの計画には現在の状態と取得可能な最終適用RSoP情報を含める。ポリシー更新後の永続性は保証しない。 (#393) (@Shirofune-Security)
- Windows標準チャネルを一律に`Enabled`と表示していた処理を、実際の有効状態・ログモード・ACLの読み取りとプロバイダーの前提条件の確認に置き換えた。AppLocker、NTLM、Defenderなどのイベント生成は検証されるまで条件付きとし、チャネルが有効なだけではルールを利用可能と判定しない。アクセス拒否・未登録の状態とソースの確認結果を保持するJSON/HTML監査レポート出力を追加した。ルールのチャネルパターンを具体的なカタログのチャネル名と照合する処理を、ルールの絞り込みとソースの対応付けで統一した。 (#395) (@Shirofune-Security)
- `configure`が既定で送信NTLM認証をブロックしていた問題を修正した。未設定またはAllow allの場合はAudit all (`RestrictSendingNTLMTraffic=1`)を設定し、既存のDeny all (`2`)や不明な値・型は維持する。拒否設定を明示的に監査へ変更するには`-OutgoingNtlmMode Audit`、ブロックを有効にするには`Deny`を指定する。書き込み前にポリシーを再確認し、変更後の値の検証、失敗の報告、確認できたポリシー状態と取得可能な最終適用RSoP情報の表示に対応した。 (#388) (@Shirofune-Security)
- `audit-settings`でホストの役割に適用されない監査ポリシーを`Not applicable`と表示し、カテゴリの有効・無効の集計から除外するようにした。NTLMポリシーの値は、DWORD型で保存されている場合にのみ有効な設定値として解釈・検証する。 (#392) (@Shirofune-Security)
- 設定時に外部コマンドの終了コードと変更後の設定値を確認し、処理の終了前にも再確認するようにした。書き込み失敗、設定の未反映、CAサービスの再起動失敗、最終確認時の設定の不一致を明示的に報告し、一律に成功とせず、0以外の終了コードを返すようにした。 (#392) (@Shirofune-Security)
+1
View File
@@ -21,6 +21,7 @@
**Bug Fixes:**
- Both `configure` paths now journal and verify `SCENoApplyLegacyAuditPolicy=1` (DWORD) before applying advanced audit subcategories. Failed or declined precedence changes block dependent writes; pre-write and final checks detect drift. Profile plans report precedence state and available last-applied RSoP evidence without claiming persistence through policy refresh. (#393) (@Shirofune-Security)
- Replaced static native-channel `Enabled` claims with actual channel state, mode and ACL reads plus provider prerequisite observations. AppLocker, NTLM, Defender and other native sources remain conditional until event generation is validated; channel enablement alone grants no usable-rule credit. Added JSON/HTML audit assessment exports preserving denied/absent states and source evidence. Rule channel patterns now match concrete catalog channels consistently during filtering and source mapping. (#395) (@Shirofune-Security)
- Fixed `configure` enabling outgoing NTLM blocking by default. It now sets Audit all (`RestrictSendingNTLMTraffic=1`) for unset or Allow policies while preserving existing Deny all (`2`) and unknown values/types. Use `-OutgoingNtlmMode Audit` to explicitly replace a deny policy, or `Deny` to enable blocking. Configuration rechecks policy before writing, verifies changes, reports failures, and displays the observed policy and available last-applied RSoP information. (#388) (@Shirofune-Security)
- `audit-settings` now reports role-inapplicable audit policies as `Not applicable` and excludes them from category enablement totals. NTLM policy values are interpreted and verified only when stored as DWORDs. (#392) (@Shirofune-Security)
- Configuration now checks native command exit codes, verifies settings after applying changes, and checks them again before finishing. Failed writes, ineffective changes, CA restart failures and settings that no longer match at the final check produce explicit results and a nonzero exit code instead of unconditional success. (#392) (@Shirofune-Security)