@moonquake2004/dsh-security 0.1.6 → 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 +8 -2
- 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 +17 -1
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-security",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Unified security check framework for DeepSeek Harness ecosystem — covers the full plugin lifecycle (discovery → install → runtime → update → retirement)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test test/*.mjs"
|
|
18
18
|
},
|
|
19
|
-
"keywords": [
|
|
19
|
+
"keywords": [
|
|
20
|
+
"dsh",
|
|
21
|
+
"deepseek-harness",
|
|
22
|
+
"security",
|
|
23
|
+
"audit",
|
|
24
|
+
"plugin"
|
|
25
|
+
],
|
|
20
26
|
"author": "moonquake2004",
|
|
21
27
|
"license": "MIT",
|
|
22
28
|
"engines": {
|
package/src/checks/index.mjs
CHANGED
|
@@ -9,6 +9,9 @@ export { sp7Check } from './sp7-client-syntax.mjs';
|
|
|
9
9
|
export { sp8Check } from './sp8-dist-tag-health.mjs';
|
|
10
10
|
export { sp9Check } from './sp9-dual-instance-guard.mjs';
|
|
11
11
|
export { sp10Check } from './sp10-poison-pattern.mjs';
|
|
12
|
+
export { sp11Check } from './sp11-patch-security-override.mjs';
|
|
13
|
+
export { sp12Check } from './sp12-config-as-code-tag.mjs';
|
|
14
|
+
export { sp13Check } from './sp13-tools-mode-sandbox.mjs';
|
|
12
15
|
|
|
13
16
|
// Layer 2: Runtime Checks
|
|
14
17
|
export { sr1Check } from './sr1-sandbox-violation.mjs';
|
|
@@ -62,6 +62,7 @@ export function compareVersions(localPkg, registryInfo, localLockIntegrity = nul
|
|
|
62
62
|
issues.push({
|
|
63
63
|
type: 'version-mismatch',
|
|
64
64
|
severity: 'medium',
|
|
65
|
+
informational: true, // 只是"不新鲜",不是完整性/供应链风险 —— 不参与判定(2026-09 审计结论)
|
|
65
66
|
detail: `本地版本 ${localVersion} ≠ registry latest ${latestVersion}`,
|
|
66
67
|
});
|
|
67
68
|
}
|
|
@@ -152,25 +153,29 @@ export async function run(profileDir) {
|
|
|
152
153
|
return skip(id, Severity.HIGH, `${errors} 个包 registry 查询失败(离线或网络受限),跳过供应链验证`);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
|
|
156
|
+
const material = allIssues.filter(i => !i.informational); // 完整性类才是"问题"
|
|
157
|
+
const outdated = allIssues.filter(i => i.informational);
|
|
158
|
+
|
|
159
|
+
if (material.length === 0) {
|
|
156
160
|
return pass(id, Severity.HIGH,
|
|
157
161
|
`验证 ${checked} 个包的供应链完整性,未发现异常${errors > 0 ? `(${errors} 个包查询失败)` : ''}`
|
|
162
|
+
+ (outdated.length ? `;另有 ${outdated.length} 个包版本落后于 registry latest(不新鲜,非完整性问题)` : '')
|
|
158
163
|
);
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
const criticalIssues =
|
|
162
|
-
const overallSeverity = criticalIssues.length > 0 ? Severity.CRITICAL : maxSeverity(
|
|
166
|
+
const criticalIssues = material.filter(i => i.severity === 'critical');
|
|
167
|
+
const overallSeverity = criticalIssues.length > 0 ? Severity.CRITICAL : maxSeverity(material.map(i => i.severity === 'critical' ? Severity.CRITICAL : i.severity === 'medium' ? Severity.MEDIUM : Severity.LOW));
|
|
163
168
|
|
|
164
|
-
const details =
|
|
169
|
+
const details = material
|
|
165
170
|
.map(i => `[${i.severity}] ${i.package} — ${i.type}: ${i.detail}`)
|
|
166
|
-
.join('\n');
|
|
171
|
+
.join('\n') + (outdated.length ? `\n(另有 ${outdated.length} 个包版本落后,未计入问题)` : '');
|
|
167
172
|
|
|
168
173
|
const fix = criticalIssues.length > 0
|
|
169
174
|
? '检测到 integrity hash 不匹配,可能是包被篡改。立即重新安装受影响的包'
|
|
170
175
|
: '部分包版本落后于 registry latest,建议更新';
|
|
171
176
|
|
|
172
177
|
return fail(id, overallSeverity,
|
|
173
|
-
`检测到 ${
|
|
178
|
+
`检测到 ${material.length} 个供应链问题(${checked} 个包已验证${errors > 0 ? `,${errors} 个查询失败` : ''}):\n${details}`,
|
|
174
179
|
fix
|
|
175
180
|
);
|
|
176
181
|
}
|
|
@@ -57,6 +57,7 @@ function checkCompatibility(localVersion, distTags) {
|
|
|
57
57
|
issues.push({
|
|
58
58
|
type: 'latest-is-prerelease',
|
|
59
59
|
severity: 'medium',
|
|
60
|
+
informational: true, // 全生态都在 rc 线上,这属常态描述而非兼容性问题(2026-09 审计结论)
|
|
60
61
|
detail: `latest 标签指向预发布版本 ${distTags.latest},可能不稳定`,
|
|
61
62
|
});
|
|
62
63
|
}
|
|
@@ -67,6 +68,7 @@ function checkCompatibility(localVersion, distTags) {
|
|
|
67
68
|
issues.push({
|
|
68
69
|
type: 'next-available',
|
|
69
70
|
severity: 'low',
|
|
71
|
+
informational: true,
|
|
70
72
|
detail: `有 next 标签 ${distTags.next} 可用(当前 latest: ${distTags.latest})`,
|
|
71
73
|
});
|
|
72
74
|
}
|
|
@@ -140,17 +142,23 @@ export async function run(profileDir) {
|
|
|
140
142
|
);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
const
|
|
145
|
+
const material = allIssues.filter(i => !i.informational);
|
|
146
|
+
if (material.length === 0) {
|
|
147
|
+
return pass(id, Severity.LOW,
|
|
148
|
+
`验证 ${checked} 个包的发布兼容性,未发现兼容性问题`
|
|
149
|
+
+ (allIssues.length ? `;另有 ${allIssues.length} 条常态提示(预发布 latest / next 可用)未计入` : ''));
|
|
150
|
+
}
|
|
151
|
+
const mediumIssues = material.filter(i => i.severity === 'medium');
|
|
144
152
|
const overallSeverity = mediumIssues.length > 0 ? Severity.MEDIUM : Severity.LOW;
|
|
145
153
|
|
|
146
|
-
const details =
|
|
154
|
+
const details = material
|
|
147
155
|
.map(i => `[${i.severity}] ${i.package} — ${i.type}: ${i.detail}`)
|
|
148
156
|
.join('\n');
|
|
149
157
|
|
|
150
158
|
const fix = '检查是否有重要更新需要应用,或确认当前版本满足需求';
|
|
151
159
|
|
|
152
160
|
return fail(id, overallSeverity,
|
|
153
|
-
`检测到 ${
|
|
161
|
+
`检测到 ${material.length} 个发布兼容性问题(${checked} 个包已验证):\n${details}`,
|
|
154
162
|
fix
|
|
155
163
|
);
|
|
156
164
|
}
|
|
@@ -1,82 +1,88 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SP1: Dependency Audit — 依赖链漏洞扫描
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 对 profile 的依赖锁文件执行真实审计:
|
|
5
|
+
* - pnpm-lock.yaml → `pnpm audit --json --prod`
|
|
6
|
+
* - package-lock.json → `npm audit --json --omit=dev`
|
|
7
|
+
*
|
|
8
|
+
* 契约(2026-09 上游兼容审计 R9 修复):
|
|
9
|
+
* 任何"审计没有真正执行"的情况都必须返回 **skip + 原因**,
|
|
10
|
+
* 绝不允许静默 PASS。旧实现在 pnpm profile(无 package-lock.json)上
|
|
11
|
+
* 把 npm 的 ENOLOCK 错误当成"无漏洞",永久输出
|
|
12
|
+
* `依赖链无已知漏洞(扫描 ? 个依赖)` 的假 PASS。
|
|
6
13
|
*
|
|
7
14
|
* Severity: HIGH(有 critical/high 漏洞时)
|
|
8
15
|
* Phase: POST_INSTALL
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
|
-
import {
|
|
12
|
-
import { existsSync
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
13
20
|
import { join } from 'node:path';
|
|
14
21
|
import { Severity, maxSeverity } from '../protocol/severity.mjs';
|
|
15
22
|
import { CheckPhase } from '../protocol/phase.mjs';
|
|
16
|
-
import { pass, fail } from '../protocol/check.mjs';
|
|
23
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
24
|
+
|
|
25
|
+
const AUDIT_TIMEOUT_MS = 60000;
|
|
26
|
+
const MAX_BUFFER = 32 * 1024 * 1024;
|
|
17
27
|
|
|
18
28
|
/**
|
|
19
|
-
*
|
|
29
|
+
* 选审计后端:锁文件决定包管理器。没有锁文件就没有可审计的依赖图。
|
|
30
|
+
* @returns {{pm: string, lock: string, args: string[]}|null}
|
|
20
31
|
*/
|
|
21
|
-
function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return { vulns: [], error: 'package.json 不存在' };
|
|
32
|
+
export function detectBackend(profileDir) {
|
|
33
|
+
if (existsSync(join(profileDir, 'pnpm-lock.yaml'))) {
|
|
34
|
+
return { pm: 'pnpm', lock: 'pnpm-lock.yaml', args: ['audit', '--json', '--prod'] };
|
|
25
35
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const output = execSync('npm audit --json --omit=dev', {
|
|
29
|
-
cwd: profileDir,
|
|
30
|
-
encoding: 'utf8',
|
|
31
|
-
timeout: 60000,
|
|
32
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
-
});
|
|
34
|
-
const audit = JSON.parse(output);
|
|
35
|
-
return parseNpmAudit(audit);
|
|
36
|
-
} catch (e) {
|
|
37
|
-
// npm audit 在有漏洞时 exit code 1,这是正常的
|
|
38
|
-
if (e.stdout) {
|
|
39
|
-
try {
|
|
40
|
-
const audit = JSON.parse(e.stdout);
|
|
41
|
-
return parseNpmAudit(audit);
|
|
42
|
-
} catch {
|
|
43
|
-
return { vulns: [], error: `npm audit 解析失败: ${e.message}` };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return { vulns: [], error: `npm audit 执行失败: ${e.message}` };
|
|
36
|
+
if (existsSync(join(profileDir, 'package-lock.json'))) {
|
|
37
|
+
return { pm: 'npm', lock: 'package-lock.json', args: ['audit', '--json', '--omit=dev'] };
|
|
47
38
|
}
|
|
39
|
+
return null;
|
|
48
40
|
}
|
|
49
41
|
|
|
50
42
|
/**
|
|
51
|
-
*
|
|
43
|
+
* 解析审计 JSON(同时兼容 npm v6 `advisories` 与 npm v7+/pnpm `vulnerabilities` 两种格式)。
|
|
44
|
+
* 返回 `{ vulns, totalDependencies, totalVulnerabilities }`;
|
|
45
|
+
* 若输出里带 `error`(如 ENOLOCK / ERR_PNPM_AUDIT_NO_LOCKFILE),返回 `{ auditError, auditErrorCode }`。
|
|
52
46
|
*/
|
|
53
|
-
function
|
|
47
|
+
export function parseAuditJson(audit) {
|
|
48
|
+
if (audit && typeof audit === 'object' && audit.error) {
|
|
49
|
+
const err = audit.error;
|
|
50
|
+
const code = err.code || 'AUDIT_ERROR';
|
|
51
|
+
return {
|
|
52
|
+
auditError: `${code}: ${err.summary || err.detail || '审计未执行'}`,
|
|
53
|
+
auditErrorCode: code,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
54
57
|
const vulns = [];
|
|
55
|
-
const
|
|
56
|
-
const metadata = audit.metadata || {};
|
|
58
|
+
const metadata = (audit && audit.metadata) || {};
|
|
57
59
|
|
|
58
|
-
// npm v7+
|
|
59
|
-
if (audit.vulnerabilities) {
|
|
60
|
+
// npm v7+ / pnpm v9+:vulnerabilities 按包聚合
|
|
61
|
+
if (audit && audit.vulnerabilities && typeof audit.vulnerabilities === 'object') {
|
|
60
62
|
for (const [name, vuln] of Object.entries(audit.vulnerabilities)) {
|
|
61
|
-
|
|
63
|
+
const severity = vuln && vuln.severity;
|
|
64
|
+
if (!severity || severity === 'info') continue;
|
|
65
|
+
const via = Array.isArray(vuln.via) ? vuln.via.filter(v => v && typeof v === 'object') : [];
|
|
62
66
|
vulns.push({
|
|
63
67
|
package: name,
|
|
64
|
-
severity
|
|
65
|
-
title:
|
|
68
|
+
severity,
|
|
69
|
+
title: via[0]?.title || (typeof vuln.via?.[0] === 'string' ? vuln.via[0] : 'Unknown vulnerability'),
|
|
66
70
|
range: vuln.range || 'Unknown',
|
|
67
71
|
fixAvailable: !!vuln.fixAvailable,
|
|
68
|
-
url:
|
|
72
|
+
url: via[0]?.url || '',
|
|
69
73
|
});
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
// npm v6
|
|
74
|
-
|
|
77
|
+
// npm v6 / pnpm <= 9:advisories(keyed by id)
|
|
78
|
+
const advisories = (audit && audit.advisories) || {};
|
|
79
|
+
for (const adv of Object.values(advisories)) {
|
|
80
|
+
if (!adv || adv.severity === 'info') continue;
|
|
75
81
|
vulns.push({
|
|
76
|
-
package: adv.module_name,
|
|
77
|
-
severity: adv.severity,
|
|
78
|
-
title: adv.title,
|
|
79
|
-
range: adv.vulnerable_versions,
|
|
82
|
+
package: adv.module_name || 'unknown',
|
|
83
|
+
severity: adv.severity || 'unknown',
|
|
84
|
+
title: adv.title || 'Unknown vulnerability',
|
|
85
|
+
range: adv.vulnerable_versions || 'Unknown',
|
|
80
86
|
fixAvailable: !!adv.patched_versions,
|
|
81
87
|
url: adv.url || '',
|
|
82
88
|
});
|
|
@@ -84,11 +90,89 @@ function parseNpmAudit(audit) {
|
|
|
84
90
|
|
|
85
91
|
return {
|
|
86
92
|
vulns,
|
|
87
|
-
totalDependencies: metadata
|
|
88
|
-
totalVulnerabilities: metadata.vulnerabilities?.total
|
|
93
|
+
totalDependencies: resolveDependencyCount(metadata),
|
|
94
|
+
totalVulnerabilities: metadata.vulnerabilities?.total ?? vulns.length,
|
|
89
95
|
};
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 依赖总数:npm v6/pnpm 用 `metadata.totalDependencies`(数字);
|
|
100
|
+
* npm v7+ 用 `metadata.dependencies.total`;pnpm 某些版本直接给数字。
|
|
101
|
+
*/
|
|
102
|
+
function resolveDependencyCount(metadata) {
|
|
103
|
+
if (typeof metadata.totalDependencies === 'number') return metadata.totalDependencies;
|
|
104
|
+
const deps = metadata.dependencies;
|
|
105
|
+
if (deps && typeof deps === 'object' && typeof deps.total === 'number') return deps.total;
|
|
106
|
+
if (typeof deps === 'number') return deps;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 判断 stderr 是否是"没有 lockfile 可审计" */
|
|
111
|
+
function isNoLockfileMessage(text) {
|
|
112
|
+
return /ERR_PNPM_AUDIT_NO_LOCKFILE|ENOLOCK|requires an existing lockfile|Cannot audit a project without a lockfile/i.test(text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 执行审计。任何未真正产出审计结论的情况 → `{ error, code }`。
|
|
117
|
+
*/
|
|
118
|
+
export function runAudit(backend, profileDir) {
|
|
119
|
+
let res;
|
|
120
|
+
try {
|
|
121
|
+
res = spawnSync(backend.pm, backend.args, {
|
|
122
|
+
cwd: profileDir,
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
timeout: AUDIT_TIMEOUT_MS,
|
|
125
|
+
maxBuffer: MAX_BUFFER,
|
|
126
|
+
});
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return { error: `执行 ${backend.pm} audit 失败:${e.message}`, code: 'SPAWN_FAILED' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (res.error) {
|
|
132
|
+
if (res.error.code === 'ENOENT') {
|
|
133
|
+
return { error: `未找到 ${backend.pm} 命令(ENOENT)`, code: 'PM_NOT_FOUND' };
|
|
134
|
+
}
|
|
135
|
+
if (res.error.code === 'ETIMEDOUT') {
|
|
136
|
+
return { error: `${backend.pm} audit 超时(${AUDIT_TIMEOUT_MS}ms)`, code: 'AUDIT_TIMEOUT' };
|
|
137
|
+
}
|
|
138
|
+
return { error: `${backend.pm} audit 执行失败:${res.error.message}`, code: res.error.code || 'SPAWN_FAILED' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const stdout = (res.stdout || '').trim();
|
|
142
|
+
const stderr = (res.stderr || '').trim();
|
|
143
|
+
|
|
144
|
+
// 无 stdout 通常意味着包管理器拒绝了审计(无锁文件 / 无网络 / 未登录)。
|
|
145
|
+
// pnpm 12 的 ERR_PNPM_AUDIT_NO_LOCKFILE 就只写 stderr、stdout 为空。
|
|
146
|
+
if (!stdout) {
|
|
147
|
+
if (isNoLockfileMessage(stderr)) {
|
|
148
|
+
return { error: `无锁文件,无法审计:${firstLine(stderr)}`, code: 'NO_LOCKFILE' };
|
|
149
|
+
}
|
|
150
|
+
return { error: `audit 无 JSON 输出(exit ${res.status}):${firstLine(stderr) || 'stderr 为空'}`, code: 'AUDIT_NO_OUTPUT' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let audit;
|
|
154
|
+
try {
|
|
155
|
+
audit = JSON.parse(stdout);
|
|
156
|
+
} catch {
|
|
157
|
+
return { error: `audit 输出不是合法 JSON(exit ${res.status})`, code: 'AUDIT_BAD_JSON' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const parsed = parseAuditJson(audit);
|
|
161
|
+
if (parsed.auditError) return { error: parsed.auditError, code: parsed.auditErrorCode };
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function firstLine(text) {
|
|
166
|
+
return (text || '').split('\n').map(l => l.trim()).filter(Boolean)[0] || '';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function auditSeverityToEnum(sev) {
|
|
170
|
+
if (sev === 'critical') return Severity.CRITICAL;
|
|
171
|
+
if (sev === 'high') return Severity.HIGH;
|
|
172
|
+
if (sev === 'moderate' || sev === 'medium') return Severity.MEDIUM;
|
|
173
|
+
return Severity.LOW;
|
|
174
|
+
}
|
|
175
|
+
|
|
92
176
|
/**
|
|
93
177
|
* SP1 检查:扫描 profile 依赖链中的已知漏洞
|
|
94
178
|
* @param {string} profileDir - profile 目录路径
|
|
@@ -98,20 +182,29 @@ export async function run(profileDir) {
|
|
|
98
182
|
const id = 'SP1';
|
|
99
183
|
|
|
100
184
|
if (!existsSync(join(profileDir, 'package.json'))) {
|
|
101
|
-
return
|
|
185
|
+
return skip(id, Severity.HIGH, 'profile 无 package.json,没有可审计的依赖清单,跳过依赖审计');
|
|
102
186
|
}
|
|
103
187
|
|
|
104
|
-
const
|
|
188
|
+
const backend = detectBackend(profileDir);
|
|
189
|
+
if (!backend) {
|
|
190
|
+
return skip(id, Severity.HIGH,
|
|
191
|
+
'profile 无 pnpm-lock.yaml / package-lock.json,npm/pnpm audit 均无锁文件可审计(NO_LOCKFILE),跳过依赖审计');
|
|
192
|
+
}
|
|
105
193
|
|
|
106
|
-
|
|
107
|
-
|
|
194
|
+
const result = runAudit(backend, profileDir);
|
|
195
|
+
if (result.error) {
|
|
196
|
+
return skip(id, Severity.HIGH, `依赖审计未执行(${result.code}):${result.error}`);
|
|
108
197
|
}
|
|
109
198
|
|
|
199
|
+
const { vulns } = result;
|
|
200
|
+
const depCount = result.totalDependencies;
|
|
201
|
+
|
|
110
202
|
if (vulns.length === 0) {
|
|
111
|
-
|
|
203
|
+
const countText = typeof depCount === 'number' ? `${depCount}` : '计数不可用';
|
|
204
|
+
return pass(id, Severity.HIGH,
|
|
205
|
+
`依赖链无已知漏洞(${backend.pm} audit --json,扫描 ${countText} 个依赖)`);
|
|
112
206
|
}
|
|
113
207
|
|
|
114
|
-
// 按 severity 分组
|
|
115
208
|
const bySeverity = {};
|
|
116
209
|
for (const v of vulns) {
|
|
117
210
|
if (!bySeverity[v.severity]) bySeverity[v.severity] = [];
|
|
@@ -129,19 +222,16 @@ export async function run(profileDir) {
|
|
|
129
222
|
|
|
130
223
|
const fixable = vulns.filter(v => v.fixAvailable).length;
|
|
131
224
|
const fixHint = fixable > 0
|
|
132
|
-
? `${fixable}
|
|
225
|
+
? `${fixable} 个漏洞有可用补丁版本,可运行 ${backend.pm} audit --fix / audit fix 修复`
|
|
133
226
|
: '部分漏洞可能需要升级主版本或更换依赖';
|
|
134
227
|
|
|
135
|
-
const overallSeverity = maxSeverity(vulns.map(v =>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (v.severity === 'moderate') return Severity.MEDIUM;
|
|
139
|
-
return Severity.LOW;
|
|
140
|
-
}));
|
|
228
|
+
const overallSeverity = maxSeverity(vulns.map(v => auditSeverityToEnum(v.severity)));
|
|
229
|
+
|
|
230
|
+
const countText = typeof depCount === 'number' ? `,共扫描 ${depCount} 个依赖` : '';
|
|
141
231
|
|
|
142
232
|
return fail(id, overallSeverity,
|
|
143
|
-
`检测到 ${vulns.length} 个已知漏洞(${summary}):\n${details}\n${fixHint}`,
|
|
144
|
-
|
|
233
|
+
`检测到 ${vulns.length} 个已知漏洞(${summary}${countText},来源 ${backend.pm} audit --json):\n${details}\n${fixHint}`,
|
|
234
|
+
`运行 ${backend.pm} audit 查看详情并升级受影响依赖;优先修复 critical/high`,
|
|
145
235
|
vulns.filter(v => v.url).slice(0, 3).map(v => v.url)
|
|
146
236
|
);
|
|
147
237
|
}
|
|
@@ -151,7 +241,7 @@ export const sp1Check = {
|
|
|
151
241
|
name: 'dependency-audit',
|
|
152
242
|
severity: Severity.HIGH,
|
|
153
243
|
phase: CheckPhase.POST_INSTALL,
|
|
154
|
-
description: '依赖链已知漏洞扫描(npm audit)',
|
|
244
|
+
description: '依赖链已知漏洞扫描(pnpm/npm audit)',
|
|
155
245
|
src: 'builtin',
|
|
156
246
|
runner: (profileDir) => run(profileDir),
|
|
157
247
|
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP11: Patch-layer security-row override —— 第三方 patch 层静默改写安全配置
|
|
3
|
+
*
|
|
4
|
+
* 威胁(#587 / #4094):Cordis patch 语义是"按 id 覆盖 + 后写获胜",且**没有受保护行**。
|
|
5
|
+
* 于是一个第三方 bundle 的 cordis.patch.yml 可以在 boot 时静默改写
|
|
6
|
+
* sandbox / approval / permission 这类安全行(例如把 sandbox 模式放宽到 danger-full-access、
|
|
7
|
+
* 把 approval 策略改成 never),而用户与既有检查都看不到——SP3/SP4 只做文本模式扫描,从不看**组合结果**。
|
|
8
|
+
*
|
|
9
|
+
* 本检查离线判定:把各层 patch 里触及安全行的条目找出来,并按**层来源**分级——
|
|
10
|
+
* - 第三方 bundle / profile 的 node_modules 内 → error(用户并未主动同意)
|
|
11
|
+
* - 用户自己的 profile patch → 只作提示(用户有权自己改)
|
|
12
|
+
*
|
|
13
|
+
* Severity: CRITICAL Phase: POST_INSTALL
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { Severity } from '../protocol/severity.mjs';
|
|
19
|
+
import { CheckPhase } from '../protocol/phase.mjs';
|
|
20
|
+
import { pass, fail, skip } from '../protocol/check.mjs';
|
|
21
|
+
|
|
22
|
+
/** 安全行 id / 配置键:被改写即影响宿主安全姿态 */
|
|
23
|
+
const SECURITY_ROW_IDS = ['sandbox-policy', 'approval', 'permission'];
|
|
24
|
+
const SECURITY_KEYS = ['sandbox-policy', 'sandboxPolicy', 'approval', 'permission', 'defaultPreset', 'presets', 'policy'];
|
|
25
|
+
|
|
26
|
+
/** 收集各层 patch 文件,并标注来源层(bundle=第三方 / user=用户自有) */
|
|
27
|
+
export function collectPatchLayers(profileDir) {
|
|
28
|
+
const layers = [];
|
|
29
|
+
const userPatch = join(profileDir, 'cordis.patch.yml');
|
|
30
|
+
if (existsSync(userPatch)) layers.push({ file: userPatch, layer: 'user' });
|
|
31
|
+
|
|
32
|
+
const nmDir = join(profileDir, 'node_modules');
|
|
33
|
+
if (existsSync(nmDir)) {
|
|
34
|
+
let entries = [];
|
|
35
|
+
try { entries = readdirSync(nmDir, { withFileTypes: true }); } catch { entries = []; }
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (entry.name.startsWith('.')) continue;
|
|
38
|
+
if (entry.name.startsWith('@')) {
|
|
39
|
+
const scopeDir = join(nmDir, entry.name);
|
|
40
|
+
let pkgs = [];
|
|
41
|
+
try { pkgs = readdirSync(scopeDir, { withFileTypes: true }); } catch { continue; }
|
|
42
|
+
for (const pkg of pkgs) {
|
|
43
|
+
const f = join(scopeDir, pkg.name, 'cordis.patch.yml');
|
|
44
|
+
if (existsSync(f)) layers.push({ file: f, layer: 'bundle', pkg: `${entry.name}/${pkg.name}` });
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
const f = join(nmDir, entry.name, 'cordis.patch.yml');
|
|
48
|
+
if (existsSync(f)) layers.push({ file: f, layer: 'bundle', pkg: entry.name });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return layers;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从 patch 文本里找出触及安全行的条目。
|
|
57
|
+
* 采用保守的行扫描(不引入 YAML 依赖):命中安全 id 或安全键即记录,并带上该行上下文。
|
|
58
|
+
*/
|
|
59
|
+
export function securityTouches(text) {
|
|
60
|
+
const hits = [];
|
|
61
|
+
const lines = text.split('\n');
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i];
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed || trimmed.startsWith('#')) continue; // 注释不算
|
|
66
|
+
for (const id of SECURITY_ROW_IDS) {
|
|
67
|
+
if (new RegExp(`(^|[-\\s'"])${id}(['"\\s:]|$)`).test(trimmed)) {
|
|
68
|
+
hits.push({ line: i + 1, kind: 'row-id', value: id, text: trimmed.slice(0, 120) });
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const key of SECURITY_KEYS) {
|
|
73
|
+
if (new RegExp(`^\\s*${key}\\s*:`).test(line) || new RegExp(`['"]?${key}['"]?\\s*:`).test(trimmed)) {
|
|
74
|
+
if (!hits.some((h) => h.line === i + 1)) hits.push({ line: i + 1, kind: 'key', value: key, text: trimmed.slice(0, 120) });
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return hits;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function run(profileDir) {
|
|
83
|
+
const id = 'SP11';
|
|
84
|
+
if (!profileDir || !existsSync(profileDir)) {
|
|
85
|
+
return skip(id, Severity.CRITICAL, '无 profile 目录,跳过 patch 层安全行覆盖检测');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const layers = collectPatchLayers(profileDir);
|
|
89
|
+
if (layers.length === 0) {
|
|
90
|
+
return skip(id, Severity.CRITICAL, '未找到任何 cordis.patch.yml,无法判定 patch 层是否改写安全行');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const bundleHits = [];
|
|
94
|
+
const userHits = [];
|
|
95
|
+
for (const { file, layer, pkg } of layers) {
|
|
96
|
+
let text;
|
|
97
|
+
try { text = readFileSync(file, 'utf8'); } catch { continue; }
|
|
98
|
+
const hits = securityTouches(text);
|
|
99
|
+
if (hits.length === 0) continue;
|
|
100
|
+
const sample = hits.slice(0, 3).map((h) => `行${h.line} ${h.value}: ${h.text}`).join('; ');
|
|
101
|
+
if (layer === 'bundle') bundleHits.push(`${pkg || file}(${sample})`);
|
|
102
|
+
else userHits.push(`${file}(${hits.length} 处:${sample})`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (bundleHits.length > 0) {
|
|
106
|
+
return fail(id, Severity.CRITICAL,
|
|
107
|
+
`第三方 patch 层改写了安全配置行(sandbox / approval / permission)——patch 按 id 覆盖且后写获胜,`
|
|
108
|
+
+ `宿主没有受保护行,因此这类改写会在 boot 时静默生效(#587/#4094):\n ${bundleHits.join('\n ')}`
|
|
109
|
+
+ (userHits.length ? `\n(用户自有 patch 另有 ${userHits.length} 处,属用户自主配置)` : ''),
|
|
110
|
+
'审查上述插件的 cordis.patch.yml,确认其修改安全行是必要的;如非必要,向作者反馈或移除该插件;'
|
|
111
|
+
+ '在宿主提供"受保护行"机制前,安全相关配置应只由用户层设置',
|
|
112
|
+
['#587', '#4094']
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (userHits.length > 0) {
|
|
117
|
+
return pass(id, Severity.CRITICAL,
|
|
118
|
+
`安全行仅由用户自有 patch 触及(${userHits.length} 处),无第三方层改写:${userHits.join('; ')}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return pass(id, Severity.CRITICAL, `扫描 ${layers.length} 个 patch 层,无任何层改写 sandbox/approval/permission 安全行`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const sp11Check = {
|
|
125
|
+
id: 'SP11',
|
|
126
|
+
name: 'patch-security-override',
|
|
127
|
+
severity: Severity.CRITICAL,
|
|
128
|
+
phase: CheckPhase.POST_INSTALL,
|
|
129
|
+
description: '第三方 patch 层静默改写安全配置行(sandbox/approval/permission)——patch 无受保护行(#587/#4094)',
|
|
130
|
+
src: 'builtin',
|
|
131
|
+
runner: (profileDir) => run(profileDir),
|
|
132
|
+
};
|