Gate historical controls and require evidence for Windows defaults (#409)

* Gate historical controls and require provenance for Windows defaults

* Bind default evidence to UTC provenance and native architecture

* Reference PR 409 in applicability changelogs
This commit is contained in:
田中ザック Isaac Mathis authored and GitHub committed 2026-09-20 14:00:10 +09:00
1 parent d35b1374d0
commit 14ac8667d4
13 files changed
+666 -6

No files matched your search

+9
View File
@@ -2,5 +2,14 @@
/config/security_rules.json text eol=lf
/config/eid_subcategory_mapping.csv text eol=lf
/config/rule_eligibility_manifest.json text eol=lf
# Exact-context default evidence pins these source/collector bytes.
/config/baselines.json text eol=lf
/config/audit_profiles.json text eol=lf
/config/control_applicability.json text eol=lf
/scripts/ControlApplicability.ps1 text eol=lf
/scripts/Configuration.ps1 text eol=lf
/modules/NativeProviders.psm1 text eol=lf
/modules/AuditProfiles.psm1 text eol=lf
# Full upstream rule artifacts retain their exact pinned bytes on every platform.
/config/provider_rule_sources/*.yml -text whitespace=-blank-at-eol
@@ -0,0 +1,29 @@
name: Control applicability and default evidence
on:
push:
branches: ['**']
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
control-applicability:
strategy:
fail-fast: false
matrix:
os: [windows-2022, windows-2025]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Fixtures on Windows PowerShell 5.1
shell: powershell
run: ./tests/ControlApplicability.Tests.ps1
- name: Native read-only capture on Windows PowerShell 5.1
shell: powershell
run: ./tests/ControlApplicability.Windows.Tests.ps1
- name: Fixtures on PowerShell 7
shell: pwsh
run: ./tests/ControlApplicability.Tests.ps1
- name: Native read-only capture on PowerShell 7
shell: pwsh
run: ./tests/ControlApplicability.Windows.Tests.ps1
+1
View File
@@ -4,6 +4,7 @@
**改善:**
- 読み取り専用の`control-applicability`を追加し、旧CISのApplication Guard監査要件を保持しつつ、Windows 11 24H2以降では削除済み・対象外と表示します。`default-evidence`で正確なビルド・パッチ・ドメイン参加・役割を含む現状を記録し、出典とレビュー情報が一致する参照環境と比較できます。旧ベースラインの既定値は履歴情報として保持し、未検証の既定値はUnknownと表示します。合成テストと読み取り専用CIからクリーンインストールやイベント生成の証明は行いません。 (#409) (@Shirofune-Security)
- Windows標準のDNS Client/Server、CAPI2、WinRM、RDP Clientについて、明示的に選択するprovider-packsの監査・計画とチャネル設定を追加しました。固定した完全なルール定義と実際のプロバイダー・チャネル・イベントスキーマを確認し、DNSのチャネル名不一致や不明な前提条件を保持します。復旧記録付きの変更では大きいバッファ・保持方式・ACLを維持し、Analyticalと従来のDNSログは手動確認のみとします。イベント生成・バックエンド検証や検知範囲の向上は未確認です。 (#411) (@Shirofune-Security)
- 任意実行の`audit-notifications`を追加し、OneSettings監査とSecurityログ警告しきい値の監査・計画・設定に対応しました。対象とチャネル変更の明示指定、OS・ADMX確認、型付き復旧記録と変更検出を行い、既存の低いしきい値とチャネルACL・保存方式を保持します。ポリシー一致と実イベント生成を分け、Windowsラボの証拠とSigma利用可能性は未検証と表示します。 (#408) (@Shirofune-Security)
+1
View File
@@ -4,6 +4,7 @@
**Improvements:**
- Added read-only `control-applicability` for the historical CIS Application Guard audit requirement, reporting removal on Windows 11 24H2+ without remediation. Added exact build/patch/join/role native snapshots and provenance-bound reference comparison through `default-evidence`. Legacy baseline default strings are retained as historical hints while public defaults remain Unknown without reviewed scenario evidence. Synthetic fixtures and native read-only CI do not claim clean-install or event-generation proof. (#409) (@Shirofune-Security)
- Added explicit native DNS Client/Server, CAPI2, WinRM and RDP Client provider-pack inventory and selective channel configuration. Pinned full rule definitions and live provider/channel/event schemas retain DNS channel mismatches and unknown prerequisites; journaled opt-in changes preserve larger buffers, retention and ACLs. Analytical/classic DNS remain manual-only, and no event/backend readiness uplift is claimed. (#411) (@Shirofune-Security)
- Added opt-in `audit-notifications` audit/plan/configure for OneSettings auditing and Security log warning thresholds, with explicit control/channel selections, reviewed host and ADMX gates, typed recovery journals and drift checks. Earlier warning thresholds and channel ACL/retention are preserved. Reports separate policy matches from warning/event generation; Windows lab evidence and Sigma eligibility remain unverified. (#408) (@Shirofune-Security)
+35 -5
View File
@@ -49,6 +49,8 @@
[ValidateRange(1,2147483647)][int]$LdapSearchTimeMs,
[ValidateRange(1,2147483647)][int]$LdapExpensiveThreshold,
[ValidateRange(1,2147483647)][int]$LdapInefficientThreshold,
[ValidateSet('Capture','Compare')][string]$DefaultEvidenceAction = 'Capture',
[string]$DefaultEvidencePath,
[ValidateSet('List','Audit','Plan','Configure')][string]$ProviderAction = 'List',
[string[]]$ProviderPack,
[ValidateSet('Audit','Plan','Configure')][string]$NotificationAction = 'Audit',
@@ -72,6 +74,7 @@ $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
. (Join-Path $ScriptRoot "scripts/FirewallLogging.ps1")
. (Join-Path $ScriptRoot "scripts/SmbAuditing.ps1")
. (Join-Path $ScriptRoot "scripts/LdapDiagnostics.ps1")
. (Join-Path $ScriptRoot "scripts/ControlApplicability.ps1")
. (Join-Path $ScriptRoot "scripts/AuditNotifications.ps1")
. (Join-Path $ScriptRoot "scripts/AdObjectSacl.ps1")
. (Join-Path $ScriptRoot "scripts/AppLockerReadiness.ps1")
@@ -107,7 +110,9 @@ class WELA {
[array] $NativeSources = @()
[array] $Rules
[hashtable] $RulesCount
[string] $DefaultSetting = ""
[string] $DefaultSetting = "Unknown"
[string] $LegacyDefaultHint = ""
[string] $DefaultEvidence = "No exact-context reviewed default evidence; historical hints are not host defaults."
[string] $RecommendedSetting = ""
[string] $Volume = ""
[string] $Note = ""
@@ -126,7 +131,8 @@ class WELA {
$this.SubCategory = $SubCategory
$this.CurrentSetting = $CurrentSetting
$this.Rules = $Rules
$this.DefaultSetting = $DefaultSetting
$this.LegacyDefaultHint = $DefaultSetting
$this.DefaultSetting = "Unknown"
$this.RecommendedSetting = $RecommendedSetting
$this.Volume = $Volume
$this.Note = $Note
@@ -175,6 +181,7 @@ class WELA {
}
if ($this.DefaultSetting) {
Write-Host " - Default Setting: $($this.DefaultSetting)"
Write-Host " - Default Evidence: $($this.DefaultEvidence)"
}
if ($this.CurrentSetting) {
Write-Host " - Current Setting: $($this.CurrentSetting)"
@@ -749,7 +756,7 @@ function AuditLogSetting {
Write-Host ""
}
} elseif ($outType -eq "table") {
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, DefaultSetting, CurrentSetting, ChannelState, GenerationReadiness, RecommendedSetting, Volume | Format-Table
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, DefaultSetting, DefaultEvidence, CurrentSetting, ChannelState, GenerationReadiness, RecommendedSetting, Volume | Format-Table
}
# 1つのルールが複数カテゴリに属するため、集計とCSVはルールID単位で重複排除する
@@ -764,7 +771,7 @@ 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, ChannelState, GenerationReadiness, RecommendedSetting, Volume, Note,
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, DefaultEvidence, LegacyDefaultHint, 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, EligibilityState, EligibilityReasons | Export-Csv -Path $usableCsv -NoTypeInformation
@@ -778,7 +785,7 @@ function AuditLogSetting {
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, ChannelState, GenerationReadiness, RecommendedSetting, Volume, Note | Out-GridView -Title "WELA Audit Result"
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, DefaultEvidence, LegacyDefaultHint, CurrentSetting, ChannelState, GenerationReadiness, RecommendedSetting, Volume, Note | Out-GridView -Title "WELA Audit Result"
}
Write-Output "Audit check result saved to: $auditCsv"
@@ -1791,6 +1798,8 @@ Usage:
./WELA.ps1 configure-sacl # Add targeted File System/Registry audit SACLs (ASEP keys + sensitive files) needed by the rules, without global auditing
./WELA.ps1 configure-sacl -Auto # ...automatically without prompts
./WELA.ps1 update-rules # Update rule config files from https://github.com/Yamato-Security/WELA
./WELA.ps1 control-applicability # Read-only historical native feature/build assessment
./WELA.ps1 default-evidence -Help # Exact-context observed snapshots and reviewed reference comparison
./WELA.ps1 audit-notifications -Help # OneSettings audit and Security warning policy
./WELA.ps1 version # Show the WELA version
./WELA.ps1 help # Show this help
@@ -1803,6 +1812,10 @@ Write-Host ""
Write-Host "WELA v$WELAVersion - $WELAReleaseName"
Write-Host ""
if ($Cmd -ne 'default-evidence' -and @($PSBoundParameters.Keys | Where-Object { $_ -in @('DefaultEvidenceAction','DefaultEvidencePath') }).Count) {
throw 'Default evidence options require default-evidence. No command was run.'
}
if ($Cmd -eq 'audit-notifications' -and @($PSBoundParameters.Keys | Where-Object { $_ -notin @('Cmd','NotificationAction','NotificationControl','WarningPercent','EnablePrivacyChannel','Auto','DryRun','BackupPath','ResultsPath','Help') }).Count) {
throw 'audit-notifications accepts only notification, consent/dry-run, recovery and JSON output options. No command was run.'
}
@@ -1881,6 +1894,23 @@ if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'confi
}
switch ($Cmd.ToLower()) {
'control-applicability' {
if ($Help) { Write-Host 'Usage: ./WELA.ps1 control-applicability [-ResultsPath report.json]. Read-only historical feature/build assessment; see docs/control-applicability.md.'; return }
if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath -or $Auto -or $BackupPath -or $PlanPath) { throw 'control-applicability reads actual local context and only accepts -ResultsPath; configuration and context overrides are unsupported.' }
$context=Get-WelaDefaultContext
$controls=@(Get-WelaHistoricalControls -Context $context)
$report=[pscustomobject]@{Scope='Native historical controls; Sysmon excluded';Context=$context;Controls=$controls;Catalog=(Get-WelaControlCatalog)}
if ($ResultsPath) { $report | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
$report
if (@($controls | Where-Object {$_.Applicability.Status -eq 'Unknown' -or $_.PolicyState -eq 'Unknown'}).Count) { exit 1 }
}
'default-evidence' {
if ($Help) { Write-Host 'Usage: ./WELA.ps1 default-evidence [-DefaultEvidenceAction Capture|Compare] [-DefaultEvidencePath reviewed-snapshot.json] [-ResultsPath report.json]. Capture observes current settings and never labels them as defaults. See docs/control-applicability.md.'; return }
if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath -or $Auto -or $BackupPath -or $PlanPath) { throw 'default-evidence reads actual local context and accepts only evidence/output options.' }
$report=Invoke-WelaDefaultEvidenceCommand -Action $DefaultEvidenceAction -ReferencePath $DefaultEvidencePath -ResultsPath $ResultsPath
$report
if ($report.ExitCode) { exit $report.ExitCode }
}
'audit-notifications' {
if ($Help) { Write-Host 'Usage: ./WELA.ps1 audit-notifications [-NotificationAction Audit|Plan|Configure] [-NotificationControl OneSettings,SecurityWarning] [-WarningPercent 1..90] [-EnablePrivacyChannel] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath report.json]. See docs/audit-notifications.md.'; return }
if ($Profile -or $Baseline -or $Role -or $Build -or $HtmlPath) { throw 'audit-notifications uses actual host context and -ResultsPath; profile/role/build overrides and HTML are unsupported.' }
+33
View File
@@ -0,0 +1,33 @@
{
"schemaVersion": 1,
"reviewedOn": "2026-09-19",
"scope": "Historical native controls; not a replacement for command-specific applicability checks",
"controls": [
{
"id": "application-guard-auditing",
"title": "Application Guard audit events",
"productTypes": [1],
"minBuild": 22000,
"maxBuild": 26099,
"reviewedBuilds": [22000, 22621, 22631],
"editions": ["Professional", "ProfessionalN", "ProfessionalEducation", "ProfessionalEducationN", "Enterprise", "EnterpriseN", "Education", "EducationN"],
"feature": "Windows-Defender-ApplicationGuard",
"registryPath": "HKLM:\\SOFTWARE\\Policies\\Microsoft\\AppHVSI",
"valueName": "AuditApplicationGuard",
"requiredType": "DWord",
"requiredValue": 1,
"source": {
"title": "CIS Microsoft Windows 11 Enterprise Benchmark",
"version": "4.0.0 (historical)",
"control": "18.10.44.1",
"requirement": "Enabled",
"url": "https://www.cisecurity.org/benchmark/microsoft_windows_desktop"
},
"removal": {
"firstBuild": 26100,
"release": "Windows 11 24H2",
"url": "https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/microsoft-defender-application-guard/md-app-guard-overview"
}
}
]
}
+122
View File
@@ -0,0 +1,122 @@
# Historical controls and Windows default evidence
These read-only commands address issue #385. Sysmon is out of scope. They do not
install optional features, modify registry/audit policy or grant Sigma eligibility.
```powershell
./WELA.ps1 control-applicability -ResultsPath applicability.json
./WELA.ps1 default-evidence -ResultsPath observed.json
./WELA.ps1 default-evidence -DefaultEvidenceAction Compare -DefaultEvidencePath reviewed-clean-install.json -ResultsPath comparison.json
```
Run in 64-bit PowerShell. Complete native role/feature and effective audit-policy
reads can require elevation. Context is always observed locally; `-Role`/`-Build`
overrides and configuration options are rejected. A partial capture preserves
successful observations and reports failed ones as Unknown, returning a nonzero
exit code. Failed reference qualification also returns nonzero.
## Application Guard
The versioned `config/control_applicability.json` retains CIS Windows 11 Enterprise
v4.0.0 control 18.10.44.1 and its historical `AuditApplicationGuard=1` requirement.
It records a Windows 11 minimum build (22000), maximum pre-removal build (26099),
reviewed releases (22000/22621/22631), editions, optional feature and source/removal
metadata. It is an initial historical-control catalog; existing advanced audit
profiles and SMB/provider commands keep their own applicability checks.
Microsoft removed Application Guard starting Windows 11 24H2 (build 26100).
24H2/25H2 and later client builds therefore report NotApplicable without querying
or attempting to reinstall it, even if stale registry or feature metadata remains.
Servers, including DC/AD CS, are outside this historical client source scope.
On reviewed older builds, an enabled `Windows-Defender-ApplicationGuard` feature
allows read-only auditing of the typed registry value. Disabled/absent features
are NotApplicable; unreviewed builds/editions, inaccessible or pending feature
states remain Unknown. The original source requirement is always retained.
There is no automatic historical remediation, and an enabled feature is not proof
of application readiness or event generation.
## Default column correction
The existing baseline JSON contains historical default strings without sufficient
build/patch/role provenance. Public audit assessments now show `DefaultSetting`
as Unknown and retain those strings separately as `LegacyDefaultHint` in CSV,
JSON, HTML and GUI output. `DefaultEvidence` explains the distinction. Standard
output likewise explains why the default is Unknown. Recommendations/current
observations are unchanged. The versioned `windows-defaults-*` advanced-policy
profiles remain explicitly documentary reference profiles, not clean-machine
snapshots. This PR does not replace those reference profiles or fabricate lab data.
To use a reviewed scenario snapshot, run the dedicated `default-evidence Compare`
command. It does not silently rewrite normal audit columns or baseline metadata.
Use a separate `ResultsPath`: a comparison refuses an output path that resolves
to its `DefaultEvidencePath`, preserving the reviewed input artifact.
The comparison reports a per-control default/reference and match/difference only
when reference provenance and exact context qualify; other defaults stay Unknown.
Channel observations retain individual channel names and states; registry records
retain each path, type, value and absence. They are not collapsed to assumed defaults.
## Capturing and reviewing reference scenarios
Capture records the exact Windows build and UBR patch, edition, localized OS
architecture and independently observed `Win32_Processor.Architecture` platform
code (x64 = 9, ARM64 = 12),
product type, domain role, join state/domain, and a complete installed feature/role
inventory. A CA role or DC promotion creates a distinct scenario rather than a
universal "Server default". It includes current typed registry state, native channel
metadata and all 59 canonical advanced audit subcategories. Individual failures
remain Unknown. It also records source versions and SHA-256 fingerprints for the
baseline/profile/applicability catalogs, this collector script and its native
reader helpers (`Configuration.ps1`, `NativeProviders.psm1`, `AuditProfiles.psm1`);
these inputs use LF checkouts so byte fingerprints are portable. Missing,
unsupported or inconsistent processor codes leave context Unknown; the localized
OS architecture string alone cannot qualify a reference.
Every capture is `EvidenceKind: ObservedState`. WELA cannot prove that a machine is
a clean installation. A domain-joined machine's effective state may include domain
policy; a CI runner is customized. Neither is automatically a Windows default.
An independent reviewer should preserve the original capture and create a reviewed
copy only after checking the clean image, provisioning and effective GPO/MDM policy
evidence. Change `EvidenceKind` to `ReviewedCleanInstall`, and populate `Review`:
| Field | Required evidence |
|---|---|
| `Reviewer`, `ReviewedUtc` | Identified reviewer and explicit ISO-8601 UTC review time at/after capture, ending in `Z` |
| `ImageSha256` | SHA-256 of the clean installation image |
| `SnapshotId` | Reproducible VM/image snapshot identifier |
| `PolicyEvidenceSha256` | SHA-256 of retained effective GPO/MDM/provisioning evidence |
| `ProvisioningNotes` | Patch installation, join/promotion/CA steps, policy scope and deviations |
Do not remove observed Unknown entries or rewrite observations to match desired
recommendations. Keep the hashed image/policy artifacts with the review record.
`CapturedUtc` and `ReviewedUtc` must use `YYYY-MM-DDTHH:mm:ss[.fffffff]Z` (optional
one to seven fractional digits). Localized dates, missing timezone suffixes,
offset spellings and future timestamps are rejected; there is no clock grace
period. Preserve their original strings when editing the JSON artifact.
PowerShell 7.5 and later preserve timestamp strings with `-DateKind String`;
Windows PowerShell 5.1 preserves strings without that option. Earlier PowerShell 7
releases deserialize timestamps automatically, so an already deserialized
`DateTime` is accepted only with `Kind=Utc`, then subjected to the same ordering
and assessment-time checks. Local and unspecified `DateTime` values are rejected;
original lexical spelling cannot be revalidated after an older engine normalizes it.
The importer validates structure, fingerprints and exact scenario context;
it does not authenticate the reviewer or inspect external artifacts. Its report
explicitly labels that provenance as operator-declared. Duplicate IDs, missing
review fields, source/collector drift or any context mismatch prevent defaults
from being used. Unknown/missing individual observations remain Unknown even if
the reference otherwise qualifies. Hashes must be refreshed through a new review
when source or collector files change; do not relabel old observations blindly.
Before closing #385, capture independently reviewed clean Windows 11 24H2/25H2,
Server 2022/2025 member, promoted DC and AD CS scenarios with exact patches and
policy evidence. Include historical Application Guard-enabled/disabled clients
where still available. Synthetic fixtures test the comparison boundaries; native
Windows CI demonstrates read-only collection only. This PR includes no measured
clean-install snapshots or end-to-end event/ingestion evidence.
Sources: [Microsoft Application Guard removal](https://learn.microsoft.com/en-us/windows/security/application-security/application-isolation/microsoft-defender-application-guard/md-app-guard-overview),
[Win32 processor platform codes](https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-processor),
[localized OS architecture property](https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem),
[CIS Windows benchmarks](https://www.cisecurity.org/benchmark/microsoft_windows_desktop)
(reviewed Windows 11 Enterprise v4.0.0, 18.10.44.1), and the versioned source records
in `config/audit_profiles.json`.
+1 -1
View File
@@ -187,7 +187,7 @@ function Export-WelaAuditAssessment {
Note = 'Only evidence-qualified Ready rules are usable. Policy/channel matches are configuration estimates; missing full rule logic, fields, outcomes, SACL, ingestion or query evidence remains Conditional.'
}
Eligibility = $Eligibility
Results = @($Rows | Select-Object Category, SubCategory, CurrentSetting, DefaultSetting, RecommendedSetting, RuleCount, ChannelState, GenerationReadiness, NativeSources, Note)
Results = @($Rows | Select-Object Category, SubCategory, CurrentSetting, DefaultSetting, DefaultEvidence, LegacyDefaultHint, RecommendedSetting, RuleCount, ChannelState, GenerationReadiness, NativeSources, Note)
}
if ($ResultsPath) { $report | ConvertTo-Json -Depth 16 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
if ($HtmlPath) {
+254
View File
@@ -0,0 +1,254 @@
# Read-only historical control applicability and provenance-bound default evidence.
function Get-WelaDefaultProcessorArchitecture {
# Win32_OperatingSystem.OSArchitecture is localized; retain an independent,
# documented platform code rather than inferring x64/ARM64 from that string.
$processors=@(Get-CimInstance Win32_Processor -ErrorAction Stop)
if (-not $processors.Count) { throw 'Processor architecture evidence is missing.' }
$codes=@(foreach ($processor in $processors) {
if ($processor.Architecture -isnot [ValueType] -or $processor.Architecture -notin @(9,12)) {
throw 'Processor architecture is missing or outside the reviewed x64/ARM64 scope.'
}
[int]$processor.Architecture
})
$unique=@($codes | Sort-Object -Unique)
if ($unique.Count -ne 1) { throw 'Conflicting processor architecture evidence.' }
return $unique[0]
}
function Get-WelaDefaultContext {
$result=[ordered]@{Status='Unknown';Build=$null;UBR=$null;Edition=$null;ProductType=$null;DomainRole=$null;DomainJoined=$null;Domain=$null;Architecture=$null;ProcessorArchitecture=$null;InstalledRoles=@();RolesStatus='Unknown';Diagnostic=''}
try {
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows is required for a native default snapshot.' }
if (-not [Environment]::Is64BitProcess) { throw 'Use 64-bit PowerShell for exact native registry and role observations.' }
$os=Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
$computer=Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
$version=Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
if ([string]$os.BuildNumber -notmatch '^\d+$' -or $null -eq $version.UBR -or -not $version.EditionID -or
$os.ProductType -notin @(1,2,3) -or $computer.PartOfDomain -isnot [bool]) { throw 'Incomplete host build, patch, edition or join evidence.' }
if (($os.ProductType -eq 1 -and $computer.DomainRole -notin @(0,1)) -or
($os.ProductType -eq 2 -and $computer.DomainRole -notin @(4,5)) -or
($os.ProductType -eq 3 -and $computer.DomainRole -notin @(2,3))) { throw 'Conflicting host role observations.' }
$result.Build=[int]$os.BuildNumber; $result.UBR=[int]$version.UBR; $result.Edition=[string]$version.EditionID
$result.ProductType=[int]$os.ProductType; $result.DomainRole=[int]$computer.DomainRole
$result.DomainJoined=[bool]$computer.PartOfDomain; $result.Domain=[string]$computer.Domain; $result.Architecture=[string]$os.OSArchitecture
$result.ProcessorArchitecture=Get-WelaDefaultProcessorArchitecture
if ($os.ProductType -eq 1) {
$result.InstalledRoles=@(Get-WindowsOptionalFeature -Online -ErrorAction Stop | Where-Object State -eq 'Enabled' | ForEach-Object { [string]$_.FeatureName } | Sort-Object -Unique)
} else {
$result.InstalledRoles=@(Get-WindowsFeature -ErrorAction Stop | Where-Object Installed -eq $true | ForEach-Object { [string]$_.Name } | Sort-Object -Unique)
}
$result.RolesStatus='Observed'; $result.Status='Observed'
} catch { $result.Diagnostic=$_.Exception.Message }
[pscustomobject]$result
}
function Get-WelaControlCatalog {
$catalog=Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/control_applicability.json') -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
if ($catalog.schemaVersion -ne 1) { throw 'Unsupported control applicability catalog.' }
return $catalog
}
function Resolve-WelaControlApplicability {
param($Definition,$Context,[string]$FeatureState='Unknown')
$status='Unknown'; $reason='Host build/product type could not be established.'
if ($Context.ProductType -in @(1,2,3) -and $Context.Build -is [ValueType] -and $Context.Build -gt 0) {
if ($Context.ProductType -notin $Definition.productTypes) { $status='NotApplicable'; $reason='Historical source requirement applies to Windows 11 clients only.' }
elseif ($Context.Build -ge $Definition.removal.firstBuild) { $status='NotApplicable'; $reason="Removed starting $($Definition.removal.release); no remediation is proposed." }
elseif ($Context.Build -lt $Definition.minBuild) { $status='NotApplicable'; $reason='Outside the Windows 11 source scope.' }
elseif ($Context.Build -gt $Definition.maxBuild -or $Context.Build -notin $Definition.reviewedBuilds) { $reason='Build not reviewed; no inferred support.' }
elseif ($Context.Edition -notin $Definition.editions) { $reason='Edition is not in the reviewed historical support set.' }
elseif ($FeatureState -in @('Disabled','DisabledWithPayloadRemoved','NotPresent')) { $status='NotApplicable'; $reason='Optional feature is unavailable or disabled; retained source requirement is conditional on its presence.' }
elseif ($FeatureState -eq 'Enabled') { $status='Applicable'; $reason='Historical build and enabled feature observed; application/runtime event generation remains unverified.' }
else { $reason='Optional feature state is unknown or pending; no remediation is proposed.' }
}
[pscustomobject]@{Status=$status;Reason=$reason;FeatureState=$FeatureState;Remediation=$null;SourceRequirement=$Definition.source;MinBuild=$Definition.minBuild;MaxBuild=$Definition.maxBuild;Removal=$Definition.removal}
}
function Get-WelaHistoricalControls {
param($Context=(Get-WelaDefaultContext))
foreach ($definition in (Get-WelaControlCatalog).controls) {
$feature='Unknown'; $featureError=$null; $policy=$null; $policyState='Not assessed'
$gate=Resolve-WelaControlApplicability $definition $Context
# Never query DISM or propose reinstalling a feature on removed/server builds.
if ($gate.Status -eq 'Unknown' -and $Context.Build -in $definition.reviewedBuilds -and $Context.ProductType -in $definition.productTypes -and $Context.Edition -in $definition.editions) {
try {
$observed=@(Get-WindowsOptionalFeature -Online -FeatureName $definition.feature -ErrorAction Stop)
if ($observed.Count -ne 1 -or $observed[0].FeatureName -ne $definition.feature) { throw 'Exact optional feature result missing or ambiguous.' }
$feature=[string]$observed[0].State
} catch { $featureError=$_.Exception.Message }
}
$gate=Resolve-WelaControlApplicability $definition $Context $feature
if ($gate.Status -eq 'Applicable') {
try {
$policy=Get-WelaRegistryState -Path $definition.registryPath -Name $definition.valueName
$policyState=if (-not $policy.ValueExists) {'Not configured'} elseif ($policy.Type -ne $definition.requiredType) {'Unknown'} elseif ($policy.Value -eq $definition.requiredValue) {'Policy matches'} else {'Policy differs'}
} catch { $policyState='Unknown'; $featureError=$_.Exception.Message }
}
[pscustomobject]@{Id=$definition.id;Title=$definition.title;Applicability=$gate;FeatureDiagnostic=$featureError;Policy=$policy;PolicyState=$policyState;AutomaticConfiguration='Unavailable: read-only historical assessment';EventGeneration='Unverified';DefaultSetting='Unknown'}
}
}
function Get-WelaDefaultSourceFingerprints {
foreach ($path in @('config/baselines.json','config/audit_profiles.json','config/control_applicability.json','scripts/ControlApplicability.ps1',
'scripts/Configuration.ps1','modules/NativeProviders.psm1','modules/AuditProfiles.psm1')) {
[pscustomobject]@{Path=$path;Sha256=(Get-FileHash -LiteralPath (Join-Path $PSScriptRoot "../$path") -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
}
}
function ConvertFrom-WelaDefaultEvidenceJson {
param([string]$Text)
$arguments=@{InputObject=$Text;ErrorAction='Stop'}
# Preserve the timestamp's exact source spelling for strict UTC validation.
if ((Get-Command ConvertFrom-Json).Parameters.ContainsKey('DateKind')) { $arguments.DateKind='String' }
ConvertFrom-Json @arguments
}
function Get-WelaDefaultControlDefinitions {
$config=Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/baselines.json') -Raw -ErrorAction Stop | ConvertFrom-Json
$seen=@{}
foreach ($item in $config.catalog) {
if ($item.currentSetting.type -eq 'auditpol') { continue }
$seen[$item.id]=$true
[pscustomobject]@{Id=$item.id;Kind=$item.currentSetting.type;Definition=$item.currentSetting}
}
$profiles=Import-WelaAuditProfiles
foreach ($item in $profiles.catalog) {
$id='audit:'+([string]$item.guid).ToLowerInvariant()
if ($seen.ContainsKey($id)) { throw 'Duplicate default evidence audit GUID.' }; $seen[$id]=$true
[pscustomobject]@{Id=$id;Kind='auditpol';Definition=$item}
}
}
function Get-WelaDefaultObservation {
param($Control)
$state='Unknown'; $value=$null; $raw=@(); $diagnostic=''
try {
switch ($Control.Kind) {
'auditpol' {
$mask=Get-WelaAuditPolicyMask -Guid $Control.Definition.guid
$value=Format-WelaAuditMask $mask; $raw=@([pscustomobject]@{Guid=$Control.Definition.guid;Mask=$mask}); $state='Observed'
}
'native-channel' {
$raw=@(foreach ($channel in $Control.Definition.channels) { Get-WelaNativeChannel -Name $channel })
if (-not $raw.Count -or @($raw | Where-Object State -notin @('Enabled','Disabled','Not installed')).Count) { throw 'One or more channel observations are unknown.' }
$value=(@($raw | ForEach-Object { "$($_.Name)=$($_.State)" }) -join '; '); $state='Observed'
}
'registry' {
$raw=@(foreach ($path in $Control.Definition.paths) { [pscustomobject]@{Path=$path;Name=$Control.Definition.name;State=(Get-WelaRegistryState -Path $path -Name $Control.Definition.name)} })
if (-not $raw.Count) { throw 'Registry definition has no paths.' }
$value=(@($raw | ForEach-Object { if (-not $_.State.ValueExists) {"$($_.Path)=Not configured"} else {"$($_.Path)=$($_.State.Type):$($_.State.Value)"} }) -join '; '); $state='Observed'
}
default { throw 'Default observation kind is not supported.' }
}
} catch { $state='Unknown'; $value=$null; $diagnostic=$_.Exception.Message }
[pscustomobject]@{Id=$Control.Id;Kind=$Control.Kind;Status=$state;Value=$value;Raw=$raw;Diagnostic=$diagnostic}
}
function New-WelaDefaultSnapshot {
[pscustomobject][ordered]@{
SchemaVersion=1;EvidenceKind='ObservedState';CapturedUtc=[DateTime]::UtcNow.ToString('o');ComputerName=$env:COMPUTERNAME
Context=(Get-WelaDefaultContext);Sources=@(Get-WelaDefaultSourceFingerprints)
SourceVersions=(Import-WelaAuditProfiles).sources
Review=[pscustomobject]@{Reviewer=$null;ReviewedUtc=$null;ImageSha256=$null;SnapshotId=$null;PolicyEvidenceSha256=$null;ProvisioningNotes=$null}
Observations=@(Get-WelaDefaultControlDefinitions | ForEach-Object { Get-WelaDefaultObservation $_ })
Warning='Observed settings are not Windows defaults. Only independently reviewed clean-install provenance can qualify a reference; domain join, promotion and CA installation define distinct scenarios.'
}
}
function Test-WelaDefaultContextComplete {
param($Context)
$consistent=($Context.ProductType -eq 1 -and $Context.DomainRole -in @(0,1)) -or
($Context.ProductType -eq 2 -and $Context.DomainRole -in @(4,5)) -or ($Context.ProductType -eq 3 -and $Context.DomainRole -in @(2,3))
$joined=$Context.DomainRole -in @(1,3,4,5)
return $consistent -and $Context.DomainJoined -eq $joined -and $Context.Status -eq 'Observed' -and $Context.RolesStatus -eq 'Observed' -and
($Context.Build -is [int] -or $Context.Build -is [long]) -and $Context.Build -gt 0 -and
($Context.UBR -is [int] -or $Context.UBR -is [long]) -and $Context.UBR -ge 0 -and
-not [string]::IsNullOrWhiteSpace($Context.Edition) -and $Context.ProductType -in @(1,2,3) -and
$Context.DomainRole -in @(0,1,2,3,4,5) -and $Context.DomainJoined -is [bool] -and
-not [string]::IsNullOrWhiteSpace($Context.Domain) -and -not [string]::IsNullOrWhiteSpace($Context.Architecture) -and
($Context.ProcessorArchitecture -is [int] -or $Context.ProcessorArchitecture -is [long]) -and $Context.ProcessorArchitecture -in @(9,12) -and
$null -ne $Context.InstalledRoles
}
function Get-WelaDefaultContextKey {
param($Context)
# Fixed property order; host name intentionally excluded so matching lab peers can compare.
[ordered]@{Build=$Context.Build;UBR=$Context.UBR;Edition=$Context.Edition;ProductType=$Context.ProductType;DomainRole=$Context.DomainRole;DomainJoined=$Context.DomainJoined;Domain=$Context.Domain;Architecture=$Context.Architecture;ProcessorArchitecture=$Context.ProcessorArchitecture;InstalledRoles=@($Context.InstalledRoles | Sort-Object -Unique)} | ConvertTo-Json -Depth 5 -Compress
}
function Test-WelaReviewedDefaultEvidence {
param($Evidence,$Context,$Sources,[DateTimeOffset]$AssessmentUtc=[DateTimeOffset]::UtcNow)
$reason=''
if (-not $Evidence -or $Evidence.SchemaVersion -ne 1 -or $Evidence.EvidenceKind -ne 'ReviewedCleanInstall') { $reason='Reference is not a reviewed clean-install scenario.' }
elseif (-not (Test-WelaDefaultContextComplete $Context) -or -not (Test-WelaDefaultContextComplete $Evidence.Context)) { $reason='Exact host/role/patch evidence is incomplete.' }
elseif ((Get-WelaDefaultContextKey $Context) -cne (Get-WelaDefaultContextKey $Evidence.Context)) { $reason='Build, patch, edition, architecture, domain/join or installed-role context differs.' }
else {
$review=$Evidence.Review
$captured=[DateTimeOffset]::MinValue; $reviewed=[DateTimeOffset]::MinValue
$capturedText=$Evidence.CapturedUtc; $reviewedText=$review.ReviewedUtc
# PowerShell 6 through 7.4 deserialize Z timestamps as UTC DateTime and
# have no -DateKind String option. Local/Unspecified values remain invalid.
if ($capturedText -is [DateTime] -and $capturedText.Kind -eq [DateTimeKind]::Utc) { $capturedText=$capturedText.ToString('o') }
if ($reviewedText -is [DateTime] -and $reviewedText.Kind -eq [DateTimeKind]::Utc) { $reviewedText=$reviewedText.ToString('o') }
$utcPattern='\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,7})?Z\z'
if ($capturedText -isnot [string] -or $reviewedText -isnot [string] -or
$capturedText -cnotmatch $utcPattern -or $reviewedText -cnotmatch $utcPattern -or
-not [DateTimeOffset]::TryParse($capturedText,[Globalization.CultureInfo]::InvariantCulture,[Globalization.DateTimeStyles]::AssumeUniversal,[ref]$captured) -or
-not [DateTimeOffset]::TryParse($reviewedText,[Globalization.CultureInfo]::InvariantCulture,[Globalization.DateTimeStyles]::AssumeUniversal,[ref]$reviewed) -or
$reviewed -lt $captured -or $captured -gt $AssessmentUtc -or $reviewed -gt $AssessmentUtc -or
[string]::IsNullOrWhiteSpace($review.Reviewer) -or [string]::IsNullOrWhiteSpace($review.SnapshotId) -or
[string]::IsNullOrWhiteSpace($review.ProvisioningNotes) -or $review.ImageSha256 -notmatch '^[a-fA-F0-9]{64}$' -or
$review.PolicyEvidenceSha256 -notmatch '^[a-fA-F0-9]{64}$') { $reason='Clean-install review/image/snapshot/policy provenance is incomplete.' }
elseif (@($Sources).Count -ne @($Evidence.Sources).Count) { $reason='Source fingerprint set differs.' }
else {
foreach ($source in $Sources) {
$matches=@($Evidence.Sources | Where-Object Path -eq $source.Path)
if ($matches.Count -ne 1 -or $matches[0].Sha256 -cne $source.Sha256) { $reason='Source or collector version fingerprint differs.'; break }
}
$ids=@{}
foreach ($observation in $Evidence.Observations) {
if (-not $observation.Id -or $ids.ContainsKey($observation.Id)) { $reason='Missing or duplicated observation ID.'; break }
$ids[$observation.Id]=$true
}
}
}
[pscustomobject]@{Accepted=($reason -eq '');Reason=$(if ($reason) {$reason} else {'Operator-reviewed scenario reference; provenance is declared, not independently authenticated by WELA.'})}
}
function Get-WelaDefaultComparison {
param($Snapshot,$Reference)
$review=Test-WelaReviewedDefaultEvidence -Evidence $Reference -Context $Snapshot.Context -Sources $Snapshot.Sources
$rows=foreach ($current in $Snapshot.Observations) {
$match=@($Reference.Observations | Where-Object Id -eq $current.Id)
$default='Unknown'; $reason=$review.Reason; $comparison='Unknown'
if ($review.Accepted -and $match.Count -eq 1 -and $match[0].Status -eq 'Observed' -and
$match[0].Value -is [string] -and $match[0].Value.Length -gt 0 -and $match[0].Kind -eq $current.Kind) {
$default=$match[0].Value
if ($current.Status -eq 'Observed') { $comparison=if ($current.Value -ceq $default) {'Matches reference'} else {'Differs from reference'} }
} elseif ($review.Accepted) { $reason='Reference control is missing, unknown or malformed.' }
[pscustomobject]@{Id=$current.Id;Current=$current;DefaultSetting=$default;DefaultEvidence=$reason;Comparison=$comparison}
}
[pscustomobject]@{ReferenceReview=$review;Results=@($rows);Scope='Scenario-specific native settings only; no event generation or Sigma eligibility claim.'}
}
function Invoke-WelaDefaultEvidenceCommand {
param([ValidateSet('Capture','Compare')][string]$Action='Capture',[string]$ReferencePath,[string]$ResultsPath)
if ($Action -eq 'Compare' -and -not $ReferencePath) { throw 'Compare requires DefaultEvidencePath.' }
if ($Action -eq 'Capture' -and $ReferencePath) { throw 'DefaultEvidencePath requires Compare.' }
if ($Action -eq 'Compare' -and $ResultsPath) {
$referenceFull=[IO.Path]::GetFullPath($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ReferencePath))
$resultsFull=[IO.Path]::GetFullPath($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ResultsPath))
if ([string]::Equals($referenceFull,$resultsFull,[StringComparison]::OrdinalIgnoreCase)) {
throw 'ResultsPath must differ from DefaultEvidencePath; comparison output cannot overwrite its reference artifact.'
}
}
$snapshot=New-WelaDefaultSnapshot
$report=if ($Action -eq 'Compare') {
$reference=ConvertFrom-WelaDefaultEvidenceJson (Get-Content -LiteralPath $ReferencePath -Raw -ErrorAction Stop)
$comparison=Get-WelaDefaultComparison -Snapshot $snapshot -Reference $reference
[pscustomobject]@{Action=$Action;Snapshot=$snapshot;Comparison=$comparison;ExitCode=$(if ($comparison.ReferenceReview.Accepted -and @($snapshot.Observations | Where-Object Status -eq 'Unknown').Count -eq 0) {0} else {1})}
} else { [pscustomobject]@{Action=$Action;Snapshot=$snapshot;ExitCode=$(if ((Test-WelaDefaultContextComplete $snapshot.Context) -and @($snapshot.Observations | Where-Object Status -eq 'Unknown').Count -eq 0) {0} else {1})} }
# Capture exports the snapshot directly so its exact artifact can be reviewed.
if ($ResultsPath) { $(if ($Action -eq 'Capture') {$snapshot} else {$report}) | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop }
return $report
}
+160
View File
@@ -0,0 +1,160 @@
$ErrorActionPreference='Stop'
$root=Split-Path $PSScriptRoot -Parent
. (Join-Path $root 'scripts/ControlApplicability.ps1')
Import-Module (Join-Path $root 'modules/AuditProfiles.psm1')
$script:count=0
function Assert($Condition,$Message) { if (-not $Condition) { throw $Message }; $script:count++ }
function Copy-Object($Value) { ConvertFrom-WelaDefaultEvidenceJson ($Value | ConvertTo-Json -Depth 20) }
$context=[pscustomobject]@{Status='Observed';Build=22631;UBR=1234;Edition='Enterprise';ProductType=1;DomainRole=1;DomainJoined=$true;Domain='lab.test';Architecture='64-bit';ProcessorArchitecture=9;InstalledRoles=@('FeatureB','FeatureA');RolesStatus='Observed';Diagnostic='fixture'}
$script:processorRows=@()
function Get-CimInstance { param($ClassName,$ErrorAction) if ($ClassName -ne 'Win32_Processor') { throw 'Unexpected fixture CIM class.' }; $script:processorRows }
foreach ($code in @(9,12)) {
$script:processorRows=@([pscustomobject]@{Architecture=[uint16]$code},[pscustomobject]@{Architecture=[uint16]$code})
Assert ((Get-WelaDefaultProcessorArchitecture) -eq $code) 'Native processor platform code distinguishes x64 and ARM64 independently of OS string/process emulation.'
}
foreach ($codes in @(@(),@(9,12),@($null),@(99),@('9'))) {
$script:processorRows=@($codes | ForEach-Object { [pscustomobject]@{Architecture=$_} })
$rejected=$false; try { Get-WelaDefaultProcessorArchitecture | Out-Null } catch { $rejected=$true }
Assert $rejected 'Missing, conflicting, unsupported or malformed processor architecture cannot qualify exact context.'
}
$definition=(Get-WelaControlCatalog).controls[0]
foreach ($build in @(26100,26200,30000)) {
$ctx=Copy-Object $context; $ctx.Build=$build
foreach ($feature in @('Enabled','Disabled','Unknown')) {
$row=Resolve-WelaControlApplicability $definition $ctx $feature
Assert ($row.Status -eq 'NotApplicable' -and $null -eq $row.Remediation -and $row.Reason -match 'Removed') 'Removed Application Guard has no misleading remediation, even if stale feature metadata says Enabled.'
}
}
foreach ($product in @(2,3)) {
$ctx=Copy-Object $context; $ctx.ProductType=$product
Assert ((Resolve-WelaControlApplicability $definition $ctx Enabled).Status -eq 'NotApplicable') 'Client historical source does not apply to a server/DC/CA.'
}
foreach ($state in @('Disabled','DisabledWithPayloadRemoved','NotPresent')) {
Assert ((Resolve-WelaControlApplicability $definition $context $state).Status -eq 'NotApplicable') 'Absent/disabled feature is distinguished from an auditing mismatch.'
}
foreach ($state in @('Unknown','EnablePending','DisablePending')) {
Assert ((Resolve-WelaControlApplicability $definition $context $state).Status -eq 'Unknown') 'Unknown/pending feature cannot claim applicability.'
}
Assert ((Resolve-WelaControlApplicability $definition $context Enabled).Status -eq 'Applicable') 'Reviewed historical enabled feature is applicable.'
$ctx=Copy-Object $context; $ctx.Build=25000
Assert ((Resolve-WelaControlApplicability $definition $ctx Enabled).Status -eq 'Unknown') 'Unreviewed insider build remains unknown.'
$ctx=Copy-Object $context; $ctx.Edition='Core'
Assert ((Resolve-WelaControlApplicability $definition $ctx Enabled).Status -eq 'Unknown') 'Unreviewed edition is never given remediation.'
$script:featureReads=0; $script:registryReads=0
function Get-WindowsOptionalFeature { param([switch]$Online,$FeatureName,$ErrorAction) $script:featureReads++; [pscustomobject]@{FeatureName=$FeatureName;State='Enabled'} }
function Get-WelaRegistryState { param($Path,$Name) $script:registryReads++; [pscustomobject]@{KeyExists=$true;ValueExists=$true;Type='DWord';Value=1} }
$ctx=Copy-Object $context; $ctx.Build=26100
$row=@(Get-WelaHistoricalControls $ctx)[0]
Assert ($row.Applicability.Status -eq 'NotApplicable' -and $script:featureReads -eq 0 -and $script:registryReads -eq 0) 'Removed feature is not queried or remediated.'
$row=@(Get-WelaHistoricalControls $context)[0]
Assert ($row.PolicyState -eq 'Policy matches' -and $row.EventGeneration -eq 'Unverified') 'Actual policy observation stays separate from event proof.'
function Get-WelaDefaultContext { $context }
function Get-WelaNativeChannel { param($Name) [pscustomobject]@{Name=$Name;State='Enabled';IsEnabled=$true;LogMode='Circular'} }
function Get-WelaAuditPolicyMask { param($Guid) 1 }
$snapshot=New-WelaDefaultSnapshot
Assert ($snapshot.EvidenceKind -eq 'ObservedState' -and $snapshot.Observations.Count -gt 59) 'Capture always labels current observations, including canonical audit categories.'
$reference=Copy-Object $snapshot
$compare=Get-WelaDefaultComparison $snapshot $reference
Assert (-not $compare.ReferenceReview.Accepted -and @($compare.Results | Where-Object DefaultSetting -ne 'Unknown').Count -eq 0) 'Observed current settings are never promoted to defaults.'
$reference.EvidenceKind='ReviewedCleanInstall'
$reference.Review=[pscustomobject]@{Reviewer='Fixture reviewer';ReviewedUtc=[DateTime]::UtcNow.ToString('o');ImageSha256=('a'*64);SnapshotId='test-only';PolicyEvidenceSha256=('b'*64);ProvisioningNotes='Synthetic validation fixture, not Windows lab evidence'}
$reference.Context.InstalledRoles=@('FeatureA','FeatureB')
$compare=Get-WelaDefaultComparison $snapshot $reference
Assert ($compare.ReferenceReview.Accepted -and @($compare.Results | Where-Object Comparison -ne 'Matches reference').Count -eq 0) 'Exact reviewed context can provide a scenario reference, with order-independent role inventory.'
if ((Get-Command ConvertFrom-Json).Parameters.ContainsKey('DateKind') -or $PSVersionTable.PSVersion.Major -le 5) {
Assert ($reference.CapturedUtc -is [string] -and $reference.Review.ReviewedUtc -is [string]) 'JSON import retains exact timestamp strings instead of silently localizing them.'
} else {
Assert ($reference.CapturedUtc -is [DateTime] -and $reference.CapturedUtc.Kind -eq [DateTimeKind]::Utc) 'Earlier PowerShell 7 imports explicit Z timestamps as UTC, not Local or Unspecified.'
}
$candidate=Copy-Object $reference
$candidate.CapturedUtc=([DateTimeOffset]$candidate.CapturedUtc).UtcDateTime
$candidate.Review.ReviewedUtc=([DateTimeOffset]$candidate.Review.ReviewedUtc).UtcDateTime
Assert ((Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) 'Earlier PowerShell 7 deserialized UTC DateTime values retain their explicit UTC semantics.'
foreach ($kind in @([DateTimeKind]::Local,[DateTimeKind]::Unspecified)) {
foreach ($field in @('Capture','Review')) {
$candidate=Copy-Object $reference
if ($field -eq 'Capture') { $candidate.CapturedUtc=[DateTime]::SpecifyKind(([DateTimeOffset]$candidate.CapturedUtc).UtcDateTime,$kind) }
else { $candidate.Review.ReviewedUtc=[DateTime]::SpecifyKind(([DateTimeOffset]$candidate.Review.ReviewedUtc).UtcDateTime,$kind) }
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) 'Local or Unspecified DateTime cannot qualify UTC provenance even on a UTC-configured host.'
}
}
$candidate=Copy-Object $reference; $candidate.Review.ReviewedUtc=[DateTime]::UtcNow.AddDays(1)
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) 'Deserialized UTC DateTime retains the nonfuture validation boundary.'
foreach ($time in @('2040-01-01T00:00:00Z','09/19/2026 01:00:00','2026-09-19T01:00:00','2026-09-19T01:00:00+00:00','2026-99-99T01:00:00Z',"2020-01-01T00:00:00Z`n")) {
foreach ($field in @('Capture','Review')) {
$candidate=Copy-Object $reference
if ($field -eq 'Capture') {$candidate.CapturedUtc=$time} else {$candidate.Review.ReviewedUtc=$time}
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) 'Future, malformed, localized or non-Z timestamps cannot qualify a clean-install reference.'
}
}
$candidate=Copy-Object $reference; $candidate.CapturedUtc='2040-01-01T00:00:00Z'; $candidate.Review.ReviewedUtc='2040-01-01T00:00:01Z'
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources ([DateTimeOffset]'2039-12-31T23:59:59Z')).Accepted) 'Chronologically ordered future capture and review still fail the assessment-time boundary.'
foreach ($helper in @('scripts/Configuration.ps1','modules/NativeProviders.psm1','modules/AuditProfiles.psm1')) {
$candidate=Copy-Object $reference
$source=@($candidate.Sources | Where-Object Path -eq $helper)
Assert ($source.Count -eq 1) "Actual collector helper is fingerprinted: $helper"
$source[0].Sha256=('d'*64)
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) "Changed native reader semantics invalidate reference provenance: $helper"
}
foreach ($field in @('Build','UBR','Edition','ProductType','DomainRole','DomainJoined','Domain','Architecture','ProcessorArchitecture','InstalledRoles','Status','RolesStatus')) {
$candidate=Copy-Object $reference
switch ($field) {
'Build' {$candidate.Context.Build=26100}
'UBR' {$candidate.Context.UBR=1235}
'ProductType' {$candidate.Context.ProductType=3}
'DomainRole' {$candidate.Context.DomainRole=3}
'DomainJoined' {$candidate.Context.DomainJoined=$false}
'ProcessorArchitecture' {$candidate.Context.ProcessorArchitecture=12}
'InstalledRoles' {$candidate.Context.InstalledRoles=@('ADCS-Cert-Authority')}
default {$candidate.Context.$field='Different'}
}
$compare=Get-WelaDefaultComparison $snapshot $candidate
Assert (-not $compare.ReferenceReview.Accepted -and @($compare.Results | Where-Object DefaultSetting -ne 'Unknown').Count -eq 0) "Context mismatch $field cannot leak a default."
}
$candidate=Copy-Object $reference; $candidate.Context.PSObject.Properties.Remove('ProcessorArchitecture')
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) 'Legacy evidence without native processor architecture remains Unknown, even when localized OS architecture matches.'
foreach ($field in @('Reviewer','ReviewedUtc','ImageSha256','SnapshotId','PolicyEvidenceSha256','ProvisioningNotes')) {
$candidate=Copy-Object $reference; $candidate.Review.$field=$null
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) "Missing $field fails review."
}
$candidate=Copy-Object $reference; $candidate.Sources[0].Sha256=('c'*64)
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) 'Changed source/catalog/collector bytes invalidate the reference.'
$candidate=Copy-Object $reference; $candidate.Observations+=@($candidate.Observations[0])
Assert (-not (Test-WelaReviewedDefaultEvidence $candidate $snapshot.Context $snapshot.Sources).Accepted) 'Duplicate IDs invalidate reference.'
$candidate=Copy-Object $reference; $candidate.Observations[0].Status='Unknown'
$compare=Get-WelaDefaultComparison $snapshot $candidate
Assert ($compare.ReferenceReview.Accepted -and $compare.Results[0].DefaultSetting -eq 'Unknown') 'Unknown individual control remains unknown in an otherwise reviewed artifact.'
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-default-reference-'+[guid]::NewGuid().ToString('N'))
$null=New-Item -ItemType Directory -Path $temp
try {
$referencePath=Join-Path $temp 'reference.json'
$reference | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $referencePath -Encoding UTF8
$beforeHash=(Get-FileHash -LiteralPath $referencePath -Algorithm SHA256).Hash
foreach ($output in @($referencePath,(Join-Path $temp './reference.json'))) {
$rejected=$false
try { Invoke-WelaDefaultEvidenceCommand -Action Compare -ReferencePath $referencePath -ResultsPath $output | Out-Null }
catch { $rejected=$_.Exception.Message -match 'cannot overwrite its reference artifact' }
Assert $rejected 'Equivalent input/output paths are rejected before exporting over reviewed evidence.'
Assert ((Get-FileHash -LiteralPath $referencePath -Algorithm SHA256).Hash -ceq $beforeHash) 'Rejected reference/output collision preserves exact evidence bytes.'
}
$output=Join-Path $temp 'comparison.json'
$report=Invoke-WelaDefaultEvidenceCommand -Action Compare -ReferencePath $referencePath -ResultsPath $output
Assert ($report.Comparison.ReferenceReview.Accepted -and (Test-Path -LiteralPath $output) -and (Get-FileHash -LiteralPath $referencePath -Algorithm SHA256).Hash -ceq $beforeHash) 'Distinct comparison output imports strict timestamps and preserves its reviewed input.'
} finally { Remove-Item -LiteralPath $temp -Recurse -Force }
function Get-WelaNativeChannel { param($Name) throw 'channel read denied' }
$partial=New-WelaDefaultSnapshot
Assert (@($partial.Observations | Where-Object {$_.Kind -eq 'native-channel' -and $_.Status -eq 'Unknown'}).Count -gt 0 -and @($partial.Observations | Where-Object {$_.Kind -eq 'auditpol' -and $_.Status -eq 'Observed'}).Count -eq 59) 'One failed source does not discard independent successful observations.'
# The real renderer keeps source-less legacy strings explicitly separate in all assessment rows.
$tokens=$null; $errors=$null
$ast=[System.Management.Automation.Language.Parser]::ParseFile((Join-Path $root 'WELA.ps1'),[ref]$tokens,[ref]$errors)
Assert ($errors.Count -eq 0) 'Public script parses.'
$class=$ast.Find({param($node) $node -is [System.Management.Automation.Language.TypeDefinitionAst] -and $node.Name -eq 'WELA'},$true)
. ([scriptblock]::Create($class.Extent.Text))
$row=[WELA]::New('Test','Test','Enabled',@(),'Enabled','Enabled','','')
Assert ($row.DefaultSetting -eq 'Unknown' -and $row.LegacyDefaultHint -eq 'Enabled' -and $row.DefaultEvidence -match 'No exact-context') 'Legacy default hints cannot be shown as current host defaults.'
$exe=(Get-Process -Id $PID).Path
$ErrorActionPreference='Continue'
try { $out=& $exe -NoProfile -File (Join-Path $root 'WELA.ps1') configure -Profile wela-2.2.0 -DefaultEvidenceAction Capture 2>&1; $code=$LASTEXITCODE } finally { $ErrorActionPreference='Stop' }
Assert ($code -ne 0 -and ($out -join "`n") -match 'Default evidence options require') 'Wrong-command evidence options fail before profile configuration.'
Write-Host "PASS: $script:count applicability/default evidence assertions (synthetic fixtures, no default claims)."
$global:LASTEXITCODE=0
@@ -0,0 +1,19 @@
$ErrorActionPreference='Stop'
$root=Split-Path $PSScriptRoot -Parent
. (Join-Path $root 'scripts/Configuration.ps1')
. (Join-Path $root 'scripts/ControlApplicability.ps1')
Import-Module (Join-Path $root 'modules/AuditProfiles.psm1')
Import-Module (Join-Path $root 'modules/NativeProviders.psm1')
$report=Invoke-WelaDefaultEvidenceCommand -Action Capture
$snapshot=$report.Snapshot
if (-not (Test-WelaDefaultContextComplete $snapshot.Context)) { throw "Native snapshot context incomplete: $($snapshot.Context.Diagnostic)" }
if ($snapshot.EvidenceKind -ne 'ObservedState' -or $snapshot.Review.Reviewer) { throw 'Native observed state was incorrectly promoted to a reviewed default.' }
if (@($snapshot.Observations | Where-Object Kind -eq 'auditpol').Count -ne 59) { throw 'Canonical audit inventory incomplete.' }
$comparison=Get-WelaDefaultComparison $snapshot $snapshot
if ($comparison.ReferenceReview.Accepted -or @($comparison.Results | Where-Object DefaultSetting -ne 'Unknown').Count) { throw 'Observed state cannot establish defaults.' }
$controls=@(Get-WelaHistoricalControls -Context $snapshot.Context)
if ($controls[0].Applicability.Status -ne 'NotApplicable' -or $controls[0].Applicability.Remediation) { throw 'Server must not receive historical client Application Guard remediation.' }
$snapshot.Context | ConvertTo-Json -Depth 5 | Write-Host
$snapshot.Observations | Select-Object Id,Status,Value,Diagnostic | Format-Table -AutoSize | Out-Host
Write-Host 'PASS: native build/patch/join/role and policy/channel capture is read-only. This runner is not a clean-install reference.'
$global:LASTEXITCODE=0
+1
View File
@@ -7,6 +7,7 @@
**改善:**
- 読み取り専用の`control-applicability`を追加し、旧CISのApplication Guard監査要件を保持しつつ、Windows 11 24H2以降では削除済み・対象外と表示します。`default-evidence`で正確なビルド・パッチ・ドメイン参加・役割を含む現状を記録し、出典とレビュー情報が一致する参照環境と比較できます。旧ベースラインの既定値は履歴情報として保持し、未検証の既定値はUnknownと表示します。合成テストと読み取り専用CIからクリーンインストールやイベント生成の証明は行いません。 (#409) (@Shirofune-Security)
- Windows標準のDNS Client/Server、CAPI2、WinRM、RDP Clientについて、明示的に選択するprovider-packsの監査・計画とチャネル設定を追加しました。固定した完全なルール定義と実際のプロバイダー・チャネル・イベントスキーマを確認し、DNSのチャネル名不一致や不明な前提条件を保持します。復旧記録付きの変更では大きいバッファ・保持方式・ACLを維持し、Analyticalと従来のDNSログは手動確認のみとします。イベント生成・バックエンド検証や検知範囲の向上は未確認です。 (#411) (@Shirofune-Security)
- 任意実行の`audit-notifications`を追加し、OneSettings監査とSecurityログ警告しきい値の監査・計画・設定に対応しました。対象とチャネル変更の明示指定、OS・ADMX確認、型付き復旧記録と変更検出を行い、既存の低いしきい値とチャネルACL・保存方式を保持します。ポリシー一致と実イベント生成を分け、Windowsラボの証拠とSigma利用可能性は未検証と表示します。 (#408) (@Shirofune-Security)
+1
View File
@@ -7,6 +7,7 @@
**Improvements:**
- Added read-only `control-applicability` for the historical CIS Application Guard audit requirement, reporting removal on Windows 11 24H2+ without remediation. Added exact build/patch/join/role native snapshots and provenance-bound reference comparison through `default-evidence`. Legacy baseline default strings are retained as historical hints while public defaults remain Unknown without reviewed scenario evidence. Synthetic fixtures and native read-only CI do not claim clean-install or event-generation proof. (#409) (@Shirofune-Security)
- Added explicit native DNS Client/Server, CAPI2, WinRM and RDP Client provider-pack inventory and selective channel configuration. Pinned full rule definitions and live provider/channel/event schemas retain DNS channel mismatches and unknown prerequisites; journaled opt-in changes preserve larger buffers, retention and ACLs. Analytical/classic DNS remain manual-only, and no event/backend readiness uplift is claimed. (#411) (@Shirofune-Security)
- Added opt-in `audit-notifications` audit/plan/configure for OneSettings auditing and Security log warning thresholds, with explicit control/channel selections, reviewed host and ADMX gates, typed recovery journals and drift checks. Earlier warning thresholds and channel ACL/retention are preserved. Reports separate policy matches from warning/event generation; Windows lab evidence and Sigma eligibility remain unverified. (#408) (@Shirofune-Security)