|
#!/usr/bin/env bash |
|
# lolgai.sh — Living Off Legitimate GenAI Agents |
|
# |
|
# Discover GenAI coding CLIs (and companion binaries like cursor-tunnel) |
|
# and map them to LOLBin-style abuse surfaces. |
|
# |
|
# How --scan / --probe work: |
|
# 1. Walk an embedded catalog of known GenAI CLIs + companion bins |
|
# 2. Resolve each binary on PATH and common install / app-bundle paths |
|
# 3. Optionally run --help/--version (timeout-bounded) and classify |
|
# matched tokens into Bypass / Tunnel / MCP / Headless buckets |
|
# 4. Write markdown + JSON reports when --out is set |
|
# |
|
# Default modes are read-only. Canaries only write LOLGAI_CANARY.txt. |
|
# |
|
# Usage: |
|
# bash lolgai.sh --catalog |
|
# bash lolgai.sh --scan |
|
# bash lolgai.sh --scan --probe --out ./out |
|
# bash lolgai.sh --tool claude --canary --yes --out ./out |
|
# |
|
# Author: Mika Ayenson (@Mikaayenson) |
|
# License: MIT |
|
# Version: 0.2.0 |
|
|
|
set -euo pipefail |
|
|
|
VERSION="0.2.1" |
|
OUT_DIR="" |
|
DO_CATALOG=false |
|
DO_SCAN=false |
|
DO_PROBE=false |
|
DO_CANARY=false |
|
DO_JSON=false |
|
YES=false |
|
ALLOW_DANGEROUS=false |
|
TOOL_FILTER="" |
|
NO_COLOR=false |
|
|
|
RED="" GRN="" YLW="" BLU="" DIM="" BLD="" RST="" |
|
|
|
setup_colors() { |
|
if [[ "$NO_COLOR" == true ]] || [[ ! -t 1 ]]; then |
|
RED="" GRN="" YLW="" BLU="" DIM="" BLD="" RST="" |
|
else |
|
RED=$'\033[0;31m' GRN=$'\033[0;32m' YLW=$'\033[0;33m' |
|
BLU=$'\033[0;34m' DIM=$'\033[2m' BLD=$'\033[1m' RST=$'\033[0m' |
|
fi |
|
} |
|
|
|
die() { echo "${RED}error:${RST} $*" >&2; exit 1; } |
|
info() { echo "${BLU}[*]${RST} $*"; } |
|
ok() { echo "${GRN}[+]${RST} $*"; } |
|
warn() { echo "${YLW}[!]${RST} $*"; } |
|
dim() { echo "${DIM}$*${RST}"; } |
|
|
|
usage() { |
|
cat <<EOF |
|
lolgai.sh — Living Off Legitimate GenAI Agents (v${VERSION}) |
|
|
|
Modes: |
|
--catalog Print embedded abuse-surface catalog |
|
--scan Discover installed GenAI CLIs / companions |
|
--probe Parse --help into Bypass / Tunnel / MCP / Headless |
|
--canary Opt-in: ask one agent to write LOLGAI_CANARY.txt |
|
--tool <id|bin> Limit to one catalog id or binary name |
|
--out <dir> Write reports + per-tool help.txt under this dir |
|
--json Also print JSON summary to stdout |
|
--yes Skip canary confirmation |
|
--allow-dangerous Emphasize Bypass inventory (canaries still refuse it) |
|
--no-color Disable ANSI colors |
|
-h, --help This help |
|
--version Print version |
|
|
|
Examples: |
|
bash lolgai.sh --catalog |
|
bash lolgai.sh --scan --probe --out ./lolgai-out |
|
bash lolgai.sh --tool cursor-tunnel --scan --probe |
|
bash lolgai.sh --tool claude --canary --yes --out ./lolgai-out |
|
EOF |
|
} |
|
|
|
# ── catalog ────────────────────────────────────────────────────────── |
|
# id|bins|vendor|functions|bypass_flags|proxy_notes|canary_cmd|refs |
|
# |
|
# functions include: Execute,Bypass,Proxy,CredAccess,Persist,Exfil, |
|
# SandboxEscape,Tunnel,Remote |
|
|
|
CATALOG_TSV=$(cat <<'EOF' |
|
claude_code|claude|Anthropic|Execute,Bypass,Proxy,CredAccess,Persist,Exfil,SandboxEscape,Remote|--dangerously-skip-permissions,--permission-mode,--allow-dangerously-skip-permissions|--print headless; --remote-control; --background agents; --chrome; --add-dir expands trust|claude -p "{PROMPT}" --output-format text|https://docs.anthropic.com/en/docs/claude-code |
|
codex|codex|OpenAI|Execute,Bypass,Proxy,CredAccess,Persist,Exfil|--full-auto,--dangerously-bypass-approvals-and-sandbox,--sandbox|Non-interactive exec; may pivot certutil/bitsadmin when downloads blocked|codex exec "{PROMPT}"|https://github.com/openai/codex |
|
antigravity_cli|agy|Google|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Terminal agent (Gemini CLI successor); shared harness with Antigravity IDE|agy -p "{PROMPT}"|https://antigravity.google/docs/cli/overview |
|
gemini_cli|gemini|Google|Execute,Bypass,Proxy,CredAccess,Persist,Exfil|--yolo,--approval-mode|Headless -p; YOLO auto-approves all tools; MCP subcommand|gemini -p "{PROMPT}"|https://github.com/google-gemini/gemini-cli |
|
cursor_agent|cursor-agent,agent|Anysphere|Execute,Bypass,Proxy,CredAccess,Persist,Exfil|--force,--yolo,--trust,--approve-mcps|--print headless with full tools; --cloud; companion tunnels below|cursor-agent -p "{PROMPT}" --print|https://cursor.com/docs |
|
cursor_tunnel|cursor-tunnel|Anysphere|Tunnel,Proxy,Persist,Exfil| |Microsoft Dev Tunnels LOTL — signed companion under Cursor.app; remote access / sandbox breakout research (Straiker NomShub)| |https://cursor.com/docs |
|
code_tunnel|code-tunnel|Microsoft/Anysphere|Tunnel,Proxy,Persist,Exfil| |VS Code remote tunnel binary bundled with Cursor; same Dev Tunnels protocol| |https://code.visualstudio.com/docs/remote/tunnels |
|
cline_cli|cline|Cline|Execute,Bypass,Proxy,CredAccess,Persist,Exfil|--yolo,--auto-approve|Standalone CLI (npm: cline); headless/CI; MCP + hub/kanban|cline -y "{PROMPT}"|https://docs.cline.bot/cli/cli-reference |
|
opencode|opencode|anomalyco|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Model-agnostic TUI (75+ providers); distinct from Charm Crush|opencode run "{PROMPT}"|https://opencode.ai/docs/ |
|
pi|pi|Earendil|Execute,Bypass,Proxy,CredAccess,Exfil| |Minimal coding-agent harness; npm @earendil-works/pi-coding-agent|pi "{PROMPT}"|https://github.com/earendil-works/pi |
|
plandex|plandex,pdx|Plandex|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Multi-step / large-task CLI; WSL on Windows|plandex|https://docs.plandex.ai/ |
|
copilot_cli|copilot|GitHub|Execute,Bypass,Proxy,CredAccess,Persist,Exfil|--allow-all,--yolo,--allow-all-tools,--allow-all-paths,--allow-all-urls|GitHub MCP tools; --acp server mode|copilot -p "{PROMPT}"|https://docs.github.com/en/copilot |
|
aider|aider|OSS|Execute,Bypass,Proxy,CredAccess,Exfil|--yes,--auto-commits|/run shell; CONVENTIONS.md injection|aider --yes --message "{PROMPT}"|https://aider.chat |
|
amp|amp|Sourcegraph|Execute,Bypass,Proxy,CredAccess,Persist,Exfil|--dangerously-allow-all|Agentic coding CLI|amp -m "{PROMPT}"| |
|
goose|goose|Block/AAIF|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Recipe / non-interactive runs|goose run -t "{PROMPT}"|https://block.github.io/goose |
|
crush|crush|Charmbracelet|Execute,Bypass,Proxy,CredAccess,Exfil| |TUI agent| | |
|
continue_cli|cn|Continue|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |MCP-capable cn binary| | |
|
factory_droid|droid|Factory.ai|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Agentic CLI| | |
|
kiro_cli|kiro-cli,q|AWS|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Amazon Q / Kiro lineage| | |
|
qwen_code|qwen|Alibaba|Execute,Bypass,Proxy,CredAccess,Exfil| |Terminal coding agent| | |
|
openhands|openhands|All Hands|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Docker/host.exec surfaces| | |
|
jules|jules|Google|Execute,Bypass,Proxy,CredAccess,Exfil| |Autonomous coding CLI| | |
|
gitlab_duo|duo|GitLab|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Duo agentic CLI| | |
|
codebuff|codebuff|Codebuff|Execute,Bypass,Proxy,CredAccess,Exfil| |Terminal coding agent| | |
|
kilo|kilo|Kilo|Execute,Bypass,Proxy,CredAccess,Exfil| |Kilo Code CLI| | |
|
pochi|pochi|TabbyML|Execute,Bypass,Proxy,CredAccess,Exfil| |Pochi agent CLI| | |
|
qodo|qodo|Qodo|Execute,Bypass,Proxy,CredAccess,Exfil| |Qodo Command CLI| | |
|
augment|auggie|Augment|Execute,Bypass,Proxy,CredAccess,Exfil| |Augment Code CLI| | |
|
hermes|hermes|Nous|Execute,Bypass,Proxy,CredAccess,Persist,Exfil| |Hermes agent platform| | |
|
genaiscript|genaiscript|Microsoft|Execute,Proxy,CredAccess,Exfil| |host.exec() outside Docker| | |
|
llm_cli|llm|Simon Willison|Execute,Proxy,Exfil| |Plugin host| | |
|
openai_cli|openai|OpenAI|Proxy,Exfil| |API console (lower agent surface)| | |
|
EOF |
|
) |
|
|
|
# Classified token patterns (probe buckets) |
|
BYPASS_REGEX='dangerously|skip-permission|skip.permission|yolo|auto-approve|auto.approve|full-auto|bypass.approval|bypass-approval|allow-all|no-sandbox|workspace-write|permission-mode|trust-all|--force[^-]|--trust[^-]|approve-mcp' |
|
TUNNEL_REGEX='tunnel|remote-control|remote.control|dev.?tunnel|--cloud[^-]|port.forward|port-forward|expose|listen|sshtunnel' |
|
MCP_REGEX='\bmcp\b|mcp-config|mcp.config|approve-mcp' |
|
HEADLESS_REGEX='headless|non-interactive|--print[^-]|-p,|--prompt|background|--bare[^-]' |
|
|
|
has_cmd() { |
|
command -v "$1" >/dev/null 2>&1 |
|
} |
|
|
|
bin_on_path() { |
|
local bin="$1" |
|
if has_cmd "$bin"; then |
|
command -v "$bin" |
|
return 0 |
|
fi |
|
local cand |
|
for cand in \ |
|
"$HOME/.local/bin/$bin" \ |
|
"$HOME/.claude/local/$bin" \ |
|
"$HOME/.npm-global/bin/$bin" \ |
|
"/opt/homebrew/bin/$bin" \ |
|
"/usr/local/bin/$bin" \ |
|
"/Applications/Cursor.app/Contents/Resources/app/bin/$bin" \ |
|
"/Applications/Antigravity.app/Contents/Resources/app/bin/$bin" \ |
|
"/Applications/Visual Studio Code.app/Contents/Resources/app/bin/$bin" \ |
|
"$HOME/AppData/Local/Programs/cursor/resources/app/bin/$bin.exe" \ |
|
"$HOME/AppData/Local/Programs/Cursor/resources/app/bin/$bin.exe"; do |
|
if [[ -x "$cand" ]]; then |
|
echo "$cand" |
|
return 0 |
|
fi |
|
done |
|
return 1 |
|
} |
|
|
|
parse_catalog_row() { |
|
local row="$1" |
|
IFS='|' read -r C_ID C_BINS C_VENDOR C_FUNCS C_BYPASS C_PROXY C_CANARY C_REFS <<<"$row" |
|
} |
|
|
|
row_matches_filter() { |
|
local row="$1" |
|
[[ -z "$TOOL_FILTER" ]] && return 0 |
|
parse_catalog_row "$row" |
|
local f="${TOOL_FILTER,,}" |
|
[[ "${C_ID,,}" == "$f" ]] && return 0 |
|
local b |
|
IFS=',' read -ra _bins <<<"$C_BINS" |
|
for b in "${_bins[@]}"; do |
|
b="${b// /}" |
|
[[ "${b,,}" == "$f" ]] && return 0 |
|
done |
|
return 1 |
|
} |
|
|
|
print_catalog() { |
|
echo "${BLD}LOL-GAI catalog${RST} (v${VERSION})" |
|
echo "" |
|
printf "%-18s %-22s %-14s %s\n" "ID" "BINARIES" "VENDOR" "FUNCTIONS" |
|
printf "%-18s %-22s %-14s %s\n" "------------------" "----------------------" "--------------" "----------------" |
|
while IFS= read -r row; do |
|
[[ -z "$row" || "$row" =~ ^# ]] && continue |
|
row_matches_filter "$row" || continue |
|
parse_catalog_row "$row" |
|
printf "%-18s %-22s %-14s %s\n" "$C_ID" "$C_BINS" "$C_VENDOR" "$C_FUNCS" |
|
[[ -n "${C_BYPASS// /}" ]] && dim " Bypass flags: $C_BYPASS" |
|
[[ -n "${C_PROXY// /}" ]] && dim " Notes: $C_PROXY" |
|
[[ -n "${C_REFS// /}" ]] && dim " Refs: $C_REFS" |
|
done <<<"$CATALOG_TSV" |
|
} |
|
|
|
SCAN_IDS=() |
|
SCAN_BINS=() |
|
SCAN_PATHS=() |
|
SCAN_VENDORS=() |
|
SCAN_FUNCS=() |
|
SCAN_BYPASS=() |
|
SCAN_PROXY=() |
|
SCAN_CANARY=() |
|
SCAN_REFS=() |
|
SCAN_HELP=() |
|
SCAN_BYPASS_HITS=() |
|
SCAN_TUNNEL_HITS=() |
|
SCAN_MCP_HITS=() |
|
SCAN_HEADLESS_HITS=() |
|
|
|
scan_host() { |
|
SCAN_IDS=(); SCAN_BINS=(); SCAN_PATHS=(); SCAN_VENDORS=() |
|
SCAN_FUNCS=(); SCAN_BYPASS=(); SCAN_PROXY=(); SCAN_CANARY=() |
|
SCAN_REFS=(); SCAN_HELP=() |
|
SCAN_BYPASS_HITS=(); SCAN_TUNNEL_HITS=(); SCAN_MCP_HITS=(); SCAN_HEADLESS_HITS=() |
|
|
|
info "Scanning PATH + common install / app-bundle dirs…" |
|
local row found_bin found_path b |
|
while IFS= read -r row; do |
|
[[ -z "$row" || "$row" =~ ^# ]] && continue |
|
row_matches_filter "$row" || continue |
|
parse_catalog_row "$row" |
|
found_bin=""; found_path="" |
|
IFS=',' read -ra _bins <<<"$C_BINS" |
|
for b in "${_bins[@]}"; do |
|
b="${b// /}" |
|
[[ -z "$b" ]] && continue |
|
if found_path=$(bin_on_path "$b"); then |
|
found_bin="$b" |
|
break |
|
fi |
|
done |
|
if [[ -n "$found_bin" ]]; then |
|
ok "found ${BLD}$C_ID${RST} → $found_bin ($found_path)" |
|
SCAN_IDS+=("$C_ID") |
|
SCAN_BINS+=("$found_bin") |
|
SCAN_PATHS+=("$found_path") |
|
SCAN_VENDORS+=("$C_VENDOR") |
|
SCAN_FUNCS+=("$C_FUNCS") |
|
SCAN_BYPASS+=("$C_BYPASS") |
|
SCAN_PROXY+=("$C_PROXY") |
|
SCAN_CANARY+=("$C_CANARY") |
|
SCAN_REFS+=("$C_REFS") |
|
SCAN_HELP+=("") |
|
SCAN_BYPASS_HITS+=("") |
|
SCAN_TUNNEL_HITS+=("") |
|
SCAN_MCP_HITS+=("") |
|
SCAN_HEADLESS_HITS+=("") |
|
else |
|
dim "miss $C_ID ($C_BINS)" |
|
fi |
|
done <<<"$CATALOG_TSV" |
|
|
|
echo "" |
|
info "Installed matches: ${#SCAN_IDS[@]}" |
|
} |
|
|
|
run_with_timeout() { |
|
local secs="$1"; shift |
|
if has_cmd timeout; then |
|
timeout --signal=KILL "$secs" "$@" 2>&1 || true |
|
elif has_cmd gtimeout; then |
|
gtimeout --signal=KILL "$secs" "$@" 2>&1 || true |
|
else |
|
local tmp pid waited |
|
tmp="$(mktemp -t lolgai-to.XXXXXX)" |
|
"$@" >"$tmp" 2>&1 & |
|
pid=$! |
|
waited=0 |
|
while kill -0 "$pid" 2>/dev/null; do |
|
if [[ "$waited" -ge "$secs" ]]; then |
|
kill -9 "$pid" 2>/dev/null || true |
|
wait "$pid" 2>/dev/null || true |
|
echo "[lolgai] timed out after ${secs}s: $*" |
|
break |
|
fi |
|
sleep 1 |
|
waited=$((waited + 1)) |
|
done |
|
wait "$pid" 2>/dev/null || true |
|
cat "$tmp" 2>/dev/null || true |
|
rm -f "$tmp" |
|
fi |
|
} |
|
|
|
extract_bucket() { |
|
# extract_bucket <regex> <file> → comma-separated unique matches |
|
local regex="$1" file="$2" |
|
grep -Eii -o -- "$regex" "$file" 2>/dev/null | sort -u | tr '\n' ',' | sed 's/,$//' || true |
|
} |
|
|
|
probe_helps() { |
|
local i help_file |
|
[[ ${#SCAN_IDS[@]} -eq 0 ]] && die "nothing to probe — run --scan first or no tools installed" |
|
|
|
info "Probing --help / --version (classifying Bypass / Tunnel / MCP / Headless)…" |
|
export PAGER=cat CI=1 NO_COLOR=1 TERM="${TERM:-dumb}" |
|
for i in "${!SCAN_IDS[@]}"; do |
|
local bin="${SCAN_BINS[$i]}" |
|
local path="${SCAN_PATHS[$i]}" |
|
local id="${SCAN_IDS[$i]}" |
|
if [[ -n "$OUT_DIR" ]]; then |
|
mkdir -p "$OUT_DIR/$id" |
|
help_file="$OUT_DIR/$id/help.txt" |
|
else |
|
help_file="$(mktemp -t lolgai-help.XXXXXX)" |
|
fi |
|
|
|
{ |
|
echo "=== $bin --version ===" |
|
run_with_timeout 8 "$path" --version |
|
echo "" |
|
echo "=== $bin --help ===" |
|
run_with_timeout 8 "$path" --help |
|
echo "" |
|
echo "=== $bin -h ===" |
|
run_with_timeout 8 "$path" -h |
|
# Tunnel companions often use `tunnel --help` subcommand |
|
if [[ "$id" == *tunnel* ]] || grep -Eiq 'Commands:|tunnel' "$help_file" 2>/dev/null; then |
|
echo "" |
|
echo "=== $bin tunnel --help ===" |
|
run_with_timeout 8 "$path" tunnel --help |
|
fi |
|
} >"$help_file" 2>&1 || true |
|
|
|
local bypass tunnel mcp headless flag cat_hit="" |
|
bypass="$(extract_bucket "$BYPASS_REGEX" "$help_file")" |
|
tunnel="$(extract_bucket "$TUNNEL_REGEX" "$help_file")" |
|
mcp="$(extract_bucket "$MCP_REGEX" "$help_file")" |
|
headless="$(extract_bucket "$HEADLESS_REGEX" "$help_file")" |
|
|
|
IFS=',' read -ra _flags <<<"${SCAN_BYPASS[$i]}" |
|
for flag in "${_flags[@]}"; do |
|
flag="${flag// /}" |
|
[[ -z "$flag" ]] && continue |
|
if grep -Fqi -- "$flag" "$help_file" 2>/dev/null; then |
|
cat_hit="${cat_hit}${cat_hit:+,}${flag}" |
|
fi |
|
done |
|
[[ -n "$cat_hit" ]] && bypass="${bypass}${bypass:+,}${cat_hit}" |
|
|
|
# Companion tunnel binaries: mark Tunnel even if help is sparse |
|
if [[ "${SCAN_FUNCS[$i]}" == *Tunnel* && -z "$tunnel" ]]; then |
|
tunnel="(binary present — Dev Tunnels / remote companion)" |
|
fi |
|
|
|
SCAN_HELP[$i]="$help_file" |
|
SCAN_BYPASS_HITS[$i]="$bypass" |
|
SCAN_TUNNEL_HITS[$i]="$tunnel" |
|
SCAN_MCP_HITS[$i]="$mcp" |
|
SCAN_HEADLESS_HITS[$i]="$headless" |
|
|
|
echo "" |
|
ok "$id ($bin)" |
|
[[ -n "$bypass" ]] && warn " Bypass: $bypass" |
|
[[ -n "$tunnel" ]] && warn " Tunnel: $tunnel" |
|
[[ -n "$mcp" ]] && dim " MCP: $mcp" |
|
[[ -n "$headless" ]] && dim " Headless: $headless" |
|
[[ -z "$bypass$tunnel$mcp$headless" ]] && dim " (no classified tokens in help — see catalog notes)" |
|
[[ -n "${SCAN_PROXY[$i]// /}" ]] && dim " Catalog: ${SCAN_PROXY[$i]}" |
|
|
|
if [[ -z "$OUT_DIR" ]]; then |
|
rm -f "$help_file" |
|
SCAN_HELP[$i]="" |
|
fi |
|
done |
|
} |
|
|
|
json_escape() { |
|
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' |
|
} |
|
|
|
write_reports() { |
|
[[ -z "$OUT_DIR" ]] && return 0 |
|
mkdir -p "$OUT_DIR" |
|
local md="$OUT_DIR/lolgai-report.md" |
|
local json="$OUT_DIR/lolgai-report.json" |
|
local i |
|
|
|
{ |
|
echo "# LOL-GAI scan report" |
|
echo "" |
|
echo "- Host: \`$(hostname)\`" |
|
echo "- Date: \`$(date -u +%Y-%m-%dT%H:%M:%SZ)\`" |
|
echo "- Version: \`$VERSION\`" |
|
echo "- Matches: \`${#SCAN_IDS[@]}\`" |
|
echo "" |
|
echo "## How to read this" |
|
echo "" |
|
echo "| Bucket | Meaning |" |
|
echo "|--------|---------|" |
|
echo "| **Bypass** | Flags that skip human approval / sandbox (YOLO, dangerously-skip-permissions, --force, --allow-all) |" |
|
echo "| **Tunnel** | Remote access / Dev Tunnels / remote-control / cloud agent surfaces |" |
|
echo "| **MCP** | Model Context Protocol config — supply-chain + tool injection surface |" |
|
echo "| **Headless** | Non-interactive / print / background modes — easy to drive from scripts or malware |" |
|
echo "" |
|
echo "| ID | Binary | Path | Functions | Bypass | Tunnel | MCP | Headless |" |
|
echo "|----|--------|------|-----------|--------|--------|-----|----------|" |
|
for i in "${!SCAN_IDS[@]}"; do |
|
echo "| ${SCAN_IDS[$i]} | ${SCAN_BINS[$i]} | \`${SCAN_PATHS[$i]}\` | ${SCAN_FUNCS[$i]} | ${SCAN_BYPASS_HITS[$i]:-_} | ${SCAN_TUNNEL_HITS[$i]:-_} | ${SCAN_MCP_HITS[$i]:-_} | ${SCAN_HEADLESS_HITS[$i]:-_} |" |
|
done |
|
echo "" |
|
echo "## Per-tool notes" |
|
echo "" |
|
for i in "${!SCAN_IDS[@]}"; do |
|
echo "### ${SCAN_IDS[$i]}" |
|
echo "" |
|
echo "- Vendor: ${SCAN_VENDORS[$i]}" |
|
echo "- Catalog Bypass flags: \`${SCAN_BYPASS[$i]:-}\`" |
|
echo "- Notes: ${SCAN_PROXY[$i]}" |
|
echo "- Refs: ${SCAN_REFS[$i]}" |
|
echo "" |
|
done |
|
echo "## Defender hunts (starting points)" |
|
echo "" |
|
echo "1. GenAI CLI parent (\`claude\`, \`codex\`, \`cursor-agent\`, \`agy\`, …) spawning shell / LOLBins" |
|
echo "2. \`cursor-tunnel\` / \`code-tunnel\` establishing Dev Tunnels outside IT-approved remote access" |
|
echo "3. \`claude --remote-control\` or background agents on endpoints that should be interactive-only" |
|
echo "4. Invocation with Bypass flags from this report" |
|
echo "5. Canary file \`LOLGAI_CANARY.txt\` as a positive control" |
|
echo "" |
|
} >"$md" |
|
ok "wrote $md" |
|
|
|
{ |
|
echo "{" |
|
echo " \"version\": \"$VERSION\"," |
|
echo " \"host\": \"$(json_escape "$(hostname)")\"," |
|
echo " \"generated_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"," |
|
echo " \"tools\": [" |
|
for i in "${!SCAN_IDS[@]}"; do |
|
local comma="," |
|
[[ $i -eq $((${#SCAN_IDS[@]} - 1)) ]] && comma="" |
|
printf ' {"id":"%s","bin":"%s","path":"%s","vendor":"%s","functions":"%s","bypass":"%s","tunnel":"%s","mcp":"%s","headless":"%s","notes":"%s"}%s\n' \ |
|
"$(json_escape "${SCAN_IDS[$i]}")" \ |
|
"$(json_escape "${SCAN_BINS[$i]}")" \ |
|
"$(json_escape "${SCAN_PATHS[$i]}")" \ |
|
"$(json_escape "${SCAN_VENDORS[$i]}")" \ |
|
"$(json_escape "${SCAN_FUNCS[$i]}")" \ |
|
"$(json_escape "${SCAN_BYPASS_HITS[$i]}")" \ |
|
"$(json_escape "${SCAN_TUNNEL_HITS[$i]}")" \ |
|
"$(json_escape "${SCAN_MCP_HITS[$i]}")" \ |
|
"$(json_escape "${SCAN_HEADLESS_HITS[$i]}")" \ |
|
"$(json_escape "${SCAN_PROXY[$i]}")" \ |
|
"$comma" |
|
done |
|
echo " ]" |
|
echo "}" |
|
} >"$json" |
|
ok "wrote $json" |
|
|
|
if [[ "$DO_JSON" == true ]]; then |
|
cat "$json" |
|
fi |
|
} |
|
|
|
run_canary() { |
|
[[ ${#SCAN_IDS[@]} -eq 0 ]] && die "canary needs a scanned tool — use --scan --tool <id>" |
|
[[ ${#SCAN_IDS[@]} -gt 1 ]] && die "canary requires exactly one tool — pass --tool <id>" |
|
|
|
local i=0 |
|
local id="${SCAN_IDS[$i]}" |
|
local bin="${SCAN_BINS[$i]}" |
|
local tmpl="${SCAN_CANARY[$i]}" |
|
[[ -z "${tmpl// /}" ]] && die "no canary template for $id" |
|
|
|
local work |
|
if [[ -n "$OUT_DIR" ]]; then |
|
work="$OUT_DIR/$id/workspace" |
|
else |
|
work="$(mktemp -d -t lolgai-canary.XXXXXX)" |
|
fi |
|
mkdir -p "$work" |
|
local prompt="Create a file named LOLGAI_CANARY.txt in the current working directory containing only the line: LOL-GAI canary ok. Do not read secrets, do not modify other files, then stop." |
|
|
|
if [[ "$YES" != true ]]; then |
|
warn "About to invoke ${BLD}$bin${RST} in $work (may use API quota)" |
|
read -r -p "Continue? [y/N] " ans |
|
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || die "aborted" |
|
fi |
|
|
|
local cmd="${tmpl//\{PROMPT\}/$prompt}" |
|
cmd="${cmd//\{WORKDIR\}/$work}" |
|
info "canary cwd=$work" |
|
dim "cmd: $cmd" |
|
local out="${OUT_DIR:+$OUT_DIR/$id/}canary_stdout.txt" |
|
( |
|
cd "$work" |
|
# shellcheck disable=SC2086 |
|
eval "$cmd" |
|
) | tee "${out:-/dev/null}" || warn "canary exited non-zero" |
|
|
|
if [[ -f "$work/LOLGAI_CANARY.txt" ]]; then |
|
ok "canary file present: $work/LOLGAI_CANARY.txt" |
|
cat "$work/LOLGAI_CANARY.txt" |
|
else |
|
warn "canary file not found — auth / approval may be required" |
|
fi |
|
} |
|
|
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--catalog) DO_CATALOG=true; shift ;; |
|
--scan) DO_SCAN=true; shift ;; |
|
--probe) DO_PROBE=true; shift ;; |
|
--canary) DO_CANARY=true; shift ;; |
|
--tool) TOOL_FILTER="${2:-}"; [[ -n "$TOOL_FILTER" ]] || die "--tool needs a value"; shift 2 ;; |
|
--out) OUT_DIR="${2:-}"; [[ -n "$OUT_DIR" ]] || die "--out needs a dir"; shift 2 ;; |
|
--json) DO_JSON=true; shift ;; |
|
--yes) YES=true; shift ;; |
|
--allow-dangerous) ALLOW_DANGEROUS=true; shift ;; |
|
--no-color) NO_COLOR=true; shift ;; |
|
--version) echo "$VERSION"; exit 0 ;; |
|
-h|--help) usage; exit 0 ;; |
|
*) die "unknown arg: $1 (try --help)" ;; |
|
esac |
|
done |
|
|
|
setup_colors |
|
|
|
if [[ "$DO_CATALOG" != true && "$DO_SCAN" != true && "$DO_CANARY" != true ]]; then |
|
DO_CATALOG=true |
|
DO_SCAN=true |
|
fi |
|
[[ "$DO_PROBE" == true && "$DO_SCAN" != true ]] && DO_SCAN=true |
|
[[ "$DO_CANARY" == true && "$DO_SCAN" != true ]] && DO_SCAN=true |
|
|
|
echo "${BLD}LOL-GAI${RST} v${VERSION} — Living Off Legitimate GenAI Agents" |
|
[[ "$ALLOW_DANGEROUS" == true ]] && warn "--allow-dangerous: Bypass flags emphasized (canaries still refuse bypass modes)" |
|
echo "" |
|
|
|
if [[ "$DO_CATALOG" == true && "$DO_SCAN" != true ]]; then |
|
print_catalog |
|
fi |
|
|
|
if [[ "$DO_SCAN" == true ]]; then |
|
[[ "$DO_CATALOG" == true ]] && { print_catalog; echo ""; } |
|
scan_host |
|
if [[ "$DO_PROBE" == true ]]; then |
|
echo "" |
|
probe_helps |
|
fi |
|
[[ -n "$OUT_DIR" ]] && { echo ""; write_reports; } |
|
fi |
|
|
|
if [[ "$DO_CANARY" == true ]]; then |
|
echo "" |
|
run_canary |
|
fi |
|
|
|
echo "" |
|
dim "Research only. Prefer isolated VMs. Do not weaponize." |