Preserve configured user-file suffixes in SACL plans

This commit is contained in:
Shirofune-Security committed 2026-09-19 06:21:18 +09:00
1 parent 1acfec66a3
commit 521fe3b826
7 files changed
+60 -10

No files matched your search

+1 -1
View File
@@ -52,7 +52,7 @@
**新機能:**
- プロファイルの plan/audit/configure に対象を限定した SACL の読み取り専用計画を追加しました。オブジェクト監査ポリシー、ユーザーハイブ・フォルダーリダイレクトの未確認箇所、WEF Run/RunOnce の監査エントリを表示し、`-SaclMode Skip` による省略も明示します。SACL の書き込みや未検証の検知率向上は行いません。 (#398) (@Shirofune-Security)
- プロファイルの plan/audit/configure に対象を限定した SACL の読み取り専用計画を追加しました。オブジェクト監査ポリシー、ユーザーハイブ・フォルダーリダイレクトの未確認箇所、WEF Run/RunOnce の監査エントリを表示し、`-SaclMode Skip` による省略も明示します。ユーザーファイルの対象は、そのユーザーの AppData または Startup 既知フォルダー配下の相対パスを保持し、未対応・曖昧なパスは未解決として扱います。SACL の書き込みや未検証の検知率向上は行いません。 (#398) (@Shirofune-Security)
- MITRE ATT&CK Navigatorヒートマップに対応した。 (#11) (@fukusuket)
+1 -1
View File
@@ -54,7 +54,7 @@
**New Features:**
- Profile plan/audit/configure now include read-only targeted SACL prerequisites with object policy masks, per-user hive and redirected-folder gaps, exact WEF Run/RunOnce audit entries, and an explicit `-SaclMode Skip`. No SACL writes or unverified detection uplift are implied. (#398) (@Shirofune-Security)
- Profile plan/audit/configure now include read-only targeted SACL prerequisites with object policy masks, per-user hive and redirected-folder gaps, exact WEF Run/RunOnce audit entries, and an explicit `-SaclMode Skip`. User-file targets retain their configured suffix under the user's AppData or Startup known folder; unsupported or ambiguous paths remain unresolved. No SACL writes or unverified detection uplift are implied. (#398) (@Shirofune-Security)
- Support for MITRE ATT&CK Navigator heatmaps. (#11) (@fukusuket)
+2
View File
@@ -17,6 +17,8 @@ No SACL is written by a profile command. Audit policy configuration retains its
On a matching local Windows role/build, the plan inventories ProfileList, Default and loaded HKU hives. Unloaded hives remain unresolved; it never silently substitutes the operator's user environment for another user's paths. Loaded-user Startup/AppData locations come from that user's unexpanded `User Shell Folders` values. Redirected, remote, missing, inaccessible and unresolved paths are explicit; UNC destinations are not contacted. Mapped network drives and reparse points in any path component are flagged before descendant or SACL inspection. These are point-in-time observations, not an atomic guard against concurrent path replacement. Enumeration failures, per-profile path errors and unmatched loaded hives make the inventory incomplete and remain visible as diagnostics. ProfileList paths expand only known machine variables; operator user variables are never substituted. An offline plan (or plan for a different role/build) inspects no host targets. `Skip` performs no user/target inspection.
Configured user-file paths beneath `AppData\Roaming` retain their complete relative suffix under the user's AppData known folder. The exact `AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup` root and its descendants use the separate Startup known folder, including its redirection; an unrelated directory named `Startup` does not. Unsupported roots, dot segments, unresolved variables and ambiguous path components remain unresolved before any known-folder read. This resolution adds no targets and does not enumerate other application folders.
`Exists` and `SaclReadState=Readable` mean only that the object and its SACL could be read. They do **not** prove the necessary audit ACE is present, inheritance reaches every descendant, mandatory/temporary profiles are covered, or a Security event was generated. All rows remain `GenerationReadiness=Conditional`, with zero usable-rule credit. A failure to read SACLs is reported separately from a missing path; elevated privileges may be necessary for those reads.
## Source-specific distinctions
+17 -2
View File
@@ -58,7 +58,22 @@ function Resolve-WelaSaclUserFile {
if (-not $User.HiveLoaded) { return [pscustomobject]@{ Path = $null; State = 'UnloadedHive'; Diagnostic = 'Known-folder redirection cannot be read without loading the user hive; no hive was loaded.' } }
if (-not $User.ProfilePath) { return [pscustomobject]@{ Path = $null; State = 'UnresolvedUserPath'; Diagnostic = 'Profile path is unavailable.' } }
try {
$folder = if ($RelativePath -like '*\Startup') { 'Startup' } else { 'AppData' }
# Match complete known-folder roots, not an arbitrary directory named
# Startup. Keep the configured suffix rather than substituting an app.
$parts = @($RelativePath -split '\\')
if ($RelativePath -match '[<>:"/|?*\x00-\x1F]' -or $RelativePath -match '%[^%]+%' -or
@($parts | Where-Object { -not $_ -or $_ -in @('.', '..') -or $_ -match '[ .]$' }).Count) {
throw 'User target contains unsupported or ambiguous path components.'
}
$startupRoot = 'AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup'
$appDataRoot = 'AppData\Roaming'
if ($RelativePath -ieq $startupRoot) {
$folder = 'Startup'; $suffix = ''
} elseif ($RelativePath.StartsWith($startupRoot + '\', [StringComparison]::OrdinalIgnoreCase)) {
$folder = 'Startup'; $suffix = $RelativePath.Substring($startupRoot.Length + 1)
} elseif ($RelativePath.StartsWith($appDataRoot + '\', [StringComparison]::OrdinalIgnoreCase)) {
$folder = 'AppData'; $suffix = $RelativePath.Substring($appDataRoot.Length + 1)
} else { throw 'User target must be the Startup known folder or a child of the supported AppData\Roaming known-folder root.' }
$keyPath = "Registry::HKEY_USERS\$($User.Sid)\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders"
$key = Get-Item -LiteralPath $keyPath -ErrorAction Stop
# DoNotExpandEnvironmentNames is essential when reading another user's hive.
@@ -66,7 +81,7 @@ function Resolve-WelaSaclUserFile {
if (-not $raw) { throw "Known-folder value '$folder' is absent." }
$resolved = [regex]::Replace($raw, '(?i)%USERPROFILE%', [System.Text.RegularExpressions.MatchEvaluator]{ param($match) $User.ProfilePath })
if ($resolved -match '%[^%]+%' -or $resolved -notmatch '^(?:[A-Za-z]:\\|\\\\)') { throw 'Known-folder path contains unresolved user variables or is not absolute.' }
if ($folder -eq 'AppData') { $resolved = $resolved.TrimEnd('\') + '\Signal' }
if ($suffix) { $resolved = $resolved.TrimEnd('\') + '\' + $suffix }
$expected = $User.ProfilePath.TrimEnd('\') + '\' + $RelativePath
$state = if ($resolved -ine $expected) { 'Redirected' } else { 'Resolved' }
[pscustomobject]@{ Path = $resolved; State = $state; Diagnostic = 'Resolved from this user hive; remote paths are reported without network access.' }
+37 -4
View File
@@ -64,18 +64,51 @@ $unloaded = Resolve-WelaSaclUserFile -User ([pscustomobject]@{HiveLoaded=$false}
Assert ($unloaded.State -eq 'UnloadedHive') 'Unloaded hive must not fall back to operator APPDATA.'
# Verify actual resolver against real catalog escaping, not a pre-normalized fixture.
$script:knownFolder = '%USERPROFILE%\AppData\Roaming'
$script:knownFolderName = $null; $script:knownFolderReads = 0
$script:key = [pscustomobject]@{}
$script:key | Add-Member ScriptMethod GetValue { param($Name,$Default,$Options) if ($Options -ne [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) { throw 'Unsafe variable expansion mode' }; return $script:knownFolder }
function Get-Item { param($LiteralPath,[switch]$Force,$ErrorAction) return $script:key }
$script:key | Add-Member ScriptMethod GetValue { param($Name,$Default,$Options) if ($Options -ne [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) { throw 'Unsafe variable expansion mode' }; $script:knownFolderName = $Name; return $script:knownFolder }
function Get-Item {
param($LiteralPath,[switch]$Force,$ErrorAction)
if ($LiteralPath -ne 'Registry::HKEY_USERS\S-1-5-21-1\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders') { throw 'Unexpected known-folder read scope.' }
$script:knownFolderReads++; return $script:key
}
$definitions = Get-Content -LiteralPath (Join-Path $PSScriptRoot '../config/audit_sacl_targets.json') -Raw | ConvertFrom-Json
$user = [pscustomobject]@{Sid='S-1-5-21-1';ProfilePath='C:\Users\One';HiveLoaded=$true}
$signal = Resolve-WelaSaclUserFile -User $user -RelativePath $definitions.user_files[1].relpath
Assert ($signal.State -eq 'Resolved' -and $signal.Path -eq 'C:\Users\One\AppData\Roaming\Signal') 'Actual doubled-separator catalog path must not mislabel a default known folder as redirected.'
# Additional catalog-shaped targets retain their complete suffix under the
# selected user's known folder. Resolving a remote root never accesses it.
$userFileFixtures = @(
[pscustomobject]@{ relpath = 'AppData\\Roaming\\Foo'; suffix = 'Foo' },
[pscustomobject]@{ relpath = 'appdata\roaming\Vendor\Cache'; suffix = 'Vendor\Cache' },
[pscustomobject]@{ relpath = 'AppData\Roaming\Foo\Startup'; suffix = 'Foo\Startup' }
)
foreach ($fixture in $userFileFixtures) {
$script:knownFolder = '%USERPROFILE%\AppData\Roaming'
$result = Resolve-WelaSaclUserFile -User $user -RelativePath $fixture.relpath
Assert ($result.State -eq 'Resolved' -and $result.Path -ieq ('C:\Users\One\AppData\Roaming\' + $fixture.suffix) -and $script:knownFolderName -eq 'AppData') 'Additional AppData target must retain its suffix and use the AppData known folder.'
$script:knownFolder = '\\server\share\Roaming'
$result = Resolve-WelaSaclUserFile -User $user -RelativePath $fixture.relpath
Assert ($result.State -eq 'Redirected' -and $result.Path -eq ('\\server\share\Roaming\' + $fixture.suffix) -and $script:knownFolderName -eq 'AppData') 'Redirected AppData target must retain its own suffix.'
$readsBefore = $script:knownFolderReads
Assert ((Get-WelaSaclTargetObservation -Path $result.Path -Kind FileSystem).PathState -eq 'RemoteNotInspected' -and $script:knownFolderReads -eq $readsBefore) 'Remote AppData target must be reported without target access.'
}
$script:knownFolder = '%USERPROFILE%\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup'
$startup = Resolve-WelaSaclUserFile -User $user -RelativePath $definitions.user_files[0].relpath
Assert ($startup.State -eq 'Resolved') 'Actual Startup catalog path normalizes before comparison.'
Assert ($startup.State -eq 'Resolved' -and $script:knownFolderName -eq 'Startup') 'Actual Startup catalog path normalizes before comparison and uses its own known folder.'
$script:knownFolder = '\\server\share\Startup'
Assert ((Resolve-WelaSaclUserFile -User $user -RelativePath $definitions.user_files[0].relpath).State -eq 'Redirected') 'Real redirected Startup remains distinguished.'
$startup = Resolve-WelaSaclUserFile -User $user -RelativePath $definitions.user_files[0].relpath
Assert ($startup.State -eq 'Redirected' -and $startup.Path -eq $script:knownFolder -and $script:knownFolderName -eq 'Startup') 'Real redirected Startup remains distinguished without appending its catalog suffix twice.'
$startupChild = Resolve-WelaSaclUserFile -User $user -RelativePath 'AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\Child'
Assert ($startupChild.Path -eq '\\server\share\Startup\Child' -and $script:knownFolderName -eq 'Startup') 'A target below Startup must retain Startup redirection rather than fall back to AppData.'
foreach ($invalid in @('Desktop\Startup', 'AppData\Local\Foo', 'AppData\RoamingOther\Foo', 'AppData\Roaming',
'AppData\Roaming\..\Local\Foo', 'AppData\Roaming\.\Foo', 'AppData\Roaming\Foo.\Bar', 'AppData\Roaming\Foo \Bar',
'AppData\Roaming\Foo:stream', 'AppData/Roaming/Foo', 'AppData\Roaming\*', 'AppData\Roaming\Foo\',
'AppData\Roaming\%APPDATA%', 'C:\Users\Other\AppData\Roaming\Foo')) {
$readsBefore = $script:knownFolderReads
$result = Resolve-WelaSaclUserFile -User $user -RelativePath $invalid
Assert ($result.State -eq 'UnresolvedUserPath' -and -not $result.Path -and $script:knownFolderReads -eq $readsBefore) "Unsupported user target must remain unresolved without path reads: $invalid"
}
Assert (@($live.Targets | Where-Object { $_.Scope -eq 'user_registry' -and $_.Path -match '\\\\' }).Count -eq 0) 'User registry keys normalize catalog separators.'
# Failures inside an individual profile must affect global inventory completeness.
function Get-ChildItem {
+1 -1
View File
@@ -55,7 +55,7 @@
**新機能:**
- プロファイルの plan/audit/configure に対象を限定した SACL の読み取り専用計画を追加しました。オブジェクト監査ポリシー、ユーザーハイブ・フォルダーリダイレクトの未確認箇所、WEF Run/RunOnce の監査エントリを表示し、`-SaclMode Skip` による省略も明示します。SACL の書き込みや未検証の検知率向上は行いません。 (#398) (@Shirofune-Security)
- プロファイルの plan/audit/configure に対象を限定した SACL の読み取り専用計画を追加しました。オブジェクト監査ポリシー、ユーザーハイブ・フォルダーリダイレクトの未確認箇所、WEF Run/RunOnce の監査エントリを表示し、`-SaclMode Skip` による省略も明示します。ユーザーファイルの対象は、そのユーザーの AppData または Startup 既知フォルダー配下の相対パスを保持し、未対応・曖昧なパスは未解決として扱います。SACL の書き込みや未検証の検知率向上は行いません。 (#398) (@Shirofune-Security)
- MITRE ATT&CK Navigatorヒートマップに対応した。 (#11) (@fukusuket)
+1 -1
View File
@@ -57,7 +57,7 @@
**New Features:**
- Profile plan/audit/configure now include read-only targeted SACL prerequisites with object policy masks, per-user hive and redirected-folder gaps, exact WEF Run/RunOnce audit entries, and an explicit `-SaclMode Skip`. No SACL writes or unverified detection uplift are implied. (#398) (@Shirofune-Security)
- Profile plan/audit/configure now include read-only targeted SACL prerequisites with object policy masks, per-user hive and redirected-folder gaps, exact WEF Run/RunOnce audit entries, and an explicit `-SaclMode Skip`. User-file targets retain their configured suffix under the user's AppData or Startup known folder; unsupported or ambiguous paths remain unresolved. No SACL writes or unverified detection uplift are implied. (#398) (@Shirofune-Security)
- Support for MITRE ATT&CK Navigator heatmaps. (#11) (@fukusuket)