When the operator binds the server to a non-loopback address
(T3MP3ST_HOST=0.0.0.0 or a LAN IP) they've explicitly opted into network
access — the same decision that already stands the anti-rebinding Host guard
down. But the CSRF origin guard and the SSE guard were NOT updated to match:
they still only trust loopback origins, so the operator's own LAN-served UI
(e.g. http://192.168.x.x:3333/ui) had every state-changing request and the
event stream rejected with "Cross-origin request rejected". That makes hunts
fail on any target from a LAN-accessed War Room (issue #84).
Fix: add isSameOriginAsHost() and, ONLY on a non-loopback bind, also trust a
request whose Origin/Referer host:port equals this server's own Host header —
i.e. a genuine same-origin request from the operator's own UI. A foreign origin
(evil.com) still mismatches the Host and is rejected, so the CSRF/drive-by
protection is preserved. On a loopback bind the branch is skipped entirely:
the strict loopback-only gate plus the Host guard remain fully in force, so
there is no new CSRF or DNS-rebinding surface for the default configuration.
Verified against a 0.0.0.0-bound instance:
- same-origin LAN request (Origin==Host, non-loopback) → passes the guard ✅
- foreign origin (evil.com) → still 403 ✅
- no Origin (curl/CLI) → allowed ✅
Typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds whatweb, wafw00f, amass, dnsx, testssl.sh, and waybackurls to the
arsenal catalog — all keyless, non-dangerous recon/OSINT tools that fill
live gaps in web fingerprinting, WAF detection, passive attack-surface
mapping, DNS validation, TLS posture, and historical-URL discovery.
Each adapter follows the CONTRIBUTING.md checklist and each gets an explicit
ARG_TEMPLATES entry in adapter-tools.ts, so:
- amass uses the required `enum -passive -d <target>` form (not a bare
positional), and dnsx passes the target via `-d` with resolve flags —
both fixing the broken default-positional invocation flagged in review.
- all six are accounted-for (templated), so they never land in the
arsenal-count-honesty guard's UNACCOUNTED set — no KNOWN_DEBT needed.
Scoped to the six adapters and their templates only. typecheck clean,
tests pass.
Addresses review feedback on the Windows detection PR.
1. Command injection (the important one). spawnAgent ran .cmd/.bat shims via a
hand-rolled `spawn('cmd.exe', ['/d','/s','/c', shim, ...args])`. cmd.exe
re-parses its tail with its own rules and ignores `\"`-escaping, so an
adversarial prompt (claude `-p <prompt>`, codex `exec <prompt>`) containing
`"` + `& | > %VAR%` broke out and executed — the BatBadBut / CVE-2024-1874
class. Verified with a `x" & echo PWNED` payload: it ran.
Fix: use Node's own safe `.cmd` handling — `spawn(shim, args, {shell:true})`.
On Node >=18.20.2 / >=20.12.2 / >=21 (CVE-2024-27980) Node escapes each arg
for cmd, so the whole prompt lands as ONE literal argument and cannot break
out (verified on Node 24: the same payload arrived intact in %1). .exe/POSIX
still spawn directly with shell:false.
2. POSIX detection regression. Detection gated on resolveBin() returning a
value; on a bash-less/busybox image where both `command -v` and `which` are
absent, an installed agent flipped to "not installed". Fix: on POSIX fall
back to the bare name (`resolveBin(bin) || (isWindows ? undefined : bin)`) —
the OS resolves PATH at exec time and the ENOENT branch still catches
genuinely-absent binaries. Windows keeps resolveBin authoritative.
Typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
detectOne() ran the `--version` probe via execFile INSIDE a `new Promise`
executor but only handled the async error path. execFile can throw
SYNCHRONOUSLY (e.g. `spawn EPERM` when child-process creation is restricted),
and a synchronous throw in a Promise executor REJECTS the promise. Because
detectLocalAgents() used Promise.all, one agent's spawn failure rejected the
whole batch → GET /api/agents/local/detect returned 500 → the Settings "Local
Agents" card silently rendered "0/0 installed" even though all CLIs were on PATH.
- detectOne: wrap execFile in try/catch. The binary was already found on PATH
(resolveBin returned a path) so it IS installed; on a sync throw we resolve
installed:true with version '?' and filesystem-based auth, never rejecting.
- detectLocalAgents: Promise.allSettled instead of Promise.all, so one agent's
failure degrades to "not installed" for that agent only, never the batch.
Verified: /detect returns 200 with claude/codex/hermes installed+ready.
Typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Detection reported installed npm-global agents (Claude Code, Codex) as
'binary not found on PATH' on Windows, and an authed Hermes desktop as
'NOT AUTHED'. Two root causes:
1. resolvePath() used POSIX-only 'command -v'/'which', and
execFile/spawn('claude', ...) cannot resolve npm's .cmd shims via
PATHEXT -> ENOENT -> installed:false. Add resolveBin() (where.exe on
Windows, preferring .exe > .cmd > .bat) and route detection + both
spawn call sites through the resolved path.
2. Hermes desktop on Windows stores its login under %LOCALAPPDATA%\hermes not ~/.hermes/. Add localAppData() and check both (presence-only).
Windows .cmd shims are launched via a new spawnAgent() helper that invokes
cmd.exe with an argv ARRAY (shell:false) instead of shell:true, so the
untrusted prompt is never re-parsed by cmd.exe -> no command-injection surface.
No new deps; typecheck clean.
Fixes all issues raised in PR #47 review:
1. Add .dockerignore to prevent:
- Real .env secrets baked into image layers
- Host node_modules (glibc) overwriting alpine (musl) → ABI mismatch
- .git, reports/, evidence/ bloating the image
2. Fix compose host-binding:
- Remove T3MP3ST_HOST=127.0.0.1 override (was breaking port forwarding)
- Server now binds to 0.0.0.0 in-container (DOCKER=true from Dockerfile)
- Host mapping stays 127.0.0.1:3333:3333 for security
3. Add critical security warning in DOCKER.md:
- Explain exposure risk of 0.0.0.0 binding + disabled Host-header guard
- Document safe vs unsafe deployment patterns
- Warn against network_mode: host and -p 3333:3333 without 127.0.0.1
4. Update base image:
- node:18-alpine → node:22-alpine (Node 18 EOL)
- Pin by digest @sha256:16e22a550... (prevent floating tag drift)
5. Fix healthcheck in compose:
- Was always returning success (process.exit(0))
- Now actually tests /api/health endpoint
- Add start_period: 40s to align with Dockerfile
6. Fix port typos in DOCKER.md:
- 3000 → 3333 in troubleshooting examples
- Update all example snippets to node:22-alpine with digest
The quickstart tells users to run `npx tempest config` to switch to the local
provider, but there is no `config` subcommand — the CLI exposes `interactive`
(default), `setup`, `status`, `test`, and `models`. The "Change default
provider" flow lives in the default `interactive` command, so the correct
invocation is `npx tempest`.
Add a first-class `gemini` provider so operators can drive missions with a
direct GEMINI_API_KEY, instead of only reaching Gemini through OpenRouter.
Gemini is wired exactly like xAI: dispatched through OpenAIAdapter against
Google's OpenAI-compatible surface. The base URL is
https://generativelanguage.googleapis.com/v1beta/openai — the `/openai`
segment is required because OpenAIAdapter posts to `${baseUrl}/chat/completions`;
without it requests hit `/v1beta/chat/completions`, which Google does not serve.
Wires the provider through the type union, adapter factory, config
(interface, defaults, model catalog, key env map, getLLMConfig, fallback
chain, redaction, .env template), and adds regression tests pinning the
`/openai` base-URL segment.
The Kali+ adapter factory only carries bespoke ARG_TEMPLATES for the network
and source/supply-chain scanners. The reverse-engineering, mobile and
smart-contract analysers catalogued by the cloud/mobile/binary-RE loadouts fall
through to DEFAULT_TEMPLATE and spawn a bare `<binary> <file>` — a broken
invocation for a subcommand/flag-driven tool, and for radare2 one that drops
into an INTERACTIVE prompt and hangs the agent loop until the timeout burns out.
Add conservative, read-only ARG_TEMPLATES (mirroring each catalog commandHint)
for objdump, readelf, checksec, r2, exiftool, myth (mythril), apkleaks, slither
and mobsfscan. File-oriented tools resolve their artifact through a new
`artifactPath` helper that refuses an absent path or an inherited http(s) URL by
throwing, which the handler turns into a clean {success:false} result (never a
bare `objdump ''`). No intrusive or code-executing flag is added — risk stays
where the catalog put it, and the existing option-looking-target / scope gates
still apply.
Add an invocation-honesty guard test (sibling of the tool-count honesty test):
every mintable adapter must be either templated or explicitly filed under one
fall-through reason — POSITIONAL_TARGET_OK, OPERATOR_DRIVEN, or KNOWN_DEBT — so a
newly catalogued subcommand tool can never silently ship a broken positional
invocation, and the remaining debt (feroxbuster, osv-scanner, hashcat, apktool,
yara) is tracked in the open rather than hidden.
All offline gate checks pass (lint typecheck test doctor verify-claims
test:no-fitting test:no-self-fitting test:gate prompt:audit smoke
test:arsenal-tools arsenal:smoke).