diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..08bbd04 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Shell scripts must keep LF endings — a CRLF in the shebang breaks them under +# bash/WSL ("bad interpreter"). Applies to all hooks and shell helpers. +*.sh text eol=lf +*.zsh text eol=lf +*.bash text eol=lf + +# Python hooks (e.g. mitmproxy addon) also run under Unix interpreters. +*.py text eol=lf diff --git a/README.md b/README.md index fc81961..119d3b2 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ npm run build npx electron-builder --mac # or --win / --linux ``` +> **Windows & WSL:** see [docs/windows-wsl-setup.md](docs/windows-wsl-setup.md) +> for build tools, PowerShell notes, packaging, WSL mirrored networking, logging +> from WSL, and operational privacy / isolating private activity. + ## Features ### Recording Engine diff --git a/docs/windows-wsl-setup.md b/docs/windows-wsl-setup.md new file mode 100644 index 0000000..c745574 --- /dev/null +++ b/docs/windows-wsl-setup.md @@ -0,0 +1,172 @@ +# Windows & WSL Setup + +Setup, packaging, and WSL integration for running RedLog on Windows — plus how +to keep an operator's private activity out of the engagement record. + +--- + +## 1. Prerequisites + +RedLog uses the native module `better-sqlite3`, which must be compiled for +Electron's ABI (`npm run rebuild`). That needs a C/C++ toolchain. + +| Requirement | Notes | +|---|---| +| **Node.js 20 or 22 (LTS)** | **Not 24+** — newer Node has no prebuilt `better-sqlite3` binary yet, forcing a source build. | +| **Visual Studio Build Tools** | Install the **"Desktop development with C++"** workload. | +| **Python 3** | Required by `node-gyp` for the native rebuild. | + +```powershell +# Build tools (then tick "Desktop development with C++" in the installer) +winget install Microsoft.VisualStudio.2022.BuildTools + +# Node 22 LTS +winget install --id OpenJS.NodeJS.22 -e +``` + +> If you already have Node 24, remove it first (or use a version manager) so +> `node -v` reports 20.x or 22.x. + +### PowerShell notes + +- Windows PowerShell 5.1 does **not** support `&&`. Either install PowerShell 7 + (`winget install Microsoft.PowerShell`, run as `pwsh`) or chain with + `cmd1; if ($?) { cmd2 }`. +- If `npm` in PowerShell reports *"running scripts is disabled"*, allow user + scripts once: `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`. + +--- + +## 2. Build & run + +```powershell +npm install +npm run rebuild # compile better-sqlite3 for Electron's ABI +npm run dev # launch the app +npm run build # production compile (no installer) +``` + +--- + +## 3. Packaging (Windows installer) + +Installers are produced by [electron-builder](https://www.electron.build) +(config: `electron-builder.yml`; the win target builds an NSIS installer and a +portable exe). Native deps are rebuilt for the target Electron automatically +during packaging. Releases are also produced by the GitHub Actions workflow +(`.github/workflows/release.yml`) on tags. + +```powershell +npm run build # compile main/preload/renderer +npx electron-builder --win # NSIS + portable -> dist\ +``` + +**First-run note:** electron-builder downloads `winCodeSign`, which contains +macOS symlinks. Extracting symlinks on Windows needs a privilege standard users +lack. With electron-builder 26 this generally works unattended; if you hit +*"Cannot create symbolic link"*, either enable Windows **Developer Mode** +(Settings → For developers) or run the packaging command once from an elevated +terminal to populate the cache — subsequent builds work without elevation. + +Output (installer + `win-unpacked/`) lands in `dist/`. + +--- + +## 4. WSL integration + +Pentest tooling often runs in WSL. To let a WSL shell log into RedLog running on +Windows, two things must line up. + +### 4.1 Token/port location + +RedLog writes `api-token` and `api-port` to the **Windows** user profile +(`%USERPROFILE%\.redlog\`), not WSL's Linux `$HOME`. The hook scripts resolve +this automatically via `%USERPROFILE%` + `wslpath` — note the profile folder +name can differ from `%USERNAME%`, so `%USERPROFILE%` is the reliable anchor. + +### 4.2 Networking — mirrored mode is required + +The API binds `127.0.0.1` on Windows. Under the default WSL2 **NAT** networking, +WSL's `127.0.0.1` is a separate loopback and **cannot reach it**. Enable +**mirrored networking** so localhost is shared: + +1. Create `%USERPROFILE%\.wslconfig`: + ```ini + [wsl2] + networkingMode=mirrored + ``` +2. `wsl --shutdown` (from Windows PowerShell), then reopen WSL. +3. Verify: `wslinfo --networking-mode` → `mirrored`. + +To revert, remove that line and `wsl --shutdown` again. + +### 4.3 Logging from WSL + +Two helper scripts live in `hooks/`: + +```bash +# Diagnose the WSL -> RedLog link (env, token path, reachability, round-trip) +bash /mnt/c/Users//Desktop/REDLOG/hooks/wsl-redlog-test.sh + +# Send an event from any script/hook (fire-and-forget; no-ops if unreachable) +hooks/redlog-send.sh "nmap -sV $TARGET" command_start +nmap -sV "$TARGET" +hooks/redlog-send.sh "nmap -sV $TARGET" command_end "{\"exit_code\":$?}" +``` + +`redlog-send.sh` resolves the token path (native or WSL), probes a reachable host +(shared loopback under mirrored networking; the WSL2 gateway otherwise), caches +it, and silently no-ops when RedLog is not running or reachable. + +--- + +## 5. Operational privacy & isolating private activity + +RedLog is a passive recorder for an engagement. An operator also does **private** +things on the same machine (personal browsing, personal shells, credentials). +The goal: keep private activity out of the tamper-evident engagement DB. + +### 5.1 Instrument only the engagement workspace (primary control) + +Isolation is most reliable at the **source** — control *where* producers run, +not just what the UI shows. + +- **Dedicated engagement shell/distro.** Source the shell hook (or call + `redlog-send.sh`) **only** in the shell, WSL distro, VM, or OS user you use for + the engagement. Commands you run in your personal shell are never hooked. +- **Hooks fail safe.** Every hook no-ops when RedLog isn't running or the API + isn't reachable, so activity outside an active engagement session isn't logged. +- **Screenshots are deliberate.** Captures are manual / API-triggered, not a + passive desktop grabber — you choose when a screenshot (which may include + private windows) is taken. + +A clean pattern on Windows: do all engagement work inside a dedicated **WSL +distro** with the hook sourced in that distro's `~/.bashrc`, and keep personal +work on the Windows host (unhooked). + +### 5.2 Pausing — understand the current limitation + +The status-bar recording toggle sets a paused flag. **Today this only hides +events from the live timeline — it does not stop database writes.** Any producer +that POSTs to `/api/events` while "paused" is still persisted. Treat the toggle +as *hide*, not *stop*. + +For genuine isolation right now, **stop the producer** (unsource/disable the +hook, or close the engagement workspace) rather than relying on pause. + +> Planned hardening: gate persistence on the paused flag so the toggle truly +> stops capture, while still recording a pause/resume boundary marker for audit +> integrity; plus per-producer enable/disable in project config. Until then, use +> workspace isolation (§5.1) as the real control. + +### 5.3 Per-engagement isolation (built in) + +Each project is a separate directory and SQLite DB +(`~/.redlog/projects//`), so engagements never cross-contaminate. Close / +switch the project when you stop working an engagement. + +### 5.4 Scope + +Configure `scope.targets` / `excludeTargets` so out-of-scope hosts are flagged. +Combined with workspace isolation, this keeps the record focused on the +engagement. diff --git a/hooks/redlog-send.sh b/hooks/redlog-send.sh new file mode 100755 index 0000000..cc8f931 --- /dev/null +++ b/hooks/redlog-send.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# RedLog event sender — WSL-aware +# -------------------------------- +# Send a single event to the RedLog timeline from any script or hook. Works on +# native Linux/macOS and inside WSL (reaching the RedLog API on the Windows +# host). Fire-and-forget: it silently no-ops (exit 0) if RedLog isn't running +# or isn't reachable, so it never breaks the calling script. +# +# Usage: +# redlog-send.sh "" # subtype defaults to command_start +# redlog-send.sh "" command_end # different subtype +# redlog-send.sh "" command_end '{"exit_code":0}' # extra JSON merged into data{} +# AGENT_TYPE=agent redlog-send.sh "" # override agent_type (default: shell) +# +# Example — wrap a tool in a script hook: +# redlog-send.sh "nmap -sV $TARGET" command_start +# nmap -sV "$TARGET" +# redlog-send.sh "nmap -sV $TARGET" command_end "{\"exit_code\":$?}" + +set -uo pipefail + +CMD="${1:-}" +SUBTYPE="${2:-command_start}" +EXTRA="${3:-}" +AGENT_TYPE="${AGENT_TYPE:-shell}" +[[ -n "${CMD}" ]] || exit 0 + +# --- locate api-port / api-token ------------------------------------------- +# Native: $HOME/.redlog. WSL: the Windows app writes to the Windows user +# profile, so resolve %USERPROFILE% via wslpath. +_dir="" +if [[ -f "${HOME}/.redlog/api-port" ]]; then + _dir="${HOME}/.redlog" +elif grep -qi microsoft /proc/version 2>/dev/null && command -v wslpath >/dev/null 2>&1; then + _wp="$(cmd.exe /c 'echo %USERPROFILE%' 2>/dev/null | tr -d '\r')" + _wp="$(wslpath "${_wp}" 2>/dev/null || true)" + [[ -n "${_wp}" && -f "${_wp}/.redlog/api-port" ]] && _dir="${_wp}/.redlog" +fi +[[ -n "${_dir}" ]] || exit 0 + +PORT="$(tr -d '\r\n' < "${_dir}/api-port" 2>/dev/null)" +TOKEN="$(tr -d '\r\n' < "${_dir}/api-token" 2>/dev/null)" +[[ -n "${PORT}" && -n "${TOKEN}" ]] || exit 0 + +# --- resolve a reachable host (cached to avoid re-probing every call) ------- +# Under WSL2 mirrored networking / WSL1, 127.0.0.1 is shared. Under NAT, try +# the default gateway (Windows host) — reachable only if the API isn't bound to +# loopback-only; otherwise this sender simply no-ops. +_cache="${TMPDIR:-/tmp}/.redlog-host-${PORT}" +HOST="" +if [[ -f "${_cache}" ]]; then + HOST="$(cat "${_cache}" 2>/dev/null)" + curl -sf --connect-timeout 1 --max-time 2 "http://${HOST}:${PORT}/api/health" >/dev/null 2>&1 || HOST="" +fi +if [[ -z "${HOST}" ]]; then + _gw="$(ip route show default 2>/dev/null | awk '{print $3; exit}')" + for _c in 127.0.0.1 "${_gw}"; do + [[ -z "${_c}" ]] && continue + if curl -sf --connect-timeout 1 --max-time 2 "http://${_c}:${PORT}/api/health" >/dev/null 2>&1; then + HOST="${_c}" + echo "${_c}" > "${_cache}" 2>/dev/null || true + break + fi + done +fi +[[ -n "${HOST}" ]] || exit 0 + +# --- build payload ---------------------------------------------------------- +if command -v python3 >/dev/null 2>&1; then + PAYLOAD="$(CMD="${CMD}" SUBTYPE="${SUBTYPE}" EXTRA="${EXTRA}" AGENT_TYPE="${AGENT_TYPE}" python3 -c ' +import json, os +src = "native" +try: + if "microsoft" in open("/proc/version").read().lower(): + src = "wsl" +except Exception: + pass +d = {"agent_type": os.environ["AGENT_TYPE"], + "data": {"subtype": os.environ["SUBTYPE"], + "command": os.environ["CMD"], + "shell": os.path.basename(os.environ.get("SHELL", "")), + "source": src}} +ex = os.environ.get("EXTRA", "") +if ex: + try: + d["data"].update(json.loads(ex)) + except Exception: + pass +print(json.dumps(d))')" || exit 0 +else + # Minimal fallback without python3 (no EXTRA merge; basic escaping). + _esc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; } + PAYLOAD="{\"agent_type\":\"${AGENT_TYPE}\",\"data\":{\"subtype\":\"${SUBTYPE}\",\"command\":\"$(_esc "${CMD}")\",\"source\":\"native\"}}" +fi + +# --- fire-and-forget send --------------------------------------------------- +curl -sf -X POST "http://${HOST}:${PORT}/api/events" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "${PAYLOAD}" \ + --connect-timeout 1 --max-time 2 >/dev/null 2>&1 || true +exit 0 diff --git a/hooks/wsl-redlog-test.sh b/hooks/wsl-redlog-test.sh new file mode 100755 index 0000000..4866f5d --- /dev/null +++ b/hooks/wsl-redlog-test.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# RedLog × WSL hook test +# ---------------------- +# Verifies that a shell running inside WSL can reach the RedLog API on the +# Windows host and log an event — exactly the path hooks/shell-preexec-hook.sh +# and hooks/claude-code-hook.sh use. +# +# Run from inside a WSL shell: +# bash hooks/wsl-redlog-test.sh +# +# Why WSL needs special handling: +# 1. Token/port files: the Windows app writes ~/.redlog/api-{token,port} to the +# Windows user profile, not WSL's Linux $HOME. This test resolves +# %USERPROFILE% via wslpath. +# 2. Networking: the API binds 127.0.0.1 on Windows. Under WSL2 *mirrored* +# networking (or WSL1) that loopback is shared, so 127.0.0.1 works. Under +# the default WSL2 *NAT* mode it does not — enable mirrored networking. + +set -uo pipefail + +pass=0 +fail=0 +ok() { echo " [PASS] $*"; pass=$((pass + 1)); } +no() { echo " [FAIL] $*"; fail=$((fail + 1)); } +info() { echo " [info] $*"; } + +summary() { + echo + echo "Result: ${pass} passed, ${fail} failed" + if [[ "${fail}" -eq 0 ]]; then + echo "WSL → RedLog hook path: OK" + else + echo "WSL → RedLog hook path: NOT working (see notes above)" + fi + exit $(( fail > 0 ? 1 : 0 )) +} + +echo "== RedLog WSL hook test ==" + +# 1) Confirm we are in WSL -------------------------------------------------- +if grep -qi microsoft /proc/version 2>/dev/null; then + ok "running inside WSL ($(uname -r))" +else + no "not running inside WSL — run this from a WSL shell (wsl.exe bash hooks/wsl-redlog-test.sh)" + summary +fi + +# 2) Required tools --------------------------------------------------------- +for bin in curl wslpath; do + command -v "${bin}" >/dev/null 2>&1 || no "missing required tool: ${bin}" +done + +# 3) Locate the RedLog api-port / api-token --------------------------------- +REDLOG_DIR="" +if [[ -f "${HOME}/.redlog/api-port" ]]; then + REDLOG_DIR="${HOME}/.redlog" + info "using RedLog dir in Linux home: ${REDLOG_DIR}" +else + # Resolve the Windows user profile (folder name may differ from %USERNAME%). + winprofile="$(cmd.exe /c 'echo %USERPROFILE%' 2>/dev/null | tr -d '\r')" + if [[ -n "${winprofile}" ]]; then + wprof="$(wslpath "${winprofile}" 2>/dev/null || true)" + if [[ -n "${wprof}" && -f "${wprof}/.redlog/api-port" ]]; then + REDLOG_DIR="${wprof}/.redlog" + info "using RedLog dir on Windows profile: ${REDLOG_DIR}" + fi + fi +fi + +if [[ -z "${REDLOG_DIR}" ]]; then + no "cannot find .redlog/api-port" + echo + echo " RedLog must be running with a project open (that starts the API and" + echo " writes api-port/api-token). Open a project in RedLog, then re-run." + summary +fi +ok "located RedLog api files" + +PORT="$(tr -d '\r\n' < "${REDLOG_DIR}/api-port" 2>/dev/null)" +TOKEN="$(tr -d '\r\n' < "${REDLOG_DIR}/api-token" 2>/dev/null)" +[[ -n "${PORT}" ]] && ok "api-port = ${PORT}" || no "api-port is empty" +[[ -n "${TOKEN}" ]] && ok "api-token present (${#TOKEN} chars)" || no "api-token is empty" +[[ -n "${PORT}" && -n "${TOKEN}" ]] || summary + +# 4) Find a reachable host -------------------------------------------------- +# Try shared loopback first (mirrored networking / WSL1), then the WSL2 +# default gateway (the Windows host under NAT). +gw="$(ip route show default 2>/dev/null | awk '{print $3; exit}')" +HOST="" +for cand in 127.0.0.1 "${gw}"; do + [[ -z "${cand}" ]] && continue + if curl -sf --connect-timeout 1 --max-time 2 "http://${cand}:${PORT}/api/health" >/dev/null 2>&1; then + HOST="${cand}" + ok "/api/health reachable at ${cand}:${PORT}" + break + fi + info "not reachable at ${cand}:${PORT}" +done + +if [[ -z "${HOST}" ]]; then + no "RedLog API not reachable from WSL" + cat <<'EOF' + + The API binds 127.0.0.1 on Windows. Under WSL2 NAT networking (the default) + WSL's localhost is a separate loopback, so it cannot reach it. + + Fix — enable WSL2 mirrored networking: + 1. Create/edit %USERPROFILE%\.wslconfig on Windows: + [wsl2] + networkingMode=mirrored + 2. From Windows PowerShell: wsl --shutdown + 3. Reopen WSL and re-run this test. localhost is then shared with Windows. +EOF + summary +fi + +# 5) Round-trip: post an event and confirm the count increments ------------- +base="http://${HOST}:${PORT}" +before="$(curl -sf -H "Authorization: Bearer ${TOKEN}" "${base}/api/events/count" 2>/dev/null | grep -o '[0-9]\+' | head -1)" +code="$(curl -s -o /dev/null -w '%{http_code}' -X POST "${base}/api/events" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"agent_type":"shell","data":{"subtype":"command_start","command":"redlog-wsl-test echo hello","shell":"wsl-test"}}' \ + 2>/dev/null)" +[[ "${code}" == "201" ]] && ok "POST /api/events accepted (HTTP 201)" || no "POST /api/events returned HTTP ${code:-none}" + +after="$(curl -sf -H "Authorization: Bearer ${TOKEN}" "${base}/api/events/count" 2>/dev/null | grep -o '[0-9]\+' | head -1)" +if [[ -n "${before}" && -n "${after}" && "${after}" -gt "${before}" ]]; then + ok "event recorded (count ${before} → ${after})" +else + no "event count did not increase (${before:-?} → ${after:-?})" +fi + +summary diff --git a/src/main/windows.ts b/src/main/windows.ts index 493445b..f59a803 100644 --- a/src/main/windows.ts +++ b/src/main/windows.ts @@ -2,6 +2,8 @@ import { BrowserWindow, screen } from 'electron' import { join } from 'path' import { is } from '@electron-toolkit/utils' +const isMac = process.platform === 'darwin' + export function createMainWindow(savedBounds?: Electron.Rectangle): BrowserWindow { const win = new BrowserWindow({ width: savedBounds?.width ?? 1100, @@ -13,7 +15,19 @@ export function createMainWindow(savedBounds?: Electron.Rectangle): BrowserWindo show: false, icon: join(__dirname, '../../resources/icon-256.png'), backgroundColor: '#0a0a0a', - titleBarStyle: 'hiddenInset', + // macOS uses the inset traffic-light layout; Windows/Linux use a hidden + // title bar with a native window-controls overlay so min/max/close remain + // usable behind our custom drag region. + titleBarStyle: isMac ? 'hiddenInset' : 'hidden', + ...(isMac + ? {} + : { + titleBarOverlay: { + color: '#0a0a0a', + symbolColor: '#a1a1aa', + height: 40 + } + }), webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: true, diff --git a/src/preload/index.ts b/src/preload/index.ts index eec43f6..38c7812 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,6 +1,7 @@ import { contextBridge, ipcRenderer } from 'electron' contextBridge.exposeInMainWorld('redlog', { + platform: process.platform, project: { list: () => ipcRenderer.invoke('project:list'), create: (name: string, initialConfig?: unknown) => ipcRenderer.invoke('project:create', name, initialConfig), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4b7f110..7523f7f 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -23,6 +23,9 @@ type View = 'dashboard' | 'terminal' | 'timeline' | 'screenshots' | 'targets' | const VIEW_KEYS: View[] = ['dashboard', 'terminal', 'timeline', 'screenshots', 'targets', 'scope', 'loot', 'marks', 'settings'] +const isMac = window.redlog.platform === 'darwin' +const modKey = isMac ? '⌘' : 'Ctrl+' + export default function App(): JSX.Element { const [project, setProject] = useState<{ id: string; name: string } | null>(null) const [view, setView] = useState('dashboard') @@ -81,18 +84,21 @@ export default function App(): JSX.Element { className="h-10 flex items-center px-4 select-none shrink-0 border-b border-redlog-border bg-redlog-bg" style={{ WebkitAppRegion: 'drag' } as React.CSSProperties} > -
+
{t('app.title')} v{__APP_VERSION__}
{project.name} -
+
@@ -350,9 +356,9 @@ function DashboardView({ onNavigate }: { onNavigate: (v: string) => void }): JSX
{[ - ...VIEW_KEYS.map((v, i) => [`⌘${i + 1}`, t(`sidebar.${v === 'screenshots' ? 'screens' : v}`)] as [string, string]), - ['⌘⇧M', t('dashboard.addMarker')] as [string, string], - ['⌘/', t('dashboard.search')] as [string, string] + ...VIEW_KEYS.map((v, i) => [`${modKey}${i + 1}`, t(`sidebar.${v === 'screenshots' ? 'screens' : v}`)] as [string, string]), + [isMac ? '⌘⇧M' : 'Ctrl+⇧M', t('dashboard.addMarker')] as [string, string], + [isMac ? '⌘/' : 'Ctrl+/', t('dashboard.search')] as [string, string] ].map(([key, label]) => (
{key} diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index 5182e93..b585200 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -56,6 +56,7 @@ interface BrowserTabInfo { } interface RedLogAPI { + platform: string project: { list: () => Promise create: (name: string, initialConfig?: Partial) => Promise