# Phase 2g 逐行走读 — 脚本执行平台（新增/扩容）

**标准**: `docs/project/line-walk-audit-standard.md`  
**日期**: 2026-05-22  
**范围**: 脚本执行 Redis 层 + 后台刷盘 + 长任务 + `script_service.py` 全量重走（Phase 2b 时仅 264 行）  
**本批文件数**: 5（达单会话上限）  
**下批建议 2h**: `server/api/scripts.py`（executions 路由）、`server/api/agent.py`（script-callback）、`web/app/script-executions.html`、`web/app/scripts.html`（状态条段）、`web/agent/agent.py`（exec/wait/kill）

---

## 走读签字

| 项 | 值 |
|----|-----|
| 计划文件数 | 5 |
| 完成文件数 | 5 |
| 新增 FINDING | 6 |
| Phase 2b 复核 | `script_service.py` 自 F2b-07/09 行号已漂移，本批 **VERIFY + 增量** |

---

## 文件清单

| 文件 | 行数 N | Read 范围 | rg 安全命中 H | Closure | 状态 |
|------|--------|-----------|---------------|---------|------|
| `server/infrastructure/redis/script_execution_store.py` | 411 | 1–411 | 3 | 3 | 逐行完成 ✓ |
| `server/background/script_execution_flush.py` | 46 | 1–46 | 1 | 1 | 逐行完成 ✓ |
| `server/application/services/script_jobs.py` | 114 | 1–114 | 2 | 2 | 逐行完成 ✓ |
| `server/application/services/script_job_callback.py` | 105 | 1–105 | 0 | 0（规则零命中） | 逐行完成 ✓ |
| `server/application/services/script_service.py` | 803 | 1–803 | 12 | 12 | 逐行完成 ✓（全量重走） |

---

## Findings（仅 FINDING）

| ID | 文件:行 | 类 | 度 | 描述 | 修复建议 |
|----|---------|----|----|------|----------|
| F2g-01 | `script_jobs.py:38-58` | VULN | P1 | 用户脚本嵌入 `bash -c '…'`，仅转义 `'`；与 F2f-01 同根因（认证后远程 shell） | 文档化信任边界；可选 heredoc/base64 包装；Agent 网络隔离 |
| F2g-02 | `script_jobs.py:47-49` | RISK | P2 | 子机 `curl` 回调 URL 随 `API_BASE_URL`；若为 `http://` 则 secret 明文传输 | 强制生产 `NEXUS_API_BASE_URL` 为 HTTPS |
| F2g-03 | `script_execution_store.py:192` | BUG | P2 | `apply_update` 从 MySQL 重建 live 时 `json.loads(execution.server_ids)` 无 try | 与 102–104 行一致加 `JSONDecodeError` 处理 |
| F2g-04 | `script_service.py:477-485` | UX | P2 | `retry_execution` 调用 `execute_command` **新建** execution 记录，非续跑原 id | 文档/UI 标明新 `#id`；或改为同 id 子集重跑 |
| F2g-05 | `script_service.py:477-485` | BUG | P2 | 重试未传递原执行的 `credential_id`（执行记录未存该字段） | `script_executions` 增列或重试 API 允许传 credential_id |
| F2g-06 | `agent.py:209` + 回调链 | RISK | P2 | `POST /api/agent/script-callback` 无速率限制，可撞库 job_id（secret 32 字节仍宜限流） | Redis 计数 / IP 限速 404 |

---

## Closure 全表

### `script_execution_store.py`（H=3）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| script_execution_store.py | 117-118 | PY-01 | SAFE | — | 事件文案 f-string，无 shell |
| script_execution_store.py | 192 | PY-04 | FINDING | P2 | 见 F2g-03 |
| script_execution_store.py | 313 | PY-10 | SAFE | — | scan 前缀 `script_exec:`；非数字后缀（如 flush_queue）int 失败 continue |

其余行：JSON 序列化、Redis TTL、无 SQL/无 shell。

**输入→sink**: 无 HTTP 入口；数据来自 DB/Redis 内部。  
**八类**: BUG×1；其余无。

---

### `script_execution_flush.py`（H=1）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| script_execution_flush.py | 44-45 | PY-09 | SAFE | — | 捕获 Exception 打 log，循环继续；后台任务可接受，宜告警计数 |

**规则零命中**（除上项广义异常处理）：全文 46 行已 Read。  
**八类**: 无 FINDING。

---

### `script_jobs.py`（H=2）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| script_jobs.py | 38-58 | PY-01 | FINDING | P1 | 见 F2g-01；`job_id`/`secret` 为 `token_urlsafe` 可安全进 JSON |
| script_jobs.py | 110 | PY-08 | SAFE | — | `secrets.compare_digest` 校验回调 secret |

**输入→sink**: `user_command`（管理员）→ 远程 `bash -c`；`callback_url`（配置）→ 子机 `curl`。  
**八类**: VULN×1，RISK×1（F2g-02）。

---

### `script_job_callback.py`（H=0）

规则零命中。全文 105 行已 Read。

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| — | — | — | — | — | 无 PY/FE 规则命中；逻辑为 Redis 消费 + `apply_update` |

**输入→sink**: `job_id`/`secret`/`server_id`（子机 POST）→ `consume_script_job` → Redis/MySQL。  
**八类**: 无新增（回调鉴权在 `script_jobs.consume_script_job`）。

---

### `script_service.py`（H=12，全量 803 行）

| 文件 | 行号 | 规则 | 判定 | 严重度 | 理由 |
|------|------|------|------|--------|------|
| script_service.py | 58-69,117-206 | PY-01 | SAFE | — | 审计/事件字符串，无远程 shell |
| script_service.py | 106 | PY-01 | SAFE | — | 存库命令前缀 `[long_task]`，非包装脚本 |
| script_service.py | 302 | PY-01 | SAFE | — | `pid` 经 `int()`，kill 命令数值注入不可行 |
| script_service.py | 304-307 | PY-01 | SAFE | — | `log_path` 须匹配 `_NEXUS_JOB_LOG_RE` 且前缀 `/var/log/nexus-job/` 才进入 `shlex.quote` |
| script_service.py | 513 | PY-01 | SAFE | — | `tail -n {n}` n 有界；路径已 quote |
| script_service.py | 530 | PY-01 | SAFE | — | 异常信息进 UI 字段，非 shell |
| script_service.py | 556 | PY-01 | SAFE | — | 错误消息 f-string |
| script_service.py | 686-694 | PY-01 | RISK | — | `$DB_PASS` 替换进命令发 Agent；设计如此，凭据经加密存储；F2b-09 已修 API 不回显 |
| script_service.py | 698-723 | PY-05 | SAFE | — | `_agent_post` 无路由；经 `agent_url` + `X-API-Key` |
| script_service.py | 701-702 | RISK | — | `agent_url`：若 `API_BASE_URL` 为 https 则用 https，否则 http（与 F2b-07 缓解一致） |
| script_service.py | 477-485 | UX/BUG | FINDING | P2 | 见 F2g-04、F2g-05 |
| script_service.py | 313-394 | — | SAFE | — | `stop_execution` 仅处理 `phase==pending`；revoke job + kill；符合设计 |

**Phase 2b VERIFY**:

| 原 ID | 原行 | 现状态 |
|-------|------|--------|
| F2b-07 | ~216 `_agent_post` http | **VERIFY 缓解**：`agent_url()` 随 `API_BASE_URL` scheme；生产应 HTTPS |
| F2b-09 | ~179 凭据测试返回 command | **VERIFY 已修**：`test_credential_connection` 不返回 command（657-665） |

**输入→sink**:

| 输入 | sink |
|------|------|
| JWT 管理员 `command` / `server_ids` | Agent `/exec`、`/exec/wait`、`/exec/kill` |
| `credential_id` | `_substitute_db_vars` → 命令中的密码 |
| 执行结果 `stdout` 中 log 路径 | `tail` 经 Agent |

**入口表**: 无（由 `server/api/scripts.py` 调用，见下批 2h）。

**八类**: VULN×0 新增（F2g-01 在 script_jobs）；BUG×1；UX×1；RISK×1（Agent scheme）。

---

## 数据流（批次级）

```mermaid
sequenceDiagram
  participant UI as scripts.html
  participant API as scripts.py
  participant SVC as ScriptService
  participant Redis as script_exec:*
  participant Agent as 子机 Agent
  participant CB as script-callback

  UI->>API: POST /exec JWT
  API->>SVC: execute_command
  SVC->>Redis: init_live / apply_update
  SVC->>Agent: /exec [/wait|/kill]
  Agent-->>CB: curl job_id+secret
  CB->>Redis: apply_script_job_callback
  Note over Redis: flush 60s → MySQL
```

---

## 修复优先级（本批）

1. **F2g-03** — 小 patch，`apply_update` JSON 容错  
2. **F2g-02** — 运维：HTTPS `NEXUS_API_BASE_URL`  
3. **F2g-04/05** — 产品确认重试语义 + credential 持久化  
4. **F2g-01** — 与 F2f-01 统一策略（文档 / Agent 隔离）  
5. **F2g-06** — 回调限流（可选）

---

## 报告索引

| 批次 | 路径 |
|------|------|
| 2g | 本文件 |
| 2h（建议） | `scripts.py` + `agent.py` callback + 前端 2 页 + Agent exec |
