#!/bin/bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# SecuBox OpenClaw controller — privileged LXC tool-runner control plane.
# The ONLY privileged surface for the openclaw module (invoked via sudo).
set -uo pipefail

CONFIG_FILE="/etc/secubox/openclaw.toml"
LXC_PATH="${SECUBOX_LXC_PATH:-/data/lxc}"
CONTAINER="${SECUBOX_LXC_NAME:-openclaw}"
LXC_BRIDGE="${SECUBOX_LXC_BRIDGE:-br-lxc}"
LXC_IP="${SECUBOX_LXC_IP:-10.100.0.41}"
LXC_GW="${SECUBOX_LXC_GW:-10.100.0.1}"
SCANS_DIR="${SECUBOX_OPENCLAW_SCANS:-/var/lib/secubox/openclaw/scans}"

err() { echo "[ERROR] $*" >&2; }

# ---- injection guards (defense in depth; API validates too) ----
# Require an alphanumeric first char so a leading '-' can never be parsed as a
# flag by dig/nmap/whois (flag-injection). Domains/IPs/emails/CIDRs all qualify.
_valid_target() { [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._:@/-]*$ ]]; }
_valid_scanid() { [[ "$1" =~ ^[a-f0-9]{8}$ ]]; }
_valid_type()   { case "$1" in domain|ip|email|dns|whois|certs|ports) return 0;; *) return 1;; esac; }

# ---- lxc helpers (mirror nextcloudctl) ----
lxc_running() { local _s; _s=$(lxc-info -n "$CONTAINER" -P "$LXC_PATH" 2>/dev/null); [[ "$_s" == *"State:"*"RUNNING"* ]]; }
lxc_exists()  { [ -d "$LXC_PATH/$CONTAINER/rootfs" ]; }
# Run a command in the container. Extra args after the command string are
# passed as positional $1.. to `sh -c` — NEVER interpolate targets into the
# command string.
lxc_attach()  { local cmd="$1"; shift; lxc-attach -n "$CONTAINER" -P "$LXC_PATH" -- sh -c "$cmd" _ "$@"; }

has_tool() { lxc_running && lxc_attach 'command -v "$1" >/dev/null' "$1"; }

# CT-log lookup, run INSIDE the container with the domain as $1. certSpotter
# first (reliable JSON), crt.sh as fallback, then a clean one-line message.
# crt.sh is frequently down and answers 502/404 *HTML*; without this guard that
# raw HTML leaked straight into the scan result. Never emits non-JSON garbage.
readonly CT_REMOTE='d="$1"; j="$(curl -sS -L --max-time 25 -A "SecuBox-OpenClaw" "https://api.certspotter.com/v1/issuances?domain=$d&include_subdomains=true&expand=dns_names&expand=issuer&expand=not_before&expand=not_after" 2>/dev/null)"; case "$j" in \[*) printf "%s" "$j";; *) c="$(curl -sS -L --max-time 25 "https://crt.sh/?q=$d&output=json" 2>/dev/null)"; case "$c" in \[*) printf "%s" "$c";; *) printf "Certificate Transparency lookup unavailable — certSpotter and crt.sh both returned no JSON (services down or rate-limited). Retry later.";; esac;; esac'

# Registrable domain (last two labels): whois answers for the registrable
# domain, not sub-domains (a sub-domain query returns nothing). Heuristic —
# imperfect for multi-label ccTLDs (co.uk), acceptable for OSINT triage.
_registrable() { printf '%s' "$1" | awk -F. 'NF>=2{print $(NF-1)"."$NF} NF<2{print $0}'; }

cmd_status() {
    local running=false installed=false
    lxc_exists && installed=true
    lxc_running && running=true
    local nmap=false dig=false whois=false curl=false
    if [ "$running" = true ]; then
        has_tool nmap  && nmap=true
        has_tool dig   && dig=true
        has_tool whois && whois=true
        has_tool curl  && curl=true
    fi
    printf '{"running":%s,"installed":%s,"ip":"%s","tools":{"nmap":%s,"dig":%s,"whois":%s,"curl":%s}}\n' \
        "$running" "$installed" "$LXC_IP" "$nmap" "$dig" "$whois" "$curl"
}

cmd_start() { lxc_exists || { err "not installed"; return 1; }; lxc-start -n "$CONTAINER" -P "$LXC_PATH" 2>/dev/null; sleep 1; lxc_running; }
cmd_stop()  { lxc-stop -n "$CONTAINER" -P "$LXC_PATH" 2>/dev/null; ! lxc_running; }
cmd_selftest() { lxc_running || { err "container not running"; return 1; }; lxc_attach 'nmap --version && dig -v 2>&1 | head -1 && whois --version 2>&1 | head -1'; }

_write_lxc_config() {
    cat > "$LXC_PATH/$CONTAINER/config" <<EOF
lxc.include = /usr/share/lxc/config/debian.common.conf
lxc.arch = linux64
lxc.uts.name = $CONTAINER
lxc.rootfs.path = dir:$LXC_PATH/$CONTAINER/rootfs
lxc.net.0.type = veth
lxc.net.0.link = $LXC_BRIDGE
lxc.net.0.flags = up
lxc.net.0.name = eth0
lxc.net.0.ipv4.address = $LXC_IP/24
lxc.net.0.ipv4.gateway = $LXC_GW
lxc.start.auto = 1
lxc.start.delay = 5
lxc.cgroup2.memory.max = 512M
EOF
}

cmd_install() {
    [ "$(id -u)" -eq 0 ] || { err "root required"; return 1; }
    mkdir -p "$LXC_PATH/$CONTAINER" "$SCANS_DIR"
    if [ ! -x "$LXC_PATH/$CONTAINER/rootfs/bin/bash" ]; then
        echo "[openclaw] debootstrapping bookworm rootfs…"
        debootstrap --variant=minbase \
            --include=systemd,systemd-sysv,dbus,ca-certificates,iproute2 \
            bookworm "$LXC_PATH/$CONTAINER/rootfs" http://deb.debian.org/debian || { err "debootstrap failed"; return 1; }
    fi
    echo "$CONTAINER" > "$LXC_PATH/$CONTAINER/rootfs/etc/hostname"
    printf 'nameserver %s\nnameserver 1.1.1.1\n' "$LXC_GW" > "$LXC_PATH/$CONTAINER/rootfs/etc/resolv.conf"
    _write_lxc_config
    lxc_running || lxc-start -n "$CONTAINER" -P "$LXC_PATH" 2>/dev/null
    sleep 2
    echo "[openclaw] installing toolchain…"
    lxc_attach 'export DEBIAN_FRONTEND=noninteractive; apt-get update -qq && apt-get install -y --no-install-recommends nmap ndiff dnsutils whois curl ca-certificates >/dev/null 2>&1'
    lxc_attach 'command -v nmap >/dev/null && command -v dig >/dev/null && command -v whois >/dev/null' \
        && echo "[openclaw] install complete" || { err "toolchain missing after install"; return 1; }
}

# jq is available on the HOST (SecuBox base). Build the record on the host from
# captured container stdout; never let the target reach a shell unquoted.
_scan_write() { # $1=id $2=json-object-of-record
    local f="$SCANS_DIR/$1.json"; mkdir -p "$SCANS_DIR"; printf '%s\n' "$2" > "$f"; chmod 640 "$f"
}

cmd_scan() {
    local type="${1:-}" target="${2:-}" id="${3:-}"
    _valid_type "$type"    || { err "bad type"; return 2; }
    _valid_target "$target"|| { err "bad target"; return 2; }
    _valid_scanid "$id"    || { err "bad id"; return 2; }
    lxc_running || { local now; now="$(date -u +%FT%TZ)"; _scan_write "$id" "$(jq -nc --arg i "$id" --arg t "$type" --arg g "$target" --arg n "$now" '{id:$i,type:$t,target:$g,status:"failed",started_at:$n,finished_at:$n,results:null,error:"container not running"}')"; return 1; }
    local started; started="$(date -u +%FT%TZ)"
    local raw rc
    case "$type" in
        dns)    raw="$(lxc_attach 'dig +noall +answer ANY "$1"' "$target" 2>&1)"; rc=$? ;;
        whois)  local reg; reg="$(_registrable "$target")"
                raw="$(lxc_attach 'if [ "$1" != "$2" ]; then printf "note: %s is a sub-domain; showing whois for the registrable domain %s\n\n" "$1" "$2"; fi; whois -- "$2"' "$target" "$reg" 2>&1)"; rc=$? ;;
        certs)  raw="$(lxc_attach "$CT_REMOTE" "$target" 2>&1)"; rc=$? ;;
        ports)  raw="$(lxc_attach 'nmap -Pn -T4 --top-ports 100 -oG - "$1"' "$target" 2>&1)"; rc=$? ;;
        ip)     raw="$(lxc_attach 'nmap -Pn -T4 -sV --top-ports 200 "$1"' "$target" 2>&1)"; rc=$? ;;
        # domain/email are OSINT aggregates: partial data is expected and the
        # trailing head/grep would mask the real tool rc, so always report done.
        domain) local reg; reg="$(_registrable "$target")"
                raw="$(lxc_attach 'echo "== DNS =="; dig +noall +answer ANY "$1"; echo "== WHOIS =="; [ "$1" != "$2" ] && echo "(sub-domain — registrable: $2)"; whois -- "$2" 2>/dev/null | head -40; echo "== CERTS =="; '"$CT_REMOTE"' | head -c 20000' "$target" "$reg" 2>&1)"; rc=0 ;;
        # Derive the post-@ domain ON THE HOST and re-validate it: _valid_target
        # anchors only the first char of the whole string, so 'a@-f/etc/hostname'
        # passes as an email but its domain '-f/etc/hostname' would be a dig flag.
        email)  local _d="${target#*@}"
                _valid_target "$_d" || { err "bad email domain"; return 2; }
                raw="$(lxc_attach 'echo "== MX =="; dig +short MX "$1"; echo "== SPF =="; dig +short TXT "$1" | grep -i spf' "$_d" 2>&1)"; rc=0 ;;
        *) err "unhandled type"; return 2 ;;
    esac
    local status="completed"; [ "$rc" -ne 0 ] && status="failed"
    _scan_write "$id" "$(jq -nc --arg i "$id" --arg t "$type" --arg g "$target" --arg s "$status" \
        --arg st "$started" --arg fi "$(date -u +%FT%TZ)" --arg raw "$raw" \
        '{id:$i,type:$t,target:$g,status:$s,started_at:$st,finished_at:$fi,results:{raw:$raw},error:(if $s=="failed" then "tool exit non-zero" else null end)}')"
    [ "$status" = completed ]
}

usage() { echo "usage: openclawctl {install|start|stop|status [--json]|scan <type> <target> <id>|selftest}" >&2; }

case "${1:-}" in
    __guard) # hidden: validate a value, exit 0/1 (for tests)
        case "${2:-}" in
            target) _valid_target "${3:-}";;
            scanid) _valid_scanid "${3:-}";;
            type)   _valid_type "${3:-}";;
            *) exit 2;;
        esac ;;
    status)   shift; cmd_status ;;
    start)    cmd_start ;;
    stop)     cmd_stop ;;
    selftest) cmd_selftest ;;
    install)  cmd_install ;;          # Task 2
    scan)     shift; cmd_scan "$@" ;; # Task 3
    *) usage; exit 2 ;;
esac
