# Режимы зачистки
SECDEL_MODE=0
FSTRIM_MODE=1

ITEMS_DELIMER=','

# Режимы защиты Astra Linux
declare -A ASTRA_MODE=(
    [0]=Base
    [1]=Advanced
    [2]=Maximum
)

# Группа администраторов
declare ADMINS_GROUP="astra-admin"
# Путь к файлу исключений очистки
declare EXCLUSION_LIST_PATH='/tmp/astra-clean-exclusions.txt'
# Путь к файлу логов событий
declare ASTRA_EVENTS_LOG_PATH='/parsec/log/astra/events'

#================================================================================
# Функции
#================================================================================
## @getAstraMode
## @brief - Функция вернёт текущий режим защиты Astra Linux
## @return Вернёт текущий режим защиты Astra Linux
function getAstraMode() {
    echo $(astra-modeswitch get)
}

## @checkExecutionAllowed
## @brief - Функция проверит, допустимо ли использование функционала зачистки
function checkExecutionAllowed() {
    if [ $(getAstraMode) == 0 ]; then
        echo "Current Astra Linux mode '${ASTRA_MODE[0]}'. Minimum mode '${ASTRA_MODE[1]}' required!"
        exit 1
    fi
}

## findFiles
## @brief - Функция поиска файлов
## @param $1 - Директория, в которой нужно искать
## @param $2 - Шаблон имени искомых файлов
## @param $3 - Заполняемый список файлов
function findFiles() {
    local FIND_PATH="$1"
    local FILE_NAME="$2"
    declare -n OUT_FILES_LIST=$3

    local OLDIFS=$IFS
    IFS=$ITEMS_DELIMER

    local DIR_FILES=( $(find "$FIND_PATH" -type f -name "$FILE_NAME" -print0 | tr "\0" "$ITEMS_DELIMER") )

    for File in ${DIR_FILES[@]}
    do
        OUT_FILES_LIST[${#OUT_FILES_LIST[*]}]="$File"
    done

    IFS=$OLDIFS
}

## @useMode
## @brief - Функция вернёт тип зачистки для указанной директории
## @param $1 - Директория, подлежащая зачистке
## @return Вернёт тип зачистки для указанной директории
function useMode() {
    local DRIVE_NAME="$1"
    local Mode=0

    if $(driveIsHDD $DRIVE_NAME); then
        # HDD
        Mode=$SECDEL_MODE
    else
        # SSD
        if $(driveIsRemovable $DRIVE_NAME); then
            Mode=$SECDEL_MODE
        else
            if $(driveSupportTRIM $DRIVE_NAME); then
                Mode=$FSTRIM_MODE
            else
                Mode=$SECDEL_MODE
            fi # driveSupportTRIM
        fi # driveIsRemovable
    fi # driveIsHDD

    # Да, можно проще! Но так нагляднее. Код читается куда чаще чем пишется.

     echo $Mode
}
#================================================================================
# User
#================================================================================
## @userList
## @brief - Функция вернёт полный список пользователей системы
## @return Вернёт полный список пользователей системы
function userList() {
    declare -n OUT_USERS=$1
    OUT_USERS=( $(awk -F: '{ print $1 }' /etc/passwd) )
}

## @adminList
## @brief - Функция вернёт список администраторов системы
## @return Вернёт список администраторов системы
function adminList() {
    declare -n OUT_ADMINS=$1
    declare -a Users
    userList Users

    for User in "${Users[@]}"
    do
         if [[ "$(groups $User)" =~ (' '|^)"$ADMINS_GROUP"(' '|$) ]]; then
            OUT_ADMINS[${#OUT_ADMINS[*]}]="$User"
        fi
    done
}

## @userGetHomeDir
## @brief - Функция вернёт домашнюю директорию пользователя
## @param $1 - Имя пользователя
## @return Вернёт домашнюю директорию пользователя
function userGetHomeDir() {
    echo "$(awk -F ':' -v username="$1" '{if ($1==username) print $6}' /etc/passwd)"
}

## @userGetLastSessionStart
## @brief - Функция вернёт время последнего входа пользователя
## @param $1 - Имя пользователя
## @return Вернёт время последнего входа пользователя (timestamp)
function userGetLastSessionStart() {
    local USER="$1"
    local sessioStartTime=0

    # !ВНИМАНИЕ!
    # 1) Порядок полей в json объекте не гарантируется. Бедем использовать 2 grep'а с регулярками вмето одного;
    # 2) Формат $ASTRA_EVENTS_LOG_PATH меняется от версии к версии. Между идентификатором и значением может быть 0 или 1 пробел. Для поддержки 2-х форматов в регулярку добавил: ' ?';

    # Сперва отфильтруем все события успешного логина для пользователя $USER
    # И возьмём последнее
    local line=$(grep -E "\"message_id\": ?\"successed_authorization\"" "$ASTRA_EVENTS_LOG_PATH" | grep -E "\"user\": ?\"$USER\"" | tail -1)
    local regex='(unixtime)": ?("([^""]+)"|\[[^[]+])'
    # Если строка включает соответствие шаблону
    if [[ $line =~ $regex ]]; then
        # Получаем 3-ю группу, распознанную регуляркой
        sessioStartTime=${BASH_REMATCH[3]}
    #        0: "unixtime":"1663915507"
    #        1: unixtime
    #        2: "1663915507"
    #        3: 1663915507
    fi

    echo $sessioStartTime
}

## @userGetLastSessionEnd
## @brief - Функция вернёт время последнего выхода пользователя
## @param $1 - Имя пользователя
## @return Вернёт время последнего выхода пользователя (timestamp)
function userGetLastSessionEnd() {
    local USER="$1"
    local sessioEndTime=0

    # !ВНИМАНИЕ!
    # 1) Порядок полей в json объекте не гарантируется. Бедем использовать 2 grep'а с регулярками вмето одного;
    # 2) Формат $ASTRA_EVENTS_LOG_PATH меняется от версии к версии. Между идентификатором и значением может быть 0 или 1 пробел. Для поддержки 2-х форматов в регулярку добавил: ' ?';

    # Сперва отфильтруем все события завершения сессии для пользователя $USER
    # И возьмём последнее
    local line=$(grep -E "\"message_id\": ?\"user_session\"" "$ASTRA_EVENTS_LOG_PATH" | grep -E "\"user\": ?\"$USER\"" | tail -1)
    local regex='(end_time)": ?("([^""]+)"|\[[^[]+])'
    # Если строка включает соответствие шаблону
    if [[ $line =~ $regex ]]; then
        # Получаем 3-ю группу, распознанную регуляркой
        sessioEndTime=${BASH_REMATCH[3]}
#        0: end_time":"1663915505"
#        1: end_time
#        2: "1663915505"
#        3: 1663915505
    fi

    echo $sessioEndTime
}

#================================================================================
# SecDel
#================================================================================
## @secdelIsEnable
## @brief - Функция проверит активен ли SecDel
## @return Вернёт признак активности SecDel
function secdelIsEnable() {
    [ "$(astra-secdel-control is-enabled)" != "ВЫКЛЮЧЕНО" ]
}

## @secdelPartitionsList
## @brief - Функция вернёт список разделов, контролируемых SecDel
## @return Вернёт список разделов, контролируемых SecDel
function secdelPartitionsList() {
    local outList=()

    if $(secdelIsEnable); then
        outList=( $(astra-secdel-control status | tail -1 | awk '{ $1=""; print $0 }') )
    fi

    echo ${outList[@]}
}

## @secdelEnable
## @brief - Функция активирует SecDel
## Для всех разделов ОС типа ext* xfs будут добавлены флаги монтирования secdel
## Все разделы ОС типа ext* xfs будут перемонтированы для применения добавленного флага secdel
function secdelEnable() {
    astra-secdel-control enable
}

## @secdelDisable
## @brief - Функция деактивирует SecDel
## Для всех разделов ОС типа ext* xfs будут удалены флаги монтирования secdel
## Все разделы ОС типа ext* xfs будут перемонтированы для применения удалённого флага secdel
function secdelDisable() {
    astra-secdel-control disable
}

## @secdelSupportTypeFS
## @brief - Функция проверит, поддерживает ли SecDel указанную файловую систему
## @param $1 - Имя файловой системы
## @return Вернёт признак того что SecDelg поддерживает указанную файловую систему
function secdelSupportTypeFS() {
    local TYPE_FS="$1"
    local SUPPORTED_TYPES=( ext2 ext3 ext4 xfs )
    [[ ${SUPPORTED_TYPES[@]} =~ $TYPE_FS ]]
}
#================================================================================
# Partition
#================================================================================
## @partitionFSByObjectPath
## @brief - Функция вернёт раздел ФС по имени объекта на нём
## @param $1 - Путь к объекту (директории\файлу)
## @return Вернёт раздел ФС объекта
function partitionFSByObjectPath() {
    local outPartition="$1"

    while [ 1 ]
    do
        # Запрашиваем данные раздела по пути
        line=( $(df --output=source,fstype,target "$outPartition" | tail -1) )
        # Получаем точку монтирования
        outPartition=${line[2]}

        # Игнорируем виртуальные файловые системы
        [ "${line[0]}" != "${line[1]}" ] && break
        # Если ситема виртуальная, опускаемся на уровень ниже
        outPartition="$(dirname "${outPartition}")"
    done

    echo $outPartition
}

## @partitionIsLogicalVolume
## @brief - Функция проверит является ли раздел логическим томом
## @param $1 - Раздел ФС
## @return Вернёт признак того что раздел является логическим томом
function partitionIsLogicalVolume() {
    local PARTITION="$1"
    [ "$(lsblk -n -o PATH,TYPE | awk -v part="$PARTITION" '{ if ($1==part) print $2 }')" == 'lvm' ]
}

## @partLogicalGetPhisical
## @brief - Функция вернёт имя физического раздела по имени логического
## @param $1 - Логический раздел
## @return Вернёт признак того что раздел является логическим томом
function partLogicalGetPhisical() {
    local PARTITION="$1"
    local phisicalPart=""

    if $(partitionIsLogicalVolume "$PARTITION"); then
        phisicalPart=$(lvdisplay -am "$PARTITION" | grep 'Physical volume' | awk '{ print $3}')
    else
        phisicalPart="$PARTITION"
    fi

    echo "$phisicalPart"
}

## @partitionGetTypeFS
## @brief - Функция вернёт тип ФС раздела
## @param $1 - Раздел ФС
## @return Вернёт тип ФС
function partitionGetTypeFS() {
    local PARTITION="$1"
    echo $(df --output=fstype "$PARTITION" | tail -1)
}

## @partitionGetMountFlags
## @brief - Функция вернёт флаги, с которыми смонтирован раздел ФС
## @param $1 - Раздел ФС
## @return Вернёт флаги монтирования
function partitionGetMountFlags() {
    local PARTITION=$1
    echo "$(mount -v | grep $PARTITION | sed -e 's/^.*(\([^()]*\)).*$/\1/')"
}

## @partitionRemountWithSecDel
## @brief - Функция перемонтирует раздел с флагом secdel (если это возможно и ещё не сделано)
## @param $1 - Раздел ФС
## @return Вернёт признак успеха операции
function partitionRemountWithSecDel() {
    local PARTITION=$1
    local mountFlags="$(partitionGetMountFlags $PARTITION)"
    local outResult=false

    # Если раздел не смонтирован с secdel
    if [[ $mountFlags != *"secdel"* ]]; then
        # И тип файловой системы раздела поддерживается secdel
        if $(secdelSupportTypeFS "$(partitionGetTypeFS $PARTITION)"); then
            # Перемонтируем его с параметром secdel
            Err=$(mount -o remount,secdel $PARTITION 2>&1 >/dev/null)
            if [ -z "$Err" ]; then outResult=true; fi
        fi
    fi

    [ $outResult == true ]
}

## @partitionRemountWithoutSecDel
## @brief - Функция перемонтирует раздел без флага secdel (если раздел смонтирован с ним)
## @param $1 - Раздел ФС
## @return Вернёт признак успеха операции
function partitionRemountWithoutSecDel() {
    local PARTITION="$1"
    local mountFlags="$(partitionGetMountFlags $PARTITION)"
    local outResult=false

    # Если раздел смонтирован с secdel
    if [[ $mountFlags == *"secdel"* ]]; then
        # Удалим флаг secdel
        mountFlags=${mountFlags/',secdel'}
        mountFlags=${mountFlags/'secdel,'}
        # Перемонтируем его без параметра secdel
        Err=$(mount -o remount,$mountFlags $PARTITION 2>&1 >/dev/null)
        if [ -z "$Err" ]; then outResult=true; fi
    fi

    [ $outResult == true ]
}
#================================================================================
# Drive
#================================================================================
## @drivePartitionByPartFS
## @brief - Функция вернёт раздел диска по имени раздела ФС
## @param $1 - Имя раздела ФС
## @return Вернёт раздел диска
function drivePartitionByPartFS() {
    local PARTITION_FS="$(partitionFSByObjectPath "$1")"
    echo $(lsblk -n -o PATH,MOUNTPOINT | awk -v systempart="$PARTITION_FS" '{ if ($2==systempart) print $1 }')
}

## @driveNameByPartition
## @brief - Функция вернёт имя диска указанного раздела ФС
## @param $1 - Раздел ФС
## @return Вернёт имя диска указанного раздела ФС
function driveNameByPartition() {
    local PARTITION="$1"
    # Логический раздел нужно преобразовать в физический
    if $(partitionIsLogicalVolume "$PARTITION"); then
        PARTITION=$(partLogicalGetPhisical "$PARTITION")
    fi

    PARTITION=${PARTITION#/dev/}

    local Drive=$(readlink /sys/class/block/$PARTITION)
    Drive=${Drive%/*}
    Drive=/dev/${Drive##*/}

    if [ ! -b $Drive ]; then Drive=""; fi

    echo $Drive
}

## @driveIsHDD
## @brief - Функция проверит, является ли указанный диск является HDD
## @param $1 - Имя диска
## @return Вернёт признак того, что диск HDD
function driveIsHDD() {
    local DRIVE_NAME="$1"
    DRIVE_NAME=${DRIVE_NAME#/dev/}

    [ $(cat /sys/block/$DRIVE_NAME/queue/rotational) == 1 ]
}

## @driveIsRemovable
## @brief - Функция проверит, является ли указанный диск съёмным носителем
## @param $1 - Имя диска
## @return Вернёт признак того, что диск является съёмным носителем
function driveIsRemovable() {
    local DRIVE_NAME="$1"
    DRIVE_NAME=${DRIVE_NAME#/dev/}

    [ $(lsblk -n -o NAME,RM | awk -v drivename="$DRIVE_NAME" '{ if ($1==drivename) print $2 }') == 1 ]
}

## @driveSupportTRIM
## @brief - Функция проверит, поддерживает ли указанный диск TRIM
## @param $1 - Имя диска
## @return Вернёт признак того, что диск поддерживает TRIM
function driveSupportTRIM() {
    local DRIVE_NAME="$1"
    DRIVE_NAME=${DRIVE_NAME#/dev/}

    local res=( $(lsblk -n -b -o NAME,DISC-GRAN,DISC-MAX --discard | awk -v drivename="$DRIVE_NAME" '{ if ($1==drivename) print $2,$3 }') )

    [[ ${#res[@]} == 2 && ${res[0]} != 0 && ${res[1]} != 0 ]]
}
#================================================================================
# Exclusions
#================================================================================
## @exclusionsAdd
## @brief - Функция добавит файл в список исключений зачистки
## @param $1 - Путь к файлу
function exclusionsAdd(){
    if [ ! -f "$EXCLUSION_LIST_PATH" ]; then
        echo "$EXCLUSION_LIST_PATH" > "$EXCLUSION_LIST_PATH"
    fi

    echo "$@" >> "$EXCLUSION_LIST_PATH"
}

## @exclusionsGet
## @brief - Функция вернёт перечень исключений, разделённых \n
function exclusionsGet() {
    if [ -f "$EXCLUSION_LIST_PATH" ]; then
        echo $(cat "$EXCLUSION_LIST_PATH")
    fi
}

## @exclusionsContain
## @brief - Функция проверит, входит ли указанный файл в список исключений
## @param $1 - Путь к файлу
## @return Вернёт признак того, что файл входит в список исключений
function exclusionsContain() {
    local PATH="$1"
    local IsContain=false

    if [ -f "$EXCLUSION_LIST_PATH" ]; then
        while read -r Exclusion
        do
            if [ "$PATH" == "$Exclusion"  ]; then
                IsContain=true
                break;
            fi
        done < "$EXCLUSION_LIST_PATH"
    fi

    [ $IsContain == true ]
}

## @exclusionsClear
## @brief - Функция очистит список исключений
function exclusionsClear() {
    if [ -f "$EXCLUSION_LIST_PATH" ]; then
        rm "$EXCLUSION_LIST_PATH"
    fi
}
#================================================================================
# Processing
#================================================================================
## @fileNeedBeProcessed
## @brief - Функция проверит, нужно ли обрабатывать указанный файл
## @param $1 - Путь к файлу
## @return Вернёт признак того, что файл требуется обработать
function fileNeedBeProcessed() {
    local FILE_PATH="$1"
    [ -f "$FILE_PATH" ] && ! $(exclusionsContain "$FILE_PATH")
}

## @processFile
## @brief - Функция обработает указанный файл (в дефолтной реализации удалит [возможно пользовательское расширение])
## @param $1 - Путь к файлу
## Для изменения дефолтного поведения, допускается замена или модификация данной функции Администратором СЗИ
function processFile() {
    local FILE_PATH="$1"
    if $(fileNeedBeProcessed "$FILE_PATH"); then
         rm -f "$FILE_PATH"
#         echo "Remove: $FILE_PATH"
#     else
#         echo "Ignore: $FILE_PATH"
    fi
}

## @processFilesSecDel
## @brief - Функция обработает файлы, используя функционал SecDel
## @param $1 - Список файлов на обработку
function processFilesSecDel() {
    declare -n FILES=$1

    if [ ${#FILES[@]} -gt 0 ]; then
        # Перечень смонтированных вручную разделов
        local RemountedPartitions=()

        for File in "${FILES[@]}"
        do
            # Получаем раздел системы, на котором находится File
            local SystemPartition=$(partitionFSByObjectPath "$File")
            # Если отдел перемонтировался с secdel
            if $(partitionRemountWithSecDel "$SystemPartition"); then
                # Запоминаем этот раздел для последующего перемонтирования
                RemountedPartitions[${#RemountedPartitions[*]}]="$SystemPartition"
            fi

            # Обрабатываем файл
            processFile "$File"
        done

        # Перемонтируем все разделы, которые были смонтированы нами вручную
        for Partition in "${RemountedPartitions[@]}"
        do
            partitionRemountWithoutSecDel "$Partition"
        done
    fi
}

## @processFilesFSTrim
## @brief - Функция обработает файлы, используя функционал FSTrim
## @param $1 - Список файлов на обработку
function processFilesFSTrim() {
    declare -n FILES=$1

    if [ ${#FILES[@]} -gt 0 ]; then
        local TrimPatitionList=()

        for File in "${FILES[@]}"
        do
            # Получаем раздел системы, на котором находится File
            local SystemPartition=$(partitionFSByObjectPath "$File")
            TrimPatitionList[${#TrimPatitionList[*]}]="$SystemPartition"

            # Обрабатываем файл
            processFile "$File"
        done

        # Формируем уникальный список разделов
        TrimPatitionList=( $(echo "${TrimPatitionList[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ') )
        # Применяем TRIM ко всем затронутым разделам
        for Partition in "${TrimPatitionList[@]}"
        do
            echo $(fstrim -v "$Partition")
        done
    fi
}

## @processDirsSecDel
## @brief - Функция обработает директории, используя функционал SecDel
## @param $1 - Список директорий на обработку
function processDirsSecDel() {
    declare -n DIRS=$1

    if [ ${#DIRS[@]} -gt 0 ]; then
        local Files=()

        for Dir in "${DIRS[@]}"
        do
            if [ -d "$Dir" ]; then
                findFiles "$Dir" "*" Files
            fi
        done

        processFilesSecDel Files
    fi
}

## @processDirsFSTrim
## @brief - Функция обработает директории, используя функционал FSTrim
## @param $1 - Список директорий на обработку
function processDirsFSTrim() {
    declare -n DIRS=$1

    if [ ${#DIRS[@]} -gt 0 ]; then
        local Files=()

        for Dir in "${DIRS[@]}"
        do
            if [ -d "$Dir" ]; then
                findFiles "$Dir" "*" Files
            fi
        done

        processFilesFSTrim Files
    fi
}
