Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 /
Expand Down
36 changes: 36 additions & 0 deletions docs/RELEASE-v0.18.3.md
Original file line number Diff line number Diff line change
@@ -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`。
5 changes: 5 additions & 0 deletions hooks/redlog-relay.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redlog",
"version": "0.18.2",
"version": "0.18.3",
"description": "Red Team Operation Log",
"author": "guan4tou2",
"repository": {
Expand Down
15 changes: 15 additions & 0 deletions specs/052-terminal-auto-capture/verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
54 changes: 54 additions & 0 deletions test/relay-pty-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number | null>((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'] })
Expand Down
Loading