@moonquake2004/dsh-security 0.1.7 → 0.2.0
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/package.json +1 -1
- package/src/checks/index.mjs +3 -0
- package/src/checks/sl1-supply-chain.mjs +11 -6
- package/src/checks/sl4-release-compat.mjs +11 -3
- package/src/checks/sp1-dependency-audit.mjs +155 -65
- package/src/checks/sp11-patch-security-override.mjs +132 -0
- package/src/checks/sp12-config-as-code-tag.mjs +125 -0
- package/src/checks/sp13-tools-mode-sandbox.mjs +631 -0
- package/src/checks/sp3-sandbox-consistency.mjs +293 -147
- package/src/checks/sp5-permission-model.mjs +267 -53
- package/src/checks/sp8-dist-tag-health.mjs +16 -0
- package/src/checks/sp9-dual-instance-guard.mjs +241 -57
- package/src/checks/sr1-sandbox-violation.mjs +22 -23
- package/src/checks/sr2-privilege-escalation.mjs +26 -11
- package/src/checks/sr3-data-exfiltration.mjs +6 -14
- package/src/checks/sr4-isolation-verify.mjs +4 -5
- package/src/checks/ss2-pii-exposure.mjs +8 -3
- package/src/checks/ss3-sensitive-output.mjs +9 -8
- package/src/checks/ss4-session-integrity.mjs +20 -18
- package/src/dsh-config.mjs +204 -0
- package/src/index.mjs +3 -0
- package/src/install-tree.mjs +293 -0
- package/src/integrations/ecosystem.mjs +232 -48
- package/src/integrations/index.mjs +72 -24
- package/src/integrations/plugin-reducer.mjs +190 -40
- package/src/integrations/poison-guard.mjs +162 -39
- package/src/integrations/sandbox-audit.mjs +51 -47
- package/src/integrations/tool-exec.mjs +130 -0
- package/src/registry.mjs +3 -0
- package/src/session-reader.mjs +134 -0
|
@@ -1,210 +1,356 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SP3: Sandbox Consistency — 沙箱策略配置一致性审计
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 2026-09 上游兼容审计 R6 修复:真实的 sandbox/approval/permission 配置
|
|
5
|
+
* **不在 profile 里**,而在 CLI 自带 bundle 的 patch 层与用户设置里:
|
|
6
|
+
* 1. `@deepseek-ai/dsh-base/cordis.patch.yml`(经安装树解析)
|
|
7
|
+
* - `id: sandbox-policy` → `config.mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'`
|
|
8
|
+
* - `id: approval` → `config.policy`(mode 为 danger-full-access 时 never,否则 ask)
|
|
9
|
+
* - `id: permission` → `config.presets.{read-only,workspace-write,danger-full-access}`
|
|
10
|
+
* 2. `$DSH_HOME/settings.yaml` → `permission.defaultPreset`
|
|
6
11
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
12
|
+
* 旧的 id 表已失效:`str_replace_editor` 是 tool *名* 而非 loader id,
|
|
13
|
+
* `tool-glob` / `tool-grep` / `tool-fs-write` 已不存在(并入 `tool-fs-search`)。
|
|
14
|
+
* 现在的 loader id 只有 `tool-fs` 与 `tool-fs-search`,沙箱由全局服务
|
|
15
|
+
* `sandbox` / `sandbox-policy` 承载,而非逐工具声明。
|
|
16
|
+
*
|
|
17
|
+
* 每个配置源缺失时返回 **skip + 原因**,绝不因为"没扫到"就 PASS。
|
|
11
18
|
*
|
|
12
19
|
* Severity: MEDIUM(默认)
|
|
13
20
|
* Phase: POST_INSTALL
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
|
-
import { readFileSync, existsSync,
|
|
17
|
-
import {
|
|
23
|
+
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { dirname, join } from 'node:path';
|
|
26
|
+
import { homedir } from 'node:os';
|
|
18
27
|
import { Severity } from '../protocol/severity.mjs';
|
|
19
28
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
20
|
-
import { pass, fail } from '../protocol/check.mjs';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const SEARCH_TOOLS = [
|
|
31
|
-
'tool-fs-search',
|
|
32
|
-
'tool-glob',
|
|
33
|
-
'tool-grep',
|
|
34
|
-
];
|
|
29
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
30
|
+
|
|
31
|
+
/** 真实存在的变文件系统工具 loader id(`str_replace_editor` 是 tool 名,不是 loader id) */
|
|
32
|
+
export const MUTATING_FS_TOOL_IDS = ['tool-fs'];
|
|
33
|
+
|
|
34
|
+
/** 真实存在的只读搜索工具 loader id(`tool-glob` / `tool-grep` 已并入它) */
|
|
35
|
+
export const READONLY_SEARCH_TOOL_IDS = ['tool-fs-search'];
|
|
36
|
+
|
|
37
|
+
/** 承载沙箱/审批/权限的真实服务 id */
|
|
38
|
+
export const SANDBOX_SERVICE_IDS = ['sandbox', 'sandbox-policy', 'bash-sandbox', 'pwsh-sandbox', 'approval', 'permission'];
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
|
-
*
|
|
41
|
+
* 解析 CLI 安装树里的 dsh-base bundle patch。
|
|
42
|
+
* 顺序:显式覆盖 → profile 内 → module-fallback 锚点 → `which dsh` 指向的全局安装树。
|
|
43
|
+
* @returns {string|null}
|
|
38
44
|
*/
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
45
|
+
export function resolveDshBasePatch(profileDir) {
|
|
46
|
+
const candidates = [];
|
|
47
|
+
if (process.env.DSH_BASE_PATCH) candidates.push(process.env.DSH_BASE_PATCH);
|
|
48
|
+
if (profileDir) {
|
|
49
|
+
candidates.push(join(profileDir, 'node_modules', '@deepseek-ai', 'dsh-base', 'cordis.patch.yml'));
|
|
50
|
+
candidates.push(join(profileDir, '.dsh-module-fallback', 'node_modules', '@deepseek-ai', 'dsh-base', 'cordis.patch.yml'));
|
|
51
|
+
}
|
|
52
|
+
for (const candidate of candidates) {
|
|
53
|
+
if (candidate && existsSync(candidate)) return candidate;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 全局安装树:<prefix>/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-base/
|
|
57
|
+
let whichRes;
|
|
58
|
+
try {
|
|
59
|
+
whichRes = spawnSync('which', ['dsh'], { encoding: 'utf8' });
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const binPath = ((whichRes && whichRes.stdout) || '').trim().split('\n').filter(Boolean)[0];
|
|
64
|
+
if (!binPath) return null;
|
|
65
|
+
|
|
66
|
+
let real = binPath;
|
|
67
|
+
try { real = realpathSync(binPath); } catch { /* 用原路径 */ }
|
|
68
|
+
|
|
69
|
+
let dir = dirname(real);
|
|
70
|
+
for (let i = 0; i < 8; i++) {
|
|
71
|
+
const candidate = join(dir, 'node_modules', '@deepseek-ai', 'dsh-base', 'cordis.patch.yml');
|
|
72
|
+
if (existsSync(candidate)) return candidate;
|
|
73
|
+
const parent = dirname(dir);
|
|
74
|
+
if (parent === dir) break;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
/** $DSH_HOME/settings.yaml(默认 ~/.dsh/settings.yaml) */
|
|
81
|
+
export function resolveSettingsPath() {
|
|
82
|
+
const home = process.env.DSH_HOME && process.env.DSH_HOME.trim()
|
|
83
|
+
? process.env.DSH_HOME.trim()
|
|
84
|
+
: join(homedir(), '.dsh');
|
|
85
|
+
return join(home, 'settings.yaml');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 去掉一层首尾匹配引号 */
|
|
89
|
+
function stripOuterQuotes(s) {
|
|
90
|
+
if (s.length >= 2 &&
|
|
91
|
+
((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"')))) {
|
|
92
|
+
return s.slice(1, -1);
|
|
93
|
+
}
|
|
94
|
+
return s;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stripInlineComment(s) {
|
|
98
|
+
return s.replace(/\s+#.*$/, '').trim();
|
|
62
99
|
}
|
|
63
100
|
|
|
64
101
|
/**
|
|
65
|
-
*
|
|
102
|
+
* 解析 YAML 标量:支持 `!!js` 表达式(含 `??` 默认值与三元表达式)与普通带引号标量。
|
|
103
|
+
* 返回该表达式在"无环境覆盖"下的默认值;无法静态求值时返回 null。
|
|
66
104
|
*/
|
|
67
|
-
function
|
|
68
|
-
|
|
105
|
+
export function resolveJsDefault(raw) {
|
|
106
|
+
if (raw === null || raw === undefined) return null;
|
|
107
|
+
let s = stripInlineComment(String(raw));
|
|
108
|
+
if (s.startsWith('!!js')) {
|
|
109
|
+
s = stripOuterQuotes(s.slice(4).trim());
|
|
110
|
+
// 三元表达式:取 else 分支(最后一个带引号的值)
|
|
111
|
+
if (s.includes('?') && s.includes(':')) {
|
|
112
|
+
const quoted = [...s.matchAll(/'([^']*)'|"([^"]*)"/g)].map(m => m[1] ?? m[2]);
|
|
113
|
+
if (quoted.length) return quoted[quoted.length - 1];
|
|
114
|
+
}
|
|
115
|
+
const nullish = s.match(/\?\?\s*'([^']*)'|\?\?\s*"([^"]*)"/);
|
|
116
|
+
if (nullish) return nullish[1] ?? nullish[2];
|
|
117
|
+
const quoted = [...s.matchAll(/'([^']*)'|"([^"]*)"/g)].map(m => m[1] ?? m[2]);
|
|
118
|
+
if (quoted.length) return quoted[quoted.length - 1];
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return stripOuterQuotes(s) || null;
|
|
69
122
|
}
|
|
70
123
|
|
|
71
124
|
/**
|
|
72
|
-
*
|
|
125
|
+
* 从 patch 内容中切出 `- id: <x>` 条目块。
|
|
126
|
+
* @returns {Array<{id: string, block: string, disabled: boolean}>}
|
|
73
127
|
*/
|
|
74
|
-
function
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// 检查 danger-full-access 模式
|
|
81
|
-
if (/mode:\s*['"]?danger-full-access['"]?/.test(content) &&
|
|
82
|
-
!/mode:\s*.*win32/.test(content)) {
|
|
83
|
-
issues.push({
|
|
84
|
-
severity: 'medium',
|
|
85
|
-
tool: 'sandbox-policy',
|
|
86
|
-
finding: 'sandbox-policy 使用 danger-full-access 模式(非 Windows),所有工具不受沙箱限制',
|
|
87
|
-
});
|
|
128
|
+
export function parseEntries(content) {
|
|
129
|
+
const re = /^[ \t]*-[ \t]*id:[ \t]*['"]?([^'"\n]+?)['"]?[ \t]*$/gm;
|
|
130
|
+
const matches = [];
|
|
131
|
+
let m;
|
|
132
|
+
while ((m = re.exec(content)) !== null) {
|
|
133
|
+
matches.push({ id: m[1].trim(), index: m.index, end: m.index + m[0].length });
|
|
88
134
|
}
|
|
135
|
+
return matches.map((entry, i) => {
|
|
136
|
+
const block = content.slice(entry.end, i + 1 < matches.length ? matches[i + 1].index : content.length);
|
|
137
|
+
const disabledRaw = extractEntryScalar(block, 'disabled');
|
|
138
|
+
return { id: entry.id, block, disabled: disabledRaw === 'true' };
|
|
139
|
+
});
|
|
140
|
+
}
|
|
89
141
|
|
|
90
|
-
|
|
142
|
+
/** 在条目块里取一个顶层标量(如 `disabled:` / `mode:` / `policy:`) */
|
|
143
|
+
function extractEntryScalar(block, key) {
|
|
144
|
+
const m = block.match(new RegExp(`^[ \\t]+${key}:[ \\t]*(.+?)[ \\t]*$`, 'm'));
|
|
145
|
+
return m ? m[1] : null;
|
|
91
146
|
}
|
|
92
147
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
/** 解析 permission 条目的 `config.presets` 表 → { name: { sandbox, approval } } */
|
|
149
|
+
export function parsePresets(block) {
|
|
150
|
+
const presets = {};
|
|
151
|
+
const lines = block.split('\n');
|
|
152
|
+
const start = lines.findIndex(l => /^[ \t]*presets:[ \t]*$/.test(l));
|
|
153
|
+
if (start === -1) return presets;
|
|
154
|
+
|
|
155
|
+
const baseIndent = lines[start].match(/^([ \t]*)/)[1].length;
|
|
156
|
+
let current = null;
|
|
157
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
158
|
+
const line = lines[i];
|
|
159
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
160
|
+
const indent = line.match(/^([ \t]*)/)[1].length;
|
|
161
|
+
if (indent <= baseIndent) break;
|
|
162
|
+
const presetName = line.match(/^[ \t]+([A-Za-z0-9_.-]+):[ \t]*$/);
|
|
163
|
+
if (presetName && indent === baseIndent + 2) {
|
|
164
|
+
current = presetName[1];
|
|
165
|
+
presets[current] = {};
|
|
166
|
+
continue;
|
|
112
167
|
}
|
|
168
|
+
const kv = line.match(/^[ \t]+([A-Za-z0-9_.-]+):[ \t]*(.+?)[ \t]*$/);
|
|
169
|
+
if (kv && current) presets[current][kv[1]] = resolveJsDefault(kv[2]);
|
|
113
170
|
}
|
|
171
|
+
return presets;
|
|
172
|
+
}
|
|
114
173
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
174
|
+
/**
|
|
175
|
+
* 从 settings.yaml 读取顶层 `permission.defaultPreset`。
|
|
176
|
+
* 找不到 `permission` 命名空间或该键时返回 null。
|
|
177
|
+
*/
|
|
178
|
+
export function readDefaultPreset(content) {
|
|
179
|
+
const lines = String(content).split('\n');
|
|
180
|
+
let inPermission = false;
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
183
|
+
const indent = line.match(/^([ \t]*)/)[1].length;
|
|
184
|
+
if (/^permission:[ \t]*$/.test(line)) { inPermission = true; continue; }
|
|
185
|
+
if (inPermission) {
|
|
186
|
+
if (indent === 0) { inPermission = false; continue; }
|
|
187
|
+
const m = line.match(/^[ \t]+defaultPreset:[ \t]*(.+?)[ \t]*$/);
|
|
188
|
+
if (m) return resolveJsDefault(m[1]);
|
|
127
189
|
}
|
|
128
190
|
}
|
|
129
|
-
|
|
130
|
-
return issues;
|
|
191
|
+
return null;
|
|
131
192
|
}
|
|
132
193
|
|
|
133
194
|
/**
|
|
134
|
-
* SP3
|
|
195
|
+
* SP3 检查:沙箱/审批/权限解析配置一致性
|
|
135
196
|
* @param {string} profileDir - profile 目录路径
|
|
136
197
|
* @returns {Promise<import('../protocol/check.mjs').SecurityCheckResult>}
|
|
137
198
|
*/
|
|
138
199
|
export async function run(profileDir) {
|
|
139
200
|
const id = 'SP3';
|
|
201
|
+
const patchPath = resolveDshBasePatch(profileDir);
|
|
202
|
+
const settingsPath = resolveSettingsPath();
|
|
203
|
+
const settingsExists = existsSync(settingsPath);
|
|
140
204
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const nmDir = join(profileDir, 'node_modules');
|
|
148
|
-
if (existsSync(nmDir)) {
|
|
149
|
-
const entries = readdirSync(nmDir, { withFileTypes: true });
|
|
150
|
-
for (const entry of entries) {
|
|
151
|
-
if (entry.name.startsWith('.') || entry.name === '.bin') continue;
|
|
152
|
-
if (entry.name.startsWith('@')) {
|
|
153
|
-
// scoped package
|
|
154
|
-
const scopeDir = join(nmDir, entry.name);
|
|
155
|
-
const pkgs = readdirSync(scopeDir, { withFileTypes: true });
|
|
156
|
-
for (const pkg of pkgs) {
|
|
157
|
-
const patchPath = join(scopeDir, pkg.name, 'cordis.patch.yml');
|
|
158
|
-
if (existsSync(patchPath)) patchFiles.push(patchPath);
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
const patchPath = join(nmDir, entry.name, 'cordis.patch.yml');
|
|
162
|
-
if (existsSync(patchPath)) patchFiles.push(patchPath);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
205
|
+
if (!patchPath) {
|
|
206
|
+
const settingsNote = settingsExists
|
|
207
|
+
? ';settings.yaml 存在但缺少 dsh-base bundle patch,无法解析 permission presets 表'
|
|
208
|
+
: ';settings.yaml 也不存在';
|
|
209
|
+
return skip(id, Severity.MEDIUM,
|
|
210
|
+
`未找到 @deepseek-ai/dsh-base/cordis.patch.yml(真实沙箱策略配置位置),跳过一致性审计${settingsNote}`);
|
|
165
211
|
}
|
|
166
212
|
|
|
167
|
-
|
|
168
|
-
|
|
213
|
+
let content;
|
|
214
|
+
try {
|
|
215
|
+
content = readFileSync(patchPath, 'utf8');
|
|
216
|
+
} catch (e) {
|
|
217
|
+
return skip(id, Severity.MEDIUM, `dsh-base patch 不可读(${patchPath}):${e.message},跳过一致性审计`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const byId = new Map(parseEntries(content).filter(e => !e.disabled).map(e => [e.id, e]));
|
|
221
|
+
const sandboxPolicy = byId.get('sandbox-policy');
|
|
222
|
+
const approvalEntry = byId.get('approval');
|
|
223
|
+
const permissionEntry = byId.get('permission');
|
|
224
|
+
|
|
225
|
+
if (!sandboxPolicy && !approvalEntry && !permissionEntry) {
|
|
226
|
+
return skip(id, Severity.MEDIUM,
|
|
227
|
+
`dsh-base patch(${patchPath})中未找到 sandbox-policy/approval/permission 条目,沙箱配置源缺失,跳过一致性审计`);
|
|
169
228
|
}
|
|
170
229
|
|
|
171
|
-
const
|
|
172
|
-
|
|
230
|
+
const issues = [];
|
|
231
|
+
const envMode = (process.env.DSH_PERMISSION_MODE || '').trim() || null;
|
|
232
|
+
const patchMode = sandboxPolicy ? resolveJsDefault(extractEntryScalar(sandboxPolicy.block, 'mode')) : null;
|
|
233
|
+
const patchApproval = approvalEntry ? resolveJsDefault(extractEntryScalar(approvalEntry.block, 'policy')) : null;
|
|
234
|
+
const presets = permissionEntry ? parsePresets(permissionEntry.block) : {};
|
|
235
|
+
|
|
236
|
+
let settingsPreset = null;
|
|
237
|
+
if (settingsExists) {
|
|
173
238
|
try {
|
|
174
|
-
|
|
175
|
-
allIssues.push(...checkSandboxPolicy(content));
|
|
176
|
-
allIssues.push(...checkToolSandbox(content));
|
|
239
|
+
settingsPreset = readDefaultPreset(readFileSync(settingsPath, 'utf8'));
|
|
177
240
|
} catch {
|
|
178
|
-
|
|
241
|
+
settingsPreset = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let mode;
|
|
246
|
+
let approvalPolicy;
|
|
247
|
+
let source;
|
|
248
|
+
|
|
249
|
+
if (envMode) {
|
|
250
|
+
mode = envMode;
|
|
251
|
+
approvalPolicy = envMode === 'danger-full-access' ? 'never' : 'ask';
|
|
252
|
+
source = `DSH_PERMISSION_MODE=${envMode}(环境覆盖)`;
|
|
253
|
+
} else if (settingsPreset) {
|
|
254
|
+
if (presets[settingsPreset]) {
|
|
255
|
+
mode = presets[settingsPreset].sandbox ?? patchMode;
|
|
256
|
+
approvalPolicy = presets[settingsPreset].approval ?? patchApproval;
|
|
257
|
+
source = `settings.yaml permission.defaultPreset=${settingsPreset}`;
|
|
258
|
+
} else {
|
|
259
|
+
issues.push({
|
|
260
|
+
severity: 'medium',
|
|
261
|
+
tool: 'permission.defaultPreset',
|
|
262
|
+
finding: `settings.yaml 指定 permission.defaultPreset=${settingsPreset},但 dsh-base patch 的 presets 表中没有该预设(可选:${Object.keys(presets).join(', ') || '无'})`,
|
|
263
|
+
});
|
|
264
|
+
mode = patchMode;
|
|
265
|
+
approvalPolicy = patchApproval;
|
|
266
|
+
source = 'dsh-base patch 默认值(defaultPreset 无法解析)';
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
mode = patchMode;
|
|
270
|
+
approvalPolicy = patchApproval;
|
|
271
|
+
source = 'dsh-base patch 默认值';
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 1) 沙箱被解析为完全放行(非 Windows)
|
|
275
|
+
if (mode === 'danger-full-access' && process.platform !== 'win32') {
|
|
276
|
+
issues.push({
|
|
277
|
+
severity: 'medium',
|
|
278
|
+
tool: 'sandbox-policy',
|
|
279
|
+
finding: `沙箱模式解析为 danger-full-access(来源:${source}),非 Windows 下所有工具不受文件系统沙箱限制`
|
|
280
|
+
+ (approvalPolicy === 'never' ? ',且审批策略为 never(无人工闸门)' : ''),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 2) 审批闸门与沙箱模式不一致
|
|
285
|
+
if (approvalPolicy === 'never' && mode && mode !== 'danger-full-access') {
|
|
286
|
+
issues.push({
|
|
287
|
+
severity: 'medium',
|
|
288
|
+
tool: 'approval',
|
|
289
|
+
finding: `审批策略解析为 never,但沙箱模式为 ${mode}(来源:${source}),二者不一致——审批闸门缺失而沙箱并未完全放行`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 3) 解析出的 (sandbox, approval) 组合不对应任何已声明预设
|
|
294
|
+
const presetNames = Object.keys(presets);
|
|
295
|
+
if (mode && approvalPolicy && presetNames.length > 0 &&
|
|
296
|
+
!presetNames.some(n => presets[n].sandbox === mode && presets[n].approval === approvalPolicy)) {
|
|
297
|
+
issues.push({
|
|
298
|
+
severity: 'low',
|
|
299
|
+
tool: 'permission.presets',
|
|
300
|
+
finding: `解析出的组合 sandbox=${mode} / approval=${approvalPolicy}(来源:${source})不对应 presets 表中的任何预设(${presetNames.join(', ')})`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 4) 工具接线:工具 id 存在但全局沙箱服务缺失
|
|
305
|
+
const presentSandboxServices = SANDBOX_SERVICE_IDS.filter(serviceId => byId.has(serviceId));
|
|
306
|
+
const hasSandboxService = presentSandboxServices.includes('sandbox') || presentSandboxServices.includes('sandbox-policy');
|
|
307
|
+
for (const toolId of MUTATING_FS_TOOL_IDS) {
|
|
308
|
+
if (byId.has(toolId) && !hasSandboxService) {
|
|
309
|
+
issues.push({
|
|
310
|
+
severity: 'medium',
|
|
311
|
+
tool: toolId,
|
|
312
|
+
finding: `${toolId} 已挂载,但 bundle 中没有 sandbox/sandbox-policy 服务,文件系统工具可能使用 bare backend(策略被静默忽略)`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const toolId of READONLY_SEARCH_TOOL_IDS) {
|
|
317
|
+
if (byId.has(toolId) && !hasSandboxService) {
|
|
318
|
+
issues.push({
|
|
319
|
+
severity: 'low',
|
|
320
|
+
tool: toolId,
|
|
321
|
+
finding: `${toolId} 已挂载,但 bundle 中没有 sandbox/sandbox-policy 服务,只读搜索可能读取写策略外的路径`,
|
|
322
|
+
});
|
|
179
323
|
}
|
|
180
324
|
}
|
|
181
325
|
|
|
182
|
-
|
|
183
|
-
|
|
326
|
+
const sourceNote = [
|
|
327
|
+
`patch=${patchPath}`,
|
|
328
|
+
`settings=${settingsExists ? settingsPath : '不存在'}`,
|
|
329
|
+
`mode=${mode ?? '未知'}`,
|
|
330
|
+
`approval=${approvalPolicy ?? '未知'}`,
|
|
331
|
+
`sandboxServices=${presentSandboxServices.join('+') || '无'}`,
|
|
332
|
+
].join(', ');
|
|
333
|
+
|
|
334
|
+
if (issues.length === 0) {
|
|
335
|
+
return pass(id, Severity.MEDIUM,
|
|
336
|
+
`沙箱/审批/权限配置一致(${sourceNote};来源:${source})`);
|
|
184
337
|
}
|
|
185
338
|
|
|
186
339
|
const bySeverity = {};
|
|
187
|
-
for (const issue of
|
|
340
|
+
for (const issue of issues) {
|
|
188
341
|
if (!bySeverity[issue.severity]) bySeverity[issue.severity] = [];
|
|
189
342
|
bySeverity[issue.severity].push(issue);
|
|
190
343
|
}
|
|
344
|
+
const summary = Object.entries(bySeverity).map(([sev, items]) => `${sev}: ${items.length}`).join(', ');
|
|
345
|
+
const details = issues.slice(0, 10).map(i => `[${i.severity}] ${i.tool} — ${i.finding}`).join('\n');
|
|
191
346
|
|
|
192
|
-
const
|
|
193
|
-
.
|
|
194
|
-
.join(', ');
|
|
195
|
-
|
|
196
|
-
const details = allIssues
|
|
197
|
-
.slice(0, 10)
|
|
198
|
-
.map(i => `[${i.severity}] ${i.tool} — ${i.finding}`)
|
|
199
|
-
.join('\n');
|
|
200
|
-
|
|
201
|
-
const overallSeverity = allIssues.some(i => i.severity === 'high') ? Severity.HIGH
|
|
202
|
-
: allIssues.some(i => i.severity === 'medium') ? Severity.MEDIUM
|
|
347
|
+
const overallSeverity = issues.some(i => i.severity === 'high') ? Severity.HIGH
|
|
348
|
+
: issues.some(i => i.severity === 'medium') ? Severity.MEDIUM
|
|
203
349
|
: Severity.LOW;
|
|
204
350
|
|
|
205
351
|
return fail(id, overallSeverity,
|
|
206
|
-
`检测到 ${
|
|
207
|
-
'
|
|
352
|
+
`检测到 ${issues.length} 个沙箱配置不一致(${summary};${sourceNote}):\n${details}`,
|
|
353
|
+
'核对 @deepseek-ai/dsh-base/cordis.patch.yml 的 sandbox-policy/approval/permission 与 $DSH_HOME/settings.yaml 的 permission.defaultPreset',
|
|
208
354
|
['#2066']
|
|
209
355
|
);
|
|
210
356
|
}
|
|
@@ -214,7 +360,7 @@ export const sp3Check = {
|
|
|
214
360
|
name: 'sandbox-consistency',
|
|
215
361
|
severity: Severity.MEDIUM,
|
|
216
362
|
phase: CheckPhase.POST_INSTALL,
|
|
217
|
-
description: '
|
|
363
|
+
description: '沙箱策略配置一致性审计(dsh-base bundle patch + settings.yaml)',
|
|
218
364
|
src: 'builtin',
|
|
219
365
|
runner: (profileDir) => run(profileDir),
|
|
220
366
|
};
|