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
6 changes: 3 additions & 3 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 配置的报告目录",
"scope": "~/.dsh/dsh-code-review/**、默认报告目录(~ 展开后的 ~/dsh-code-review-reports/**)及用户经 report_dir 配置的报告目录(写入前校验:须为用户 home 下绝对路径,解析符号链接后确认无目录遍历逃逸)",
"reason": "落盘任务簿(tasks/ 任务文件)/ 插件运行配置(plugin-config.json)/ 评审报告留档"
},
{
"name": "process.spawn",
"scope": "插件包 bin/ 或 PATH 中的 codecheck(含用户经 engine_bin 显式配置、写入前做本地文件校验的路径);辅助命令 git / where|which / taskkill(平台子进程树终止);以及以 process.execPath 拉起的本插件后台 serve 进程",
"scope": "插件包 bin/ 或 PATH 中的 codecheck(含用户经 engine_bin 显式配置的路径——engine_bin 须为绝对路径,且写入前校验文件存在、非符号链接、非 setuid 位);辅助命令 git / where|which / taskkill(平台子进程树终止);以及以 process.execPath 拉起的本插件后台 serve 进程",
"reason": "评审引擎与后台 serve 均为外部子进程(由 runner / task-book 以子进程方式拉起执行),辅助命令服务于待提交清单(git)、引擎路径探测(where/which)与超时终止(taskkill)"
},
{
"name": "credentials.read",
"scope": "~/.dsh/.credentials.yaml 与环境变量中的模型凭据引用",
"reason": "调用评审 LLM 所需的 API key,复用宿主既有凭据通道"
"reason": "调用评审 LLM 所需的 API key,复用宿主既有凭据通道;凭据仅用于 API 调用,绝不写入日志或评审报告"
}
],
"contributes": {
Expand Down
8 changes: 8 additions & 0 deletions scripts/engine-e2e-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ try {
// 提示不再硬编码 codecheck.exe——平台化路径由
// BIN 计算得出,提示直接引用它(Linux/macOS 上出现 .exe 字面会误导)。
check(`引擎存在(${BIN})`, false, `请先把引擎二进制放到插件包 bin/(预期 ${BIN})`);
} else if (typeof key !== 'string' || key.length < 8) {
// issue #22:key 未解析(undefined)时旧代码仍构造 OCR_LLM_TOKEN: key
// (成字面量 'undefined')并 spawn 引擎空等 180s 必败——无 key 环境把
// e2e 误报成「引擎失败」。短路跳过引擎调用:每次只注入自己的 key 判定
// (与上方 check 同一口径 `typeof key === 'string' && key.length >= 8` 取反),
// 明确输出跳过原因,不再空等、不误报失败。
check('跳过引擎调用(key 缺失)', true,
'无 DEEPSEEK_API_KEY:不构造 OCR_LLM_TOKEN=undefined 的 spawn(避免空等 180s 必败),这是无 key 环境的预期跳过形态');
} else {
const env = {
...process.env,
Expand Down
16 changes: 12 additions & 4 deletions scripts/sensitive-e2e-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,20 +102,28 @@ try {
};
if (existsSync(reportDir)) walk(reportDir);
check('报告文件已落盘(按日期/仓库分目录)', candidates.length >= 1, candidates[0] ?? '无');
// envProbes 提前定义(报告与引擎两条泄露断言共用同一夹具串——
// 改夹具必须同步两处断言,防「改夹具不改断言 = 恒真假绿」)。
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));
}
// issue #22:引擎输出泄露断言**不得嵌套在报告分支内**——reviewOnce 静默失败
// 且报告未落盘时整段跳过 = 假绿。提出为独立断言:评审失败(text 空)也
// 显式失败归因(不许空输出借「不含泄露样本」恒过);空输出本身由上方
// 「评审返回非空文本」check 钉住失败,两条失败信息叠加让排查指向同一根因。
if (typeof text === 'string' && text.length > 0) {
const leakedOut = envProbes.filter((s) => text.includes(s));
check('引擎输出未含 .env 内容(送审链路也未触碰)', leakedOut.length === 0, leakedOut.join(',') || '无泄露');
} else {
check('引擎输出未含 .env 内容(送审链路也未触碰)——评审无输出', false, '评审返回空文本,无法验证送审链路(根因见上方「评审返回非空文本」失败项)');
}
} finally {
rmSync(work, { recursive: true, force: true });
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 依赖损坏/缺失时此前静默 return null——路由表为空、
// 评审 0.0s 失败且失败文案不指向原因,排查者无从下手。输出可诊断信息
// (含真实错误原因)后**仍保持 return null 契约**:运行态降级链与
// scripts/switch-model-e2e.mjs 的 check('解析出端点', !!ep) + ep?.keyEnv
// 安全链都依赖「解析失败 = 返回 null 不抛」,此分支只允许加诊断,绝不
// 能改成抛异常/改返回值。
console.error(
'[dsh-code-review] js-yaml 加载失败:评审模型配置(profile 的 cordis.patch.yml)无法解析,' +
'路由表将为空(评审会因缺少 LLM 端点而失败)。原因:' + String(error?.message ?? error),
);
return null; // 契约不变:解析失败 = 返回 null 不抛(路由表空时由 resolveProvider 返回 null,调用方明确报错)
}
// 根目录统一走 resolveDshRoot:优先 DSH_HOME,回退 ~/.dsh(issue #13)。
const profilesRoot = join(resolveDshRoot(), 'profiles');
Expand Down
25 changes: 20 additions & 5 deletions src/sensitive-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export const SENSITIVE_PATH_PATTERNS = [
// 下方 id_* 行覆盖、*.pem 由凭据扩展名行覆盖);.ssh 目录段单独无覆盖属
// 已知盲区(引擎侧路径保护另有 .ssh 排除),勿据此删除下方覆盖行。
/(^|\/)\.[^/]*key[^/]*$/i,
/(^|\/)\.[^/]*secret/i,
// issue #24:r2 与 r9 覆盖重叠归一——r2 只管「点开头的 secret 隐藏文件」(.secret/.secrets/.x.secret.yml),
// r9 只管「非点开头词段」((?!\.) 与 r2 互斥,见 r9 注释);锚定收紧为 .env 式
// (后缀段可选且必须结尾),去掉旧无锚定写法(.anythingsecretanything 乱接形态不再命中)。
/(^|\/)\.[^/]*secrets?(\.[^/]+)?$/i,
/(^|\/)\.env(\.[^/]+)?$/i, // .env / .env.production 等
// 凭据扩展名必须作为「独立扩展名」出现(my.key、cert.pfx、server.crt.bak 等):
// 敏感词前必须由点号分隔([^/]*\.),后跟行尾或再一个点(如 .key.bak 的 .bak)。
Expand All @@ -37,15 +40,21 @@ 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:凭据扩展名锚定对齐 .env 模式((\.[^/]+)?$)——扩展名后要么结尾、
// 要么跟**一个后缀段**再结尾,且整段必须到行尾(旧 (\\.|$) 无尾锚,扩展名后
// 跟随任意内容也命中,与 .env 的锚定口径不一致)。
/(^|\/)[^/]*\.(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:token(s) 显式词边界((?=[._-]|$))——tokens? 之后必须紧跟分隔符
// 或路径结尾,杜绝 tokenizer-decoder.yml 这类「token 词段后接普通字符」的形态
// 在未来正则结构调整时被误收(本次在结构上锁死「独立词段」语义)。
/(^|\/)(.*[.\-_])?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 后不是点而是下划线/横线。此处不写
Expand All @@ -57,7 +66,10 @@ export const SENSITIVE_PATH_PATTERNS = [
// secrets/credentials 命名(原 credentials 行只覆盖含字面量 credentials 的
// 基名、\.secret 要求点开头)。
/(^|\/)\.(npmrc|netrc|git-credentials)$/i,
/(^|\/)(secrets?|credentials?)(\.|\/|$)/i,
// issue #24:r9 与 r2 覆盖重叠归一——本行只管**非点开头**的 secrets/credentials
// 词段((?!\.) 负向前瞻:点开头形态由 r2 负责,两行职责互斥不重叠,
// 改一处不会留下另一处没同步的隐式覆盖)。
/(^|\/)(?!\.)(secrets?|credentials?)(\.|\/|$)/i,
// 个人/隐私信息。
/(^|\/)(.*\.)?(id_rsa|id_dsa|id_ecdsa|id_ed25519)$/i,
// 常见凭据兜底(作业本/测试台账类命名)。
Expand Down Expand Up @@ -117,7 +129,10 @@ export function buildSensitiveBlock(pendingPaths) {
// 路径未经转义直接进反引号代码块——git 文件名可含
// 反引号/换行(git 允许),会闭合代码块并向报告注入任意 markdown。拼接前
// 剔除反引号与换行(点名目的是人眼确认路径,替换为空格不影响辨识)。
const safe = String(h.path).replace(/[`\r\n*_[\]<>|]+/g, ' ');
// issue #24:清洗字符类补 ~ # &——文件名可含删除线 ~、标题/锚点 #、HTML 实体
// 起始 &、代码块反引号等,均会破坏报告 markdown 结构(文件名注入报告格式);
// 点名目的是人眼确认路径,替换为空格不影响辨识。
const safe = String(h.path).replace(/[`\r\n*_[\]<>|~#&]+/g, ' ');
return `${i + 1}. \`${safe}\``;
});
return [
Expand Down
Loading
Loading