#!/bin/sh

module_name=alterator-snort
po_domain="$module_name"
alterator_api_version=1

. alterator-sh-functions
. shell-config
. alterator-net-functions

CONFDIR=/etc/snort
CONFIG="$CONFDIR/snort.conf"
RULESDIR=
RULESET_BEGIN_DEFAULT_TEMPLATE='# Include all relevant rulesets here'
RULESET_BEGIN_ALTERATOR_SNORT_TEMPLATE="# Rulesets list. Generated by $module_name."
MYSQL_PASSWD="$(sed -n 's;^output[[:blank:]]\+database:.*mysql.*password=\([^[:blank:]]\+\).*;\1;p' "$CONFIG")"
MYSQL_COMM="/usr/bin/mysql --skip-column-names --batch -u snort --password='$MYSQL_PASSWD' snort"
SERVICE=/sbin/service
CHKCONFIG=/sbin/chkconfig
SNORT=/usr/sbin/snort
RULES_URL='http://www.snort.org/pub-bin/oinkmaster.cgi'
RULES_FILENAME=snortrules-snapshot
RULES_EXT=tar.gz
OINKMASTER=/usr/bin/oinkmaster
OINKMASTER_CONF=/etc/oinkmaster.conf
CRON_FILE=/etc/cron.d/alterator-snort

###

list_weekday()
{
    write_enum_item "1" "`_ "monday"`"
    write_enum_item "2" "`_ "tuesday"`"
    write_enum_item "3" "`_ "wednesday"`"
    write_enum_item "4" "`_ "thursday"`"
    write_enum_item "5" "`_ "friday"`"
    write_enum_item "6" "`_ "saturday"`"
    write_enum_item "0" "`_ "sunday"`"
}

set_rules_dir()
{
    local rules_path=

    [ -r "$CONFIG" ] || return
    rules_path="$(sed -n 's;^var[[:blank:]]\+RULE_PATH[[:blank:]]\+\(.\+\)$;\1;p' "$CONFIG")"
    [ -n "$rules_path" ] && RULESDIR="$rules_path"
}

list_rules()
{
    sed -n "s;^[[:blank:]]*include[[:blank:]]\+\$RULE_PATH/\(.*\).rules.*$;\1;p" "$CONFIG" 
}

list_rules_unused()
{
    for rule in $(ls -1 "$RULESDIR" | sed -n 's;^\(.\+\)\.rules$;\1;p'); do
        if grep -qs "^[[:blank:]]*include[[:blank:]]\$RULE_PATH/$rule.rules" "$CONFIG" || \
           ! grep -qs '^[^#]' "$RULESDIR/$rule.rules"; then
            continue
        fi
        echo "$rule"
    done
}

read_rule_description()
{
    local id_str='^#[[:blank:]]*\$Id:'
    [ -n "$1" ] || return

    sed -n "/$id_str/,/^[^#]/ { /$id_str/d; s|^#[[:blank:]]*\(.*\)$|\1|p }" "$RULESDIR/$1.rules"
}

rules_list_to_ruleset()
{
    [ -n "$1" ] || return
    local IFS=';'
    local ruleset_str=
    for i in $1; do
        ruleset_str="$ruleset_str${ruleset_str:+\n}include \$RULE_PATH/$i.rules"
    done
    echo "$ruleset_str"
}

write_ruleset()
{
    local rule_regexp='^#*[[:blank:]]*include[[:blank:]]\+\$RULE_PATH/.*\.rules.*$'

    [ -n "$1" ] || return

    if ! grep -qs "^$RULESET_BEGIN_ALTERATOR_SNORT_TEMPLATE" "$CONFIG"; then
        sed -i "\|$RULESET_BEGIN_DEFAULT_TEMPLATE|,\|$rule_regexp| { \|$rule_regexp|i$RULESET_BEGIN_ALTERATOR_SNORT_TEMPLATE
            }" "$CONFIG"
        grep -qs "^$RULESET_BEGIN_ALTERATOR_SNORT_TEMPLATE" "$CONFIG" ||
        printf "\n$RULESET_BEGIN_ALTERATOR_SNORT_TEMPLATE" >>"$CONFIG"
    fi  
    sed -i "\|$rule_regexp|d" "$CONFIG"
    sed -i "\|^$RULESET_BEGIN_ALTERATOR_SNORT_TEMPLATE|a$1" "$CONFIG"
}

num_to_ipv4addr()
{
    num="$1"; shift

    printf '%s.%s.%s.%s\n' \
        "$(($num >> 24 & 0xff))" \
        "$(($num >> 16 & 0xff))" \
        "$(($num >> 8 & 0xff))" \
        "$(($num & 0xff))"
}

utc_to_local_time()
{
    date --date=$1+00:00 +'%F %T'
}

local_to_utc_time()
{
    local z="$(date +%z)"
    date -u --date="$1$z" +'%F %T'
}

get_sig_name()
{
    echo "SELECT sig_name FROM signature WHERE sig_id=$1;" | eval "$MYSQL_COMM"
}

list_events()
{
    local start_date="$1"; shift
    local start_time="$1"; shift
    local end_date="$1"; shift
    local end_time="$1"; shift
    local iface= sid= sign_name=
    local start_datetime="$(local_to_utc_time "$start_date $start_time")"
    local end_datetime="$(local_to_utc_time "$end_date $end_time")"
    local IFS=$'\n'

    for i in $(echo "SELECT sid,interface FROM sensor;" | eval "$MYSQL_COMM"); do
        iface="$(echo "$i" | cut -f2)"
        sid="$(echo "$i" | cut -f1)"
        for j in $(echo "SELECT signature,COUNT(cid),MAX(timestamp) FROM event WHERE sid=$sid AND \
                         timestamp>='$start_datetime' AND timestamp<='$end_datetime' \
                         GROUP BY signature ORDER BY COUNT(cid) DESC;" | eval "$MYSQL_COMM"); do
            sig_id="$(echo "$j" | cut -f1)"
            count="$(echo "$j" | cut -f2)"
            [ "$count" -gt 0 ] || continue
            last_event_time="$(echo "$j" | cut -f3)"
            sign_name="$(get_sig_name "$sig_id")"
            write_table_item \
                name "$sid/$sig_id" \
                iface "$iface" \
                description "$sign_name" \
                count "$count" \
                last_event_time "$(utc_to_local_time "$last_event_time")"
        done
    done
}

is_snortd_enabled()
{
    $SERVICE snortd status >/dev/null 2>&1
}

read_state()
{
    if is_snortd_enabled; then
        write_bool_param state_enabled true
    else
        write_bool_param state_enabled false
    fi
}

read_dates()
{
    local curr_date="$(date +%F 2>/dev/null)"

    write_string_param start_date "$curr_date"
    write_string_param start_time '00:00:00'
    write_string_param end_date "$curr_date"
    write_string_param end_time '23:59:59'
}

write_state()
{
    if [ "$1" = '#f' ]; then
        if is_snortd_enabled; then
            "$CHKCONFIG" snortd off 2>/dev/null
            "$SERVICE" snortd stop >/dev/null 2>&1
        fi
    else
        if is_snortd_enabled; then
            "$SERVICE" snortd reload 2>/dev/null
        else
            "$CHKCONFIG" mysqld on 2>/dev/null
            "$CHKCONFIG" snortd on 2>/dev/null
            "$SERVICE" mysqld start >/dev/null 2>&1
            "$SERVICE" snortd start >/dev/null 2>&1
        fi
    fi
}

list_details()
{
    local list="$1"; shift
    local start_date="$1"; shift
    local start_time="$1"; shift
    local end_date="$1"; shift
    local end_time="$1"; shift
    local IFS=$'\n'
    local IFS="$IFS;"
    local iface= sid= sig_id= sig_name= source_ip= dest_ip= min_time= max_time=
    local start_datetime="$(local_to_utc_time "$start_date $start_time")"
    local end_datetime="$(local_to_utc_time "$end_date $end_time")"

    for i in $list; do
        [ "$i" = on ] && continue
        sid="${i%/*}"
        sig_id="${i#*/}"
        iface="$(echo "SELECT interface FROM sensor WHERE sid=$sid;" | eval "$MYSQL_COMM")"
        sig_name="$(get_sig_name "$sig_id")"

        for tmp in $(echo "SELECT COUNT(event.cid),MIN(event.timestamp),MAX(event.timestamp),iphdr.ip_src,iphdr.ip_dst \
                FROM event,iphdr WHERE event.sid=$sid AND iphdr.sid=$sid AND event.signature=$sig_id \
                AND event.cid=iphdr.cid AND event.timestamp>='$start_datetime' \
                AND event.timestamp<='$end_datetime' \
                GROUP BY iphdr.ip_src,iphdr.ip_dst ORDER BY COUNT(event.cid) DESC;" | eval "$MYSQL_COMM"); do
            count="$(echo "$tmp" | cut -f1)"
            min_time="$(echo "$tmp" | cut -f2)"
            max_time="$(echo "$tmp" | cut -f3)"
            source_ip="$(echo "$tmp" | cut -f4)"
            dest_ip="$(echo "$tmp" | cut -f5)"
            write_table_item \
                iface "$iface" \
                description "$sig_name" \
                source_ip "$(num_to_ipv4addr "$source_ip")" \
                dest_ip "$(num_to_ipv4addr "$dest_ip")" \
                count "$count" \
                min_time "$(utc_to_local_time "$min_time")" \
                max_time "$(utc_to_local_time "$max_time")"
        done
    done
}    

check_oinkconf_url()
{
    grep -qs "^[[:blank:]]*url[[:blank:]]*=" "$OINKMASTER_CONF"
}

write_oinkconf_url()
{
    url="$1"; shift

    [ -n "$url" ] || return

    if check_oinkconf_url; then
        sed -i "s;^[[:blank:]]*url[[:blank:]]*=.*$;url = $url;" "$OINKMASTER_CONF"
    else
        echo "url = $url" >>"$OINKMASTER_CONF"
    fi
}

read_oinkconf_url()
{
    sed -n "s;^[[:blank:]]*url[[:blank:]]*=[[:blank:]]*\(.*\)#*.*$;\1;p" "$OINKMASTER_CONF"
}

write_oinkcode()
{
    local oinkcode="$1"; shift
    local snort_version="$($SNORT -V 2>&1 | sed -n 's;^.*Version \([1-9]\+\.[0-9]\+\).*$;\1;p')"
    local url=

    [ -n "$snort_version" -a -n "$oinkcode" ] || return
    url="$RULES_URL/$oinkcode/$RULES_FILENAME-$snort_version.$RULES_EXT"

    write_oinkconf_url "$url"
}

read_oinkcode()
{
    sed -n "s;^[[:blank:]]*url[[:blank:]]*=[[:blank:]]*$RULES_URL/\(.*\)/.*$;\1;p" "$OINKMASTER_CONF"
}

read_cron_data()
{
    if [ -s "$CRON_FILE" ] ;then
        write_bool_param "auto_update" true
        while read min hour monthday month weekday rest;do
            [ -n "${min%\#*}" ] || continue

            write_string_param "time" "$hour:$min:00"
            if [ "$monthday" = "*" -a "$month" = "*" -a "$weekday" = "*" ]; then
                write_string_param "period" "daily"
            elif [ "$monthday" = "*" -a "$month" = "*" -a "$weekday" != "*" ];then
                write_string_param "period" "weekly"
                write_string_param "weekday" "$weekday"
            elif [ "$monthday" != "*" -a "$month" = "*" -a "$weekday" = "*" ]; then
                write_string_param "period" "monthly"
                write_string_param "monthday" "$monthday"
            else
                write_string_param "period" "daily"
            fi
            return
        done <"$CRON_FILE"
    else
        write_bool_param "auto_update" false
        write_string_param "time" "02:00:00"
        write_string_param "period" "daily"
    fi
}

write_cron_data()
{
    if test_bool "$in_auto_update"; then
        if [ "$in_period" = "weekly" -a -z "$in_weekday" ]; then
            write_error "`_ "Day of week should be selected"`"
            return
        fi

        if [ "$in_period" = "monthly" -a -z "$in_monthday" ];then
            write_error "`_ "Day of month should be defined"`"
            return
        fi

        in_time="${in_time%:*}"
        local hour="${in_time%:*}"
        local min="${in_time#*:}"
        local cmd="$OINKMASTER -Q -U $CONFIG -o $RULESDIR"

        local tmp="$(mktemp "$CRON_FILE.XXXXXXXXXX")"
        if [ -z "$tmp" ]; then
            write_error "`_ "Unable to create temp file"`"
            return
        fi

        printf "#autogenerated by alterator-snort\n" >"$tmp"
        case "$in_period" in
            daily)
            printf '%s %s * * * root %s\n' "$min" "$hour" "$cmd"
            ;;
            weekly)
            printf '%s %s * * %s root %s\n' "$min" "$hour" "$in_weekday" "$cmd"
            ;;
            monthly)
            printf '%s %s %s * * root %s\n' "$min" "$hour" "$in_monthday" "$cmd"
            ;;
        esac >>"$tmp"
        mv -f "$tmp" "$CRON_FILE"
    else
        rm -f "$CRON_FILE"
    fi
}

download_rules()
{
    if ! check_oinkconf_url; then
        write_error "`_ "URL not specified"`"
        return 1
    fi

    "$OINKMASTER" -Q -U $CONFIG -o "$RULESDIR" ||
        write_error "`_ "Download rules failed"`"
}

on_message()
{
    [ -n "$RULESDIR" ] || set_rules_dir
	case "$in_action" in
        type)
        write_type_item start_date date
        write_type_item start_time time
        write_type_item end_date date
        write_type_item end_time time
        write_type_item time time
		;;
		read)
		case "$in__objects" in
			state_enabled)
            read_state
            ;;
            rule-description)
            [ -n "$in_rules" ] &&
            write_string_param rule_description "$(read_rule_description "$in_rules")"
			;;
            dates)
            read_dates
            ;;
            download-data)
            local oinkcode="$(read_oinkcode)"
            if [ -n "$oinkcode" ]; then
                write_string_param rules_url 'oinkcode'
                write_string_param oinkcode "$oinkcode"
            else
                local custom_url="$(read_oinkconf_url)"
                if [ -n "$custom_url" ]; then
                    write_string_param rules_url 'custom'
                    write_string_param custom_url "$custom_url"
                fi
            fi
            read_cron_data
            ;;
		esac
		;;
		write)
		case "$in__objects" in
			state_enabled)
            [ -n "$in_state_enabled" ] && write_state "$in_state_enabled"
			;;
            rules-list)
            [ -n "$in_rules_list" ] && write_ruleset "$(rules_list_to_ruleset "$in_rules_list")"
            ;;
            download-now)
            download_rules
            ;;
            download-data)
            case "$in_rules_url" in
                oinkcode)
                if [ -n "$in_oinkcode" ]; then
                    write_oinkcode "$in_oinkcode"
                elif test_bool "$in_auto_update"; then
                    write_error "`_ "Oinkcode not specified!"`"
                    return
                fi
                ;;
                custom)
                if [ -n "$in_custom_url" ]; then
                    write_oinkconf_url "$in_custom_url"
                elif test_bool "$in_auto_update"; then
                    write_error "`_ "URL not specified!"`"
                    return
                fi
                ;;
                *)
                ;;
            esac
            write_cron_data
            ;;
		esac
		;;
		list)
		case "${in__objects##*/}" in
            avail_rules) #list_rules | write_enum
            for i in $(list_rules); do
                write_table_item \
                name "$i" \
                rule "$i"
            done
			;;
            avail_unused_rules)
            for i in $(list_rules_unused); do
                write_table_item \
                name "$i" \
                rule "$i"
            done
            ;;
            avail_weekday)
            list_weekday
            ;;
            events)
            [ -n "$in_start_date" -a -n "$in_end_date" -a -n "$in_start_time" -a -n "$in_end_time" ] &&
            list_events "$in_start_date" "$in_start_time" "$in_end_date" "$in_end_time"
            ;;
            details)
            [ -n "$in_details" -a "$in_details" != '#f' -a -n "$in_start_date" -a -n "$in_end_date" \
                                                        -a -n "$in_start_time" -a -n "$in_end_time" ] &&
            list_details "$in_details" "$in_start_date" "$in_start_time" "$in_end_date" "$in_end_time"
            ;;
		esac
		;;
	esac
}

message_loop

