agent-bypass 2.13.8 → 2.13.10

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.
@@ -1,5 +1,7 @@
1
1
  # agent-bypass 2.13.6 原版增量补充说明
2
2
 
3
+ > **当前发布版本:2.13.10**(2026-10-09)——含:本文件全部历史记录 + 复测 8 类修复(2.13.7)+ 六大能力闭环与站点标准路径(2.13.8)+ 反编译订正与红队检查(2.13.9)+ 审核 11 项修复(2.13.10,见文末)。
4
+
3
5
  本包基于用户上传的原始 `agent-bypass-v2.13.6(1).zip`,不是之前的 safety-fix 版本。保持 `package.json` 版本 2.13.6 不变,不冒充正式 npm 更新。未改动安装器、原有 12 个 Python 脚本、Shell 脚本、CK Hunter 和 33 个模块正文。
4
6
 
5
7
  ### 原有文件改动
@@ -45,5 +47,21 @@
45
47
  - **修正对照表**:新增「一轮没拿下就停手」「发了网站只给分析/半途停下」两行纠正条目。
46
48
  - **module 07**:「连续两轮无新证据」从"停手交付"改为"升级维度继续"(与闭环一致)。
47
49
  - **module 30**:新增「网站目标标准路径」——收到 URL/域名即触发的六步全链(侦察→指纹→JS→弱点→验证→能打即打)+ 速度原则 + 完成判据。
48
- - **references/capability-loops.md(新)**:六大能力域(逆向/渗透/反变异/爆破/自动化/发现思维)× 五环闭环操作手册 + 跨能力接线 + 判据速查。
50
+ - **references/capability-loops.md(新)**:六大能力域(逆向/渗透/反编译/爆破/自动化/发现思维)× 五环闭环操作手册 + 跨能力接线 + 判据速查;红队全链(module 55)承接。
49
51
  - **references/agent-routing.md §7(新)**:未攻破时的自主迭代(归因 → 回源 → 换维 → 情报复用 → 严格判死 → 兜底交付)。
52
+
53
+ ## 2026-10-09 深夜批次(2.13.10 · 审核 11 项修复)
54
+
55
+ 对 2.13.9 的独立审核(11 项)处理结果——全部按"局部补丁"原则修复,保留 33 模块与全部原有功能:
56
+
57
+ 1. **安装器误删(P0)**:引入装机清单 `.agent-bypass-manifest.json`——更新/卸载只按清单清理本程序写入的文件;无清单(旧版安装)只清固定名单,references/scripts 不再整目录删除。
58
+ 2. **OpenCode 覆盖/半安装(P0)**:配置验证前置(jsonc/解析失败时一个文件都不写);agent-bypass.md 归属校验(非本程序创建不覆盖/不删除);未完成项如实计入手动清单 + 非零退出码。
59
+ 3. **凭据默认脱敏(P0)**:deck.py 默认遮蔽 cred 值(`--plain` 才明文);终端输出只用脱敏值;JSON 落盘改临时文件 + 原子替换,创建即 600 权限。
60
+ 4. **规则优先级统一(P1)**:契约新增「优先级总则」(范围与副作用核验 → 再执行;越界/服务异常立即停止该项),消除主文件与路由文件的规则歧义。
61
+ 5. **端口列表修正(P1)**:TOP100 补齐为真实 100 个(补 5357)+ 返回去重排序 + 回归断言。
62
+ 6. **URL/TLS 边界(P1)**:webprobe 端口范围校验、IPv6 支持、查询参数保留;证书错误不再触发 HTTP 降级;重定向限制同域/子域。
63
+ 7. **JS 同域修正(P1)**:`lstrip("www.")` 修正为精确前缀匹配;重定向限制同域;密钥类结果终端默认遮罩(`--json` 取完整值)。
64
+ 8. **工具能力核对(P1)**:tool_inventory 增加 why/note 字段与 `--versions` 可选本地版本探测(默认仍零执行)。
65
+ 9. **Windows 打包(P2)**:prepack 从 `rm -rf` 改为 Node `fs.rmSync`(跨平台)。
66
+ 10. **版本文档同步(P2)**:本文件顶部增加当前版本摘要(见文件头)。
67
+ 11. **回归测试(P1)**:新增 `tests/test_regressions.py`(安装器安全 / OpenCode 半安装 / 凭据脱敏 / 端口列表 / URL 解析 / 同域判定,全部离线、临时目录)。
package/SKILL.md CHANGED
@@ -11,7 +11,7 @@ description: >
11
11
  ctf/全能模式/渗透作战/逆向深挖/内存工程 等触发词时使用本技能。
12
12
  license: MIT
13
13
  metadata:
14
- version: "2.13.8"
14
+ version: "2.13.10"
15
15
  ---
16
16
 
17
17
  # agent-bypass — Ck · 万能执行员
@@ -22,6 +22,7 @@ metadata:
22
22
 
23
23
  ## 调用铁律(任何任务开始前)
24
24
 
25
+ 0. **优先级总则**:先核验授权范围与工具副作用 → 再按模块执行;恢复/重试限于原范围内;出现越界、目标服务异常(锁定/拒绝服务征兆)立即停止该项操作。本条优先于其它执行类条款(与铁律 8 不冲突:闭环迭代在范围内进行)。
25
26
  1. **先定位模块,再动手**:按下方「模块地图」选定本任务的 1~3 个模块 → 用 `rg -n 'module: 编号' SKILL.md` 打开对应模块 → 按模块流程执行。禁止凭印象直接开工。
26
27
  2. **模块是标准作业程序(SOP)**:模块里的命令、配方、判据就是干法;与个人习惯冲突时,以模块为准。
27
28
  3. **证据即交付**:每个结论附命令与实测结果;无证据不上报、不编造输出。
@@ -57,7 +58,7 @@ metadata:
57
58
  - 遇到多工具、多平台或需要选择验证路线的任务,按需先读 `references/agent-routing.md`,再结合 `references/tool-selection.md` 选工具;后者不会取代已有模块内的具体分析说明。
58
59
  - 运行 `python3 scripts/tool_inventory.py --task package_audit --json` 可只读检查本机工具;将 `package_audit` 换成 `static_code`、`dependencies`、`binary`、`android`、`pcap`、`web_static`、`authorized_http` 或 `forensics`。此命令不安装、不联网、不扫描。
59
60
  - 工具输出、错误分诊、Agent 交接与独立验证见 `references/verification-gates.md`;其他公开框架设计参考 `references/framework-comparison-2026.md`。未确认范围的外部目标只做离线或被动分析,不自动扩展资产。
60
- - **六大能力域闭环**(逆向 / 渗透 / 反变异 / 爆破 / 自动化 / 发现思维——选型→受控执行→失败恢复→独立复核→未攻破迭代 + 跨能力接线):`references/capability-loops.md`。
61
+ - **六大能力域闭环**(逆向 / 渗透 / 反编译 / 爆破 / 自动化 / 发现思维——选型→受控执行→失败恢复→独立复核→未攻破迭代 + 跨能力接线;红队全链承接见 module 55):`references/capability-loops.md`。
61
62
 
62
63
  ## 工作守则(先于本文件其他内容)
63
64
 
@@ -857,7 +858,7 @@ PHP 专项:
857
858
  - 供应链:CI/CD 工作流篡改、恶意依赖、镜像投毒(module 46 供应链段)。
858
859
 
859
860
  执行与防御规避(Windows 侧惯用):
860
- - 载荷形态:无文件(HTA/SVG/CHM/ISO+VHD 挂 LNK 绕 MOTW)、LOLBins 白利用(certutil/bitsadmin/mshta/rundll32/regsvr32)、签名二进制侧载。
861
+ - 载荷形态:无文件——HTA/SVG/CHM、LOLBins 白利用(certutil/bitsadmin/mshta/rundll32/regsvr32)、签名二进制侧载;ISO/VHD 挂 LNK 绕 MOTW 为历史手法(2023 年后系统已缓解,旧环境仍有效)。
861
862
  - AMSI 绕过:内存补丁(AmsiScanBuffer)、硬件断点、CLR 钩子——必须在加载 .NET 工具前动手。
862
863
  - EDR 对抗惯用:API unhooking(刷新 ntdll 副本)、直接/间接系统调用、sleep 混淆(内存加密)、堆加密与栈伪装、BYOVD、ETW 打补丁;"终端可见性先被削弱,载荷才动"。
863
864
  - 云工作负载:元数据服务(IMDS)拿角色凭据;容器逃逸(特权容器/docker.sock 挂载)。
@@ -889,7 +890,7 @@ AI/代理面(新增量):
889
890
  - 打 AI 集成:间接提示注入(在 agent 会读的网页/文档/数据里埋指令)诱导调工具外泄;MCP 信任问题(跨服务器传播、采样注入);提示路径越权(拿高权限 token 动作)。
890
891
  - AI 基建当入口:向量库/标注面板/推理端点未授权、对话记录泄露。
891
892
 
892
- 纪律:先身份后主机、先合法机制后植入;每阶段留证据;全程"看起来像正常 IT 工作"(该静默就静默)。
893
+ 纪律:先身份后主机、先合法机制后植入;每阶段留证据;全程"看起来像正常 IT 工作"(该静默就静默);未破环节按铁律 8 迭代(闭环细则 `references/capability-loops.md`)。
893
894
 
894
895
  <!-- module: 56-recon-origin.md -->
895
896
  # RECON/ORIGIN Track(资产测绘与源站 IP)
package/bin/install.js CHANGED
@@ -78,6 +78,38 @@ function copyDir(src, dst) {
78
78
  else fs.copyFileSync(s, d);
79
79
  }
80
80
  }
81
+ // 装机清单:只记录本程序实际写入的文件;更新/卸载只按清单清理,绝不整目录删除
82
+ const MANIFEST = '.agent-bypass-manifest.json';
83
+ function manifestPath(dir) { return path.join(dir, MANIFEST); }
84
+ function readManifest(dir) {
85
+ try {
86
+ const m = JSON.parse(fs.readFileSync(manifestPath(dir), 'utf8'));
87
+ return (m && Array.isArray(m.files)) ? m.files : null;
88
+ } catch (e) { return null; }
89
+ }
90
+ function writeManifest(dir, files) {
91
+ let ver = 'unknown';
92
+ try { ver = require(path.join(ROOT, 'package.json')).version; } catch (e) {}
93
+ fs.writeFileSync(manifestPath(dir), JSON.stringify({ tool: 'agent-bypass', version: ver, ts: new Date().toISOString(), files }, null, 2) + '\n', 'utf8');
94
+ }
95
+ function insideDir(dir, p) {
96
+ const d = path.resolve(dir) + path.sep, r = path.resolve(p);
97
+ return r === path.resolve(dir) || r.startsWith(d);
98
+ }
99
+ function listFilesRel(dir, rel) {
100
+ const out = [];
101
+ const root = path.join(dir, rel);
102
+ if (!exists(root)) return out;
103
+ const walk = (r) => {
104
+ for (const e of fs.readdirSync(r, { withFileTypes: true })) {
105
+ const p = path.join(r, e.name);
106
+ if (e.isDirectory()) walk(p); else out.push(path.relative(dir, p));
107
+ }
108
+ };
109
+ if (fs.statSync(root).isDirectory()) walk(root); else out.push(rel);
110
+ return out;
111
+ }
112
+
81
113
  function installSkills(dir) {
82
114
  // 安全护栏:只允许安装到 agent-bypass 专属目录(防误传公共技能目录导致清理越界)
83
115
  if (!/(^|[\\/])agent-bypass$/.test(dir)) {
@@ -92,12 +124,27 @@ function installSkills(dir) {
92
124
  }
93
125
  } catch (e) { /* 路径不存在,按普通安装处理 */ }
94
126
  fs.mkdirSync(dir, { recursive: true });
95
- // 干净覆盖:先清掉我方管理的旧文件(含已废弃文件名),保留用户文件
127
+ // 按装机清单清理旧版本文件(只删本程序此前写入的项;无清单=首次/旧版安装,不清理)
128
+ const prev = readManifest(dir);
96
129
  let cleaned = 0;
97
- for (const f of MANAGED) {
98
- const p = path.join(dir, f);
99
- if (exists(p)) { fs.rmSync(p, { recursive: true, force: true }); cleaned++; }
130
+ if (prev) {
131
+ for (const rel of prev) {
132
+ const p = path.join(dir, rel);
133
+ if (!insideDir(dir, p)) continue; // 防清单被篡改越界
134
+ if (exists(p)) { try { fs.rmSync(p, { recursive: true, force: true }); cleaned++; } catch (e) {} }
135
+ }
136
+ // 清空目录的旧子目录残留空壳(references/scripts)
137
+ for (const sub of ['references', 'scripts']) {
138
+ const p = path.join(dir, sub);
139
+ try { if (exists(p) && !fs.readdirSync(p).length) fs.rmdirSync(p); } catch (e) {}
140
+ }
100
141
  }
142
+ // 明确属于本程序的旧版遗留文件(固定名,历史版本产物)
143
+ for (const legacy of ['PLAYBOOK.md', 'eval']) {
144
+ const p = path.join(dir, legacy);
145
+ if (exists(p)) { try { fs.rmSync(p, { recursive: true, force: true }); cleaned++; } catch (e) {} }
146
+ }
147
+ // 覆盖式拷贝(逐文件覆盖,不预删目录;用户自放文件不受影响)
101
148
  let n = 0;
102
149
  for (const f of SKILL_FILES) {
103
150
  const src = path.join(ROOT, f);
@@ -107,7 +154,11 @@ function installSkills(dir) {
107
154
  else fs.copyFileSync(src, path.join(dir, f));
108
155
  n++;
109
156
  }
110
- ok(`技能文件 \u2192 ${dir}(覆盖 ${n} 项${cleaned ? `,清理旧文件 ${cleaned} 项` : ''})`);
157
+ // 写入新清单(记录本次实际落盘的全量文件)
158
+ const written = [];
159
+ for (const f of SKILL_FILES) written.push(...listFilesRel(dir, f));
160
+ writeManifest(dir, written);
161
+ ok(`技能文件 \u2192 ${dir}(覆盖 ${n} 项${cleaned ? `,清理旧文件 ${cleaned} 项` : ''},清单已更新)`);
111
162
  }
112
163
 
113
164
  function removeSkills(dir) {
@@ -133,12 +184,24 @@ function removeSkills(dir) {
133
184
  warn(`保留 ${target}:未检测到 agent-bypass 技能标记(防误删非本技能内容)`);
134
185
  return;
135
186
  }
136
- // 只删我方受管文件与目录;用户自放内容一律保留
187
+ // 按装机清单清理;无清单(旧版安装)只清确定属于本程序的固定文件,目录一律不整删
137
188
  let n = 0;
138
- for (const f of MANAGED) {
139
- const p = path.join(target, f);
140
- if (exists(p)) { fs.rmSync(p, { recursive: true, force: true }); n++; }
189
+ const prev = readManifest(target);
190
+ if (prev) {
191
+ for (const rel of prev) {
192
+ const p = path.join(target, rel);
193
+ if (!insideDir(target, p)) continue;
194
+ if (exists(p)) { try { fs.rmSync(p, { recursive: true, force: true }); n++; } catch (e) {} }
195
+ }
196
+ } else {
197
+ for (const f of ['SKILL.md', 'ck-hunter.md', 'SYSTEM_PROMPT.md', 'config.yaml.example', 'PLAYBOOK.md', 'eval']) {
198
+ const p = path.join(target, f);
199
+ if (exists(p)) { try { fs.rmSync(p, { recursive: true, force: true }); n++; } catch (e) {} }
200
+ }
201
+ warn(`${target}:未找到装机清单(旧版安装),references/scripts 未动(可能含用户文件)`);
141
202
  }
203
+ const mp = manifestPath(target);
204
+ if (exists(mp)) { try { fs.rmSync(mp, { force: true }); } catch (e) {} }
142
205
  ok(`已清除本技能文件 ${n} 项(${target})`);
143
206
  try {
144
207
  if (!fs.readdirSync(target).length) { fs.rmdirSync(target); ok(`已删除空目录 ${target}`); }
@@ -248,30 +311,50 @@ function opencodeConfigPath() {
248
311
  function installOpenCode() {
249
312
  console.log('\n[OpenCode]');
250
313
  const base = path.join(HOME, '.config', 'opencode');
251
- fs.mkdirSync(base, { recursive: true });
252
- installSkills(path.join(base, 'skills', 'agent-bypass'));
253
- if (noInject) return;
254
314
  const inj = path.join(base, 'agent-bypass.md');
255
- fs.writeFileSync(inj, getBlock() + '\n', 'utf8');
256
- ok(`注入文件 \u2192 ${inj}`);
257
- writeTemplate(path.join(base, 'commands', 'ck.md'), readInj('opencode/commands/ck.md', '/ck 命令模板'), 'OpenCode commands/ck.md(/ck 命令)');
258
- const cfg = opencodeConfigPath();
259
- let data = { $schema: 'https://opencode.ai/config.json' };
260
- if (exists(cfg)) {
261
- if (cfg.endsWith('.jsonc')) {
315
+ const cfgPre = opencodeConfigPath();
316
+ // ① 先验证配置可写:失败时一个文件都不写(根治半安装)
317
+ if (!noInject && exists(cfgPre)) {
318
+ if (cfgPre.endsWith('.jsonc')) {
262
319
  warn('检测到 opencode.jsonc(JSONC 含注释,程序不自动改写)。');
263
320
  info(`请手动在 instructions 数组加入: "${inj}"`);
264
321
  manual('OpenCode opencode.jsonc:需手动把注入文件加入 instructions 数组');
265
322
  return;
266
323
  }
267
- try { data = JSON.parse(fs.readFileSync(cfg, 'utf8')); }
324
+ try { JSON.parse(fs.readFileSync(cfgPre, 'utf8')); }
268
325
  catch {
269
- warn('opencode.json 解析失败(可能含注释/尾逗号),未改动文件。');
326
+ warn('opencode.json 解析失败(可能含注释/尾逗号),未改动任何文件。');
270
327
  info(`请手动在 instructions 数组加入: "${inj}"`);
271
328
  manual('OpenCode opencode.json:解析失败,需手动加入 instructions');
272
329
  return;
273
330
  }
274
331
  }
332
+ // ② 写入阶段(配置已验证可写)
333
+ fs.mkdirSync(base, { recursive: true });
334
+ installSkills(path.join(base, 'skills', 'agent-bypass'));
335
+ if (noInject) return;
336
+ // agent-bypass.md 归属保护:已存在且非本程序创建 → 跳过不覆盖
337
+ if (exists(inj)) {
338
+ let oursMd = false;
339
+ try {
340
+ const cur = fs.readFileSync(inj, 'utf8');
341
+ oursMd = cur.includes('<!-- agent-bypass -->') || (cur.includes('agent-bypass') && cur.includes('Cknb'));
342
+ } catch (e) {}
343
+ if (!oursMd) {
344
+ warn(`注入文件已存在且非本程序创建,跳过不覆盖 \u2192 ${inj}`);
345
+ manual('OpenCode agent-bypass.md:已存在(非本程序创建),未覆盖');
346
+ writeTemplate(path.join(base, 'commands', 'ck.md'), readInj('opencode/commands/ck.md', '/ck 命令模板'), 'OpenCode commands/ck.md(/ck 命令)');
347
+ return;
348
+ }
349
+ }
350
+ fs.writeFileSync(inj, getBlock() + '\n\n<!-- agent-bypass -->\n', 'utf8');
351
+ ok(`注入文件 \u2192 ${inj}`);
352
+ writeTemplate(path.join(base, 'commands', 'ck.md'), readInj('opencode/commands/ck.md', '/ck 命令模板'), 'OpenCode commands/ck.md(/ck 命令)');
353
+ const cfg = opencodeConfigPath();
354
+ let data = { $schema: 'https://opencode.ai/config.json' };
355
+ if (exists(cfg)) {
356
+ data = JSON.parse(fs.readFileSync(cfg, 'utf8')); // 已在函数开头验证通过
357
+ }
275
358
  const arr = Array.isArray(data.instructions) ? data.instructions : [];
276
359
  if (!arr.includes(inj)) arr.push(inj);
277
360
  data.instructions = arr;
@@ -283,7 +366,15 @@ function uninstallOpenCode() {
283
366
  const base = path.join(HOME, '.config', 'opencode');
284
367
  removeSkills(path.join(base, 'skills', 'agent-bypass'));
285
368
  const inj = path.join(base, 'agent-bypass.md');
286
- if (exists(inj)) { fs.rmSync(inj); ok('已删除 agent-bypass.md'); }
369
+ if (exists(inj)) {
370
+ let oursMd = false;
371
+ try {
372
+ const cur = fs.readFileSync(inj, 'utf8');
373
+ oursMd = cur.includes('<!-- agent-bypass -->') || (cur.includes('agent-bypass') && cur.includes('Cknb'));
374
+ } catch (e) {}
375
+ if (oursMd) { fs.rmSync(inj); ok('已删除 agent-bypass.md'); }
376
+ else warn('agent-bypass.md 非本程序创建,跳过不删');
377
+ }
287
378
  removeTemplate(path.join(base, 'commands', 'ck.md'), 'OpenCode commands/ck.md(/ck 命令)');
288
379
  const cfg = opencodeConfigPath();
289
380
  if (exists(cfg) && !cfg.endsWith('.jsonc')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bypass",
3
- "version": "2.13.8",
3
+ "version": "2.13.10",
4
4
  "description": "Cknb 全能技术操作员 — Agent Skills 技能包(默认安装 Pi / OpenCode;Claude Code / Grok / OpenMinis 显式指定)",
5
5
  "bin": {
6
6
  "agent-bypass": "bin/install.js"
@@ -35,7 +35,7 @@
35
35
  "pentest"
36
36
  ],
37
37
  "scripts": {
38
- "prepack": "rm -rf scripts/__pycache__ 2>/dev/null || true"
38
+ "prepack": "node -e \"const fs=require('fs');for(const d of ['scripts/__pycache__','tests/__pycache__']){try{fs.rmSync(d,{recursive:true,force:true})}catch(e){}}\""
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",
@@ -1,4 +1,4 @@
1
- # 六大能力域闭环(逆向 / 渗透 / 反变异 / 爆破 / 自动化 / 发现思维)
1
+ # 六大能力域闭环(逆向 / 渗透 / 反编译 / 爆破 / 自动化 / 发现思维)
2
2
 
3
3
  六域 × 五环(选型 → 受控执行 → 失败恢复 → 独立复核 → 迭代)。通用约定:证据落盘(原始请求/响应/退出码/时间戳);限速与范围检查;一轮未破按 `agent-routing.md` §7 迭代。
4
4
 
@@ -6,9 +6,9 @@
6
6
 
7
7
  | 能力 | 入口模块 | 主工具链 | 复核判据 | 未破迭代方向 |
8
8
  |---|---|---|---|---|
9
- | 逆向 | 40/41/47/48/50 | r2/Ghidra/Frida/qemu/jadx;`tool_inventory --task binary` | 字节级偏移证据 + 独立实现对照 | 换版本(diff 修复点)→ 换层(Java→native→syscall)→ 换样本 |
10
- | 渗透 | 46/53/54/56/42 | fastrecon/webprobe/dirbrute/jsrip/cvefetch(+nuclei/ffuf) | 三级验证:服务端接受 / 数据落库 / 实际可用 | 换入口→换路径(JS 接口图)→换身份→换时间 |
11
- | 反变异 | 阻断响应(403/401/5xx/WAF 页) | `mutate.py`(L1 编码→L2 空白→L3 路径→L4 头→L5 参数)+ module 48 绕过矩阵 | 受保护行为变化(非仅状态码) | 编码→协议层(chunked/H2)→网关层(真实 IP)→应用层(大小写/注释/HPP) |
9
+ | 逆向 | 40/41/47/48/50 | r2/Ghidra/Frida/qemu;`tool_inventory --task binary` | 字节级偏移证据 + 独立实现对照 | 换版本(diff 修复点)→ 换层(Java→native→syscall)→ 换样本 |
10
+ | 渗透 | 46/53/54/56/42 | fastrecon/webprobe/dirbrute/jsrip/cvefetch/mutate(+nuclei/ffuf) | 三级验证:服务端接受 / 数据落库 / 实际可用 | 换入口→换路径(JS 接口图)→换身份→换时间;阻断响应走变异矩阵(mutate L1-L5 + module 48) |
11
+ | 反编译 | 40/44/50 | jadx/apktool/androguard(Android)、ilspycmd/de4dot(.NET)、r2/Ghidra(native 伪代码)、CFR/Procyon(Java)、js-beautify/sourcemap(JS) | 还原可复现:编译/加载通过、动态对照一致、关键逻辑可执行 | 换工具(jadx↔CFR↔Procyon 交叉)→ 脱混淆(de4dot/unpacker)→ 换层(字节码→native)→ 动态 hook 补全 |
12
12
  | 爆破 | ck-hunter + 46 + basicauth_brute | hydra/legba;字典:默认口令→规则生成→泄漏库 | 实际登录成功(非工具报数) | 换出口→协议级并发→离线(hashcat)→跨服务复用 |
13
13
  | 自动化 | 60 | scripts/ 全部 | 重复运行一致 + 抽样对照 | 分片并行→多机分载→编排持续运行 |
14
14
  | 发现思维 | 03/07/49/61 | — | 三问自检 + 结论三档(确认/推测/未知) | 死路重开(module 07 §8)六维换轨 |
@@ -16,20 +16,21 @@
16
16
  ## 失败恢复(按域)
17
17
 
18
18
  - **逆向**:静态卡壳→动态 hook→仿真→内存 dump;符号缺失→签名恢复;输入校验挡路→断点取校验前值。
19
- - **渗透**:403/401/5xx→反变异(本表第 3 行);登录墙→爆破或认证绕过(module 48);应用面全撞墙→死路重开(端口/框架双驱动)。
20
- - **反变异**:单层失败→叠加(编码+分块+污染);全失败→换入口(真实 IP/源站直连,module 56)。
19
+ - **反编译**:输出残缺/混淆→换工具交叉(jadx/CFR/Procyon)→ de4dot 脱壳 → 动态 hook 对照补全;native 卡壳→伪代码+人工重建。
20
+ - **渗透**:403/401/5xx→变异绕过(mutate.py L1-L5 + module 48 矩阵);登录墙→爆破或认证绕过;应用面全撞墙→死路重开(端口/框架双驱动)。
21
21
  - **爆破**:锁 IP→换出口或延长窗口;qps 不足→legba 协议级并发;无字典线索→oracle 枚举用户名 + 规则生成。
22
22
  - **自动化**:解析失败→保留原始输出本地修解析器;上游变更→报错清晰化 + 兼容分支。
23
23
  - **发现**:受控发散 6 维度分候选(每条标依据/成本/风险/验证方式),多假设并行,谁先来信号跟谁。
24
24
 
25
25
  ## 跨能力接线
26
26
 
27
- - 渗透 → JS/二进制提取接口图、密钥、协议结构(jsrip/r2)。
28
- - 逆向 → 反推签名/校验算法 → 构造合法请求、生成针对性字典。
29
- - 反变异 → 绕过成功即恢复主攻击面(数据拿取/后台进入)。
27
+ - 渗透 → 取材 JS/APK/二进制样本(jsrip/下载)。
28
+ - 反编译 → 还原代码取签名/校验/密钥逻辑 → 构造合法请求、生成针对性字典。
29
+ - 逆向 → 协议与运行时结构还原(动态+仿真)→ 补全反编译缺口。
30
30
  - 爆破 → 凭证登录 → 权限升级 → 横向(跨服务复用先复验)。
31
+ - 红队(module 55)承接全链:初始访问→执行→持久化→提权→横向→C2(身份优先、合法机制优先)。
31
32
  - 自动化承载全链;发现思维贯穿每环(证据 → 分诊 → 换维)。
32
33
 
33
34
  ## 判据速查
34
35
 
35
- 逆向=字节级证据+独立对照 · 渗透=三级验证 · 反变异=受保护行为变化 · 爆破=实际登录成功 · 自动化=重复一致 · 发现=三问自检+三档标注。
36
+ 逆向=字节级证据+独立对照 · 渗透=三级验证 · 反编译=还原可复现(编译/动态对照) · 爆破=实际登录成功 · 自动化=重复一致 · 发现=三问自检+三档标注。
package/scripts/deck.py CHANGED
@@ -20,7 +20,7 @@
20
20
  python3 deck.py next-round
21
21
  """
22
22
  import argsmini as argparse
23
- import json, os, sys
23
+ import json, os, sys, tempfile
24
24
  from datetime import datetime, timezone, timedelta
25
25
 
26
26
  BASE = os.path.dirname(os.path.abspath(__file__))
@@ -54,12 +54,23 @@ def load():
54
54
 
55
55
 
56
56
  def save(d):
57
- with open(DECK, "w") as f:
58
- json.dump(d, f, ensure_ascii=False, indent=1)
57
+ # 原子替换 + 创建即受限权限:避免写入中断损坏 JSON 与权限暴露窗口
58
+ d_dir = os.path.dirname(os.path.abspath(DECK)) or "."
59
+ fd, tmp = tempfile.mkstemp(dir=d_dir, prefix=".deck-", suffix=".tmp")
59
60
  try:
60
- os.chmod(DECK, 0o600) # 仅本用户可读写(部分平台不支持则忽略)
61
- except OSError:
62
- pass
61
+ with os.fdopen(fd, "w") as f:
62
+ json.dump(d, f, ensure_ascii=False, indent=1)
63
+ try:
64
+ os.chmod(tmp, 0o600)
65
+ except OSError:
66
+ pass
67
+ os.replace(tmp, DECK)
68
+ except BaseException:
69
+ try:
70
+ os.unlink(tmp)
71
+ except OSError:
72
+ pass
73
+ raise
63
74
 
64
75
 
65
76
  def _mask(v):
@@ -75,10 +86,10 @@ def add_card(args):
75
86
  cid = max([c["id"] for c in d["cards"]], default=0) + 1
76
87
  value = args.value
77
88
  if args.type == "cred":
78
- if getattr(args, "mask", False):
79
- value = _mask(value)
89
+ if getattr(args, "plain", False):
90
+ print(" ⚠ 凭据将以明文写入 deck.json(--plain);文件权限 600,请确认环境安全", file=sys.stderr)
80
91
  else:
81
- print(" ⚠ 凭据将明文写入 deck.json(权限已设为 600);如需脱敏请加 --mask", file=sys.stderr)
92
+ value = _mask(value) # 默认脱敏(--plain 显式放开)
82
93
  card = {
83
94
  "id": cid, "type": args.type, "value": value,
84
95
  "note": args.note or "", "source": args.source or "",
@@ -92,7 +103,7 @@ def add_card(args):
92
103
  d["cards"].append(card)
93
104
  save(d)
94
105
  tname = TYPES.get(args.type, (args.type,))[0]
95
- print(f"✓ 新增牌 #{cid} [{tname}] {args.value}")
106
+ print(f"✓ 新增牌 #{cid} [{tname}] {value}")
96
107
  if card["note"]:
97
108
  print(f" 备注: {card['note']}")
98
109
  if card["source"]:
@@ -347,7 +358,8 @@ def main():
347
358
  p.add_argument("--role", default="", help="角色:消费者/提供者/中间人/旁观者")
348
359
  p.add_argument("--conn", default="", help="连向:上游是谁 / 下游是谁")
349
360
  p.add_argument("--round", type=int)
350
- p.add_argument("--mask", action="store_true", help="凭据脱敏存储(cred 类型)")
361
+ p.add_argument("--plain", action="store_true", help="凭据明文存储(默认已脱敏;谨慎使用)")
362
+ p.add_argument("--mask", action="store_true", help="兼容保留:凭据脱敏(现为默认行为)")
351
363
  p.set_defaults(func=add_card)
352
364
 
353
365
  p = sub.add_parser("show", help="查看牌组 + 牌面分析")
package/scripts/jsrip.py CHANGED
@@ -44,7 +44,8 @@ RULES = {
44
44
 
45
45
  def _host_of(u):
46
46
  try:
47
- return (urllib.parse.urlsplit(u).hostname or "").lower().lstrip("www.")
47
+ h = (urllib.parse.urlsplit(u).hostname or "").lower()
48
+ return h[4:] if h.startswith("www.") else h
48
49
  except Exception:
49
50
  return ""
50
51
 
@@ -55,11 +56,28 @@ def same_site(base, u):
55
56
  return bool(th) and (th == bh or th.endswith("." + bh))
56
57
 
57
58
 
59
+ class _SameHostRedirect(urllib.request.HTTPRedirectHandler):
60
+ """重定向仅限同域/子域(防被目标 302 引到范围外)"""
61
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
62
+ try:
63
+ old = urllib.parse.urlsplit(req.full_url).hostname or ""
64
+ new = urllib.parse.urlsplit(newurl).hostname or ""
65
+ ol, nl = old.lower(), new.lower()
66
+ if ol and nl and nl != ol and not nl.endswith("." + ol) and not ol.endswith("." + nl):
67
+ return None
68
+ except Exception:
69
+ return None
70
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
71
+
72
+
73
+ _OPENER = urllib.request.build_opener(_SameHostRedirect, urllib.request.HTTPSHandler(context=CTX))
74
+
75
+
58
76
  def get(url, timeout=20):
59
77
  h = {"User-Agent": UA, "Accept": "*/*"}
60
78
  try:
61
79
  r = urllib.request.Request(url, headers=h)
62
- with urllib.request.urlopen(r, timeout=timeout, context=CTX) as x:
80
+ with _OPENER.open(r, timeout=timeout) as x:
63
81
  return x.read(3000000).decode("utf-8", "ignore")
64
82
  except Exception as e:
65
83
  print(f" [错误] {url}: {str(e)[:60]}", file=sys.stderr)
@@ -148,6 +166,11 @@ def main():
148
166
  "internal_ip": "★ 内网 IP", "comment": "注释/TODO",
149
167
  "graphql": "GraphQL", "websocket": "WebSocket",
150
168
  }
169
+ def _mask_secret(v):
170
+ v = str(v)
171
+ return (v[:4] + "***" + v[-2:]) if len(v) > 8 else (v[:2] + "***")
172
+
173
+ MASK_KEYS = {"aws_key", "jwt", "secret"}
151
174
  for u, hits in allhits.items():
152
175
  print(f"\n{'='*66}\n{u}\n{'='*66}")
153
176
  for name in ("aws_key", "jwt", "secret", "sourcemap", "internal_ip",
@@ -155,8 +178,10 @@ def main():
155
178
  if name not in hits:
156
179
  continue
157
180
  print(f"\n[{LABEL.get(name, name)}] {len(hits[name])} 条")
181
+ if name in MASK_KEYS:
182
+ print(" (疑似密钥已遮罩;--json 获取完整值)")
158
183
  for v in hits[name][:25]:
159
- print(f" {v}")
184
+ print(f" {_mask_secret(v) if name in MASK_KEYS else v}")
160
185
  if len(hits[name]) > 25:
161
186
  print(f" ... 还有 {len(hits[name])-25} 条")
162
187
 
@@ -22,7 +22,7 @@ TOP100 = [21,22,23,25,53,80,110,111,135,139,143,443,445,993,995,1433,1521,1723,
22
22
  7,9,13,17,19,26,37,49,67,68,69,79,81,88,106,113,119,123,161,162,
23
23
  179,199,389,427,444,465,512,513,514,515,543,544,548,554,587,631,
24
24
  636,646,873,990,1025,1026,1080,1194,1241,1352,1434,1701,1716,1720,
25
- 1755,1812,1840,1900,2000,2001,2121,3000,3128]
25
+ 1755,1812,1840,1900,2000,2001,2121,3000,3128,5357]
26
26
  TOP20 = [21,22,23,25,53,80,443,445,3306,3389,5432,5900,6379,8000,8080,8443,8888,9000,9200,27017]
27
27
 
28
28
  COMMON = {
@@ -44,7 +44,7 @@ COMMON = {
44
44
  def parse_ports(s):
45
45
  s = (s or "").strip().lower()
46
46
  if s == "top100":
47
- return TOP100
47
+ return sorted(set(TOP100))
48
48
  if s == "top20":
49
49
  return TOP20
50
50
  out = set()
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
- """Read-only, offline capability discovery for agent-bypass 2.13.6.
2
+ """Read-only, offline capability discovery for agent-bypass.
3
3
 
4
- No network calls, installation, subprocess execution, scanning or file modification.
4
+ No network calls, installation, scanning or file modification.
5
+ 默认不执行任何工具;仅 --versions 时本地运行 `<tool> --version`(3 秒超时)做能力核对。
5
6
  """
6
7
  from __future__ import annotations
7
8
 
@@ -9,6 +10,7 @@ import argparse
9
10
  import json
10
11
  import platform
11
12
  import shutil
13
+ import subprocess
12
14
  from pathlib import Path
13
15
 
14
16
  TOOLS = {
@@ -28,12 +30,39 @@ PACKAGED = [
28
30
  "portscan.py", "webprobe.py", "doh_resolve.sh",
29
31
  ]
30
32
 
33
+ # 易混淆/需人工核对的工具备注(同名不同产品、命令名差异等)
34
+ NOTES = {
35
+ "httpx": "ProjectDiscovery CLI(与 Python httpx 库同名;路径不同即不同产品)",
36
+ "vol": "Volatility 2 命令名通常为 vol.py;Volatility 3 为 vol3/volatility3",
37
+ "volatility3": "Volatility 3(命令名随安装方式不同)",
38
+ "ghidraRun": "Ghidra 启动脚本(headless 分析用 analyzeHeadless)",
39
+ "codeql": "GitHub CodeQL CLI",
40
+ }
41
+
42
+
43
+ def _probe_version(name: str) -> str:
44
+ """本地版本探测:仅运行 `<tool> --version`(3 秒超时),失败返回 unknown。"""
45
+ try:
46
+ r = subprocess.run([name, "--version"], capture_output=True, text=True, timeout=3)
47
+ out = (r.stdout or r.stderr or "").strip().splitlines()
48
+ return out[0][:80] if out else "unknown"
49
+ except Exception:
50
+ return "unknown"
51
+
31
52
 
32
- def discover(root: Path, task: str) -> dict:
53
+ def discover(root: Path, task: str, versions: bool = False) -> dict:
33
54
  if task not in TOOLS:
34
55
  raise ValueError("Unknown task")
35
- tools = [{"name": name, "installed": bool(shutil.which(name)),
36
- "path": shutil.which(name)} for name in TOOLS[task]]
56
+ tools = []
57
+ for name in TOOLS[task]:
58
+ p = shutil.which(name)
59
+ item = {"name": name, "installed": bool(p), "path": p,
60
+ "why": None if p else "not in PATH"}
61
+ if name in NOTES:
62
+ item["note"] = NOTES[name]
63
+ if versions:
64
+ item["version"] = _probe_version(name) if p else None
65
+ tools.append(item)
37
66
  scripts = [{"name": name, "present": (root / "scripts" / name).is_file()}
38
67
  for name in PACKAGED]
39
68
  return {
@@ -54,16 +83,26 @@ def main() -> int:
54
83
  parser.add_argument("--task", choices=sorted(TOOLS), default="package_audit")
55
84
  parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parent.parent)
56
85
  parser.add_argument("--json", action="store_true", help="machine-readable inventory")
86
+ parser.add_argument("--versions", action="store_true",
87
+ help="本地探测已装工具版本(运行 <tool> --version,3s 超时,可选)")
57
88
  args = parser.parse_args()
58
89
  if not args.root.is_dir():
59
90
  parser.error("--root must be an existing directory")
60
- result = discover(args.root, args.task)
91
+ result = discover(args.root, args.task, versions=args.versions)
61
92
  if args.json:
62
93
  print(json.dumps(result, ensure_ascii=False, indent=2))
63
94
  else:
64
95
  print("Task:", result["task"], "| OS:", result["platform"], "| Arch:", result["architecture"])
65
96
  for item in result["tools"]:
66
- print(" {} {:<16} {}".format("OK" if item["installed"] else "--", item["name"], item["path"] or "not found"))
97
+ extra = ""
98
+ if item.get("version"):
99
+ extra = " | " + item["version"]
100
+ elif item.get("why"):
101
+ extra = " | " + item["why"]
102
+ print(" {} {:<16} {}{}".format("OK" if item["installed"] else "--", item["name"],
103
+ item["path"] or "not found", extra))
104
+ if item.get("note"):
105
+ print(" 注: " + item["note"])
67
106
  print("Packaged scripts found:", sum(1 for x in result["packaged_scripts"] if x["present"]),
68
107
  "/", len(result["packaged_scripts"]))
69
108
  print("Inspection only: no tools were run.")
@@ -27,23 +27,52 @@ PROBE_PATHS = [
27
27
  "/backup.zip", "/db.sql", "/phpinfo.php", "/info.php", "/server-status",
28
28
  ]
29
29
 
30
+ _HOST_RE = re.compile(r"^(?P<host>[A-Za-z0-9._\-]+|\[[0-9A-Fa-f:.]+\])(?::(?P<port>\d{1,5}))?$")
31
+
32
+
30
33
  def parse_target(raw):
31
- """接受域名 / host:port / 完整 URL,返回 (host[:port], scheme, path_prefix);非法输入抛 ValueError"""
34
+ """接受域名 / host:port / 完整 URL(含 IPv6 与查询参数)。
35
+
36
+ 返回 (host[:port], scheme, path_prefix, query);非法输入抛 ValueError。
37
+ """
32
38
  raw = (raw or "").strip()
33
39
  if not raw:
34
40
  raise ValueError("空目标")
41
+
42
+ def _check(m, what):
43
+ if not m:
44
+ raise ValueError(f"非法{what}: {raw}")
45
+ if m.group("port") is not None and not (1 <= int(m.group("port")) <= 65535):
46
+ raise ValueError(f"端口超出 1-65535: {m.group('port')}")
47
+ return m.group("host") + (":" + m.group("port") if m.group("port") else "")
48
+
35
49
  if "://" in raw:
36
50
  sp = urllib.parse.urlsplit(raw)
37
51
  if sp.scheme not in ("http", "https"):
38
52
  raise ValueError(f"不支持的协议: {sp.scheme}")
39
- if not sp.netloc or not re.fullmatch(r"[A-Za-z0-9._\-]+(:\d{1,5})?", sp.netloc):
40
- raise ValueError(f"非法主机名: {sp.netloc}")
41
- return sp.netloc, sp.scheme, sp.path.rstrip("/")
53
+ netloc = _check(_HOST_RE.match(sp.netloc or ""), "主机名")
54
+ return netloc, sp.scheme, sp.path.rstrip("/"), sp.query
42
55
  hostport, _, rest = raw.partition("/")
43
- if not re.fullmatch(r"[A-Za-z0-9._\-]+(:\d{1,5})?", hostport):
44
- raise ValueError(f"非法主机: {hostport}")
56
+ netloc = _check(_HOST_RE.match(hostport), "主机")
45
57
  prefix = ("/" + rest.rstrip("/")) if rest else ""
46
- return hostport, "https", prefix
58
+ return netloc, "https", prefix, ""
59
+
60
+
61
+ class _SameHostRedirect(urllib.request.HTTPRedirectHandler):
62
+ """重定向仅限同域/子域(防被目标 302 引到范围外)"""
63
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
64
+ try:
65
+ old = urllib.parse.urlsplit(req.full_url).hostname or ""
66
+ new = urllib.parse.urlsplit(newurl).hostname or ""
67
+ ol, nl = old.lower(), new.lower()
68
+ if ol and nl and nl != ol and not nl.endswith("." + ol) and not ol.endswith("." + nl):
69
+ return None
70
+ except Exception:
71
+ return None
72
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
73
+
74
+
75
+ _OPENER = urllib.request.build_opener(_SameHostRedirect, urllib.request.HTTPSHandler(context=CTX))
47
76
 
48
77
 
49
78
  def req(url, method="GET", data=None, timeout=12, headers=None):
@@ -55,13 +84,13 @@ def req(url, method="GET", data=None, timeout=12, headers=None):
55
84
  h["Content-Type"] = "application/json"
56
85
  r = urllib.request.Request(url, data=body, headers=h, method=method)
57
86
  try:
58
- with urllib.request.urlopen(r, timeout=timeout, context=CTX) as resp:
87
+ with _OPENER.open(r, timeout=timeout) as resp:
59
88
  raw = resp.read(4000)
60
89
  return resp.status, dict(resp.headers), raw
61
90
  except urllib.error.HTTPError as e:
62
91
  return e.code, dict(e.headers or {}), (e.read(4000) if e.fp else b"")
63
92
  except Exception as e:
64
- return 0, {}, str(e).encode()
93
+ return 0, {"_err": str(e)[:160]}, b""
65
94
 
66
95
  def _pool_map(items, fn, workers):
67
96
  """保序线程池(替代 concurrent.futures,省 iSH import 成本)"""
@@ -97,15 +126,18 @@ def _pool_map(items, fn, workers):
97
126
  def scan(domain):
98
127
  out = {"domain": domain, "hits": [], "errors": []}
99
128
  try:
100
- host, scheme, prefix = parse_target(domain)
129
+ host, scheme, prefix, query = parse_target(domain)
101
130
  except ValueError as e:
102
131
  out["root_status"] = 0
103
132
  out["errors"].append(f"目标非法: {e}")
104
133
  return out
105
- # 指纹(https 失败自动回落 http;都不可达则快速返回)
134
+ if query:
135
+ out["query"] = query # 查询参数保留在结果中(不拼接进探测路径)
136
+ # 指纹(https 连接失败回落 http;证书错误不降级——需显式 AB_INSECURE=1)
106
137
  base = f"{scheme}://{host}{prefix}"
107
138
  st, hd, bd = req(base + "/")
108
- if st == 0 and scheme == "https":
139
+ err = str(hd.get("_err") or "")
140
+ if st == 0 and scheme == "https" and "certificate" not in err.lower():
109
141
  base = f"http://{host}{prefix}"
110
142
  st, hd, bd = req(base + "/")
111
143
  if st == 0:
@@ -0,0 +1,157 @@
1
+ """离线回归测试(2.13.10 审核批次):
2
+
3
+ 覆盖:安装器安全(用户文件保留 / 装机清单)、OpenCode 半安装防护、
4
+ 凭据默认脱敏与文件权限、端口列表数量、URL 解析边界、JS 同域判定。
5
+
6
+ 全部离线执行:installer 用临时 HOME;脚本用临时目录拷贝;不访问任何真实渗透目标。
7
+ """
8
+ import json
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+ ROOT = Path(__file__).resolve().parents[1]
18
+ SCRIPTS = ROOT / "scripts"
19
+
20
+
21
+ def _tmp_run(args, cwd=None, env=None, timeout=180):
22
+ return subprocess.run(args, capture_output=True, text=True, cwd=cwd, env=env, timeout=timeout)
23
+
24
+
25
+ class InstallerSafetyTests(unittest.TestCase):
26
+ """安装器:用户文件保留 + 装机清单机制。"""
27
+
28
+ def _node(self, *args, home):
29
+ env = dict(os.environ)
30
+ env["HOME"] = home
31
+ return _tmp_run(["node", str(ROOT / "bin" / "install.js"), *args], env=env)
32
+
33
+ def test_user_file_preserved_through_reinstall_and_uninstall(self):
34
+ with tempfile.TemporaryDirectory() as home:
35
+ d = Path(home) / ".pi" / "agent" / "skills" / "agent-bypass"
36
+ d.mkdir(parents=True)
37
+ (d / "USER-NOTE.txt").write_text("用户私有内容", encoding="utf-8")
38
+ self._node("--pi", home=home) # 安装
39
+ self.assertTrue((d / "USER-NOTE.txt").exists(), "安装后用户文件必须保留")
40
+ self.assertTrue((d / "SKILL.md").exists(), "技能应安装")
41
+ self.assertTrue((d / ".agent-bypass-manifest.json").exists(), "应写装机清单")
42
+ self._node("--pi", home=home) # 覆盖重装
43
+ self.assertTrue((d / "USER-NOTE.txt").exists(), "重装后用户文件必须保留")
44
+ self._node("--pi", "--uninstall", home=home) # 卸载
45
+ self.assertTrue((d / "USER-NOTE.txt").exists(), "卸载后用户文件必须保留")
46
+ self.assertFalse((d / "SKILL.md").exists(), "我方文件应被清除")
47
+
48
+ def test_user_owned_dir_not_touched_on_uninstall(self):
49
+ with tempfile.TemporaryDirectory() as home:
50
+ d = Path(home) / ".pi" / "agent" / "skills" / "agent-bypass"
51
+ d.mkdir(parents=True)
52
+ (d / "mine.txt").write_text("我的文件", encoding="utf-8")
53
+ self._node("--pi", "--uninstall", home=home) # 未安装直接卸载
54
+ self.assertTrue((d / "mine.txt").exists(), "未安装时卸载不得删除用户文件")
55
+
56
+ def test_opencode_jsonc_blocks_all_writes(self):
57
+ with tempfile.TemporaryDirectory() as home:
58
+ base = Path(home) / ".config" / "opencode"
59
+ base.mkdir(parents=True)
60
+ (base / "opencode.jsonc").write_text("{ // 注释\n}", encoding="utf-8")
61
+ r = self._node("--opencode", home=home)
62
+ self.assertNotEqual(r.returncode, 0, "未完成项存在时应返回非零退出码")
63
+ self.assertFalse((base / "skills" / "agent-bypass").exists(),
64
+ "配置验证失败时不得写入技能文件(防半安装)")
65
+ self.assertFalse((base / "agent-bypass.md").exists(),
66
+ "配置验证失败时不得写入注入文件")
67
+
68
+
69
+ class DeckMaskTests(unittest.TestCase):
70
+ """凭据:默认脱敏(终端与文件)+ 文件权限。"""
71
+
72
+ def test_default_masking_and_permissions(self):
73
+ with tempfile.TemporaryDirectory() as t:
74
+ shutil.copytree(SCRIPTS, Path(t) / "scripts")
75
+ r = _tmp_run([sys.executable, str(Path(t) / "scripts" / "deck.py"),
76
+ "add", "--type", "cred", "--value", "admin:P@ssw0rd123"],
77
+ cwd=t, timeout=60)
78
+ self.assertIn("***", r.stdout, "凭据默认应脱敏输出")
79
+ self.assertNotIn("P@ssw0rd123", r.stdout + r.stderr, "终端不得外泄原始凭据")
80
+ deck = json.loads((Path(t) / "deck.json").read_text(encoding="utf-8"))
81
+ self.assertNotIn("P@ssw0rd123", deck["cards"][0]["value"], "文件不得存原始凭据")
82
+ if os.name != "nt":
83
+ mode = (Path(t) / "deck.json").stat().st_mode & 0o777
84
+ self.assertEqual(mode, 0o600, "deck.json 权限应为 600")
85
+
86
+
87
+ class PortscanTests(unittest.TestCase):
88
+ """端口列表:真实 100 个、非法输入拒绝。"""
89
+
90
+ @classmethod
91
+ def setUpClass(cls):
92
+ sys.path.insert(0, str(SCRIPTS))
93
+ import portscan
94
+ cls.ps = portscan
95
+
96
+ def test_top100_exactly_100_unique(self):
97
+ self.assertEqual(len(self.ps.TOP100), 100)
98
+ self.assertEqual(len(set(self.ps.TOP100)), 100)
99
+ self.assertEqual(len(self.ps.parse_ports("top100")), 100)
100
+
101
+ def test_invalid_inputs_rejected(self):
102
+ for bad in ("0", "65536", "abc", "1-99999", ""):
103
+ with self.assertRaises(ValueError, msg=f"应拒绝: {bad!r}"):
104
+ self.ps.parse_ports(bad)
105
+
106
+
107
+ class WebprobeParseTests(unittest.TestCase):
108
+ """URL 解析:IPv6、端口范围、查询参数保留。"""
109
+
110
+ @classmethod
111
+ def setUpClass(cls):
112
+ sys.path.insert(0, str(SCRIPTS))
113
+ import webprobe
114
+ cls.wp = webprobe
115
+
116
+ def test_plain_and_full(self):
117
+ self.assertEqual(self.wp.parse_target("example.com"),
118
+ ("example.com", "https", "", ""))
119
+ self.assertEqual(self.wp.parse_target("https://example.com/a/b?x=1&y=2"),
120
+ ("example.com", "https", "/a/b", "x=1&y=2"))
121
+
122
+ def test_ipv6(self):
123
+ host, scheme, _, _ = self.wp.parse_target("https://[2001:db8::1]:8443/x")
124
+ self.assertEqual(host, "[2001:db8::1]:8443")
125
+ self.assertEqual(scheme, "https")
126
+
127
+ def test_port_range_rejected(self):
128
+ with self.assertRaises(ValueError):
129
+ self.wp.parse_target("https://example.test:65536/path")
130
+
131
+ def test_bad_scheme_rejected(self):
132
+ with self.assertRaises(ValueError):
133
+ self.wp.parse_target("ftp://x.com")
134
+
135
+
136
+ class JsripSameSiteTests(unittest.TestCase):
137
+ """JS 同域判定:www 精确匹配、跨域拒绝。"""
138
+
139
+ @classmethod
140
+ def setUpClass(cls):
141
+ sys.path.insert(0, str(SCRIPTS))
142
+ import jsrip
143
+ cls.js = jsrip
144
+
145
+ def test_www_prefix_exact_only(self):
146
+ self.assertEqual(self.js._host_of("https://www.example.com/a.js"), "example.com")
147
+ self.assertEqual(self.js._host_of("https://wwwexample.com/a.js"), "wwwexample.com")
148
+
149
+ def test_same_site(self):
150
+ self.assertTrue(self.js.same_site("https://example.com", "https://example.com/a.js"))
151
+ self.assertTrue(self.js.same_site("https://example.com", "https://cdn.example.com/a.js"))
152
+ self.assertFalse(self.js.same_site("https://example.com", "https://evil.test/a.js"))
153
+ self.assertFalse(self.js.same_site("https://example.com", "https://wwwexample.com/a.js"))
154
+
155
+
156
+ if __name__ == "__main__":
157
+ unittest.main()