diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e05434a..61d6568c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ for full commit body + generated notes. ## Unreleased +## v0.18.3 — 2026-10-16 + +- Recheck PTY readiness after observing command completion before closing the + retained slave copies. A previously empty poll could be stale if a command + wrote and finished while the recorder was descheduled, still losing output + on macOS. A deterministic scheduling regression covers that exact window. +- v0.18.2 was published, then marked prerelease after this remaining loss was + found by native CI. It is superseded; its original tag and evidence remain. + ## v0.18.2 — 2026-10-16 v0.18.1 was tagged but its macOS recording tests blocked publication; no diff --git a/README.md b/README.md index ee34d933..628fb06e 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,15 @@ Install RedLog on **your operator machine** (the Kali box or laptop you attack from), not on a target. It records what you do; it is not an implant. Grab the installer for your platform from the -[**releases page**](https://github.com/guan4tou2/REDLOG/releases/latest) — current version **v0.18.2**. +[**releases page**](https://github.com/guan4tou2/REDLOG/releases/latest) — current version **v0.18.3**. Builds are unsigned (ad-hoc signed, not notarised), so each OS asks you to confirm once. | Platform | File | Install | |----------|------|---------| -| Kali / Debian / Ubuntu (x64) | [`redlog_0.18.2_amd64.deb`](https://github.com/guan4tou2/REDLOG/releases/download/v0.18.2/redlog_0.18.2_amd64.deb) | `sudo apt install ./redlog_*.deb` | -| Windows (x64 installer) | [`RedLog.Setup.0.18.2.exe`](https://github.com/guan4tou2/REDLOG/releases/download/v0.18.2/RedLog.Setup.0.18.2.exe) | Run it; at SmartScreen choose **More info → Run anyway** | -| macOS (Apple Silicon) | [`RedLog-0.18.2-arm64.dmg`](https://github.com/guan4tou2/REDLOG/releases/download/v0.18.2/RedLog-0.18.2-arm64.dmg) | Drag to `/Applications`, then see below | +| Kali / Debian / Ubuntu (x64) | [`redlog_0.18.3_amd64.deb`](https://github.com/guan4tou2/REDLOG/releases/download/v0.18.3/redlog_0.18.3_amd64.deb) | `sudo apt install ./redlog_*.deb` | +| Windows (x64 installer) | [`RedLog.Setup.0.18.3.exe`](https://github.com/guan4tou2/REDLOG/releases/download/v0.18.3/RedLog.Setup.0.18.3.exe) | Run it; at SmartScreen choose **More info → Run anyway** | +| macOS (Apple Silicon) | [`RedLog-0.18.3-arm64.dmg`](https://github.com/guan4tou2/REDLOG/releases/download/v0.18.3/RedLog-0.18.3-arm64.dmg) | Drag to `/Applications`, then see below | | macOS (Intel) | — | No installer; [build from source](#development) | Official downloads are installed packages: NSIS on Windows, DEB on Kali / diff --git a/docs/RELEASE-v0.18.3.md b/docs/RELEASE-v0.18.3.md new file mode 100644 index 00000000..7e6e6bbd --- /dev/null +++ b/docs/RELEASE-v0.18.3.md @@ -0,0 +1,36 @@ +# RedLog v0.18.3 + +本版以「安裝後能開始記錄,並能核對交付證據」為主。 + +v0.18.1 未發布安裝包;v0.18.2 發布後,後續 macOS CI 仍抓到一筆時序漏記,已改列預發行並標明限制。本版修復該視窗,保留前兩版原始標籤與失敗證據。 + +## 安裝 + +- Windows x64:`RedLog.Setup.0.18.3.exe`。 +- Kali/Debian/Ubuntu x64:`redlog_0.18.3_amd64.deb`,以 `sudo apt install ./redlog_0.18.3_amd64.deb` 安裝。 +- macOS Apple Silicon:`RedLog-0.18.3-arm64.dmg`,拖入 Applications。 +- 每份安裝包都列在 `SHA256SUMS.txt`。macOS ZIP 保留為配套封裝;一般安裝使用 DMG。 +- 本版起不再發布 Windows portable 與 Linux AppImage。其他 Linux 發行版可從原始碼建置。 + +## 主要改善 + +- macOS 自動 Zsh 擷取會先讀完排隊的 stdout/stderr,再關閉 PTY 寫入端,避免短指令完成太快而漏記輸出;完成訊號之後會重新檢查排隊資料,不使用較早的空輪詢結果。 + +- 擷取設定可從 Dashboard → Capture Health → Capture setup 重新進入,略過設定不會被當成已驗證。 +- 缺少工具時可準備、重試或取消。慢下載有十五分鐘上限與已等待時間;離開、重新整理或關閉 App 會清理自己的安裝程序。 +- 內建終端先載入擷取 hook,再接受輸入,修正第一道指令可能漏記。 +- 切換專案保留執行中的終端及工作,各專案使用獨立錄製片段;停止某個 pane 也停止其原始錄影。 +- 修正離線 spool 碰撞、身分歸屬與 PTY 暫時 EOF/EIO 造成的遺漏。 +- HTTP 必須收到本次受管 addon 的私有就緒回應才顯示啟動。HTTP/HTTPS 仍分別以實際請求驗證。 +- 篩選匯出固定選取事件與附件內容;證據包驗證器能區分交付投影與完整來源鏈。 + +## 驗證與限制 + +合併前的 Windows NSIS、Linux x64 DEB 與 macOS ARM 安裝副本已跑過記錄、輸出、匯出驗證及重啟流程;正式發版另以本標籤建置並檢查資產版本、完整性與更新資訊。 + +- macOS 為 ad-hoc 封裝簽章,沒有 Developer ID 或 Apple 公證;Gatekeeper 可能阻擋。Windows 可能出現 SmartScreen 提示。 +- 原生 WSL、完整人工 Kali quickstart/首次安裝計時、macOS 真正 quarantine 下載/Dock 啟動尚未完成驗收;不宣稱所有平台流程已驗證。 +- 外部 Zsh 可自動留下有界輸出;Bash/PowerShell 的普通 hook 主要記錄指令與退出資訊,輸出請使用已驗證的內建終端或對應錄製方式。 +- 來源狀態、工具安裝成功與完整流量覆蓋是不同的事;請完成 Capture setup 的指令、輸出及 HTTP/HTTPS 驗證。 + +詳細變更見 CHANGELOG,驗收邊界見 `docs/PACKAGED-SMOKE.md`。 diff --git a/hooks/redlog-relay.py b/hooks/redlog-relay.py index cc53cd39..b004eb8a 100644 --- a/hooks/redlog-relay.py +++ b/hooks/redlog-relay.py @@ -433,6 +433,11 @@ def sync_size(): except ProcessLookupError: completed = True if completed: + # The empty poll above may predate completion: the command + # can write and close while this process is descheduled. + # Observe the boundary first, THEN prove its queue is empty. + if select.select([leg["master"] for leg in legs], [], [], 0)[0]: + continue # No queued bytes remain, and the foreground command cannot # produce more. Closing our copies now cannot flush its body. # If a background writer survives, keep forwarding until EOF diff --git a/package-lock.json b/package-lock.json index 5a22881e..18a1605b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "redlog", - "version": "0.18.2", + "version": "0.18.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "redlog", - "version": "0.18.2", + "version": "0.18.3", "license": "MIT", "dependencies": { "@dagrejs/dagre": "^3.1.1", diff --git a/package.json b/package.json index 5deee442..c0432d59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redlog", - "version": "0.18.2", + "version": "0.18.3", "description": "Red Team Operation Log", "author": "guan4tou2", "repository": { diff --git a/specs/052-terminal-auto-capture/verification.md b/specs/052-terminal-auto-capture/verification.md index 349447f2..47ec2899 100644 --- a/specs/052-terminal-auto-capture/verification.md +++ b/specs/052-terminal-auto-capture/verification.md @@ -550,3 +550,18 @@ every expected body/status: total 48,698ms, mean 483ms, p95 536ms, max 604ms (including driver settle). All terminal transparency and degradation tests pass. macOS is now included in PR unit/build CI so this native regression gates merging, rather than first being observed after a release tag. + +### Follow-up: readiness must be sampled after completion + +PR #338's unchanged 100-command capture check caught one missing stderr body +on macOS after v0.18.2 publication. The preceding empty select result could +predate command completion: a descheduled recorder could resume after bytes +arrived and the shell closed. A deterministic scheduling fixture was RED +(empty output), then GREEN after rechecking queued bytes after the completion +marker. The release was marked prerelease with this limitation; this correction +prepares v0.18.3. No original trace or earlier failure is relabeled retrospectively. + +Correction validation: all five ordered local gates and production build PASS; +3,613 tests PASS / 24 explicit SKIP. All five native PTY boundary cases pass, +including the forced stale-empty-poll window; the 100-command, degradation and +terminal transparency checks remain intact and pass. diff --git a/test/relay-pty-boundary.test.ts b/test/relay-pty-boundary.test.ts index 7103e472..f4af2c85 100644 --- a/test/relay-pty-boundary.test.ts +++ b/test/relay-pty-boundary.test.ts @@ -5,6 +5,60 @@ import os from 'node:os' import path from 'node:path' describe.skipIf(process.platform === 'win32')('automatic relay PTY completion boundary', () => { + it('rechecks queued bytes after completion arrives during an empty readiness poll', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'redlog-relay-stale-poll-')) + const wrapper = ` +import importlib.util, os, sys, time +spec = importlib.util.spec_from_file_location('relay', 'hooks/redlog-relay.py') +relay = importlib.util.module_from_spec(spec) +spec.loader.exec_module(relay) +original = relay.select.select +injected = False +def select(*args): + global injected + ready = original(*args) + if not injected and not ready[0]: + injected = True + print('EMPTY_POLL', file=sys.stderr, flush=True) + deadline = time.monotonic() + 5 + while not os.path.exists(os.path.join(sys.argv[2], 'closed')): + if time.monotonic() > deadline: raise RuntimeError('completion was not supplied') + time.sleep(0.005) + return ready +relay.select.select = select +sys.exit(relay.pty_pipe(sys.argv[1:])) +` + const child = spawn('python3', ['-c', wrapper, '--dir', dir, '--err-fd', '3'], { stdio: ['ignore', 'pipe', 'pipe', 'pipe'] }) + const finished = new Promise((resolve, reject) => { child.once('error', reject); child.once('close', resolve) }) + let stdout = '', stderr = '', diagnostic = '' + child.stdout.on('data', b => { stdout += b }) + child.stderr.on('data', b => { diagnostic += b }) + child.stdio[3]?.on('data', b => { stderr += b }) + const writers: number[] = [] + try { + await expect.poll(() => fs.existsSync(path.join(dir, 'pts'))).toBe(true) + for (const name of fs.readFileSync(path.join(dir, 'pts'), 'utf8').split('\n')) writers.push(fs.openSync(name, 'w')) + fs.writeFileSync(path.join(dir, 'opened'), '') + await expect.poll(() => diagnostic).toContain('EMPTY_POLL') + // Reproduce descheduling after select saw no bytes, then completing the + // whole command before the recorder observes the completion marker. + fs.writeSync(writers[0], 'after-poll-out\n') + fs.writeSync(writers[1], 'after-poll-err\n') + for (const fd of writers.splice(0)) fs.closeSync(fd) + fs.writeFileSync(path.join(dir, 'closed'), '') + expect(await finished).toBe(0) + expect(stdout).toBe('after-poll-out\n') + expect(stderr).toBe('after-poll-err\n') + expect(JSON.parse(fs.readFileSync(path.join(dir, 'stdout.json'), 'utf8'))).toMatchObject({ text: 'after-poll-out\n', bytes: 15 }) + expect(JSON.parse(fs.readFileSync(path.join(dir, 'stderr.json'), 'utf8'))).toMatchObject({ text: 'after-poll-err\n', bytes: 15 }) + } finally { + for (const fd of writers) fs.closeSync(fd) + child.kill('SIGKILL') + await finished + fs.rmSync(dir, { recursive: true, force: true }) + } + }) + it('drains a command that closes both writers before the relay receives its handover', async () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'redlog-relay-fast-close-')) const child = spawn('python3', ['hooks/redlog-relay.py', 'pty', '--dir', dir, '--err-fd', '3'], { stdio: ['ignore', 'pipe', 'pipe', 'pipe'] })