Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions dsh-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
"permissions": [
{
"name": "fs.write",
"scope": "~/.dsh/dsh-code-review/**、默认报告目录(~ 展开后的 ~/dsh-code-review-reports/**)及用户经 report_dir 配置的报告目录",
"reason": "落盘任务簿(tasks/ 任务文件)/ 插件运行配置(plugin-config.json)/ 评审报告留档"
"scope": "~/.dsh/dsh-code-review/**、默认报告目录(~ 展开后的 ~/dsh-code-review-reports/**)及用户经 report_dir 配置的报告目录(写入前校验:~ 展开为绝对路径、解析符号链接后无目录遍历逃逸,且限于用户 home 或会话工作目录下)",
"reason": "落盘任务簿(tasks/ 任务文件)/ 插件运行配置(plugin-config.json)/ 评审报告留档;report_dir 写入前经 set-config 校验绝对路径与符号链接逃逸(issue #23)"
},
{
"name": "process.spawn",
"scope": "插件包 bin/ 或 PATH 中的 codecheck(含用户经 engine_bin 显式配置、写入前做本地文件校验的路径);辅助命令 git / where|which / taskkill(平台子进程树终止);以及以 process.execPath 拉起的本插件后台 serve 进程",
"reason": "评审引擎与后台 serve 均为外部子进程(由 runner / task-book 以子进程方式拉起执行),辅助命令服务于待提交清单(git)、引擎路径探测(where/which)与超时终止(taskkill)"
"scope": "插件包 bin/ 或 PATH 中的 codecheck(含用户经 engine_bin 显式配置——写入前校验为绝对路径且文件存在、可执行、非 setuid/setgid 位);辅助命令 git / where|which / taskkill(平台子进程树终止);以及以 process.execPath 拉起的本插件后台 serve 进程",
"reason": "评审引擎与后台 serve 均为外部子进程(由 runner / task-book 以子进程方式拉起执行),辅助命令服务于待提交清单(git)、引擎路径探测(where/which)与超时终止(taskkill);engine_bin 写入前经 set-config 校验绝对路径/存在性/可执行/setuid 位(issue #23)"
},
{
"name": "credentials.read",
"scope": "~/.dsh/.credentials.yaml 与环境变量中的模型凭据引用",
"reason": "调用评审 LLM 所需的 API key,复用宿主既有凭据通道"
"reason": "调用评审 LLM 所需的 API key,复用宿主既有凭据通道;凭据仅用于 API 调用,绝不写入日志或评审报告(issue #23)"
}
],
"contributes": {
Expand Down
15 changes: 13 additions & 2 deletions scripts/engine-e2e-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const BIN = join(PKG_ROOT, 'bin', engineBinName());
const IS_WIN = process.platform === 'win32';

let failures = 0;
// 条件跳过计数(issue #22):key 缺失等环境性缺失属「跳过」而非「失败」,
// 计入 skipped;末尾文案显式区分,避免无 key 机器误判为引擎失败。
let skipped = 0;
const check = (label, ok, detail = '') => {
console.log(`${ok ? '✔' : '✘'} ${label}${detail ? ` —— ${detail}` : ''}`);
if (!ok) failures += 1;
Expand Down Expand Up @@ -111,6 +114,14 @@ try {
// 提示不再硬编码 codecheck.exe——平台化路径由
// BIN 计算得出,提示直接引用它(Linux/macOS 上出现 .exe 字面会误导)。
check(`引擎存在(${BIN})`, false, `请先把引擎二进制放到插件包 bin/(预期 ${BIN})`);
} else if (!(typeof key === 'string' && key.length > 0)) {
// issue #22:key 解析失败(undefined)时仍构造 OCR_LLM_TOKEN: key(即成
// 字面量 'undefined')并 spawn 引擎空等 180s 必败——短路跳过引擎调用,
// check 输出明确提示。跳过属条件形态:不计 failures(skipped 计入),
// 末尾文案显式区分,杜绝「无 key 机器误报引擎失败」。
skipped += 1;
check('跳过引擎调用(key 缺失:未配置 DEEPSEEK_API_KEY)', true,
'条件跳过——配置 ~/.dsh/.credentials.yaml 后此步执行真实评审');
} else {
const env = {
...process.env,
Expand Down Expand Up @@ -157,8 +168,8 @@ try {

// 自然退出(process.exit 在 Windows libuv 下可能触发 fetch 句柄断言崩溃)。
if (failures > 0) {
console.log(`\n✘ ${failures} 项未通过`);
console.log(`\n✘ ${failures} 项未通过${skipped > 0 ? `(另有 ${skipped} 项因 key 缺失条件跳过)` : ''}`);
process.exitCode = 1;
} else {
console.log('\n✔ 引擎端到端实证通过:评审模型(用户配置端点)真实审查跑通');
console.log(`\n✔ 引擎端到端实证通过:评审模型(用户配置端点)真实审查跑通${skipped > 0 ? '(引擎调用因 key 缺失已跳过)' : ''}`);
}
17 changes: 10 additions & 7 deletions scripts/sensitive-e2e-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,24 @@ try {
};
if (existsSync(reportDir)) walk(reportDir);
check('报告文件已落盘(按日期/仓库分目录)', candidates.length >= 1, candidates[0] ?? '无');
// 内容不送审的探针串(issue #22:断言从 candidates 嵌套中提出——
// 断言值必须与上方写入 .env 的夹具**同一串**(改夹具不改断言 = 断言恒真假绿)。
const envProbes = ['EXAMPLE_PLACEHOLDER', 'EXAMPLE_NOT_A_REAL_KEY_12345'];
if (candidates.length > 0) {
reportPath = candidates[0];
const report = readFileSync(reportPath, 'utf8');
check('报告含「敏感文件点名」节', report.includes('敏感文件点名'), '命中' );
check('报告点名 .env(内容未送审的兜底人眼检查点)', report.includes('.env'), '点名项出现');
// 内容不送审:报告正文不得出现 .env 内容里的占位值。
// 断言值必须与上方写入 .env 的夹具**同一串**(改夹具不改断言 = 断言恒真假绿)。
const envProbes = ['EXAMPLE_PLACEHOLDER', 'EXAMPLE_NOT_A_REAL_KEY_12345'];
const leaked = envProbes.filter((s) => report.includes(s));
check('报告未含 .env 内容(密钥值不出现 = 内容未送审)', leaked.length === 0, leaked.join(',') || '无泄露');
// 引擎评审输出同样不得含密钥值(送审对象只有 app.py diff)。
const engineOut = text;
const leakedOut = envProbes.filter((s) => engineOut.includes(s));
check('引擎输出未含 .env 内容(送审链路也未触碰)', leakedOut.length === 0, leakedOut.join(',') || '无泄露');
}
// 引擎输出泄露断言**不嵌套在** candidates.length > 0 内(issue #22):
// reviewOnce 静默失败(报告未落盘)时此断言也必须执行——评审失败本身由
// 上方「评审返回非空文本」判据显式记录,杜绝「评审失败 → 整段跳过 → 假绿」。
// text 为空/非字符串时 includes 恒 false = 无泄露,断言仍给出可观察结论。
const engineOut = typeof text === 'string' ? text : '';
const leakedOut = envProbes.filter((s) => engineOut.includes(s));
check('引擎输出未含 .env 内容(送审链路也未触碰)', leakedOut.length === 0, leakedOut.join(',') || '无泄露');
} finally {
rmSync(work, { recursive: true, force: true });
console.log('临时仓库/报告目录已清理。');
Expand Down
14 changes: 12 additions & 2 deletions src/model-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,18 @@ function loadProfileLlmConfig() {
let yaml;
try {
yaml = require('js-yaml');
} catch {
return null; // 开发环境若 junction 未建也能继续(路由表可能为空,resolveProvider 返回 null)
} catch (error) {
// issue #21:js-yaml 依赖损坏(残损 junction / 安装缺失)此前被静默吞掉——
// 路由表为空 → 评审 0.0s 失败且失败文案只报「端点未配置」,掩盖真实原因。
// 这里输出可诊断信息(含真实错误原因),但仍保持原 return null 契约:
// 调用方(resolveProvider → 运行态降级链 / scripts/switch-model-e2e.mjs 的
// check('解析出端点', !!ep) + ep?.keyEnv ?? '' 安全链)依赖「解析失败 =
// 返回 null 不抛」,绝不能改成抛异常或改变返回值契约。
console.error(
'[dsh-code-review] loadProfileLlmConfig: 加载 js-yaml 失败,评审模型路由表将为空(解析失败返回 null,不抛异常)。真实原因:' +
String((error && error.message) || error)
);
return null;
}
// 根目录统一走 resolveDshRoot:优先 DSH_HOME,回退 ~/.dsh(issue #13)。
const profilesRoot = join(resolveDshRoot(), 'profiles');
Expand Down
40 changes: 30 additions & 10 deletions src/sensitive-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ export const SENSITIVE_PATH_PATTERNS = [
// 下方 id_* 行覆盖、*.pem 由凭据扩展名行覆盖);.ssh 目录段单独无覆盖属
// 已知盲区(引擎侧路径保护另有 .ssh 排除),勿据此删除下方覆盖行。
/(^|\/)\.[^/]*key[^/]*$/i,
/(^|\/)\.[^/]*secret/i,
// issue #24-3:重叠归一。原两条规则「点开头段内任意位置含 secret」
// (\.asecret / .secretfile / .secrets-cache.txt)与「词段 secrets/credentials
// + 分隔符/段尾」(secrets.yml / credentials.json / secrets/)在维护责任上
// 重叠、易漂移;合并为一条交替正则,语义为两者并集(两侧原有命中面全部
// 保留,单元测试「敏感命中/不命中」组逐项钉住)。
/(^|\/)(\.[^/]*secret|(secrets?|credentials?)(\.|\/|$))/i,
/(^|\/)\.env(\.[^/]+)?$/i, // .env / .env.production 等
// 凭据扩展名必须作为「独立扩展名」出现(my.key、cert.pfx、server.crt.bak 等):
// 敏感词前必须由点号分隔([^/]*\.),后跟行尾或再一个点(如 .key.bak 的 .bak)。
Expand All @@ -37,27 +42,38 @@ export const SENSITIVE_PATH_PATTERNS = [
// 敏感文件,噪声稀释真正凭据命中的信号(issue #11)。修后路径段内嵌的
// key/cert/pem(sessionkey.go、keycmd.go 等)也不再命中;id_rsa/credentials/.env
// 等由各自独立规则覆盖,不受此行影响。
/(^|\/)[^/]*\.(pem|p12|pfx|keystore|jks|cert|crt|key|kdbx|otpkey)(\.|$)/i,
// issue #24-4:凭据扩展名锚定与 .env 模式对齐——原 `(\.|$)` 允许扩展名后
// 跟任意字符(如 server.key.bak/ 目录形态、key.c 混淆名),且未锚定路径分
// 隔符;改 .env 同款 `(\.[^/]+)?$`:敏感扩展名后只允许「再一个点 + 非斜杠
// 后缀」或行尾,且必须到路径段结尾。my.key / cert.pfx / my.key.bak /
// key.pem 合法命中面不变。
/(^|\/)[^/]*\.(pem|p12|pfx|keystore|jks|cert|crt|key|kdbx|otpkey)(\.[^/]+)?$/i,
/(^|\/)credentials[^/]*$/i,
// token 模式收紧(形态:独立词段 token(s) + **凭据类扩展名或路径结尾**)——
// 原 `.*token[^/]*$` 命中路径任意位置含 "token" 的文件(src/tokenizer.js、
// token-utils/index.js 等业务文件也被点名),噪声淹没「逐个人眼确认」信号。
// 扩展名收敛到凭据类白名单(与下方 token+分隔符规则同一白名单):token.json /
// api_token / my-tokens 命中;token.js(令牌类型定义模块)、tokens.ts、
// design-tokens.md 等业务源码放出——避免把「代码文件」当凭据文件点名。
/(^|\/)(.*[.\-_])?tokens?(\.(json|yml|yaml|txt|ini|conf|cfg|config|log|bak|old|tmp|env))?$/i,
// issue #24-1:tokens? 加词边界 (?=[._-]|$)——token 词段后只允许分隔符
// (. _ -)或路径结尾。此前缺词边界时 tokenizer-decoder.yml /
// my-api-tokenizer-decoder.yml 这类业务命名存在被误收形态(token 前缀 +
// 词尾非分隔符被前缀组吞掉);词边界保证「token 是独立词段」才命中,
// 白名单扩展名语义不变(token.json / api_token / my-tokens 仍命中)。
/(^|\/)(.*[.\-_])?tokens?(?=[._-]|$)(\.(json|yml|yaml|txt|ini|conf|cfg|config|log|bak|old|tmp|env))?$/i,
// token 后跟分隔符、且带凭据类扩展名的形态(refresh_token_backup.json、
// token_store.yaml、my_token.txt)——上一行的「token 作为独立词段 + 点号
// 扩展名或路径结尾」覆盖不到它们:token 后不是点而是下划线/横线。此处不写
// `\w*` 前缀——它会让 `subtokens.json`(sub+tokens)这类业务命名也命中,
// 而词段前缀已由 `(.*[.\-_])?` 负责。
/(^|\/)(.*[.\-_])?tokens?[._-][^/]*\.(txt|yml|yaml|json|ini|conf|cfg|config|log|bak|old|tmp)$/i,
// 补充常见凭据文件假阴性——.npmrc(含 _authToken)、
// .netrc、.git-credentials,以及 secrets/、secrets.yml 等未带点前缀的
// secrets/credentials 命名(原 credentials 行只覆盖含字面量 credentials 的
// 基名、\.secret 要求点开头)。
// issue #24-1(同为 token 词段收紧):token 后跟分隔符形态补同一个词边界
// (?=[._-]|$)(与上一行同口径,两个 token 规则不漂移)——refresh_
// token_backup.json / token_store.yaml / my_token.txt 命中不变。
/(^|\/)(.*[.\-_])?tokens?(?=[._-]|$)[._-][^/]*\.(txt|yml|yaml|json|ini|conf|cfg|config|log|bak|old|tmp)$/i,
// 补充常见凭据文件假阴性——.npmrc(含 _authToken)、.netrc、.git-credentials。
// secrets/、secrets.yml 等 secrets/credentials 词段命名已并入上方「重叠归一」
// 行(issue #24-3),不再单独保留(避免两处语义漂移)。
/(^|\/)\.(npmrc|netrc|git-credentials)$/i,
/(^|\/)(secrets?|credentials?)(\.|\/|$)/i,
// 个人/隐私信息。
/(^|\/)(.*\.)?(id_rsa|id_dsa|id_ecdsa|id_ed25519)$/i,
// 常见凭据兜底(作业本/测试台账类命名)。
Expand Down Expand Up @@ -117,7 +133,11 @@ export function buildSensitiveBlock(pendingPaths) {
// 路径未经转义直接进反引号代码块——git 文件名可含
// 反引号/换行(git 允许),会闭合代码块并向报告注入任意 markdown。拼接前
// 剔除反引号与换行(点名目的是人眼确认路径,替换为空格不影响辨识)。
const safe = String(h.path).replace(/[`\r\n*_[\]<>|]+/g, ' ');
// issue #24-2:清洗类补 `~ # &`(删除线/标题/实体引用)——此前恶意文件
// 名 key~~x~~.pem(删除线注入)、config#Heading(标题注入)、a&nbsp;b
// (实体引用)可改动报告格式;git 文件名本就允许这些字符,点名是
// 人眼确认用途,替换为空格不影响辨识。
const safe = String(h.path).replace(/[`\r\n*_[\]<>|~#&]+/g, ' ');
return `${i + 1}. \`${safe}\``;
});
return [
Expand Down
Loading
Loading