#!/bin/sh
# Syd: rock-solid unikernel
# data/syd.sh: Defines 'esyd' command, the multi functional syd helper.
#
# esyd is written in portable shell.
# It should work fine with POSIX sh, Bash and Zsh.
# If you spot a problem running this with either of them,
# please report a bug at: https://todo.sr.ht/~alip/syd
#
# Copyright (c) 2023, 2024 Ali Polatel <alip@chesswob.org>
# SPDX-License-Identifier: GPL-3.0-or-later

esyd() {
    local cmd="${1}"

    shift
    case "${cmd}" in
    api)
        echo -n 3
        ;;
    check)
        syd-chk
        local r=$?
        if [ -t 1 ]; then
            case $r in
            0)
                echo >&2 "Running under syd."
                ;;
            *)
                echo >&2 "Not running under syd."
                ;;
            esac
        fi
        return $r
        ;;
    panic|reset|stat)
        [ -c /dev/syd/${cmd} ]
        ;;
    load)
        if [ ${#} -ne 1 ]; then
            echo >&2 "esyd: ${cmd} takes exactly one extra argument"
            return 1
        fi
        [ -c "/dev/syd/load/${1}" ]
        ;;
    lock)
        [ -c '/dev/syd/lock:on' ]
        ;;
    unlock)
        [ -c '/dev/syd/lock:off' ]
        ;;
    exec_lock)
        [ -c '/dev/syd/lock:exec' ]
        ;;
    info)
        if [ -c /dev/syd ]; then
            if command -v jq >/dev/null 2>&1; then
                local out
                out=$(mktemp)
                if [ -n "$BASH_VERSION" ]; then
                    # We're in Bash
                    # This works with lock:exec
                    IFS=$'\n' readarray -t syd < /dev/syd
                    echo "${syd[*]}" > "${out}"
                elif [ -n "$ZSH_VERSION" ]; then
                    syd=$(</dev/syd)
                    echo "${syd}" > "${out}"
                else
                    # Fallback for POSIX sh
                    # This needs lock:off, doesn't work with lock:exec
                    cat /dev/syd > "${out}"
                fi

                jq "${@}" < "${out}"
                r=$?
                rm -f "${out}"
                return $r
            else
                if [ -n "$BASH_VERSION" ]; then
                    # We're in Bash
                    # This works with lock:exec
                    IFS=$'\n' readarray -t syd < /dev/syd
                    echo "${syd[*]}"
                elif [ -n "$ZSH_VERSION" ]; then
                    syd=$(</dev/syd)
                    echo "${syd}"
                else
                    # Fallback for POSIX sh
                    # This needs lock:off, doesn't work with lock:exec
                    cat /dev/syd
                fi
            fi
        else
            return 1
        fi
        ;;
    mem_max)
        if [ ${#} -ne 1 ]; then
            echo >&2 "esyd: ${cmd} takes exactly one extra argument"
            return 1
        fi
        [ -c "/dev/syd/mem/max:${1}" ]
        ;;
    vm_max)
        if [ ${#} -ne 1 ]; then
            echo >&2 "esyd: ${cmd} takes exactly one extra argument"
            return 1
        fi
        [ -c "/dev/syd/mem/vm_max:${1}" ]
        ;;
    kill_mem)
        [ -c '/dev/syd/mem/kill:1' ]
        ;;
    nokill_mem)
        [ -c '/dev/syd/mem/kill:0' ]
        ;;
    filter_mem)
        [ -c '/dev/syd/filter/mem:1' ]
        ;;
    unfilter_mem)
        [ -c '/dev/syd/filter/mem:0' ]
        ;;
    pid_max)
        if [ ${#} -ne 1 ]; then
            echo >&2 "esyd: ${cmd} takes exactly one extra argument"
            return 1
        fi
        [ -c "/dev/syd/pid/max:${1}" ]
        ;;
    kill_pid)
        [ -c '/dev/syd/pid/kill:1' ]
        ;;
    nokill_pid)
        [ -c '/dev/syd/pid/kill:0' ]
        ;;
    filter_pid)
        [ -c '/dev/syd/filter/pid:1' ]
        ;;
    unfilter_pid)
        [ -c '/dev/syd/filter/pid:0' ]
        ;;
    exec)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        [ -c "$(syd-exec ${@})" ]
        ;;
    force)
        if [ ${#} -ne 3 ]; then
            echo >&2 "esyd: ${cmd} takes exactly three arguments"
            return 1
        fi
        _esyd_path 'force' '+' "${1}:${2}:${3}"
        ;;
    enabled|enabled_path)
        [ -c '/dev/syd/sandbox/read?' ] && [ -c '/dev/syd/sandbox/stat?' ] && [ -c '/dev/syd/sandbox/write?' ]
        ;;
    enable|enable_path)
        [ -c '/dev/syd/sandbox/read:on' ] && [ -c '/dev/syd/sandbox/stat:on' ] && [ -c '/dev/syd/sandbox/write:on' ]
        ;;
    disable|disable_path)
        [ -c '/dev/syd/sandbox/read:off' ] && [ -c '/dev/syd/sandbox/stat:off' ] && [ -c '/dev/syd/sandbox/write:off' ]
        ;;
    enabled_mem)
        [ -c '/dev/syd/sandbox/mem?' ]
        ;;
    enable_mem)
        [ -c '/dev/syd/sandbox/mem:on' ]
        ;;
    disable_mem)
        [ -c '/dev/syd/sandbox/mem:off' ]
        ;;
    enabled_pid)
        [ -c '/dev/syd/sandbox/pid?' ]
        ;;
    enable_pid)
        [ -c '/dev/syd/sandbox/pid:on' ]
        ;;
    disable_pid)
        [ -c '/dev/syd/sandbox/pid:off' ]
        ;;
    enabled_read)
        [ -c '/dev/syd/sandbox/read?' ]
        ;;
    enable_read)
        [ -c '/dev/syd/sandbox/read:on' ]
        ;;
    disable_read)
        [ -c '/dev/syd/sandbox/read:off' ]
        ;;
    enabled_stat)
        [ -c '/dev/syd/sandbox/stat?' ]
        ;;
    enable_stat)
        [ -c '/dev/syd/sandbox/stat:on' ]
        ;;
    disable_stat)
        [ -c '/dev/syd/sandbox/stat:off' ]
        ;;
    enabled_exec)
        [ -c '/dev/syd/sandbox/exec?' ]
        ;;
    enable_exec)
        [ -c '/dev/syd/sandbox/exec:on' ]
        ;;
    disable_exec)
        [ -c '/dev/syd/sandbox/exec:off' ]
        ;;
    enabled_force)
        [ -c '/dev/syd/sandbox/force?' ]
        ;;
    enable_force)
        [ -c '/dev/syd/sandbox/force:on' ]
        ;;
    disable_force)
        [ -c '/dev/syd/sandbox/force:off' ]
        ;;
    enabled_net)
        [ -c '/dev/syd/sandbox/net?' ]
        ;;
    enable_net)
        [ -c '/dev/syd/sandbox/net:on' ]
        ;;
    disable_net)
        [ -c '/dev/syd/sandbox/net:off' ]
        ;;
    allow|allow_path)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        # allow is shorthand for allow_{read,write,stat}
        for capability in read write stat; do
            _esyd_path "allow/${capability}" '+' "${@}"
        done
        ;;
    disallow|disallow_path)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        # disallow is shorthand for disallow_{read,write,stat}
        for capability in read write stat; do
            _esyd_path "allow/${capability}" "${op}" "${@}"
        done
        ;;
    deny|deny_path)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        # deny is shorthand for deny_{read,write,stat}
        for capability in read write stat; do
            _esyd_path "deny/${capability}" '+' "${@}"
        done
        ;;
    nodeny|nodeny_path)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        # nodeny is shorthand for nodeny_{read,write,stat}
        for capability in read write stat; do
            _esyd_path "deny/${capability}" "${op}" "${@}"
        done
        ;;
    allow_read)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'allow/read' '+' "${@}"
        ;;
    disallow_read)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'allow/read' "${op}" "${@}"
        ;;
    deny_read)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'deny/read' '+' "${@}"
        ;;
    nodeny_read)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'deny/read' "${op}" "${@}"
        ;;
    allow_stat)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'allow/stat' '+' "${@}"
        ;;
    disallow_stat)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'allow/stat' "${op}" "${@}"
        ;;
    deny_stat)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'deny/stat' '+' "${@}"
        ;;
    nodeny_stat)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'deny/stat' "${op}" "${@}"
        ;;
    allow_exec)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'allow/exec' '+' "${@}"
        ;;
    disallow_exec)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'allow/exec' "${op}" "${@}"
        ;;
    deny_exec)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'deny/exec' '+' "${@}"
        ;;
    nodeny_exec)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'deny/exec' "${op}" "${@}"
        ;;
    allow_net)
        local op='-'
        local c='allow/net/bind'
        [ "${1}" == '--all' ] && op='^' && shift
        [ "${1}" == '--connect' ] && c='allow/net/connect' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_net "${c}" '+' "${@}"
        ;;
    disallow_net)
        local op='-'
        local c='allow/net/bind'
        [ "${1}" == '--all' ] && op='^' && shift
        [ "${1}" == '--connect' ] && c='allow/net/connect' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_net "${c}" "${op}" "${@}"
        ;;
    deny_net)
        local op='-'
        local c='deny/net/bind'
        [ "${1}" == '--all' ] && op='^' && shift
        [ "${1}" == '--connect' ] && c='deny/net/connect' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_net "${c}" '+' "${@}"
        ;;
    nodeny_net)
        local op='-'
        local c='deny/net/bind'
        [ "${1}" == '--all' ] && op='^' && shift
        [ "${1}" == '--connect' ] && c='deny/net/connect' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_net "${c}" "${op}" "${@}"
        ;;
    addfilter|addfilter_path)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/write' '+' "${@}"
        ;;
    rmfilter|rmfilter_path)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/write' "${op}" "${@}"
        ;;
    addfilter_read)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/read' '+' "${@}"
        ;;
    rmfilter_read)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/read' "${op}" "${@}"
        ;;
    addfilter_stat)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/stat' '+' "${@}"
        ;;
    rmfilter_stat)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if  [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/stat' "${op}" "${@}"
        ;;
    addfilter_exec)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/exec' '+' "${@}"
        ;;
    rmfilter_exec)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_path 'filter/exec' "${op}" "${@}"
        ;;
    addfilter_net)
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_net 'filter/net' '+' "${@}"
        ;;
    rmfilter_net)
        local op='-'
        [ "${1}" == '--all' ] && op='^' && shift
        if [ ${#} -lt 1 ]; then
            echo >&2 "esyd: ${cmd} takes at least one extra argument"
            return 1
        fi
        _esyd_net 'filter/net' "${op}" "${@}"
        ;;
    help|'')
        if [ -t 1 ]; then
            esyd help | ${PAGER:-less}
            return $?
        fi

        cat <<EOF
esyd -- multi functional syd helper
Copyright (c) 2023 Ali Polatel <alip@chesswob.org>
SPDX-License-Identifier: GPL-3.0-or-later

# Subcommands
api
    Print syd API version number
check
    Return true if running under syd
lock
    Lock syd, esyd commands will no longer work
exec_lock
    Lock syd for all processes but the syd exec child
unlock
    Unlock the syd, make it available to all processes rather than just the syd exec child
info jq-args...
    Print syd sandbox state as JSON on standard output
    If "jq" is in PATH, pass the arguments and pipe the output to jq
load fd
    This command causes syd to read configuration from the given file descriptor
panic
    This command causes syd to exit immediately with code 127
reset
    This command causes syd to reset sandboxing to the default state
    Allowlists, denylists and filters are going to be cleared
stat
    Print syd sandbox state on standard error
exec cmd args..
    Execute a command outside the sandbox without sandboxing
kill glob
    Kill any attempt to execute a path matching the given glob pattern
force path hash action
    Force pre-execution verification of the given path using the given checksum
    Action must be exactly one of warn, deny or kill.
enabled, enabled_path
    Return true if read, stat and write sandboxing are all enabled
enable, enable_path
    Enable read, stat and write sandboxing
disable, disable_path
    Disable read, stat and write sandboxing
enabled_pid
    Return true if PID sandboxing is enabled
enable_pid
    Enable PID sandboxing
disable_pid
    Disable PID sandboxing
enabled_read
    Return true if read sandboxing is enabled
enable_read
    Enable read sandboxing
disable_read
    Disable read sandboxing
enabled_stat
    Return true if stat sandboxing is enabled
enable_stat
    Enable stat sandboxing
disable_stat
    Disable stat sandboxing
enabled_exec
    Return true if exec sandboxing is enabled
enable_exec
    Enable exec sandboxing
disable_exec
    Disable exec sandboxing
enabled_force
    Return true if force sandboxing is enabled
enable_force
    Enable force sandboxing
disable_force
    Disable force sandboxing
enabled_net
    Return true if network sandboxing is enabled
enable_net
    Enable network sandboxing
disable_net
    Disable network sandboxing
allow, allow_path glob
    Allow the given glob pattern for read, write and stat sandboxing
disallow, disallow_path [--all] glob
    Removes the given glob pattern from the allowlist for read, write and stat sandboxing
deny, deny_path glob
    Deny the given glob pattern for read, write and stat sandboxing
nodeny, nodeny_path [--all] glob
    Removes the given glob pattern from the denylist for read, write and stat sandboxing
allow_read glob
    Allow the given glob pattern for read sandboxing
disallow_read [--all] glob
    Removes the given glob pattern from the allowlist for read sandboxing
deny_read glob
    Deny the given glob pattern for read sandboxing
nodeny_read [--all] glob
    Removes the given glob pattenr from the denylist for read sandboxing
allow_stat glob
    Allow the given glob pattern for stat sandboxing
disallow_stat [--all] glob
    Removes the given glob pattern from the allowlist for stat sandboxing
deny_stat glob
    Deny the given glob pattern for stat sandboxing
nodeny_stat [--all] glob
    Removes the given glob pattenr from the denylist for stat sandboxing
allow_exec glob
    Allow the given glob pattern for exec sandboxing
disallow_exec [--all] glob
    Removes the given glob pattern from the allowlist for exec sandboxing
deny_exec glob
    Deny the given glob pattern for exec sandboxing
nodeny_exec [--all] glob
    Removes the given glob pattern from the denylist for exec sandboxing
allow_net [--connect] glob|cidr!port[-port]
    Allow the given network address for network bind or connect sandboxing
disallow_net [--all] [--connect] glob|cidr!port[-port]
    Removes the given network address (Ipv4,6), or the glob pattern (UNIX domain sockets)
    from the allowlist for network bind or connect sandboxing
deny_net [--connect] glob|cidr!port[-port]
    Deny the given network address (Ipv4,6) or the glob pattern (UNIX domain sockets)
    for network bind or connect sandboxing
nodeny_net [--all] [--connect] glob|cidr!port[-port]
    Removes the given network address (Ipv4,6) or the glob pattern (UNIX domain sockets)
    from the denylist for network bind or connect sandboxing
addfilter, addfilter_path glob
    Adds the given glob pattern to the list of access violation filters for write sandboxing
rmfilter, rmfilter_path [--all] glob
    Removes the given glob pattern from the list of access violation filters for write sandboxing
addfilter_read glob
    Adds the given glob pattern to the list of access violation filters for read sandboxing
rmfilter_read [--all] glob
    Removes the given glob pattern from the list of access violation filters for read sandboxing
addfilter_stat glob
    Adds the given glob pattern to the list of access violation filters for stat sandboxing
rmfilter_stat [--all] glob
    Removes the given glob pattern from the list of access violation filters for stat sandboxing
addfilter_exec glob
    Adds the given glob pattern to the list of access violation filters for exec sandboxing
rmfilter_exec [--all] glob
    Removes the given glob pattern from the list of access violation filters for exec sandboxing
addfilter_net glob|cidr!port[-port]
    Adds the network address (Ipv4,6) or the glob pattern (UNIX domain sockets)
    to the list of access violation filters for network sandboxing
rmfilter_net [--all] glob|cidr!port[-port]
    Removes the network address (Ipv4,6) or the glob pattern (UNIX domain sockets)
    from the list of access violation filters for network sandboxing
mem_max
    Set syd maximum per-process memory usage limit for memory sandboxing
    parse-size crate is used to parse the value so formatted strings are OK
vm_max
    Set syd maximum per-process virtual memory usage limit for memory sandboxing
    parse-size crate is used to parse the value so formatted strings are OK
kill_mem
    Send SIGKILL to process on Memory access violation
nokill_mem
    Do not send SIGKILL to process on Memory access violation
    Deny system call with ENOMEM
filter_mem
    Do not report access violations for memory sandboxing
unfilter_mem
    Report access violations for memory sandboxing
pid_max
    Set syd maximum process id limit for PID sandboxing
kill_pid
    Send SIGKILL to process on PID access violation
nokill_pid
    Do not send SIGKILL to process on PID access violation
    Deny system call with EACCES
filter_pid
    Do not report access violations for PID sandboxing
unfilter_pid
    Report access violations for PID sandboxing
EOF
        ;;
    *)
        echo >&2 "esyd: subcommand '${cmd}' unrecognised!"
        echo >&2 "Use 'esyd help' for a list of supported subcommands."
        return 1
        ;;
    esac
}

_esyd_path()
{
    local cmd="${1}"
    local op="${2}"

    case "${op}" in
    '+'|'-')
        ;;
    *)
        echo >&2 "esyd_path: invalid operation character '${op}'"
        return 1
        ;;
    esac

    shift 2

    local ret=0
    local path
    for path in "${@}"; do
        case "${path}" in
        /*)
            [ -c /dev/syd/"${cmd}${op}${path}" ] || ret=$?
            ;;
        *)
            echo >&2 "esyd_path: expects absolute path, got: ${path}"
            return 1
            ;;
        esac
    done
    return $ret
}

_esyd_net()
{
    local cmd="${1}"
    local op="${2}"

    case "${op}" in
    '+'|'-')
        ;;
    *)
        echo >&2 "esyd_net: invalid operation character '${op}'"
        return 1
        ;;
    esac

    shift 2

    local ret=0
    while [ ${#} -gt 0 ] ; do
        # syd does input validation so we don't do any here.
        [ -c "/dev/syd/${cmd}${op}${1}" ] || ret=$?
        shift
    done
    return $ret
}
