@deepseek-ai/dsh-sandbox-windows-acl 0.2.0-rc.1 → 0.2.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
en: 289e65bddf20dd79
|
|
28
28
|
zh: 7ff72ed64c4b4d1c
|
|
29
29
|
/deepseek-ai-dsh-sandbox-windows-acl/use-this-package/failures-and-recovery:
|
|
30
|
-
en:
|
|
31
|
-
zh:
|
|
30
|
+
en: 7fc0707debb1e67c
|
|
31
|
+
zh: e3b5529bf82f868e
|
|
32
32
|
/deepseek-ai-dsh-sandbox-windows-acl/understand-the-implementation:
|
|
33
33
|
en: c2cde86c85c2c619
|
|
34
34
|
zh: a29a114e591f9d3f
|
package/README.md
CHANGED
|
@@ -77,11 +77,11 @@ Temp isolation is per live session/workspace pair: sessions sharing a workspace
|
|
|
77
77
|
|
|
78
78
|
A denial this backend cannot explain is diagnosed by the `diagnose-windows-sandbox-acl` skill bundled in `assets/`. `registerAclDiagnosisSkill` contributes it when `dsh-sandbox-local` uses its built-in Windows runner and a skill registry is available. Registration extracts resources for external PowerShell, including in ASAR and SEA deployments; fiber disposal unregisters the provider and removes its copy. Missing assets fail registration.
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
One run diagnoses and repairs. The script has no modes: it reads the requested path and every ancestor, adds the signed-in user's full-control allow ACE to chain directories that lack effective `WRITE_DAC` or `WRITE_OWNER`, and removes explicit package allow ACEs at their sources, ancestor first, verifying each change by re-reading it. Repairs preserve denies, owner, inheritance and SACL, and back up each changed DACL. An object is modified only when it is `-AllowRoot` or strictly inside it, so a workspace root can repair itself; reparse paths and managed application trees are refused, and a repair needs effective `WRITE_DAC`. Failed repairs restore attempted changes in reverse order and exit nonzero. Follow the [bundled skill](assets/diagnose-windows-sandbox-acl/SKILL.md) for the single command, original-operation verification and recovery.
|
|
81
81
|
|
|
82
|
-
Every run
|
|
82
|
+
Every run prints `REPORT` JSON records of observations, actions, reasons and verification, then a final `RECAP` line carrying the verdicts, changes, verifications, refusals and scans, and a summary with the counts, recovery commands and `nextAction`; unknown observations remain unknown. The same run writes every record to `-Out\acl-report-*.jsonl`, because tool output keeps only its tail. `-Out` also receives two recovery files per change: the DACL the object owned before that change and the independent script that restores it. Action completion records API execution; verification records the observed result. The [recovery decisions](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.md#acl-diagnosis-and-recovery) explain resource ownership and rollback limits.
|
|
83
83
|
|
|
84
|
-
Inspection visits the requested object and each ancestor through the filesystem root. Each ACL observation's `path` identifies the inspected object; the classification's `details.packageObjects` lists objects carrying package allow ACEs without establishing the cause of the original failure.
|
|
84
|
+
Inspection visits the requested object and each ancestor through the filesystem root. Each ACL observation's `path` identifies the inspected object; the classification's `details.packageObjects` lists objects carrying package allow ACEs without establishing the cause of the original failure. Each package removal is verified per object by comparing the recorded own ACEs with the re-read DACL, so a deny that shares the removed allow's SID remains visible in the verification details.
|
|
85
85
|
|
|
86
86
|
-----
|
|
87
87
|
|
package/README.zh.md
CHANGED
|
@@ -79,11 +79,11 @@ rmSync(tempDir, { recursive: true, force: true })
|
|
|
79
79
|
|
|
80
80
|
此后端无法解释的拒绝交给 `assets/` 中随包发布的 `diagnose-windows-sandbox-acl` 技能诊断。当 `dsh-sandbox-local` 使用内置 Windows runner 且技能注册表可用时,`registerAclDiagnosisSkill` 会注册它。注册时提取供外部 PowerShell 使用的资源,ASAR 和 SEA 部署也适用;fiber 释放时注销提供者并删除副本。缺少资源会导致注册失败。
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
一次运行同时完成诊断与修复。脚本没有模式开关:它读取请求路径及每一级祖先,为链路上缺少有效 `WRITE_DAC` 或 `WRITE_OWNER` 的目录补上当前用户的完全控制允许 ACE,并从祖先开始在其来源移除显式包允许 ACE,每处改动都通过重新读取来验证。修复保留拒绝条目、所有者、继承和 SACL,并备份每个被修改的 DACL。仅当对象就是 `-AllowRoot` 或严格位于其内部时才会被修改,因此工作区根目录可以自我修复;重解析路径和受管理的应用目录会被拒绝,修复需要有效的 `WRITE_DAC`。修复失败时按相反顺序恢复尝试修改的内容,并以非零状态退出。单条命令、原始操作验证及恢复流程见[内置技能](assets/diagnose-windows-sandbox-acl/SKILL.md)。
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
每次执行都会打印包含观察、操作、原因和验证结果的 `REPORT` JSON 记录,最后给出一行 `RECAP`(判定、改动、验证、拒绝与扫描结果)以及含计数、恢复命令和 `nextAction` 的摘要;未知观察保持未知。同一次执行还会把全部记录写入 `-Out\acl-report-*.jsonl`,因为工具输出只保留末尾部分。`-Out` 同时为每次改动写入两个恢复文件:该对象在本次改动前拥有的 DACL,以及用于恢复它的独立脚本。操作完成记录 API 执行情况,验证记录实际观察到的结果。[恢复决策](../../../.agents/notes/implemented/feature/2026-08-08-windows-acl-restricted-token-sandbox.zh.md#acl-diagnosis-and-recovery) 说明资源归属和回滚限制。
|
|
85
85
|
|
|
86
|
-
检查会从请求对象逐级访问祖先,直到文件系统根目录。每条 ACL 观察的 `path` 标识实际检查的对象;分类结果的 `details.packageObjects` 列出包含包允许 ACE
|
|
86
|
+
检查会从请求对象逐级访问祖先,直到文件系统根目录。每条 ACL 观察的 `path` 标识实际检查的对象;分类结果的 `details.packageObjects` 列出包含包允许 ACE 的对象,但不能据此确认原始失败的原因。每次包 ACE 移除都按对象验证:把记录下来的自身 ACE 与重新读取的 DACL 比对,因此与被移除允许项共用 SID 的拒绝条目仍会出现在验证详情中。
|
|
87
87
|
|
|
88
88
|
-----
|
|
89
89
|
|
|
@@ -1,46 +1,63 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: diagnose-windows-sandbox-acl
|
|
3
|
-
description: 'Use on Windows for unexpected DSH sandbox access denials: workspace writes or listing fail, or an ordinarily readable path cannot be read.
|
|
3
|
+
description: 'Use on Windows for unexpected DSH sandbox access denials: workspace writes or listing fail, or an ordinarily readable path cannot be read. One bundled command inspects the path and every ancestor and repairs the ACL problems it proves in that same run. Expected confinement denials need no ACL repair.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Diagnose Windows sandbox ACL failures
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**Write every approval request in plain words, in the user's language.** The prompt is all the user reads before widening access, so it must stand alone: which folder the script touches, that it adds the signed-in user's full-control entry where a right is missing and removes foreign package entries, that file contents and owners are unchanged, and that the printed recovery command undoes each change. Keep error codes, `WRITE_DAC`/`WRITE_OWNER`, `S-1-15-2-*` SIDs, `icacls`, ACL, ACE, verdict names and switches out of the request: write Windows file permissions, not ACL or ACE. Ask once, for one command.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## One command diagnoses and repairs
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
The script has no modes. `-Path`, `-AllowRoot` and `-Out` read the path and every ancestor and repair what the observations prove, in the same run:
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- directories on that chain that lack effective `WRITE_DAC` or `WRITE_OWNER` receive a full-control allow ACE for the signed-in user, because DSH cannot provision its workspace grant without them;
|
|
15
|
+
- explicit AppContainer package allow ACEs (`S-1-15-2-*`, except the well-known groups ending in 1 or 2) are removed at their sources, ancestor first, which also removes those packages' access;
|
|
16
|
+
- a directory the sandbox cannot provision — the state its provisioning error reports on the workspace root — or the authorized root itself also has its subtree searched for those ACEs in the same run, so a deeper entry needs no second request. That bounded walk reports `truncated` and unreadable directories; if truncated, pass the still-failing deeper path once more.
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Resolve the script from this skill's resource directory. Run it in the current sandbox first: classification completes under `workspace-write` with a writable `-Out` directory, while `read-only` cannot write one and refuses. When a denial or an unwritable report directory prevents the call, request one escalation of that same call through the normal approval path instead of assuming the diagnosis cannot run; if approval is refused, unavailable to the session, or forbidden by policy, report the path as undiagnosed and stop. Unconfined does not elevate the Windows token. Use one path per invocation; do not repeat `-Path` or pass a comma-separated string to `pwsh -File`.
|
|
18
|
+
Every change is backed up, then verified by re-reading it. `-AllowRoot` bounds all of it: an object is changed only when it is that directory or strictly inside it, so a workspace root can repair itself. Never split this into a diagnostic call and a repair call, never ask twice for one repair, and never pass a mode switch that does not exist.
|
|
19
19
|
|
|
20
20
|
```powershell
|
|
21
|
-
& '<skill-directory>\scripts\diagnose-windows-sandbox-acl.ps1' -Path '<failing-path>' -
|
|
21
|
+
& '<skill-directory>\scripts\diagnose-windows-sandbox-acl.ps1' -Path '<failing-path>' -AllowRoot '<authorized-directory>' -Out '<recovery-directory>'
|
|
22
22
|
exit $LASTEXITCODE
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Substitute full, quoted paths.
|
|
25
|
+
Substitute full, quoted paths. Keep `-Out` persistent and user-owned, preferably beside the failing workspace; never use the skill resource directory, which is deleted when the skill unloads. Pass one path per invocation.
|
|
26
|
+
|
|
27
|
+
## Run it
|
|
28
|
+
|
|
29
|
+
Repeat the failing operation once confined. The script writes permissions, which the confined token cannot do — run it there and it would report the sandbox's own restriction as a missing right — so request approval for that one command and run it unconfined. A confined run of the script is never useful. Unconfined does not elevate the Windows token.
|
|
30
|
+
|
|
31
|
+
Diagnose unexpected denials of workspace writes, listing, or plainly readable paths; explain expected ones instead: writes outside the workspace, any write in `read-only`, piped grandchild `spawn EPERM`, ConstrainedLanguage errors. If approval is refused or unavailable, report the path as undiagnosed and stop.
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
## Read the output
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
Every record reaches stdout, the `acl-report-*.jsonl` file under `-Out`, and the final `RECAP` line, which carries the verdicts, changes, verifications, refusals and scans. Tool output keeps only its tail, so read the recap first, and read specific records from the report when it is not enough. Trust `verification` records, never `completed` actions. Decide from `details.nextAction`:
|
|
36
|
+
|
|
37
|
+
| `nextAction` | Meaning |
|
|
30
38
|
|---|---|
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
|
|
39
|
+
| `verify_original_confined_operation` | Repairs verified; repeat the original operation confined. |
|
|
40
|
+
| `stop` | Nothing repaired, or a refusal ended the run. Report and stop. |
|
|
41
|
+
| `restore_pending_then_stop` | Rollback unverified. Run the printed recovery commands in order, then stop. |
|
|
42
|
+
|
|
43
|
+
A deny ACE's presence alone does not establish causation, and the script never removes one; DSH's `S-1-4-*` grants and the Everyone `DeleteSubdirectoriesAndFiles` deny are expected, not conflicts. A deny that blocks the repair ends the run without a repair (`REPAIR_REFUSED`, or `GRANT_FAILED`) after restoring what it attempted. Each change leaves two files in `-Out` (`acl-backup-<id>.json` and its `.ps1`), and the run prints the matching `ROLLBACK` command.
|
|
44
|
+
|
|
45
|
+
## After a repair, and when it stops
|
|
46
|
+
|
|
47
|
+
Repeat the original failed operation confined. A verified repair is not undone because the original operation fails for a further reason: continue from the new observations, and if a deeper path is still denied, run the same one command there — again one call, one approval.
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
**Stop after any failed or refused repair, or failed verification.** The script already restored that invocation's changes; do not repeat it or start another repair, and never remove a deny ACE by hand.
|
|
37
50
|
|
|
38
|
-
|
|
51
|
+
**A stopped run still owes the user a decision.** Name the blocking object and the right or ACE it lacks, what changed and what was rolled back, the recovery commands in order, and the report path. Only the user can lift confinement, so ask them to switch this session to **full access** (Chinese UI: 完全权限) temporarily, and say what that opens: keep working under it, or ask you to keep investigating this file-permission problem, since the report already records the mechanism and the evidence. Then ask them to send this session as feedback, quoting the report records that matter, so the unhandled scenario reaches us — only what reaches this conversation travels with it. For example:
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
> 这个工作区被 Windows 文件权限挡住了:`<对象>` 上 `<缺哪个权限 / 哪条权限项>`。你可以把本会话切到「完全权限」(full access) 临时继续工作;切完之后既能直接干活,也可以让我继续排查这个文件权限问题(报告里已经有机制和证据)。也麻烦把这个会话作为反馈发出去,好让我们补上这个场景。
|
|
54
|
+
>
|
|
55
|
+
> This workspace is blocked by Windows file permissions: `<object>` `<missing right / offending entry>`. Switch this session to full access (完全权限) to keep working meanwhile; then either continue your work or have me keep investigating this file-permission problem from the report. Please also send this session as feedback so we can cover the case.
|
|
41
56
|
|
|
42
|
-
|
|
57
|
+
## Never
|
|
43
58
|
|
|
44
|
-
|
|
59
|
+
- edit ACLs by hand, change an owner, erase a deny, or replace child permissions recursively;
|
|
60
|
+
- run the script elevated, through UAC or `runas`;
|
|
61
|
+
- widen `-AllowRoot` to reach an ancestor, or repeat a denied or failed call.
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
Report in the user's language: analyzed paths, changes, verification, recovery commands, next step. Label an authorized unconfined run as such, not as a sandbox repair.
|
|
@@ -1,75 +1,66 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
|
|
4
|
-
DSH sandbox cannot open.
|
|
3
|
+
Diagnose the DSH sandbox ACL failures on Windows and repair them in one run.
|
|
5
4
|
|
|
6
5
|
.DESCRIPTION
|
|
7
6
|
A denial inside the DSH sandbox is worth diagnosing only when it contradicts
|
|
8
7
|
what the active mode promises: writes inside the workspace, or reads that the
|
|
9
|
-
signed-in user should plainly have.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
signed-in user should plainly have. One invocation reads each requested path
|
|
9
|
+
and every ancestor, then repairs what it can in the same run:
|
|
10
|
+
|
|
11
|
+
* every directory on that chain inside -AllowRoot that lacks effective
|
|
12
|
+
WRITE_DAC or WRITE_OWNER gets a FullControl allow ACE for the current
|
|
13
|
+
user, so the sandbox can provision the grant it needs; and
|
|
14
|
+
* every explicit AppContainer package allow ACE (S-1-15-2-*, excluding the
|
|
15
|
+
well-known groups ending in 1 or 2) is removed at its source, ancestor
|
|
16
|
+
first, which also removes that package's access to the tree.
|
|
17
|
+
|
|
18
|
+
A directory the caller cannot provision is the state the sandbox reports on the
|
|
19
|
+
workspace root, while the conflicting entry often sits deeper; the authorized root
|
|
20
|
+
is examined for the same reason. For either case the same run also collects explicit
|
|
21
|
+
package allow ACEs under the requested directory, so they do not need a second call.
|
|
22
|
+
The walk is bounded, enters no reparse point or managed application tree, and reports
|
|
23
|
+
truncation and unreadable directories instead of claiming a complete walk.
|
|
24
|
+
|
|
25
|
+
Every observation, change, verification and recovery command is printed as it
|
|
26
|
+
happens, so one run shows the caller the complete set of actions.
|
|
27
|
+
|
|
28
|
+
-AllowRoot bounds every change: an object is modified only when it is that
|
|
29
|
+
directory or strictly inside it, so a caller can repair its own workspace
|
|
30
|
+
root. Reparse paths and managed application trees are refused. Each modified
|
|
31
|
+
object is backed up first, with an independent recovery script, and every DACL
|
|
32
|
+
write is verified by re-reading it. A failed repair restores all attempted
|
|
33
|
+
DACL changes from this invocation in reverse order and stops.
|
|
25
34
|
|
|
26
35
|
The script never creates, deletes, or writes the contents of any file. Its
|
|
27
|
-
only
|
|
36
|
+
only writes are the recovery artifacts under -Out and the DACLs it repairs.
|
|
28
37
|
|
|
29
38
|
.PARAMETER Path
|
|
30
|
-
One or more failing paths. Each is diagnosed together with its ancestors.
|
|
39
|
+
One or more failing paths. Each is diagnosed and repaired together with its ancestors.
|
|
31
40
|
|
|
32
41
|
.PARAMETER AllowRoot
|
|
33
|
-
Required
|
|
42
|
+
Required. Every modified object must be this directory or strictly inside it.
|
|
34
43
|
|
|
35
44
|
.PARAMETER Out
|
|
36
|
-
Required
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.PARAMETER Compact
|
|
40
|
-
Save every REPORT record under -Out and print a decision summary with the
|
|
41
|
-
inspected paths, findings, actions, verification and pending recovery commands.
|
|
42
|
-
|
|
43
|
-
.PARAMETER Fix
|
|
44
|
-
Remove individual package allow ACEs, preserving well-known package groups.
|
|
45
|
-
Without a mutation switch, only diagnose.
|
|
46
|
-
|
|
47
|
-
.PARAMETER GrantFullControl
|
|
48
|
-
Grant the current user full control on the requested path. Full control is what
|
|
49
|
-
the documented precondition needs: it carries WRITE_DAC for the DACL and
|
|
50
|
-
WRITE_OWNER for the mandatory label DSH writes in the same call, while taking
|
|
51
|
-
ownership supplies only WRITE_DAC. The owner is never changed. Mutually
|
|
52
|
-
exclusive with -Fix.
|
|
45
|
+
Required for a repair. Receives one recovery record and one recovery script
|
|
46
|
+
per change, so an object repaired twice keeps both recovery points.
|
|
53
47
|
|
|
54
48
|
.PARAMETER Restore
|
|
55
|
-
Restore the DACL recovery record for exactly one -Path
|
|
56
|
-
and WRITE_DAC, preserves owner and SACL
|
|
49
|
+
Restore the DACL recovery record for exactly one -Path instead of repairing.
|
|
50
|
+
Requires -AllowRoot and WRITE_DAC, preserves owner and SACL.
|
|
57
51
|
|
|
58
52
|
.EXAMPLE
|
|
59
|
-
pwsh -File diagnose-windows-sandbox-acl.ps1 -Path '
|
|
53
|
+
pwsh -File diagnose-windows-sandbox-acl.ps1 -Path '<failing path>' -AllowRoot '<authorized directory>' -Out '<recovery directory>'
|
|
60
54
|
|
|
61
55
|
.EXAMPLE
|
|
62
|
-
pwsh -File diagnose-windows-sandbox-acl.ps1 -Path '<
|
|
56
|
+
pwsh -File diagnose-windows-sandbox-acl.ps1 -Path '<failing path>' -AllowRoot '<authorized directory>' -Restore '<recovery record>'
|
|
63
57
|
#>
|
|
64
58
|
[CmdletBinding()]
|
|
65
59
|
param(
|
|
66
60
|
[Parameter(Mandatory = $true)][string[]]$Path,
|
|
67
61
|
[string]$AllowRoot,
|
|
68
62
|
[string]$Out,
|
|
69
|
-
[
|
|
70
|
-
[switch]$GrantFullControl,
|
|
71
|
-
[string]$Restore,
|
|
72
|
-
[switch]$Compact
|
|
63
|
+
[string]$Restore
|
|
73
64
|
)
|
|
74
65
|
|
|
75
66
|
Set-StrictMode -Version Latest
|
|
@@ -78,30 +69,34 @@ $ErrorActionPreference = 'Stop'
|
|
|
78
69
|
# These two well-known groups name all packages, not one conflicting package.
|
|
79
70
|
$PACKAGE_SID = '^S-1-15-2-(?![12]$)'
|
|
80
71
|
$LOW_LABEL_SID = 'S-1-16-4096'
|
|
72
|
+
# Upper bound on the directories one subtree scan may visit, so a huge workspace
|
|
73
|
+
# cannot turn one approved repair into an unbounded walk. Truncation is reported.
|
|
74
|
+
$SCAN_LIMIT = 2000
|
|
81
75
|
|
|
82
|
-
function Write-Line { param([string]$Text)
|
|
76
|
+
function Write-Line { param([string]$Text) Write-Output $Text }
|
|
83
77
|
|
|
84
78
|
# Reports bypass the success pipeline so diagnostics cannot become a function's
|
|
85
79
|
# return value. Each JSON record occupies one stdout line, including paths/errors.
|
|
86
80
|
function Write-Report {
|
|
87
81
|
param([string]$Kind, [string]$Operation, [string]$Target, [string]$Status, [string]$Reason, $Details = @{})
|
|
88
82
|
$record = [ordered]@{ kind = $Kind; operation = $Operation; path = $Target; status = $Status; reason = $Reason; details = $Details }
|
|
89
|
-
$script:reports.Add($record)
|
|
90
83
|
if ($Kind -eq 'observation' -and $Status -in @('unknown', 'unreadable', 'partial')) { $script:observationFailures++ }
|
|
84
|
+
$script:reports.Add($record)
|
|
91
85
|
$json = $record | ConvertTo-Json -Depth 12 -Compress
|
|
92
|
-
|
|
86
|
+
# The complete record set also lands in the report file: tool output is truncated to its
|
|
87
|
+
# tail, so a long run's early records survive only there.
|
|
88
|
+
if ($null -ne $script:reportWriter) {
|
|
93
89
|
try { $script:reportWriter.WriteLine($json); $script:reportWriter.Flush() }
|
|
94
|
-
catch {
|
|
95
|
-
|
|
90
|
+
catch [System.IO.IOException] {
|
|
91
|
+
# stdout keeps only its tail, so this file is the complete record: a write failure
|
|
92
|
+
# has to reach the caller, on stderr, instead of leaving a silently incomplete file.
|
|
93
|
+
[Console]::Error.WriteLine('REPORT_IO_FAILED {0}: {1}' -f $script:reportPath, $_.Exception.Message)
|
|
94
|
+
try { $script:reportWriter.Dispose() } catch { }
|
|
96
95
|
$script:reportWriter = $null
|
|
97
|
-
$script:reportFailed = $true
|
|
98
|
-
[Console]::Out.WriteLine('REPORT ' + $json)
|
|
99
|
-
throw
|
|
100
96
|
}
|
|
101
97
|
}
|
|
102
|
-
|
|
98
|
+
[Console]::Out.WriteLine('REPORT ' + $json)
|
|
103
99
|
}
|
|
104
|
-
|
|
105
100
|
function Invoke-ReportedOperation {
|
|
106
101
|
param([string]$Operation, [string]$Target, [string]$Reason, [string]$Effect, [scriptblock]$Action)
|
|
107
102
|
$entry = [ordered]@{ id = $script:operations.Count + 1; operation = $Operation; path = $Target; effect = $Effect; status = 'started' }
|
|
@@ -257,15 +252,16 @@ function Test-DangerousRoot {
|
|
|
257
252
|
}
|
|
258
253
|
|
|
259
254
|
function Test-UnderRoot {
|
|
260
|
-
param([string]$FullPath, [string]$Root)
|
|
255
|
+
param([string]$FullPath, [string]$Root, [switch]$AllowEqual)
|
|
261
256
|
$r = Get-NormalizedPath $Root
|
|
262
257
|
$c = Get-NormalizedPath $FullPath
|
|
258
|
+
if ($AllowEqual -and $c -ieq $r) { return $true }
|
|
263
259
|
return $c -ine $r -and $c.StartsWith($r.TrimEnd('\') + '\', [System.StringComparison]::OrdinalIgnoreCase)
|
|
264
260
|
}
|
|
265
261
|
|
|
266
262
|
function Get-RepairRefusal {
|
|
267
|
-
param([string]$FullPath, [string]$Root)
|
|
268
|
-
if (-not (Test-UnderRoot -FullPath $FullPath -Root $Root)) { return "$FullPath is outside -AllowRoot" }
|
|
263
|
+
param([string]$FullPath, [string]$Root, [switch]$AllowEqual)
|
|
264
|
+
if (-not (Test-UnderRoot -FullPath $FullPath -Root $Root -AllowEqual:$AllowEqual)) { return "$FullPath is outside -AllowRoot" }
|
|
269
265
|
$danger = Test-DangerousRoot -FullPath $FullPath
|
|
270
266
|
if ($danger) { return "$FullPath is a $danger" }
|
|
271
267
|
# Checking every component also covers a junction used as AllowRoot itself.
|
|
@@ -278,6 +274,68 @@ function Get-RepairRefusal {
|
|
|
278
274
|
return $null
|
|
279
275
|
}
|
|
280
276
|
|
|
277
|
+
# A caller that cannot provision a directory also cannot confine a child anywhere
|
|
278
|
+
# under it, so one approved run examines that subtree for explicit package allow
|
|
279
|
+
# ACEs instead of leaving them for a second call. Directories only: DSH grants and
|
|
280
|
+
# traverses directories, and no reparse point or managed application tree is entered.
|
|
281
|
+
function Get-SubtreePackageSources {
|
|
282
|
+
param([string]$Root, [int]$Limit)
|
|
283
|
+
$sources = @()
|
|
284
|
+
$visited = 0
|
|
285
|
+
$enqueued = 0
|
|
286
|
+
$failed = 0
|
|
287
|
+
$truncated = $false
|
|
288
|
+
$queue = [System.Collections.Generic.Queue[string]]::new()
|
|
289
|
+
# Unreadable directories are skipped, never guessed at: the caller reports the count.
|
|
290
|
+
try {
|
|
291
|
+
foreach ($child in [System.IO.Directory]::EnumerateDirectories($Root)) {
|
|
292
|
+
if ($enqueued -ge $Limit) { $truncated = $true; break }
|
|
293
|
+
$queue.Enqueue($child); $enqueued++
|
|
294
|
+
}
|
|
295
|
+
} catch [System.UnauthorizedAccessException] { $failed++ }
|
|
296
|
+
catch [System.IO.IOException] { $failed++ }
|
|
297
|
+
while ($queue.Count -gt 0) {
|
|
298
|
+
if ($visited -ge $Limit) { $truncated = $true; break }
|
|
299
|
+
$current = $queue.Dequeue()
|
|
300
|
+
$visited++
|
|
301
|
+
if (Test-DangerousRoot -FullPath $current) { continue }
|
|
302
|
+
$item = Get-Item -LiteralPath $current -Force -ErrorAction SilentlyContinue
|
|
303
|
+
if ($null -eq $item) { $failed++; continue }
|
|
304
|
+
if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { continue }
|
|
305
|
+
try {
|
|
306
|
+
$acl = Get-Acl -LiteralPath $current
|
|
307
|
+
foreach ($rule in $acl.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])) {
|
|
308
|
+
if ($rule.AccessControlType -eq 'Allow' -and $rule.IdentityReference.Value -match $PACKAGE_SID) { $sources += $current; break }
|
|
309
|
+
}
|
|
310
|
+
} catch [System.UnauthorizedAccessException] { $failed++ }
|
|
311
|
+
catch [System.IO.IOException] { $failed++ }
|
|
312
|
+
try {
|
|
313
|
+
foreach ($child in [System.IO.Directory]::EnumerateDirectories($current)) {
|
|
314
|
+
if ($enqueued -ge $Limit) { $truncated = $true; break }
|
|
315
|
+
$queue.Enqueue($child); $enqueued++
|
|
316
|
+
}
|
|
317
|
+
} catch [System.UnauthorizedAccessException] { $failed++ }
|
|
318
|
+
catch [System.IO.IOException] { $failed++ }
|
|
319
|
+
}
|
|
320
|
+
return [ordered]@{ sources = @($sources); visited = $visited; truncated = $truncated; failed = $failed }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function Get-OwnDaclSddl {
|
|
324
|
+
param([string]$Sddl)
|
|
325
|
+
# Inherited ACEs follow their parent: repeating them in a saved DACL forces them
|
|
326
|
+
# back onto the object on restore and stops matching once the parent re-derives
|
|
327
|
+
# them. Keep the observed control flags so the result still carries the
|
|
328
|
+
# object's inheritance protection, and keep every ACE the object owns.
|
|
329
|
+
$flags = [regex]::Match($Sddl, '^D:([A-Z]*)').Groups[1].Value
|
|
330
|
+
$aces = @([regex]::Matches($Sddl, '\([^()]*\)') | ForEach-Object { $_.Value } | Where-Object { ($_ -split ';')[1] -notmatch 'ID' })
|
|
331
|
+
return 'D:{0}{1}' -f $flags, ($aces -join '')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function Get-AceSet {
|
|
335
|
+
param([string]$Sddl)
|
|
336
|
+
return @([regex]::Matches($Sddl, '\([^()]*\)') | ForEach-Object { $_.Value } | Sort-Object)
|
|
337
|
+
}
|
|
338
|
+
|
|
281
339
|
function Save-AclBackup {
|
|
282
340
|
param([string]$FullPath, [string]$Directory, [string]$Root)
|
|
283
341
|
$fullDirectory = [System.IO.Path]::GetFullPath($Directory)
|
|
@@ -286,31 +344,32 @@ function Save-AclBackup {
|
|
|
286
344
|
New-Item -ItemType Directory -Path $fullDirectory | Out-Null
|
|
287
345
|
}
|
|
288
346
|
}
|
|
289
|
-
$
|
|
290
|
-
$record = $backup + '.json'
|
|
347
|
+
$record = Join-Path $fullDirectory ('acl-backup-{0}.json' -f ([guid]::NewGuid().ToString('N')))
|
|
291
348
|
# The skill's extracted directory expires with its registration. Recovery must
|
|
292
|
-
# remain runnable from the
|
|
293
|
-
$restoreScript = $
|
|
349
|
+
# remain runnable from the recovery directory after that registration is gone.
|
|
350
|
+
$restoreScript = $record + '.ps1'
|
|
294
351
|
$command = "pwsh -NoProfile -File '{0}' -Path '{1}' -AllowRoot '{2}' -Restore '{3}'" -f
|
|
295
352
|
$restoreScript.Replace("'", "''"), $FullPath.Replace("'", "''"), ([System.IO.Path]::GetFullPath($Root)).Replace("'", "''"), $record.Replace("'", "''")
|
|
296
|
-
Write-Report decision backup $FullPath selected '
|
|
353
|
+
Write-Report decision backup $FullPath selected 'The recovery record and an independent recovery script are written before the ACL change.' @{ files = @($record, $restoreScript) }
|
|
297
354
|
Invoke-ReportedOperation backup $FullPath 'Save the original DACL and an independent recovery command before changing permissions.' files {
|
|
298
|
-
$
|
|
299
|
-
|
|
300
|
-
@{
|
|
301
|
-
|
|
355
|
+
$backupAcl = Get-Acl -LiteralPath $FullPath
|
|
356
|
+
$observed = $backupAcl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access)
|
|
357
|
+
@{
|
|
358
|
+
Path = $FullPath
|
|
359
|
+
Dacl = Get-OwnDaclSddl -Sddl $observed
|
|
360
|
+
Protected = $backupAcl.AreAccessRulesProtected
|
|
361
|
+
Observed = $observed
|
|
362
|
+
} | ConvertTo-Json | Set-Content -LiteralPath $record -Encoding utf8
|
|
302
363
|
Copy-Item -LiteralPath $PSCommandPath -Destination $restoreScript
|
|
303
|
-
$command | Set-Content -LiteralPath ($backup + '.rollback.txt')
|
|
304
364
|
}
|
|
305
|
-
$script:recoveries.Add(@{ path = $FullPath;
|
|
365
|
+
$script:recoveries.Add(@{ path = $FullPath; record = $record; script = $restoreScript; command = $command; attempted = $false; restored = $false })
|
|
306
366
|
Write-Report observation recovery $FullPath available 'These files can restore the saved DACL; no rollback has been executed.' $script:recoveries[-1]
|
|
307
|
-
Write-Line ('BACKUP {0} -> {1}' -f $FullPath, $
|
|
367
|
+
Write-Line ('BACKUP {0} -> {1}' -f $FullPath, $record)
|
|
308
368
|
Write-Line ('ROLLBACK {0}' -f $command)
|
|
309
369
|
}
|
|
310
|
-
|
|
311
370
|
function Restore-SavedDacl {
|
|
312
371
|
param([string]$FullPath, [string]$Record, [string]$Root)
|
|
313
|
-
$refusal = Get-RepairRefusal -FullPath $FullPath -Root $Root
|
|
372
|
+
$refusal = Get-RepairRefusal -FullPath $FullPath -Root $Root -AllowEqual
|
|
314
373
|
if ($refusal) { throw [System.ArgumentException]::new("RESTORE_REFUSED $refusal") }
|
|
315
374
|
$saved = Invoke-ReportedOperation read_recovery $Record 'Read the recovery record and verify it belongs to the requested path before restoring its DACL.' none {
|
|
316
375
|
Get-Content -LiteralPath $Record -Raw | ConvertFrom-Json
|
|
@@ -322,46 +381,25 @@ function Restore-SavedDacl {
|
|
|
322
381
|
Invoke-ReportedOperation restore_dacl $FullPath 'Restore the requested backup DACL and inheritance protection, preserving owner and SACL.' acl {
|
|
323
382
|
[Dsh.TokenInfo]::SetDacl($FullPath, $saved.Dacl)
|
|
324
383
|
}
|
|
325
|
-
$
|
|
326
|
-
|
|
384
|
+
$restoredAcl = Invoke-ReportedOperation read_restored_dacl $FullPath 'Read the restored DACL to compare it with the saved record.' none {
|
|
385
|
+
Get-Acl -LiteralPath $FullPath
|
|
327
386
|
}
|
|
328
|
-
|
|
329
|
-
|
|
387
|
+
# Inherited entries follow the parent, so the same object can render a different
|
|
388
|
+
# full DACL before and after a correct restore. Compare the own ACEs and the
|
|
389
|
+
# protection state, which the restore owns.
|
|
390
|
+
$restoredProtected = $restoredAcl.AreAccessRulesProtected
|
|
391
|
+
$actual = Get-OwnDaclSddl -Sddl $restoredAcl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access)
|
|
392
|
+
$expectedAces = Get-AceSet -Sddl $saved.Dacl
|
|
393
|
+
$actualAces = Get-AceSet -Sddl $actual
|
|
394
|
+
$restored = (@($expectedAces) -join "`n") -eq (@($actualAces) -join "`n") -and $restoredProtected -eq [bool]$saved.Protected
|
|
395
|
+
Write-Report verification restore $FullPath $(if ($restored) { 'verified' } else { 'failed' }) 'Compare the restored own ACEs and protection state with the recovery record.' @{ expectedAces = @($expectedAces); actualAces = @($actualAces); protected = $restoredProtected; expectedProtected = [bool]$saved.Protected }
|
|
396
|
+
if (-not $restored) { throw 'RESTORE_FAILED the restored DACL differs from the backup' }
|
|
330
397
|
$script:restored++
|
|
331
398
|
Write-Line ('RESTORED {0}' -f $FullPath)
|
|
332
399
|
}
|
|
333
400
|
|
|
334
|
-
function Write-CompactSummary {
|
|
335
|
-
param([string]$Status, [string]$NextAction, [array]$RollbackCommands)
|
|
336
|
-
$byPath = [ordered]@{}
|
|
337
|
-
foreach ($record in $script:reports) { if ($record.operation -eq 'inspect_acl') { $byPath[$record.path] = $record } }
|
|
338
|
-
$observations = @($byPath.Values)
|
|
339
|
-
$classifications = @($script:reports | Where-Object { $_.operation -eq 'classify' })
|
|
340
|
-
$findings = @($observations | Where-Object {
|
|
341
|
-
$_.path -in $script:requestedPaths -or $_.status -ne 'read' -or
|
|
342
|
-
@($_.details['aces'] | Where-Object { $_ -and $_.sid -match $PACKAGE_SID -and $_.type -eq 'Allow' }).Count -gt 0
|
|
343
|
-
} | ForEach-Object {
|
|
344
|
-
@{ path = $_.path; status = $_.status; writeDac = $_.details['writeDac']; writeOwner = $_.details['writeOwner']
|
|
345
|
-
packageAllowSids = @($_.details['aces'] | Where-Object { $_ -and $_.sid -match $PACKAGE_SID -and $_.type -eq 'Allow' } | ForEach-Object { $_.sid })
|
|
346
|
-
denies = @($_.details['aces'] | Where-Object { $_ -and $_.type -eq 'Deny' }); errors = @($_.details['errors']; $_.details['error']) }
|
|
347
|
-
})
|
|
348
|
-
$details = [ordered]@{
|
|
349
|
-
report = $script:reportPath; exitCode = $script:exitCode; nextAction = $NextAction
|
|
350
|
-
inspectedPaths = @($observations | ForEach-Object { $_.path } | Select-Object -Unique)
|
|
351
|
-
findings = $findings
|
|
352
|
-
decisions = @($classifications | ForEach-Object { @{ path = $_.path; verdict = $_.status; reason = $_.reason; packageObjects = $_.details.packageObjects } })
|
|
353
|
-
actions = @($script:reports | Where-Object { $_.kind -eq 'action' -and $_.details['effect'] -eq 'acl' -and $_.status -ne 'started' } | ForEach-Object { @{ operation = $_.operation; path = $_.path; status = $_.status; reason = $_.reason } })
|
|
354
|
-
verification = @($script:reports | Where-Object { $_.kind -eq 'verification' } | ForEach-Object { @{ operation = $_.operation; path = $_.path; status = $_.status } })
|
|
355
|
-
rollback = $script:rollbackStatus; rollbackCommands = $RollbackCommands
|
|
356
|
-
errors = @($script:reports | Where-Object { $_.kind -eq 'error' -or ($_.kind -eq 'decision' -and $_.status -eq 'refused') } | ForEach-Object { @{ path = $_.path; reason = $_.reason; details = $_.details } })
|
|
357
|
-
}
|
|
358
|
-
$record = @{ kind = 'summary'; operation = $script:mode; path = $script:currentPath; status = $Status
|
|
359
|
-
reason = 'Full observations, actions and reasons are in the report. Follow nextAction; a failed repair must not be followed by another repair.'; details = $details }
|
|
360
|
-
[Console]::Out.WriteLine('REPORT ' + ($record | ConvertTo-Json -Depth 12 -Compress))
|
|
361
|
-
}
|
|
362
|
-
|
|
363
401
|
function Get-ObjectFacts {
|
|
364
|
-
param([string]$FullPath, [string]$MeSid)
|
|
402
|
+
param([string]$FullPath, [string]$MeSid, [switch]$CompactRecord)
|
|
365
403
|
$facts = [ordered]@{
|
|
366
404
|
Object = $FullPath
|
|
367
405
|
Readable = $false
|
|
@@ -429,7 +467,10 @@ function Get-ObjectFacts {
|
|
|
429
467
|
}
|
|
430
468
|
Write-Report observation inspect_acl $FullPath $(if ($facts.Errors.Count) { 'partial' } else { 'read' }) 'Read the ACL and check effective WRITE_DAC and WRITE_OWNER; observed ACEs alone do not identify which rule caused a denial.' @{
|
|
431
469
|
owner = $facts.Owner; writeDac = $facts.HasWriteDac; writeOwner = $facts.HasWriteOwner
|
|
432
|
-
|
|
470
|
+
# A re-read after a change repeats an ACL this run already recorded: keep the
|
|
471
|
+
# full ACE list in memory, but do not print it twice.
|
|
472
|
+
aces = $(if ($CompactRecord) { @() } else { $facts.Aces }); acesOmitted = [bool]$CompactRecord
|
|
473
|
+
lowLabel = $facts.LowLabel; errors = $facts.Errors
|
|
433
474
|
}
|
|
434
475
|
return $facts
|
|
435
476
|
}
|
|
@@ -452,9 +493,8 @@ function Get-Ancestors {
|
|
|
452
493
|
$operations = [System.Collections.Generic.List[object]]::new()
|
|
453
494
|
$recoveries = [System.Collections.Generic.List[object]]::new()
|
|
454
495
|
$reports = [System.Collections.Generic.List[object]]::new()
|
|
455
|
-
$reportWriter = $null
|
|
456
496
|
$reportPath = $null
|
|
457
|
-
$
|
|
497
|
+
$reportWriter = $null
|
|
458
498
|
$requestedPaths = @()
|
|
459
499
|
$rollbackStatus = 'not-needed'
|
|
460
500
|
$fixed = 0
|
|
@@ -463,17 +503,20 @@ $refused = 0
|
|
|
463
503
|
$restored = 0
|
|
464
504
|
$exitCode = 0
|
|
465
505
|
$observationFailures = 0
|
|
506
|
+
$scanTruncated = $false
|
|
466
507
|
$currentPath = ''
|
|
467
|
-
$mode = if ($Restore) { 'restore' }
|
|
508
|
+
$mode = if ($Restore) { 'restore' } else { 'repair' }
|
|
468
509
|
|
|
469
510
|
try {
|
|
470
511
|
$requestedPaths = @($Path | ForEach-Object { [System.IO.Path]::GetFullPath($_) })
|
|
471
|
-
Write-Report invocation $mode '' started 'Inspect the requested paths
|
|
472
|
-
paths = $Path; allowRoot = $AllowRoot; outputDirectory = $Out; recoveryRecord = $Restore
|
|
512
|
+
Write-Report invocation $mode '' started 'Inspect the requested paths, then repair the ACL problems this run can prove, before anything else is attempted.' @{
|
|
513
|
+
paths = $Path; allowRoot = $AllowRoot; outputDirectory = $Out; recoveryRecord = $Restore
|
|
473
514
|
}
|
|
474
|
-
if ($
|
|
475
|
-
|
|
476
|
-
|
|
515
|
+
if (-not $AllowRoot) { throw [System.ArgumentException]::new('Every modification requires -AllowRoot') }
|
|
516
|
+
if (-not $Restore -and -not $Out) { throw [System.ArgumentException]::new('A repair requires -Out for its recovery artifacts') }
|
|
517
|
+
if ($Restore -and $Path.Count -ne 1) { throw [System.ArgumentException]::new('-Restore requires exactly one -Path') }
|
|
518
|
+
if ($Out) {
|
|
519
|
+
Invoke-ReportedOperation prepare_report $Out 'Create the recovery directory and a new JSONL report that keeps every record, because tool output is truncated to its tail.' files {
|
|
477
520
|
$directory = [System.IO.Path]::GetFullPath($Out)
|
|
478
521
|
[System.IO.Directory]::CreateDirectory($directory) | Out-Null
|
|
479
522
|
$script:reportPath = Join-Path $directory ('acl-report-{0}.jsonl' -f [guid]::NewGuid().ToString('N'))
|
|
@@ -488,20 +531,6 @@ try {
|
|
|
488
531
|
$integrity = Get-IntegritySid
|
|
489
532
|
Write-Line ('CALLER SID={0} INTEGRITY={1}' -f $meSid, $integrity)
|
|
490
533
|
Write-Report observation caller '' read 'The caller token determines effective access; unconfined execution does not imply elevation.' @{ sid = $meSid; integrity = $integrity }
|
|
491
|
-
|
|
492
|
-
if (@(@($Fix.IsPresent, $GrantFullControl.IsPresent, [bool]$Restore) | Where-Object { $_ }).Count -gt 1) {
|
|
493
|
-
throw [System.ArgumentException]::new('-Fix, -GrantFullControl and -Restore are separate operations; run one at a time')
|
|
494
|
-
}
|
|
495
|
-
if ($Fix -or $GrantFullControl -or $Restore) {
|
|
496
|
-
$requiredArguments = if ($Restore) { @('AllowRoot') } else { @('AllowRoot', 'Out') }
|
|
497
|
-
foreach ($required in $requiredArguments) {
|
|
498
|
-
if (-not (Get-Variable -Name $required -ValueOnly)) {
|
|
499
|
-
throw [System.ArgumentException]::new("The requested repair requires -$required")
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if ($Restore -and $Path.Count -ne 1) { throw [System.ArgumentException]::new('-Restore requires exactly one -Path') }
|
|
503
|
-
}
|
|
504
|
-
|
|
505
534
|
:paths foreach ($requested in $Path) {
|
|
506
535
|
$currentPath = $requested
|
|
507
536
|
$full = Get-NormalizedPath $requested
|
|
@@ -511,23 +540,27 @@ try {
|
|
|
511
540
|
Write-Line ' MISSING'
|
|
512
541
|
Write-Line 'VERDICT=NOT_THIS_CLASS'
|
|
513
542
|
Write-Decision $mode $full skipped 'The requested path does not exist; no ACL was read or changed for it.'
|
|
514
|
-
|
|
515
|
-
|
|
543
|
+
$refused++
|
|
544
|
+
break paths
|
|
516
545
|
}
|
|
517
546
|
|
|
547
|
+
$factsByPath = @{}
|
|
548
|
+
$inspectOrder = @()
|
|
518
549
|
$packageTargets = @()
|
|
519
550
|
$targetFacts = $null
|
|
520
551
|
foreach ($ancestor in (Get-Ancestors -FullPath $full)) {
|
|
521
552
|
if (-not (Test-Path -LiteralPath $ancestor)) { continue }
|
|
522
|
-
$
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if ($
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
553
|
+
$facts = Get-ObjectFacts -FullPath $ancestor -MeSid $meSid
|
|
554
|
+
$factsByPath[$facts.Object] = $facts
|
|
555
|
+
$inspectOrder += $facts.Object
|
|
556
|
+
if ($ancestor -eq $full) { $targetFacts = $facts }
|
|
557
|
+
Write-Line (' OBJECT={0} READABLE={1}{2}' -f $facts.Object, $facts.Readable, $(if ($facts.Readable) { '' } else { ' ERROR=' + $facts.Error }))
|
|
558
|
+
if ($facts.Readable) {
|
|
559
|
+
Write-Line (' OWNER={0} IS_CURRENT_USER={1} MY_RIGHTS=[{2}] WRITE_DAC={3} WRITE_OWNER={4} LOW_LABEL={5}' -f $facts.Owner, $facts.OwnerIsMe, $facts.MyRights, $facts.HasWriteDac, $facts.HasWriteOwner, $facts.LowLabel)
|
|
560
|
+
foreach ($ace in $facts.PackageAces) { Write-Line (' PACKAGE_ACE {0}' -f $ace) }
|
|
561
|
+
foreach ($sid in ($facts.OtherAppContainerSids | Sort-Object -Unique)) { Write-Line (' OTHER_S1_15 SID={0} (reported only)' -f $sid) }
|
|
529
562
|
}
|
|
530
|
-
if ($
|
|
563
|
+
if ($facts.PackageAces.Count -gt 0) { $packageTargets += $facts.Object }
|
|
531
564
|
}
|
|
532
565
|
|
|
533
566
|
# The precondition is judged on the requested path itself. Ancestors above the
|
|
@@ -538,11 +571,32 @@ try {
|
|
|
538
571
|
$needsPrecondition = (-not $unreadable) -and
|
|
539
572
|
((-not $targetFacts.HasWriteDac) -or (-not $targetFacts.HasWriteOwner))
|
|
540
573
|
|
|
574
|
+
# A directory the caller cannot provision is the workspace root half the time: the sandbox
|
|
575
|
+
# reports its own failure there while the conflicting entry sits deeper. Examine that
|
|
576
|
+
# subtree before the verdict so the classification and its packageObjects describe what
|
|
577
|
+
# this run will actually repair.
|
|
578
|
+
$ownsRoot = $full -ieq (Get-NormalizedPath $AllowRoot)
|
|
579
|
+
if (-not $Restore -and $targetFacts.Errors.Count -eq 0 -and [System.IO.Directory]::Exists($full) -and ($needsPrecondition -or $ownsRoot)) {
|
|
580
|
+
$scan = Get-SubtreePackageSources -Root $full -Limit $SCAN_LIMIT
|
|
581
|
+
foreach ($object in $scan.sources) {
|
|
582
|
+
if ($packageTargets -notcontains $object) { $packageTargets += $object }
|
|
583
|
+
if (-not $factsByPath.ContainsKey($object)) {
|
|
584
|
+
$scanned = Get-ObjectFacts -FullPath $object -MeSid $meSid
|
|
585
|
+
$factsByPath[$scanned.Object] = $scanned
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
Write-Report observation subtree_scan $full $(if ($scan.failed -gt 0) { 'partial' } else { 'read' }) 'The caller cannot provision this directory, or it is the authorized root, so explicit package allow ACEs under it were collected in the same run; no reparse point or managed application tree is entered.' @{
|
|
589
|
+
visited = $scan.visited; packageSources = @($scan.sources); truncated = [bool]$scan.truncated; unreadable = [int]$scan.failed
|
|
590
|
+
}
|
|
591
|
+
Write-Line ('SCAN {0} VISITED={1} PACKAGE_SOURCES={2}{3}{4}' -f $full, $scan.visited, @($scan.sources).Count, $(if ($scan.truncated) { ' TRUNCATED' } else { '' }), $(if ($scan.failed -gt 0) { " UNREADABLE=$($scan.failed)" } else { '' }))
|
|
592
|
+
if ($scan.truncated) { $script:scanTruncated = $true }
|
|
593
|
+
}
|
|
594
|
+
|
|
541
595
|
if ($targetFacts.Errors.Count -gt 0) {
|
|
542
596
|
$verdict = 'INCOMPLETE'
|
|
543
597
|
$reason = 'Some observations failed; missing evidence is not evidence that a right is absent or a repair is safe.'
|
|
544
598
|
} elseif ($packageTargets.Count -gt 0) {
|
|
545
|
-
$blocked = @($packageTargets | Where-Object { -not $_.HasWriteDac })
|
|
599
|
+
$blocked = @($packageTargets | Where-Object { -not $factsByPath[$_].HasWriteDac })
|
|
546
600
|
$verdict = if ($needsPrecondition -or $blocked.Count -gt 0) { 'BOTH' } else { 'CULPRIT' }
|
|
547
601
|
$reason = 'Package allow ACEs were observed. Required access checks determine whether the caller can perform the requested repair; other causes remain possible.'
|
|
548
602
|
} elseif ($unreadable) {
|
|
@@ -556,133 +610,138 @@ try {
|
|
|
556
610
|
$reason = 'No package allow ACE was observed and both required rights are available. Other ACL restrictions and causes of the original failure are not ruled out.'
|
|
557
611
|
}
|
|
558
612
|
Write-Line ('VERDICT={0}' -f $verdict)
|
|
559
|
-
Write-Report decision classify $full $verdict $reason @{ writeDac = $targetFacts.HasWriteDac; writeOwner = $targetFacts.HasWriteOwner; packageObjects = @($packageTargets
|
|
613
|
+
Write-Report decision classify $full $verdict $reason @{ writeDac = $targetFacts.HasWriteDac; writeOwner = $targetFacts.HasWriteOwner; packageObjects = @($packageTargets) }
|
|
560
614
|
|
|
561
|
-
if ($
|
|
562
|
-
|
|
615
|
+
if ($Restore) {
|
|
616
|
+
Restore-SavedDacl -FullPath $full -Record $Restore -Root $AllowRoot
|
|
563
617
|
continue
|
|
564
618
|
}
|
|
565
619
|
if ($targetFacts.Errors.Count -gt 0) {
|
|
566
620
|
Write-Decision $mode $full refused 'Required observations are incomplete; no repair was attempted for this path.'
|
|
567
|
-
$refused
|
|
621
|
+
$refused++
|
|
622
|
+
break paths
|
|
568
623
|
}
|
|
569
624
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
625
|
+
# Nothing is changed while a package source or a grant target is out of reach: a
|
|
626
|
+
# repair that cannot finish would leave the tree half repaired before refusing.
|
|
627
|
+
foreach ($packagePath in $packageTargets) {
|
|
628
|
+
$refusal = Get-RepairRefusal -FullPath $packagePath -Root $AllowRoot -AllowEqual
|
|
629
|
+
if ($refusal) { Write-Line ('REPAIR_REFUSED {0}' -f $refusal); Write-Decision $mode $packagePath refused $refusal; $refused++; break paths }
|
|
630
|
+
if ($factsByPath[$packagePath].Errors.Count -gt 0) {
|
|
631
|
+
Write-Decision $mode $packagePath refused 'The ACL observations are incomplete; collateral changes could not be verified.'
|
|
632
|
+
$refused++
|
|
633
|
+
break paths
|
|
634
|
+
}
|
|
573
635
|
}
|
|
574
636
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
637
|
+
# Missing WRITE_DAC or WRITE_OWNER on a directory in the chain is what stops DSH
|
|
638
|
+
# from provisioning the workspace grant. One approved run repairs every such
|
|
639
|
+
# directory, including the authorized root itself.
|
|
640
|
+
$grantTargets = @($inspectOrder | Where-Object {
|
|
641
|
+
[System.IO.Directory]::Exists($_) -and $factsByPath[$_].Readable -and $factsByPath[$_].Errors.Count -eq 0 -and
|
|
642
|
+
((-not $factsByPath[$_].HasWriteDac) -or (-not $factsByPath[$_].HasWriteOwner)) -and
|
|
643
|
+
-not (Get-RepairRefusal -FullPath $_ -Root $AllowRoot -AllowEqual)
|
|
644
|
+
})
|
|
645
|
+
# Writing a DACL needs WRITE_DAC: refuse a target that lacks it before anything is
|
|
646
|
+
# changed, instead of failing mid-repair and marking a recovery for an object that
|
|
647
|
+
# the rollback then cannot restore either.
|
|
648
|
+
foreach ($object in $grantTargets) {
|
|
649
|
+
if (-not $factsByPath[$object].HasWriteDac) {
|
|
650
|
+
Write-Line ('REPAIR_REFUSED {0} needs WRITE_DAC; stop for permission-policy review' -f $object)
|
|
651
|
+
Write-Decision $mode $object refused 'Effective WRITE_DAC is absent; adding the grant requires that right.'
|
|
652
|
+
$refused++
|
|
653
|
+
break paths
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if ($grantTargets.Count -gt 0) {
|
|
657
|
+
Write-Report decision grant_targets $full selected 'These directories lack effective WRITE_DAC or WRITE_OWNER; each is backed up, then receives a FullControl allow ACE for the current user.' @{ paths = @($grantTargets) }
|
|
658
|
+
}
|
|
659
|
+
foreach ($object in $grantTargets) {
|
|
660
|
+
$grantBefore = $factsByPath[$object]
|
|
661
|
+
Save-AclBackup -FullPath $object -Directory $Out -Root $AllowRoot
|
|
662
|
+
$grantAcl = Get-Acl -LiteralPath $object
|
|
585
663
|
$grantAcl.AddAccessRule([System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
586
664
|
[System.Security.Principal.SecurityIdentifier]::new($meSid),
|
|
587
665
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
588
666
|
[System.Security.AccessControl.AccessControlType]::Allow))
|
|
589
|
-
Invoke-ReportedOperation grant_dacl $
|
|
590
|
-
[Dsh.TokenInfo]::SetDacl($
|
|
667
|
+
Invoke-ReportedOperation grant_dacl $object "WRITE_DAC or WRITE_OWNER is missing; add a FullControl allow ACE for $meSid while preserving deny ACEs, owner and SACL." acl {
|
|
668
|
+
[Dsh.TokenInfo]::SetDacl($object, $grantAcl.GetSecurityDescriptorSddlForm([System.Security.AccessControl.AccessControlSections]::Access))
|
|
591
669
|
}
|
|
592
|
-
$
|
|
593
|
-
$
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
670
|
+
$grantAfter = Get-ObjectFacts -FullPath $object -MeSid $meSid -CompactRecord
|
|
671
|
+
$factsByPath[$object] = $grantAfter
|
|
672
|
+
$verified = $grantAfter.Readable -and $grantAfter.Errors.Count -eq 0 -and $grantAfter.HasWriteDac -and $grantAfter.HasWriteOwner
|
|
673
|
+
Write-Report verification grant $object $(if ($verified) { 'verified' } else { 'failed' }) 'The DACL write completed; recheck effective access before claiming that provisioning can succeed. On failure, restore this invocation and stop.' @{
|
|
674
|
+
before = @{ writeDac = $grantBefore.HasWriteDac; writeOwner = $grantBefore.HasWriteOwner }
|
|
675
|
+
after = @{ writeDac = $grantAfter.HasWriteDac; writeOwner = $grantAfter.HasWriteOwner }
|
|
597
676
|
recovery = $recoveries[-1].command
|
|
598
677
|
}
|
|
599
678
|
if ($verified) {
|
|
600
|
-
Write-Line ('GRANTED {0} SID={1}' -f $
|
|
679
|
+
Write-Line ('GRANTED {0} SID={1}' -f $object, $meSid); $granted++
|
|
601
680
|
} else {
|
|
602
|
-
Write-Line ('GRANT_FAILED {0}; effective WRITE_DAC and WRITE_OWNER were not both confirmed; restore this invocation and stop' -f $
|
|
681
|
+
Write-Line ('GRANT_FAILED {0}; effective WRITE_DAC and WRITE_OWNER were not both confirmed; restore this invocation and stop' -f $object)
|
|
603
682
|
$refused++
|
|
604
683
|
break paths
|
|
605
684
|
}
|
|
606
|
-
continue
|
|
607
685
|
}
|
|
608
686
|
|
|
609
|
-
if (-not $Fix) { continue }
|
|
610
|
-
|
|
611
|
-
if ($unreadable) {
|
|
612
|
-
Write-Line 'FIX_REFUSED the object could not be read; repair needs an unconfined caller'
|
|
613
|
-
Write-Decision fix $full refused 'The requested ACL could not be read; no package removal was attempted.'
|
|
614
|
-
$refused++
|
|
615
|
-
break paths
|
|
616
|
-
}
|
|
617
|
-
if ($packageTargets.Count -eq 0) { Write-Decision fix $full skipped 'No package allow ACE was observed on the inspected objects; no ACL change or backup is needed.' }
|
|
618
687
|
$sources = @($packageTargets | Where-Object {
|
|
619
|
-
@($_.Aces | Where-Object { $_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID -and -not $_.inherited }).Count -gt 0
|
|
688
|
+
@($factsByPath[$_].Aces | Where-Object { $_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID -and -not $_.inherited }).Count -gt 0
|
|
620
689
|
})
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if ($packageTargets.Count -gt 0 -and $needsPrecondition) {
|
|
628
|
-
Write-Decision fix $full refused 'Package removal requires verified WRITE_DAC and WRITE_OWNER on the requested path. A failed grant must not be followed by -Fix.'
|
|
629
|
-
$refused++; break paths
|
|
630
|
-
}
|
|
631
|
-
foreach ($target in $sources) {
|
|
632
|
-
if (-not $target.HasWriteDac) {
|
|
633
|
-
Write-Line ('FIX_REFUSED {0} needs WRITE_DAC; stop for permission-policy review' -f $target.Object)
|
|
634
|
-
Write-Decision fix $target.Object refused 'Effective WRITE_DAC is absent; removing a package allow ACE requires that right.'
|
|
690
|
+
# Ancestor first, for chain and scan-discovered sources alike.
|
|
691
|
+
$sources = @($sources | Sort-Object -Property @{ Expression = { $_.Split([char]92).Count } }, @{ Expression = { $_ } })
|
|
692
|
+
foreach ($source in $sources) {
|
|
693
|
+
if (-not $factsByPath[$source].HasWriteDac) {
|
|
694
|
+
Write-Line ('REPAIR_REFUSED {0} needs WRITE_DAC; stop for permission-policy review' -f $source)
|
|
695
|
+
Write-Decision $mode $source refused 'Effective WRITE_DAC is absent; removing a package allow ACE requires that right.'
|
|
635
696
|
$refused++
|
|
636
697
|
break paths
|
|
637
698
|
}
|
|
638
699
|
}
|
|
639
|
-
|
|
640
|
-
|
|
700
|
+
if ($sources.Count -gt 0) {
|
|
701
|
+
Write-Report decision remove_package_sources $full selected 'Remove explicit package allow ACEs from their sources, ancestor first, leaving inheritance enabled so inherited copies follow their repaired source.' @{
|
|
702
|
+
sources = @($sources); affectedPaths = @($packageTargets)
|
|
703
|
+
}
|
|
641
704
|
}
|
|
642
|
-
foreach ($
|
|
643
|
-
Save-AclBackup -FullPath $
|
|
644
|
-
$sids = @($
|
|
705
|
+
foreach ($source in $sources) {
|
|
706
|
+
Save-AclBackup -FullPath $source -Directory $Out -Root $AllowRoot
|
|
707
|
+
$sids = @($factsByPath[$source].Aces | Where-Object { $_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID -and -not $_.inherited } | ForEach-Object { $_.sid } | Sort-Object -Unique)
|
|
645
708
|
foreach ($sid in $sids) {
|
|
646
|
-
$
|
|
647
|
-
|
|
648
|
-
$
|
|
649
|
-
if ($LASTEXITCODE -ne 0) { throw "icacls removal exit ${LASTEXITCODE}: $($output -join "`n")" }
|
|
709
|
+
Invoke-ReportedOperation remove_package_allow $source "Remove the observed package allow ACE for $sid; preserve deny ACEs and all other principals." acl {
|
|
710
|
+
$output = @(icacls $source /remove:g "*$sid" 2>&1)
|
|
711
|
+
if ($LASTEXITCODE -ne 0) { throw "icacls removal exit $($LASTEXITCODE): $($output -join [char]10)" }
|
|
650
712
|
}
|
|
651
713
|
}
|
|
652
714
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
$
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
$
|
|
667
|
-
} | Where-Object { $_ -notmatch $PACKAGE_SID })
|
|
668
|
-
# A package deny can share the removed allow's SID. Preserve it explicitly
|
|
669
|
-
# even though native listing comparison omits lines naming package SIDs.
|
|
670
|
-
$expectedAces = @($target.Aces | Where-Object { -not ($_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID) })
|
|
671
|
-
$collateral = (($keepBefore -join "`n") -eq ($keepAfter -join "`n")) -and
|
|
672
|
-
(($expectedAces | ConvertTo-Json -Compress) -eq ($after.Aces | ConvertTo-Json -Compress))
|
|
673
|
-
$verified = $after.Readable -and $after.Errors.Count -eq 0 -and $after.PackageAces.Count -eq 0 -and $collateral
|
|
674
|
-
Write-Report verification fix $target.Object $(if ($verified) { 'verified' } else { 'failed' }) 'The removal command completed; verify that package allow ACEs disappeared and other ACL listing entries remained unchanged. On failure, restore this invocation and stop.' @{
|
|
675
|
-
remainingPackageAces = $after.PackageAces; otherEntriesUnchanged = $collateral
|
|
715
|
+
|
|
716
|
+
# A package deny can share the removed allow's SID; only allow ACEs were removed.
|
|
717
|
+
foreach ($packagePath in $packageTargets) {
|
|
718
|
+
$fixBefore = $factsByPath[$packagePath]
|
|
719
|
+
$fixAfter = Get-ObjectFacts -FullPath $packagePath -MeSid $meSid -CompactRecord
|
|
720
|
+
# Compare the object's own ACEs as sorted sets: listing order and inherited
|
|
721
|
+
# entries follow the parent, so neither proves a collateral change.
|
|
722
|
+
$rowOf = { param($ace) '{0}|{1}|{2}|{3}|{4}' -f $ace.type, $ace.sid, $ace.rights, $ace.inheritance, $ace.propagation }
|
|
723
|
+
$ownAfter = @($fixAfter.Aces | Where-Object { -not $_.inherited } | ForEach-Object { & $rowOf $_ } | Sort-Object)
|
|
724
|
+
$expectedOwn = @($fixBefore.Aces | Where-Object { -not $_.inherited -and -not ($_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID) } | ForEach-Object { & $rowOf $_ } | Sort-Object)
|
|
725
|
+
$collateral = (@($expectedOwn) -join [char]10) -eq (@($ownAfter) -join [char]10)
|
|
726
|
+
$verified = $fixAfter.Readable -and $fixAfter.Errors.Count -eq 0 -and $fixAfter.PackageAces.Count -eq 0 -and $collateral
|
|
727
|
+
Write-Report verification fix $packagePath $(if ($verified) { 'verified' } else { 'failed' }) 'The removal command completed; verify that package allow ACEs disappeared and other own ACEs remained unchanged. On failure, restore this invocation and stop.' @{
|
|
728
|
+
remainingPackageAces = $fixAfter.PackageAces; otherEntriesUnchanged = $collateral
|
|
676
729
|
}
|
|
677
730
|
if ($verified) {
|
|
678
|
-
$
|
|
679
|
-
foreach ($sid in $
|
|
731
|
+
$removedSids = @($fixBefore.Aces | Where-Object { $_.type -eq 'Allow' -and $_.sid -match $PACKAGE_SID -and -not $_.inherited } | ForEach-Object { $_.sid } | Sort-Object -Unique)
|
|
732
|
+
foreach ($sid in $removedSids) {
|
|
733
|
+
Write-Line ('FIXED {0} SID={1}' -f $packagePath, $sid); $fixed++
|
|
734
|
+
}
|
|
680
735
|
} else {
|
|
681
|
-
Write-Line ('FIX_FAILED {0} collateral_change={1}' -f $
|
|
736
|
+
Write-Line ('FIX_FAILED {0} collateral_change={1}' -f $packagePath, (-not $collateral))
|
|
682
737
|
$refused++
|
|
683
738
|
break paths
|
|
684
739
|
}
|
|
685
740
|
}
|
|
741
|
+
|
|
742
|
+
if ($packageTargets.Count -eq 0 -and $grantTargets.Count -eq 0) {
|
|
743
|
+
Write-Decision $mode $full skipped 'No package allow ACE and no missing WRITE_DAC or WRITE_OWNER was observed on the inspected objects; no ACL change was needed.'
|
|
744
|
+
}
|
|
686
745
|
}
|
|
687
746
|
} catch {
|
|
688
747
|
$exitCode = if ($_.Exception -is [System.ArgumentException]) { 2 } else { 1 }
|
|
@@ -707,15 +766,33 @@ try {
|
|
|
707
766
|
}
|
|
708
767
|
$pending = @($recoveries | Where-Object { $_.attempted -and -not $_.restored } | ForEach-Object { $_.command })
|
|
709
768
|
[array]::Reverse($pending)
|
|
710
|
-
$
|
|
711
|
-
|
|
769
|
+
$repaired = ($fixed + $granted) -gt 0
|
|
770
|
+
$nextAction = if ($rollbackStatus -eq 'failed') { 'restore_pending_then_stop' } elseif ($exitCode -ne 0 -or $observationFailures -gt 0) { 'stop' } elseif ($repaired) { 'verify_original_confined_operation' } else { 'stop' }
|
|
771
|
+
# The tool keeps only the tail of stdout. Recapitulate the decisions here so the
|
|
772
|
+
# records a reader needs survive truncation; the report file holds every record.
|
|
773
|
+
$recap = [ordered]@{
|
|
774
|
+
verdicts = @($reports | Where-Object { $_.operation -eq 'classify' } | ForEach-Object {
|
|
775
|
+
@{ path = $_.path; verdict = $_.status; writeDac = $_.details['writeDac']; writeOwner = $_.details['writeOwner']; packageObjects = $_.details['packageObjects'] }
|
|
776
|
+
})
|
|
777
|
+
changes = @($reports | Where-Object { $_.kind -eq 'action' -and $_.details['effect'] -eq 'acl' -and $_.status -eq 'completed' } | ForEach-Object { @{ operation = $_.operation; path = $_.path } })
|
|
778
|
+
verifications = @($reports | Where-Object { $_.kind -eq 'verification' } | ForEach-Object { @{ operation = $_.operation; path = $_.path; status = $_.status } })
|
|
779
|
+
refusals = @($reports | Where-Object { $_.kind -eq 'decision' -and $_.status -eq 'refused' } | ForEach-Object { @{ path = $_.path; reason = $_.reason } })
|
|
780
|
+
scans = @($reports | Where-Object { $_.operation -eq 'subtree_scan' } | ForEach-Object {
|
|
781
|
+
@{ path = $_.path; visited = $_.details['visited']; packageSources = @($_.details['packageSources']); truncated = $_.details['truncated']; unreadable = $_.details['unreadable'] }
|
|
782
|
+
})
|
|
783
|
+
report = $reportPath
|
|
784
|
+
}
|
|
712
785
|
$status = if ($exitCode -ne 0) { 'failed' } elseif ($observationFailures -gt 0) { 'partial' } else { 'completed' }
|
|
713
786
|
Write-Report summary $mode $currentPath $status 'Operation completion records API execution; verification records the observed result. Only rerunning the original confined operation can confirm its failure is resolved.' @{
|
|
714
787
|
exitCode = $exitCode; fixed = $fixed; granted = $granted; refused = $refused; restored = $restored
|
|
715
788
|
operations = @($operations.ToArray()); recoveries = @($recoveries.ToArray()); automaticRollback = $true; observationFailures = $observationFailures
|
|
716
|
-
rollback = $rollbackStatus; rollbackCommands = $pending; nextAction = $nextAction; report = $reportPath
|
|
789
|
+
rollback = $rollbackStatus; rollbackCommands = $pending; nextAction = $nextAction; scanTruncated = $scanTruncated; report = $reportPath
|
|
717
790
|
}
|
|
791
|
+
# Last on stdout, after the summary record: whatever the tool truncates, the tail
|
|
792
|
+
# still carries the decisions, the report path and the counts.
|
|
793
|
+
Write-Line ('RECAP ' + ($recap | ConvertTo-Json -Depth 8 -Compress))
|
|
794
|
+
if ($reportPath) { Write-Line ('REPORT_FILE {0}' -f $reportPath) }
|
|
795
|
+
Write-Line ('SUMMARY FIXED={0} GRANTED={1} REFUSED={2} RESTORED={3}' -f $fixed, $granted, $refused, $restored)
|
|
718
796
|
if ($reportWriter) { $reportWriter.Dispose() }
|
|
719
|
-
if ($Compact) { Write-CompactSummary -Status $status -NextAction $nextAction -RollbackCommands $pending }
|
|
720
797
|
}
|
|
721
798
|
exit $exitCode
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-sandbox-windows-acl",
|
|
3
3
|
"description": "Windows ACL write-restriction sandbox backend (restricted-token spawn with capability-SID write allowlist) for the DeepSeek Harness sandbox seam",
|
|
4
|
-
"version": "0.2.0-rc.
|
|
4
|
+
"version": "0.2.0-rc.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -35,23 +35,23 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@deepseek-ai/cordis": "~4.0.4",
|
|
38
|
-
"@deepseek-ai/dsh-
|
|
39
|
-
"@deepseek-ai/dsh-
|
|
38
|
+
"@deepseek-ai/dsh-skill": "0.2.0-rc.2",
|
|
39
|
+
"@deepseek-ai/dsh-subprocess": "0.2.0-rc.2"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"koffi": "3.1.1",
|
|
43
43
|
"yaml": "^2.4.2",
|
|
44
|
-
"@deepseek-ai/dsh-lazy-require": "0.2.0-rc.
|
|
45
|
-
"@deepseek-ai/dsh-win32-process": "0.2.0-rc.
|
|
44
|
+
"@deepseek-ai/dsh-lazy-require": "0.2.0-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-win32-process": "0.2.0-rc.2"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@deepseek-ai/cordis-plugin-include": "~1.0.9",
|
|
49
|
-
"@deepseek-ai/dsh-pwsh-local": "0.2.0-rc.1",
|
|
50
49
|
"@deepseek-ai/cordis-plugin-loader": "~1.0.5",
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/
|
|
50
|
+
"@deepseek-ai/dsh-pwsh-local": "0.2.0-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-sandbox-local": "0.2.0-rc.2",
|
|
52
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
53
|
+
"@deepseek-ai/dsh-skill": "0.2.0-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-subprocess": "0.2.0-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.0-rc.2"
|
|
56
56
|
}
|
|
57
57
|
}
|