修掉的:空 catch 5 处;名词白名单 7 处;default-value 开关、自带 -WhatIf、lab 的明文口令与 irm|iex 各挂抑制并写明理由。 MaxDepth:它是分析器拓出来的真 bug —— 参数声明了却从未使用,也就是配置里的 CatalogMaxDepth 是假的,前缀补全实际只查 1 层,而配置注释与 README 都承诺「向下找几层」。按确认过的原则处理:**先让文档不撒谎**,所以把整条链路去掉(配置默认值、三个函数的参数、70 处实参、配置注释),而不是留一个假旋钮。零行为变化。想真的支持多层补全时,那是一个独立决定。 剩下 3 条都是分析器的误判,而且我实测确认过其中一条:$sourcePath 被报「赋值后从未使用」,我照着改成 $null = 之后,Set-StrictMode -Version 3.0 下读未定义变量直接抛错,Security 套件的 BeforeAll 挂掉、4 条用例连带失败。恢复后才绿。 这一类误判有共同成因:静态分析看不到「在传给 Test-Case / It / Where-Object 的 scriptblock 里被使用」。所以我只对能证明是误判的挂抑制并写明理由,不为了数字好看去改代码。 验收:test.ps1 9/9 全绿(7 与 5.1)、100 个文件两版解析零错、Run-RealSmoke 4/4。
128 lines
5.6 KiB
PowerShell
128 lines
5.6 KiB
PowerShell
function Get-BakNRetSecurityRecords {
|
||
<#
|
||
.SYNOPSIS
|
||
采集一组归档项的安全描述符,键是**归档内相对路径**(`<Slot>\…`)。
|
||
|
||
.DESCRIPTION
|
||
键用归档内路径而不是宿主机路径:目标机器上 `%UserProfile%` 会变、名录的前缀补全
|
||
(legendary -> legendary_2.0.4)也会变,只有归档内相对路径在两端是同一个坐标系。
|
||
|
||
遍历用显式栈,并且**跳过 reparse point**:PS 5.1 的 Get-ChildItem -Recurse 会
|
||
跟着 junction 无限转;scoop 的 `apps\<app>\current` 就是 junction,正撞在这个坑上。
|
||
|
||
$ScopeMap 由 Split-BakNRetPatternScope 产出(项下标 -> 该相对根的模式数组),
|
||
所以这里的排除判定与真正交给 7z 的 -x! / -xr! 是同一套规则。
|
||
|
||
Mode:
|
||
* Roots —— 只存每个归档项的根(最省,适合"权限只在根上"的场景)
|
||
* Smart —— 根 + 所有"继承复现不出来"的对象(默认;几万文件的树 sidecar 也只有几百 KB)
|
||
* Full —— 每一个对象都存(最保险,sidecar 会大到几 MB)
|
||
|
||
返回 [pscustomobject]@{ Records; Scanned; Kept; Errors }。
|
||
#>
|
||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '',
|
||
Justification = '同上:它采集一整棵树的记录,单数名是「读单个对象」的那个函数。')]
|
||
param(
|
||
[array]$Items = @(),
|
||
[hashtable]$ScopeMap = @{},
|
||
[ValidateSet('Roots', 'Smart', 'Full')][string]$Mode = 'Smart',
|
||
[switch]$IncludeSacl
|
||
)
|
||
|
||
$records = New-Object System.Collections.Generic.List[object]
|
||
$scanned = 0
|
||
$errorCount = 0
|
||
|
||
# 读安全描述符要 READ_CONTROL:系统目录里读不到是常态(C:\ProgramData 下就有
|
||
# Get-Acl 直接报 "Attempted to perform an unauthorized operation" 的目录)。
|
||
# SeBackupPrivilege 启用后系统会把读权限授予任何文件;连它都没有的账户,
|
||
# 读不到的对象会带 e 字段落进 sidecar,而不是被静默当成"没有特殊权限"。
|
||
$privileges = @('SeBackupPrivilege')
|
||
if ($IncludeSacl) { $privileges += 'SeSecurityPrivilege' }
|
||
Enable-BakNRetPrivilege -Name $privileges | Out-Null
|
||
|
||
for ($index = 0; $index -lt $Items.Count; $index++) {
|
||
$item = $Items[$index]
|
||
if (-not $item) { continue }
|
||
|
||
$archiveRoot = [string]$item.ArchivePath
|
||
$real = [string]$item.RealPath
|
||
if ([string]::IsNullOrWhiteSpace($archiveRoot) -or [string]::IsNullOrWhiteSpace($real)) { continue }
|
||
if (-not (Test-Path -LiteralPath $real)) { continue }
|
||
|
||
$patterns = @()
|
||
if ($ScopeMap -and $ScopeMap.ContainsKey($index)) { $patterns = @($ScopeMap[$index]) }
|
||
|
||
$rootItem = Get-Item -LiteralPath $real -Force -ErrorAction SilentlyContinue
|
||
if (-not $rootItem) { continue }
|
||
|
||
if (-not $rootItem.PSIsContainer) {
|
||
$record = Get-BakNRetSecurityRecord -Path $real -Key $archiveRoot -Kind 'f' -IncludeSacl:$IncludeSacl
|
||
$scanned++
|
||
if ($record.e) { $errorCount++ }
|
||
$records.Add($record)
|
||
continue
|
||
}
|
||
|
||
$rootRecord = Get-BakNRetSecurityRecord -Path $real -Key $archiveRoot -Kind 'd' -IncludeSacl:$IncludeSacl
|
||
$scanned++
|
||
if ($rootRecord.e) { $errorCount++ }
|
||
$records.Add($rootRecord)
|
||
|
||
if ($Mode -eq 'Roots') { continue }
|
||
|
||
$pending = New-Object System.Collections.Generic.Stack[object]
|
||
$pending.Push(@{
|
||
Dir = $rootItem
|
||
Rel = ''
|
||
Owner = $rootRecord.o
|
||
Group = $rootRecord.g
|
||
Inheritable = $rootRecord.Inheritable
|
||
Signatures = $rootRecord.AllSignatures
|
||
})
|
||
|
||
while ($pending.Count -gt 0) {
|
||
$frame = $pending.Pop()
|
||
foreach ($child in @(Get-ChildItem -LiteralPath $frame.Dir.FullName -Force -ErrorAction SilentlyContinue)) {
|
||
if ($child.Attributes -band [IO.FileAttributes]::ReparsePoint) { continue }
|
||
|
||
$childRel = if ($frame.Rel) { $frame.Rel + '\' + $child.Name } else { $child.Name }
|
||
if (Test-BakNRetPathExcluded -RelativePath $childRel -Patterns $patterns) { continue }
|
||
|
||
$kind = if ($child.PSIsContainer) { 'd' } else { 'f' }
|
||
$record = Get-BakNRetSecurityRecord -Path $child.FullName -Key ($archiveRoot + '\' + $childRel) `
|
||
-Kind $kind -IncludeSacl:$IncludeSacl -ParentSignatures $frame.Signatures
|
||
$scanned++
|
||
if ($record.e) { $errorCount++ }
|
||
|
||
if ($Mode -eq 'Full') {
|
||
$records.Add($record)
|
||
}
|
||
elseif (Test-BakNRetSecurityRecordNeeded -Record $record `
|
||
-ParentOwner $frame.Owner -ParentGroup $frame.Group `
|
||
-ParentInheritable $frame.Inheritable -ParentSignatures $frame.Signatures) {
|
||
$records.Add($record)
|
||
}
|
||
|
||
if ($child.PSIsContainer) {
|
||
$pending.Push(@{
|
||
Dir = $child
|
||
Rel = $childRel
|
||
Owner = $record.o
|
||
Group = $record.g
|
||
Inheritable = $record.Inheritable
|
||
Signatures = $record.AllSignatures
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return [pscustomobject]@{
|
||
Records = @($records.ToArray())
|
||
Scanned = $scanned
|
||
Kept = $records.Count
|
||
Errors = $errorCount
|
||
}
|
||
}
|