#!/bin/sh -efu

# Copyright (C) 2021 Paul Wolneykien <manowar@altlinux.org>
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

# ---

. shell-config

PROG="${0##*/}"
PROG_VERSION=0.1.0

AUSYSCALL="$(which ausyscall 2>/dev/null ||:)"
AUNORMARCH="$(which aunormarch 2>/dev/null ||:)"
BARCH_LIST='b32 b64'
CONFIG=

show_help()
{
	cat <<EOF
Usage: $PROG -c CONFIG [options]

$PROG is a tool to generate audit rules based on a configuration file.

Options:

  -c CONFIG, --config=CONFIG    use CONFIG to build up the rules;

  --relax                 ignore the error when no supported
                          architecture was detected;

  -v, --verbose           print more information messages (to stderr);

  -q, --quiet             print errors only;

  -V,--version            print program version and exit;

  -h,--help               show this text and exit.


For more information see mk-syscall-rules(1).
Report bugs to https://bugzilla.altlinux.org/.

EOF
}

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION
Written by: see the source for author info.

Copyright (C) 2020 BaseALT /basealt.ru/
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE.
EOF
}

show_usage()
{
	cat <<EOF
Usage: $PROG -c CONFIG [options]

Run $PROG -h to see the help page.
EOF
    return 1
}


OPTS=`getopt -n $PROG -o c:,v,V,h \
             -l config:,relax,verbose,version,help -- "$@"` || show_usage
eval set -- "$OPTS"

verbose=
quiet=
relax=

while :; do
	case "$1" in
		-c|--config)
            shift
            CONFIG="$1"
            ;;
        --relax)
            relax=1
            ;;
        -v|--verbose)
            verbose=1
            ;;
        -q|--quiet)
            quiet=1
            ;;
		-V|--version)
            print_version
            exit 0
            ;;
		-h|--help)
            show_help
            exit 0
            ;;
        --)
            shift
            break
            ;;
		*)
            print_error "ERROR: Unrecognized option: %s" "$1"
            exit 1
            ;;
	esac
	shift
done

no_verbose=
[ -n "$verbose" ] || no_verbose=1

if [ -z "$CONFIG" ]; then
    echo "ERROR: You should specify a configuration file." >&2
    show_usage
fi

if [ ! -e "$CONFIG" ]; then
    echo "ERROR: Configuration file $CONFIG not found." >&2
    exit 1
fi

if [ ! -s "$CONFIG" ]; then
    echo "ERROR: Configuration file $CONFIG is empty." >&2
    exit 1
fi

[ -z "$verbose" ] || \
    echo "Using configuration file $CONFIG" >&2

_read_conf_val() {
    local key="$1"
    shell_config_get "$CONFIG" "$key" '[[:space:]]*=[[:space:]]*'
}

read_conf_val() {
    local key="$1"
    local default="${2:-}"

    local val=
    val="$(_read_conf_val "$key")"

    [ -n "$val" ] || val="$default"

    echo "$val"
}

KEYS="$(read_conf_val 'KEYS')"
WRITE_TO="$(read_conf_val 'WRITE_TO')"
SKIP_EMPTY="$(read_conf_val 'SKIP_EMPTY')"

check_tool() {
    local varname="$1"; shift
    local default="$1"

    local tool="$(read_conf_val "$varname")"

    [ -n "$tool" ] || tool="$default"

    if [ -z "$tool" ]; then
        echo "ERROR: $varname is not configured and no default." >&2
        return 1
    fi

    local ret=0
    local atool=
    atool="$(which "$tool" 2>/dev/null)" || ret=$?
    if [ $ret -ne 0 ]; then
        echo "ERROR: Unable to find $tool" >&2
        return 1
    fi
    tool="$atool"

    if [ ! -x "$tool" ]; then
        echo "ERROR: $tool is either doesn't exist or isn't executable." >&2
        return 1
    fi

    if [ -n "$default" -a "$tool" != "$default" ]; then
        [ -z "$verbose" ] || \
            echo "Use $tool instead of $default" >&2
    fi

    echo "$tool"
}

# ---

if [ -z "$BARCH_LIST" ]; then
    [ -z "$verbose" ] || \
        echo "BARCH_LIST is empty --- nothing to do" >&2
    exit 0
fi

if [ -z "$KEYS" ]; then
    [ -z "$verbose" ] || \
        echo "KEYS is empty --- nothing to do" >&2
    exit 0
fi

AUNORMARCH="$(check_tool 'AUNORMARCH' "$AUNORMARCH")"
AUSYSCALL="$(check_tool 'AUSYSCALL' "$AUSYSCALL")"

detect_arch() {
    local barch="$1"

    local ret=0
    local arch=
    arch="$($AUNORMARCH "$barch" 2>&1)" || ret=$?
    if [ $ret -ne 0 ]; then
        if [ $ret -eq 2 ]; then
            [ -z "$verbose" ] || \
                echo "No architecture detected for $barch" >&2
            arch=
        else
            echo "ERROR: $arch" >&2
            return 1
        fi
    fi

    echo "$arch"
}

check_syscall() {
    local arch="$1"
    local syscall="$2"

    local ret=0
    local callnum=
    callnum="$("$AUSYSCALL" "$arch" "$syscall" --exact 2>/dev/null)" || ret=$?

    [ -n "$callnum" ] || ret=255

    return $ret
}

filter_syscalls() {
    local arch="$1"
    local syscalls="$2"

    local known_syscalls=
    for syscall in $(echo "$syscalls" | tr ',' ' '); do
        if check_syscall "$arch" "$syscall"; then
            known_syscalls="$known_syscalls${known_syscalls:+ }$syscall"
        else
            [ -n "$quiet" ] || \
                echo "WARNING: Skip '$syscall' syscall unknown on $arch." >&2
        fi
    done

    echo "$known_syscalls"
}

print_rules() {
    local key="$1"; shift
    local list="$1"; shift
    local action="$1"; shift
    local barch="$1"; shift
    local arch="$1"; shift
    local syscalls="$1"; shift
    local ecodes="$1"; shift
    local args="$@"
    
    local known_syscalls=
    known_syscalls="$(filter_syscalls "$arch" "$syscalls")"

    if [ -z "$known_syscalls" ]; then
        if [ -z "$SKIP_EMPTY" -o "$SKIP_EMPTY" = '0' ]; then
            echo "ERROR: Got empty syscall list for '$key'." >&2
            return 1
        fi
    fi

    known_syscalls="$(echo "$known_syscalls" | sed -e 's/[[:space:]]\+/,/g')"

    for ec in ${ecodes:-''}; do
        local keysuf=
        case "$key" in
            *-)
                keysuf="$(echo "$ec" | tr '[:upper:]' '[:lower:]')"
                ;;
        esac
        echo "-a $list,$action -F arch=$barch -S $known_syscalls${ec:+ -F exit=-$(echo "$ec" | tr '[:lower:]' '[:upper:]')}${args:+ $args} -k $key$keysuf"
    done
}

if [ -n "$WRITE_TO" ]; then
    [ -z "$verbose" ] || \
        echo "Writing the rules to $WRITE_TO" >&2
    exec 1>"$WRITE_TO"
fi

skipped=
for barch in $BARCH_LIST; do
    arch="$(detect_arch "$barch")"

    if [ -z "$arch" ]; then
        skipped="$skipped${skipped:+ }$barch"
        continue
    fi

    [ -z "$verbose" ] || \
        echo "Using $arch for $barch." >&2

    def_syscalls="$(read_conf_val 'SYSCALLS')"
    def_ecodes="$(read_conf_val 'ECODES')"
    def_list="$(read_conf_val 'LIST')"
    def_action="$(read_conf_val 'ACTION')"
    def_args="$(read_conf_val 'ARGS')"

    for key in $KEYS; do
        varkey="$(echo "${key%-}" | tr '-' '_')"
        syscalls="$(read_conf_val "${varkey}_SYSCALLS" "$def_syscalls")"
        ecodes="$(read_conf_val "${varkey}_ECODES" "$def_ecodes")"
        list="$(read_conf_val "${varkey}_LIST" "$def_list")"
        action="$(read_conf_val "${varkey}_ACTION" "$def_action")"
        args="$(read_conf_val "${varkey}_ARGS" "$def_args")"

        list="${list:-exit}"
        action="${action:-always}"

        [ -z "$verbose" ] || \
            echo "Adding rules for key '$key'." >&2

        print_rules "$key" "$list" "$action" "$barch" "$arch" \
                    "$syscalls" "$ecodes" $args
    done
done

if [ "$skipped" = "$BARCH_LIST" ]; then
    if [ -z "$relax" ]; then
        echo "ERROR: No architectures were detected for the configured list of general architectures ($BARCH_LIST). Check the BARCH_LIST or use the defaults. Possible bug in $AUNORMARCH." >&2
        exit 1
    else
        echo "WARNING: No architectures were detected for the configured list of general architectures ($BARCH_LIST). However, relaxing the error as was asked." >&2
    fi
fi
