#!/usr/bin/python3
# -*- coding: utf8 -*-

import os
import sys
import pwd
import grp
import uuid
import subprocess
import io
import json
import syslog
from ctypes import *
import glob
import stat
import locale
import gettext
import sqlite3
from datetime import datetime
import time
import socket
import struct
import fcntl
import xml.etree.ElementTree as XmlTree


try:
    # define current locale
    if -1 != locale.getlocale()[0].find('ru'):
        # load Russian translation
        ru_lang = gettext.translation('snaecctl', '/opt/secretnet/share/locale')
        ru_lang.install()
        _ = ru_lang.gettext

        # translate main messages of argparse
        def translate_argparse(text):
            text = text.replace('usage', 'использование')
            text = text.replace('show this help message and exit',
                                'показать это справочное сообщение и выйти')
            text = text.replace('error:', 'ошибка:')
            text = text.replace('the following arguments are required:',
                                'требуются следующие аргументы:')
            text = text.replace('too few arguments', 'недостаточно аргументов')
            text = text.replace('not allowed with argument', 'не разрешён с аргументом')
            text = text.replace('one of the arguments %s is required',
                                'один из аргументов %s требуется')
            text = text.replace('optional arguments', 'опциональные аргументы')
            text = text.replace('positional arguments', 'позиционные аргументы')
            return text
    else:
        raise Exception
except:
    def _(message):
        return message

    def translate_argparse(text):
        return text


gettext.gettext = translate_argparse


import argparse


# Mark a message for translation, but do not apply the translation here (Deferred translations).
# This is necessary to display the message in Russian, write to the log - in English.
def log_(message): return message


try:
    from subprocess import DEVNULL
except ImportError:
    DEVNULL = open(os.devnull, 'wb')


class InvalidArgument(ValueError):
    pass


def is_module_load_func():
    with io.open('/proc/modules', 'r') as modules_file:
        content = modules_file.read()
        ret = content.find('snbase')
        if ret == -1:
            return False
        return True
    return False


# выводит сообщение, только если не установлен флаг NO_VERBOSE_FLAG
def print_msg(message):
    if not NO_VERBOSE_FLAG:
        print(message)


# Подсвечивать вывод сообщений в консоль
class Color:
    ENDC = '\033[0m'

    @staticmethod
    def purple(msg):
        return '\033[95m' + msg + Color.ENDC

    @staticmethod
    def blue(msg):
        return '\033[94m' + msg + Color.ENDC

    @staticmethod
    def cyan(msg):
        return '\033[96m' + msg + Color.ENDC

    @staticmethod
    def green(msg):
        return '\033[92m' + msg + Color.ENDC

    @staticmethod
    def yellow(msg):
        return '\033[93m' + msg + Color.ENDC

    @staticmethod
    def red(msg):
        return '\033[91m' + msg + Color.ENDC

    @staticmethod
    def bold(msg):
        return '\033[1m' + msg + Color.ENDC

    @staticmethod
    def underline(msg):
        return '\033[4m' + msg + Color.ENDC

    @staticmethod
    def white(msg):
        return '\033[97m' + msg + Color.ENDC


# класс ошибок утилиты snaecctl. Содержит все возможные коды ошибок и их описания
# можно бросать как исключение - наследник RuntimeError
class SnaecctlError(RuntimeError):
    Success = 0  # успех
    UnknownError = 1  # неизвестная ошибка
    InvalidArgument = 2  # неправильные аргументы
    OperationCanceled = 3  # операция прервана
    NoUsersInSystem = 4  # нет таких пользователей в системе
    NoGroupsInSystem = 5  # нет таких групп в системе
    KernelIO = 6  # ошибка взаимодействия с ядром
    NoUsersInRule = 7  # в правиле/белом списке нет таких пользователей
    NoGroupsInRule = 8  # в правиле/белом списке нет таких групп
    NoResourcesInRule = 9  # в правиле/списке исключений нет таких ресурсов
    InvalidConfig = 10  # неправильный конфиг
    ConfigNotFound = 11  # конфига не существует
    ConfigWriteFailed = 12  # ошибка записи в конфиг
    CCEnabled = 13  # централизованное управление включено. Редактирование белого списка запрещено
    RuleIsNotExist = 14  # правила с таким именем не существует
    ResourceListEmpty = 15  # список ресурсов пуст
    SystemImmutableRule = 16  # системное неизменяемое правило
    RuleAllowedForAll = 17  # правило разрешено для всех пользователей
    RuleExist = 18  # правило с таким именем уже существует
    InvalidDateTime = 19  # неверный формат даты/времени
    AideFailed = 20  # ошибка расчёта КЦ с помощью snaide
    CrcFailed = 21  # нарушение целостности правил
    LimitedMode = 22  # ограниченный режим
    SnservFailed = 23  # ошибка при обращении к snserv
    ReloadRequired = 24  # требуется перерасчёт КЦ ЗПС перед редактированием правил
    ResourceNotExist = 25 # ресурс отсутствует в системе

    # описания ошибок
    error_desc = {
        UnknownError: _('unknown error'),
        InvalidArgument: _('invalid argument'),
        OperationCanceled: _('operation canceled'),
        NoUsersInSystem: _('There is no user `{0}`'),
        NoGroupsInSystem: _('There is no group `{0}`'),
        KernelIO: _('Kernel interaction error'),
        NoUsersInRule: _('in the rule there are no users: {0}'),
        NoGroupsInRule: _('in the rule there are no group: {0}'),
        NoResourcesInRule: _('in the rule there are no resources: {0}'),
        InvalidConfig: _('Invalid config {0}'),
        ConfigNotFound: _('{0} does not exist'),
        ConfigWriteFailed: _('Error writing to config file `{0}`'),
        CCEnabled: _('centralized control is enabled. '
                     'Editing is prohibited.'),
        RuleIsNotExist: _('rule `{0}` does not exists'),
        ResourceListEmpty: _('resource list is empty. Operation aborted'),
        SystemImmutableRule: _('rule `{0}` is system rule. Its change is impossible'),
        RuleAllowedForAll: _('Rule `{0}` is allowed for all users. '
                             'To apply the rule only to certain users, use the command `update`'),
        RuleExist: _('rule {0} already exists'),
        InvalidDateTime: _('invalid date/time. Example: {0}. Date is required, '
                           'Hour, minute and second are optional. '
                           'Year must be in 1900..9999. Month must be in 1..12. '
                           'Day must be in 1..31. Hour must be in 0..23. '
                           'Minute and second must be in 0..59.'),
        AideFailed: _('integrity calculation error.'),
        CrcFailed: _('violation of the integrity of the rules'),
        LimitedMode: _('settings cannot be changed. Secret Net LSP operates in limited mode'),
        SnservFailed: _('error communicating with the service management'),
        ReloadRequired: _('An update of AEC IC DB is required. '
                          'Detected integrity violation in config files:\n{}'),
        ResourceNotExist: _('resource `{0}` does not exist')
    }

    # конструктор
    # code - код ошибки
    # invalid_list - опциональный список ошибочных элементов (несуществующих пользователей,
    # групп, файлов, правил и т.д.)
    # description - опциональное описание ошибки. Если не задано, берётся значение по умолчанию
    # для данного кода ошибки
    def __init__(self, code, invalid_list=None, description=None):
        self.code = code
        self.invalid_list = invalid_list
        self.description = description

    # выводит сообщения об ошибках
    def print_message(self):
        if self.code == self.Success:
            return

        if NO_VERBOSE_FLAG:
            if self.invalid_list:
                print(('{0}'.format('\n'.join(self.invalid_list))))
            return

        if self.description is None:
            message = SnaecctlError.error_desc[self.code]
        else:
            message = self.description
        if self.invalid_list is None:
            print((Color.red(_('Error: {0}').format(message))))
        else:
            print((Color.red(_('Error: {0}').format(message.format(', '.join(self.invalid_list))))))


class SyslogMessage:
    def __init__(self, level, msg_id, msg, *args, **kwargs):
        self.level = level
        self.msg_id = msg_id
        self.msg = msg
        self.args = args
        self.kwargs = kwargs


class SnSyslog:
    AecManagementCategory = 16

    IdMsgEnableRule = 1150
    IdMsgDisableRule = 1151
    IdMsgAddUser = 1152
    IdMsgRmUser = 1153
    IdMsgCreateRule = 1154
    IdMsgRmRule = 1155
    IdMsgChangeRule = 1156
    IdMsgChangeRuleResources = 1162
    IdMsgErrorCrcRule = 1165
    IdMsgRecalcCrcRule = 1166

    def __init__(self):
        self.lib = CDLL("/opt/secretnet/lib/libsnsyslog.so")
        self.msg_cache = list()

    @staticmethod
    def print_color(msg, level):
        if level <= syslog.LOG_ERR:
            print_msg(Color.red(msg))
        elif level == syslog.LOG_WARNING:
            print_msg(Color.white(msg))
        else:
            print_msg(msg)

    def log(self, level, msg_id, msg, *args, **kwargs):
        try:
            self.lib.sn_openlog('snaecctl'.encode('ascii'), syslog.LOG_PID, syslog.LOG_DAEMON)
            self.lib.sn_syslog(level, '[{0}] {1}'.
                               format(SnSyslog.AecManagementCategory << 24 | msg_id << 8 | level,
                                      msg.format(*args, **kwargs)).encode('utf-8', errors='ignore'))
            self.lib.sn_closelog()
        except:
            pass
        try:
            SnSyslog.print_color('{0}'.format(_(msg).format(*args, **kwargs)), level)
        except:
            pass

    def apply_cache(self):
        self.lib.sn_openlog('snaecctl'.encode('ascii'), syslog.LOG_PID, syslog.LOG_DAEMON)
        for msg in self.msg_cache:
            try:
                self.lib.sn_syslog(msg.level, '[{0}] {1}'.format(
                    SnSyslog.AecManagementCategory << 24 | msg.msg_id << 8 | msg.level,
                    msg.msg.format(*msg.args, **msg.kwargs)).encode('utf-8', errors='ignore'))
            except:
                pass
            try:
                SnSyslog.print_color('{0}'.format(_(msg.msg).format(*msg.args, **msg.kwargs)),
                                     msg.level)
            except:
                pass

        self.lib.sn_closelog()
        del self.msg_cache[:]

    def add_log_to_cache(self, level, msg_id, msg, *args, **kwargs):
        self.msg_cache.append(SyslogMessage(level, msg_id, msg, *args, **kwargs))

    def clean_cache(self):
        del self.msg_cache[:]


# функция блокировки для предотвращения одновременной работы нескольких экземпляров скрипта
def snaecctl_lock():
    try:
        fd = os.open('/opt/secretnet/var/tmp/snaecctl_lock', os.O_RDWR | os.O_CREAT )
    except:
        return False

    try:
        fcntl.flock(fd, fcntl.LOCK_EX)
        return True
    except:
        return False


# класс для взаимодействия с snserv
class SnservConnection:
    def __init__(self):
        try:
            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
            self.sock.connect('/opt/secretnet/var/tmp/sn_sock')
            self.id = 0

        except Exception:
            raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                _('failed connect to management service'))

    def send(self, xml_request):
        try:
            self.id = uuid.uuid4().hex
            xml_request.set('id', self.id)
            str_request = XmlTree.tostring(xml_request, encoding='utf8', method='xml')

            self.sock.send(struct.pack('B', 27))
            self.sock.send(struct.pack('ii', 0, len(str_request)))
            self.sock.send(str_request)
        except Exception:
            raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                _('failed to send request to management service'))

    def recv(self):
        try:
            # проверить синхропосылку
            data = self.sock.recv(1)
            syncro = struct.unpack("B", data)
            if syncro[0] != 27:
                raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                    _('protocol error interact with the management service'))
            # получить заголовок
            data = self.sock.recv(8)
            if not data:
                raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                    _('failed to receive message header from management service'))
            response_len = struct.unpack("ii", data)

            # получить тело сообщения
            data = self.sock.recv(response_len[1])
            if not data:
                raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                    _('failed to receive message from management service'))

            # получить заголовок полезного сообщения
            data = self.sock.recv(8)
            if not data:
                raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                    _('failed to receive message header from management service'))
            response_len = struct.unpack("ii", data)

            # получить тело полезного сообщения
            data = self.sock.recv(response_len[1])
            if not data:
                raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                    _('failed to receive message from management service'))

            # проверить id ответа
            response = data.replace(b'\0', b'')
            xml_response = XmlTree.fromstring(response, XmlTree.XMLParser(encoding='utf-8'))
            for attr in xml_response.iter('response'):
                if list(attr.attrib.keys())[0] == 'id':
                    if list(attr.attrib.values())[0] == self.id:
                        return xml_response
                    else:
                        raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                            _('invalid response from management service'))

            raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                _('invalid response from management service'))
        except SnaecctlError as ex:
            raise ex
        except Exception as er:
            raise SnaecctlError(SnaecctlError.SnservFailed, None,
                                _('failed to receive a response from management service: '
                                  '{}').format(er))


# проверяет, в каком режиме работает LSP
def is_full_mode():
    request = XmlTree.Element("request")
    request.set('plugin', 'system')
    cmd = XmlTree.Element("cmd")
    cmd.set('name', 'get_mode')
    request.append(cmd)

    snserv_conn = SnservConnection()
    snserv_conn.send(request)
    response = snserv_conn.recv()

    for attr in response.iter('result'):
        if list(attr.attrib.keys())[0] == 'mode':
            if list(attr.attrib.values())[0] == 'full':
                return True
            else:
                return False

    return False


# получить политику блокировки
def get_lock_policy():
    try:
        conn = sqlite3.connect('/opt/secretnet/usr/db/sn_settings.db')
        c = conn.cursor()
        resp = c.execute('SELECT fsValue FROM system_plugin_policies '
                             'WHERE fsParam=\'system_lock\'')
        for row in resp:
            return row[0] == '1'
    except BaseException:
        return True


class AllUserGroupList:
    dict_uid = {}
    dict_gid = {}

    @staticmethod
    def get_uid(username):
        try:
            uid = AllUserGroupList.dict_uid.get(username)
            if not uid:
                uid = pwd.getpwnam(username).pw_uid
                AllUserGroupList.dict_uid[username] = uid
            return uid
        except KeyError:
            AllUserGroupList.dict_uid[username] = -1
            return -1

    @staticmethod
    def get_gid(group_name):
        try:
            gid = AllUserGroupList.dict_gid.get(group_name)
            if not gid:
                gid = grp.getgrnam(group_name).gr_gid
                AllUserGroupList.dict_gid[group_name] = gid
            return gid
        except KeyError:
            AllUserGroupList.dict_gid[group_name] = -1
            return -1

    @staticmethod
    def check_user_list(user_list):
        for user in user_list:
            try:
                pwd.getpwnam(user)
            except KeyError:
                raise SnaecctlError(SnaecctlError.NoUsersInSystem, {user})

    @staticmethod
    def check_group_list(group_list):
        for group in group_list:
            try:
                grp.getgrnam(group)
            except KeyError:
                raise SnaecctlError(SnaecctlError.NoGroupsInSystem, {group})


# kernel commands for AEC management
class KernelCommand:
    CONTROL_FILE = '/proc/snsecure/secure_env/control'
    ADD_RULE_CMD = 'ra'
    RM_RULE_CMD = 'rr'
    RM_ALL_RULES_CMD = 'rc'
    ADD_UID_CMD = 'ua'
    RM_UID_CMD = 'ur'
    ADD_GID_CMD = 'ga'
    RM_GID_CMD = 'gr'
    SEPARATOR_SYMBOL = ';'
    END_SYMBOL = '$'

    def __init__(self, command):
        self.command = command + self.SEPARATOR_SYMBOL

    def add_field(self, field):
        self.command += field + self.SEPARATOR_SYMBOL

    def send(self):
        self.command += self.END_SYMBOL
        KernelCommand.send_ready_cmd(self.command)

    @staticmethod
    def send_ready_cmd(command):
        if not is_module_load:
            return
        try:
            with open(KernelCommand.CONTROL_FILE, 'w') as file_control:
                file_control.write(command)
        except IOError as e:
            print_msg(Color.red(_('I/O error({0}): {1}').format(e.errno, e.strerror)))


# класс исключений, через который можно передавать массив
class ArrayException(RuntimeError):
    def __init__(self, array):
        self.array = array


class ListUtil:
    @staticmethod
    def add_lists(dst, src):
        if len(src):
            dst = list(set(dst + src))
        return dst

    @staticmethod
    def remove_list(dst, src):
        if len(src):
            invalid_uid = list(set(src) - set(dst))
            if len(invalid_uid) > 0:
                raise ArrayException(invalid_uid)
            dst = list(set(dst) - set(src))
        return dst

    @staticmethod
    def replace_list(old_list, new_list):
        removed_elements \
            = list(set(old_list) - set(new_list))
        added_resources \
            = list(set(new_list) - set(old_list))
        return added_resources, removed_elements


class NameHelper:
    @staticmethod
    def get_user_name(uid):
        try:
            return pwd.getpwuid(int(uid)).pw_name
        except KeyError:
            return 'unknown user'

    @staticmethod
    def get_group_name(gid):
        try:
            return grp.getgrgid(int(gid)).gr_name
        except KeyError:
            return 'unknown group'

    @staticmethod
    def get_group_by_username(username):
        try:
            return grp.getgrnam(username).gr_name
        except KeyError:
            return 'unknown user'

    @staticmethod
    def get_gid_by_username(username):
        try:
            return grp.getgrnam(username).gr_gid
        except KeyError:
            return 'unknown user'

    @staticmethod
    def get_gid_by_uid(uid):
        return NameHelper.get_gid_by_username(NameHelper.get_user_name(uid))

    @staticmethod
    def user_list_to_uid_list(user_list):
        uid_list = list()
        for username in user_list:
            uid = AllUserGroupList.get_uid(username)
            if uid >= 0:
                uid_list.append(uid)
        uid_list.sort()
        return uid_list

    @staticmethod
    def group_list_to_gid_list(group_list):
        gid_list = list()
        for groupname in group_list:
            gid = AllUserGroupList.get_gid(groupname)
            if gid >= 0:
                gid_list.append(gid)
        gid_list.sort()
        return gid_list


class FileDependencyHelper:
    @staticmethod
    def resolve_symlink(lib_path):
        return os.path.realpath(lib_path)

    @staticmethod
    def dependencies_list(file):
        try:
            list_lib = list()
            output_lib = subprocess.Popen(['ldd', file.strip()],
                                          stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
            for line in output_lib.stdout:
                list_lib.append(line.decode())
        except:
            return list()

        dep_list = list()
        for lib_path in list_lib:
            # parse library path
            try:
                start = lib_path.index('/')
                end = lib_path.index('(0x', start)
                lib_path = lib_path[start:end]
            except ValueError:
                continue

            dep_list.append(FileDependencyHelper.resolve_symlink(lib_path.strip()))

        return dep_list


class DirectoryHelper:
    # return list of all executable files in directory
    # if in_subdir True, method return executable files from all  subdirectories
    @staticmethod
    def get_executable_file_list(path_directory, auto_depend=True, recursive=False):
        file_list = os.listdir(path_directory)
        executable_file_list = list()

        for file in file_list:
            # define absolute path to file
            symlink = os.path.join(path_directory, file)
            abs_path = FileDependencyHelper.resolve_symlink(symlink)
            if not os.path.exists(abs_path) \
                    or (os.path.islink(symlink) and os.path.isdir(abs_path)):
                continue

            # if object is directory
            if os.path.isdir(abs_path):
                # add files from subdirectories to list  if required
                if recursive:
                    executable_file_list = ListUtil.add_lists(
                        executable_file_list,
                        DirectoryHelper.get_executable_file_list(abs_path, auto_depend,
                                                                 recursive))
                continue

            # add to list, if file is executable
            if os.stat(abs_path).st_mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH):
                executable_file_list = ListUtil.add_lists(executable_file_list, [abs_path])

                # add dependencies if necessary
                if auto_depend:
                    depends_list = FileDependencyHelper.dependencies_list(abs_path)
                    executable_file_list = ListUtil.add_lists(executable_file_list, depends_list)

        return executable_file_list

    # convert resource list (with directories) to resource list with files only.
    # in directories non-executable files are skipped
    @staticmethod
    def get_full_resource_list(input_resources, auto_depend=True, recursive=False,
                               resolve_symlink=True, print_warnings=True):
        resource_list = list()

        for resource in input_resources:

            if resolve_symlink:
                resource = FileDependencyHelper.resolve_symlink(os.path.abspath(resource))

            if not os.path.exists(resource):
                if print_warnings:
                    print_msg(Color.white(_('File `{0}` does not exist').format(resource)))
                continue
            elif os.path.isdir(resource):
                resource_list = ListUtil.add_lists(
                    resource_list, DirectoryHelper.get_executable_file_list(resource,
                                                                            auto_depend,
                                                                            recursive))
            else:
                resource_list = ListUtil.add_lists(resource_list, [resource])
                if auto_depend:
                    depends_list = FileDependencyHelper.dependencies_list(resource)
                    resource_list = ListUtil.add_lists(resource_list, depends_list)
        return resource_list


class FileConfig:
    DIRECTORY = '/opt/secretnet/etc/aec/'

    def save(self, config_name=None):
        if not config_name:
            config_name = self.config_name()

        json_string = json.dumps(self.__dict__,
                                 indent=4, sort_keys=True, ensure_ascii=False)
        with io.open(config_name, 'w', encoding='utf-8') as config_file:
            config_file.write(str(json_string))

    def load(self, config_name=None):
        if not config_name:
            config_name = self.config_name()
        try:
            with io.open(config_name, 'r', encoding='utf-8') as config_file:
                json_string = config_file.read()
                json_object = json.loads(json_string, object_hook=lambda d: type(self)(**d))
                self.__dict__ = json_object.__dict__

        except ValueError:
            raise SnaecctlError(SnaecctlError.InvalidConfig, {config_name})
        except IOError:
            raise SnaecctlError(SnaecctlError.ConfigNotFound, {config_name})


class AideConfig:
    TEMPLATE = '/opt/secretnet/share/aec/templates/aide.conf'
    DIRECTORY = os.path.join(FileConfig.DIRECTORY, 'aide-conf')
    NAME_TEMPLATE = os.path.join(DIRECTORY, '{0}.conf')
    DB_NAME_TEMPLATE = os.path.join(FileConfig.DIRECTORY, 'aide-db/{0}.db')
    DB_OUT_NAME_TEMPLATE = os.path.join(FileConfig.DIRECTORY, 'aide-db/{0}-out.db')
    COMMON_CONFIG_NAME_TEMPLATE = '/opt/secretnet/share/aec/templates/crc-rule.conf'
    COMMON_CONFIG_NAME = os.path.join(FileConfig.DIRECTORY, 'crc-rule.conf')
    COMMON_CONFIG_NAME_STDOUT = os.path.join(FileConfig.DIRECTORY, 'crc-rule-stdout.conf')
    PAM_WARNING_PATH = '/opt/secretnet/var/tmp/pam_messages/aec_warning'
    COMMON_DB_NAME = os.path.join(FileConfig.DIRECTORY, 'crc-rule.db')
    COMMON_DB_OUT_NAME = os.path.join(FileConfig.DIRECTORY, 'crc-rule-out.db')
    AIDE_BIN = '/opt/secretnet/bin/snaide'
    SNBLOCK_BIN = '/opt/secretnet/sbin/snblock'
    LOCK_REASON_FILE = '/opt/secretnet/var/tmp/lock_reason'
    INIT_MESSAGE = log_('AEC integrity control initialization')
    LOG_INTEGRITY_VIOLATION = 'detected integrity violation in object '
    DEFAULT_ALG='sha512'

    crc_algorithm = None


    crc_algorithm_values = {
        0: 'md5',
        1: 'gost3411_12',
        2: 'sha512'
    }

    # читает алгоритм хэширования из БД
    @staticmethod
    def get_db_crc_alg():
        conn = sqlite3.connect('/opt/secretnet/usr/db/sn.db')
        c = conn.cursor()

        c.execute('SELECT fiValue FROM aide_policy WHERE fsPolicy=\'alg\' ')

        try:
            crc_algorithm = AideConfig.crc_algorithm_values[c.fetchone()[0]]
        except KeyError:
            crc_algorithm = AideConfig.DEFAULT_ALG

        conn.close()

        return crc_algorithm

    # устанавливает алгоритм хэширования, беря значение из БД
    @staticmethod
    def set_db_crc_alg():
        AideConfig.crc_algorithm = AideConfig.get_db_crc_alg()

    # получает текущее значение алгоритма хэширования
    @staticmethod
    def get_crc_algorithm():
        if AideConfig.crc_algorithm:
            return AideConfig.crc_algorithm

        AideConfig.crc_algorithm = AideConfig.DEFAULT_ALG
        with open(AideConfig.COMMON_CONFIG_NAME) as f:
            for line in f:
                pos = line.find('ALG=')
                if pos == -1:
                    continue
                AideConfig.crc_algorithm = line[4:].strip()
                break

        if AideConfig.crc_algorithm != AideConfig.get_db_crc_alg():
            print((Color.white('\n*************\n'
                              + _('Run \'snaecctl reload\' to apply the new IC algorithm!!! '
                                  'Current algorithm: {}.').format(AideConfig.crc_algorithm)
                              + '\n*************\n')))

        return AideConfig.crc_algorithm

    @staticmethod
    def get_list():
        return os.listdir(AideConfig.DIRECTORY)

    # remove all configs and databases
    @staticmethod
    def remove_all():
        configs = glob.glob(AideConfig.NAME_TEMPLATE.format('*'))
        for config in configs:
            os.remove(config)
        databases = glob.glob(AideConfig.DB_NAME_TEMPLATE.format('*'))
        for db in databases:
            os.remove(db)

    # generate aide db for file config_name
    @staticmethod
    def generate_aide_db_by_uuid(uuid_file):
        aide_command = '{0} -i -c {1}'.format(AideConfig.AIDE_BIN,
                                              AideConfig.NAME_TEMPLATE.format(uuid_file))
        ret = os.system(aide_command)
        if ret:
            return ret
        os.rename(AideConfig.DB_OUT_NAME_TEMPLATE.format(uuid_file),
                  AideConfig.DB_NAME_TEMPLATE.format(uuid_file))

    def __init__(self, rule_uuid):
        self.uuid = rule_uuid
        self.aide_config_name = self.NAME_TEMPLATE.format(rule_uuid)
        self.aide_db_name = self.DB_NAME_TEMPLATE.format(rule_uuid)

    def generate_aide_db(self):
        AideConfig.generate_aide_db_by_uuid(self.uuid)

    def update_config(self, resources):
        with open(self.TEMPLATE, 'r') as template_file:
            template = template_file.read()

        ic_rules = ['={0}$ NORMAL\n'.format(resource.replace('[', '\\[').replace(' ', '\\ '))
                    for resource in resources]
        ic_rules_text = ''.join(ic_rules)

        # fill aide-config for rule
        with os.fdopen(os.open(self.aide_config_name, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600),
                       'w') as aide_config:
            aide_config.write(template.format(self.uuid, ic_rules_text,
                                              AideConfig.get_crc_algorithm()))

        self.generate_aide_db()

    def create(self, resources_list):
        self.update_config(resources_list)

    def remove(self):
        try:
            os.remove(self.aide_config_name)
        except OSError:
            pass
        try:
            os.remove(self.aide_db_name)
        except OSError:
            pass

    @staticmethod
    def generate_common_config(path, output):
        with open(AideConfig.COMMON_CONFIG_NAME_TEMPLATE, 'r') as template_file:
            template = template_file.read()

        # fill aide-config
        crc_alg = AideConfig.get_crc_algorithm()
        with os.fdopen(os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600), 'w')\
                as aide_config:
            aide_config.write(template.format(crc_alg, output))

    @staticmethod
    def generate_common_db():
        if not os.path.exists(AideConfig.COMMON_DB_NAME):
            sn_syslog.log(syslog.LOG_CRIT, SnSyslog.IdMsgRecalcCrcRule, AideConfig.INIT_MESSAGE)
            recalc_aide_configs()

        AideConfig.generate_common_config(AideConfig.COMMON_CONFIG_NAME, 'syslog')

        aide_command = '{0} -i -c {1}'.format(AideConfig.AIDE_BIN, AideConfig.COMMON_CONFIG_NAME)
        gen_code = os.system(aide_command)
        if gen_code:
            sn_syslog.add_log_to_cache(syslog.LOG_ERR, SnSyslog.IdMsgRecalcCrcRule,
                                       log_('configs checksum was recalculated with error {0}'),
                                       gen_code)
            return SnaecctlError.AideFailed

        # удалить предупрежедние о необходимости перерасчёта КЦ ЗПС
        if os.path.exists(AideConfig.PAM_WARNING_PATH):
            os.remove(AideConfig.PAM_WARNING_PATH)

        os.rename(AideConfig.COMMON_DB_OUT_NAME, AideConfig.COMMON_DB_NAME)
        return SnaecctlError.Success

    # Проверяет КЦ ЗПС и восстанавливает правила при необходимости. Пишет в журнал о нарушениях КЦ.
    # Блокируте ОС при нарушении.
    @staticmethod
    def check_common_db():
        aide_command = '{0} -C -c {1}'.format(AideConfig.AIDE_BIN, AideConfig.COMMON_CONFIG_NAME)
        gen_code = os.system(aide_command)
        if os.WIFEXITED(gen_code):
            gen_code = os.WEXITSTATUS(gen_code)
        else:
            gen_code = 256

        if gen_code and get_lock_policy():
            sn_syslog.log(
                syslog.LOG_CRIT, SnSyslog.IdMsgErrorCrcRule,
                log_('configs checksum failed, error {0}. System is blocked'), gen_code)

            with open(AideConfig.LOCK_REASON_FILE, 'w') as lock_reason_file:
                lock_reason_file.write(_('Detected integrity violation in AEC rules'))

            subprocess.run(AideConfig.SNBLOCK_BIN, stdout=DEVNULL, stderr=DEVNULL)

        if gen_code:
            print_msg(Color.white(_('Rules restored from backup due to integrity violation')))
            resource_exception.reinit()
            rule_management.reinit()

        return SnaecctlError.Success


class RemoteSourceConfig:
    PATH = os.path.join(FileConfig.DIRECTORY, 'remote.conf')

    def __init__(self):
        content = ''
        try:
            with io.open(self.PATH, 'r', encoding='utf-8') as config_file:
                content = config_file.read()
                json_sources = json.loads(content)
                self.rules = json_sources['rules']
                self.whitelist = json_sources['whitelist']
                self.exceptions = json_sources['exceptions']

            self.old_version = False
        except Exception:
            self.whitelist = content
            self.rules = ''
            self.exceptions = ''
            self.old_version = True

    def save(self):
        content = {'rules': self.rules, 'whitelist': self.whitelist, 'exceptions': self.exceptions}

        with io.open(self.PATH, 'w') as config_file:
            json.dump(content, config_file, indent=4, sort_keys=True, ensure_ascii=False)

    @staticmethod
    def is_local(object):
        if object == '' or object == 'local':
            return True
        return False

    @staticmethod
    def Source(object):
        if RemoteSourceConfig.is_local(object):
            return _('local')
        return object



class WhiteListUsers:
    def __init__(self):
        self.user_list = list()
        self.group_list = list()


class WhiteListUsersConfig:
    LIST = os.path.join(FileConfig.DIRECTORY, 'white-list.config')
    CC_CONF = os.path.join(FileConfig.DIRECTORY, 'remote.conf')

    def __init__(self):
        try:
            with io.open(self.LIST, 'r', encoding='utf-8') as config_file:
                json_list_string = config_file.read()
                json_list = json.loads(json_list_string)
                user_wl = WhiteListUsers()
                if len(json_list):
                    user_wl.__dict__ = json_list

                # для чтения пустых списков, сформированных через boost json
                if type(user_wl.user_list) is str:
                    user_wl.user_list = list()
                if type(user_wl.group_list) is str:
                    user_wl.group_list = list()
        except ValueError:
            raise SnaecctlError(SnaecctlError.InvalidConfig, {self.LIST})
        except IOError:
            raise SnaecctlError(SnaecctlError.ConfigNotFound, {self.LIST})
        self.list = user_wl

    @staticmethod
    def is_cc_enable():
        return not RemoteSourceConfig.is_local(sources.whitelist)

    def update_config(self):
        try:
            json_string = json.dumps(self.list.__dict__, indent=4, sort_keys=True,
                                     ensure_ascii=False)
            with io.open(self.LIST, 'w', encoding='utf-8') as config_file:
                config_file.write(str(json_string))
        except IOError:
            raise SnaecctlError(SnaecctlError.ConfigWriteFailed, {self.LIST})

    def update_user_list_in_kernel(self):
        cmd = KernelCommand(KernelCommand.ADD_UID_CMD)
        uid_list = NameHelper.user_list_to_uid_list(self.list.user_list)
        cmd.add_field(','.join(str(uid) for uid in uid_list))
        cmd.send()

    def update_group_list_in_kernel(self):
        cmd = KernelCommand(KernelCommand.ADD_GID_CMD)
        gid_list = NameHelper.group_list_to_gid_list(self.list.group_list)
        cmd.add_field(','.join(str(gid) for gid in gid_list))
        cmd.send()

    def add_user(self, new_user_list):
        added_user_list = list(set(new_user_list) - set(self.list.user_list))
        self.list.user_list = ListUtil.add_lists(self.list.user_list, new_user_list)
        return added_user_list

    def add_group(self, new_group_list):
        added_group_list = list(set(new_group_list) - set(self.list.group_list))
        self.list.group_list = ListUtil.add_lists(self.list.group_list, new_group_list)
        return added_group_list

    def rm_user(self, removed_user_list):
        try:
            self.list.user_list = ListUtil.remove_list(self.list.user_list, removed_user_list)
        except ArrayException as er:
            raise SnaecctlError(SnaecctlError.NoUsersInRule, er.array,
                                _('in the whitelist there are no users: {0}'))

    def rm_group(self, removed_group_list):
        try:
            self.list.group_list = ListUtil.remove_list(self.list.group_list, removed_group_list)
        except ArrayException as er:
            raise SnaecctlError(SnaecctlError.NoGroupsInRule, er.array,
                                _('in the whitelist there are no groups: {0}'))

    def apply_rules(self):
        if WhiteListUsersConfig.is_cc_enable():
            raise SnaecctlError(SnaecctlError.CCEnabled)

        self.update_user_list_in_kernel()
        self.update_group_list_in_kernel()
        self.update_config()

    def print_users(self):
        print((_('White list of users: {0}').format(', '.join(self.list.user_list))))

    def print_groups(self):
        print((_('White list of groups: {0}').format(', '.join(self.list.group_list))))


class ExceptionListConfig(FileConfig):
    def __init__(self, resource_list=None):
        if resource_list is None:
            self.resource_list = list()
        else:
            self.resource_list = resource_list

    def config_name(self):
        return os.path.join(FileConfig.DIRECTORY, 'resource-exception.conf')

    def load(self, config_name=None):
        FileConfig.load(self, config_name)

        # для чтения пустых списков, сформированных через boost json
        if type(self.resource_list) is str:
            self.resource_list = list()


class Resource:
    def __init__(self, name, rule_list=None):
        self.name = name

        if -1 == resource_exception.find_resource(self.name):
            self.is_exception = False
        else:
            self.is_exception = True

        if rule_list:
            self.rule_list = rule_list
        else:
            self.rule_list = list()

    # add rules to resources
    @staticmethod
    def add_rules_for_resources(rule_name_list, resource_name_list):
        for resource_name in resource_name_list:
            idx = rule_management.find_resource(resource_name)
            if idx != -1:
                rule_management.resource_list[idx].rule_list.extend(rule_name_list)
            else:
                idx = len(rule_management.resource_list)
                rule_management.resource_list.append(Resource(resource_name, rule_name_list))
                rule_management.resource_list[idx].create_aide_db()

    # remove rules from resources
    @staticmethod
    def remove_rules_for_resources(rule_name_list, resource_name_list):
        for resource_name in resource_name_list:
            idx = rule_management.find_resource(resource_name)

            rule_management.resource_list[idx].rule_list = \
                list(set(rule_management.resource_list[idx].rule_list) - set(rule_name_list))

            if not rule_management.resource_list[idx].rule_list:
                # don't remove aide-config if resource is in whitelist
                if not rule_management.resource_list[idx].is_exception:
                    AideConfig(Resource.get_uuid(resource_name)).remove()

                # remove resource from kernel
                Resource.rm_from_kernel(resource_name)

                del rule_management.resource_list[idx]
            else:
                rule_management.resource_list[idx].apply_to_kernel()

    @staticmethod
    def get_uuid(resource):
        return uuid.uuid5(uuid.NAMESPACE_OID, resource).hex

    @staticmethod
    def rm_from_kernel(resource_name):
        # if resource is in whitelist
        idx = resource_exception.find_resource(resource_name)
        if idx != -1:
            resource_exception.add_to_kernel(idx)
            return

        # remove resource from kernel
        cmd = KernelCommand(KernelCommand.RM_RULE_CMD)
        cmd.add_field(resource_name)
        try:
            cmd.send()
        except RuntimeError:
            pass

    def apply_to_kernel(self):
        cmd = KernelCommand(KernelCommand.ADD_RULE_CMD)
        cmd.add_field(Resource.get_uuid(self.name))

        enable = False
        extended = False
        user_list = list()
        group_list = list()
        for rule in rule_management.rule_list:
            if rule.enable and rule.name in self.rule_list:
                enable = True

                if rule.extended:
                    extended = True

                if rule.all_allowed:
                    del user_list[:]
                    del group_list[:]
                    break

                user_list = ListUtil.add_lists(user_list, rule.user_list)
                group_list = ListUtil.add_lists(group_list, rule.group_list)

        cmd.add_field(str(int(enable)))
        cmd.add_field(str(int(extended)))
        cmd.add_field(str(int(self.is_exception)))
        cmd.add_field(self.name)

        if user_list:
            uid_list = NameHelper.user_list_to_uid_list(user_list)
            uid_string = 'uids:' + ','.join(str(uid) for uid in uid_list)
            cmd.add_field(uid_string)
        else:
            uid_list = list()

        if group_list:
            gid_list = NameHelper.group_list_to_gid_list(group_list)
            gid_string = 'gids:' + ','.join(str(gid) for gid in gid_list)
            cmd.add_field(gid_string)
        else:
            gid_list = list()

        if rule.all_allowed or self.is_exception or len(uid_list) or len(gid_list):
            cmd.send()

    # create aide-config, aide-db
    def create_aide_db(self):
        # create aide-config, aide-db
        AideConfig(Resource.get_uuid(self.name)).create([self.name])


class RuleConfig(FileConfig):
    DIRECTORY = os.path.join(FileConfig.DIRECTORY, 'rules')
    NAME_TEMPLATE = os.path.join(DIRECTORY, '{0}.conf')
    status_dict = {False: _('disabled'), True: _('enabled')}

    @staticmethod
    def get_all_configs():
        configs = list()
        file_list = os.listdir(RuleConfig.DIRECTORY)
        for file_name in file_list:
            try:
                rule_config = RuleConfig()
                rule_config.load(os.path.join(RuleConfig.DIRECTORY, file_name))

                # fix for new field 'system'
                try:
                    rule_config.system
                except AttributeError:
                    rule_config.system = False

                configs.append(rule_config)
            except RuntimeError as er:
                print_msg(Color.white(_('Warning: {0}').format(er)))
                continue
        return configs

    def __init__(self, name='', extended=None, enable=None,
                 user_list=None, group_list=None, resource_list=None,
                 all_allowed=None, system=None):
        self.name = name

        if extended is None:
            self.extended = False
        else:
            self.extended = extended

        if enable is None:
            self.enable = True
        else:
            self.enable = enable

        if user_list:
            self.user_list = user_list
        else:
            self.user_list = list()

        if group_list:
            self.group_list = group_list
        else:
            self.group_list = list()

        if not user_list and not group_list:
            self.all_allowed = True
        else:
            self.all_allowed = False

        if resource_list:
            self.resource_list = resource_list
        else:
            self.resource_list = list()

        if system:
            self.system = system
        else:
            self.system = False

    @staticmethod
    def remove_config(name):
        os.remove(RuleConfig.NAME_TEMPLATE.format(name))

    @staticmethod
    def remove_all_config():
        configs = glob.glob(RuleConfig.NAME_TEMPLATE.format('*'))
        for config in configs:
            os.remove(config)

    def config_name(self):
        return RuleConfig.NAME_TEMPLATE.format(self.name)

    @staticmethod
    def strtobool(str):
        if str == 'true' or str == 'True' or str == '1':
            return True
        return False

    def load(self, config_name=None):
        FileConfig.load(self, config_name)

        # для чтения пустых списков, сформированных через boost json
        # и для чтения булевых значений
        if type(self.resource_list) is str:
            self.resource_list = list()
        if type(self.user_list) is str:
            self.user_list = list()
        if type(self.group_list) is str:
            self.group_list = list()
        if type(self.all_allowed) is str:
            self.all_allowed = RuleConfig.strtobool(self.all_allowed)
        if type(self.enable) is str:
            self.enable = RuleConfig.strtobool(self.enable)
        if type(self.extended) is str:
            self.extended = RuleConfig.strtobool(self.extended)
        if type(self.system) is str:
            self.system = RuleConfig.strtobool(self.system)

    def add_resources(self, resource_list):
        new_resource_list = ListUtil.add_lists(self.resource_list, resource_list)
        added_resources = ListUtil.remove_list(new_resource_list, self.resource_list)
        self.resource_list = new_resource_list

        # create or update config file for resources and apply it
        Resource.add_rules_for_resources([self.name], added_resources)
        return added_resources

    def remove_resources(self, resource_list):
        try:
            self.resource_list = ListUtil.remove_list(self.resource_list, resource_list)
        except ArrayException as er:
            raise SnaecctlError(SnaecctlError.NoResourcesInRule, er.array)

        Resource.remove_rules_for_resources([self.name], resource_list)

    def apply_to_kernel(self):
        for resource in self.resource_list:
            try:
                idx = rule_management.find_resource(resource)
                if idx != -1:
                    rule_management.resource_list[idx].apply_to_kernel()
            except RuntimeError as er:
                print_msg(Color.white(_('Warning: {0}').format(er)))

    def is_empty(self):
        if not self.user_list and not self.group_list and not self.all_allowed:
            return True
        else:
            return False

    def resource_print(self):
        print((_('\tResources:')))
        for resource in self.resource_list:
            print(('\t\t{0}'.format(resource)))

    def mode_string(self):
        if self.extended:
            return _('Extended')
        else:
            return _('Standard')

    def print_rule(self):
        print((_('\t{0} rule `{1}`. Status: {2}. ').
              format(self.mode_string(), self.name, RuleConfig.status_dict[self.enable])))

    def print_full_rule(self, print_resources=False):
        print((_('\n{0} rule `{1}`.\n\tStatus: {2}.').
              format(self.mode_string(), self.name, RuleConfig.status_dict[self.enable])))
        print((_('\tThe rule applies to')))
        if self.all_allowed:
            print((_('\t\tall users')))
        else:
            if len(self.user_list) > 0:
                print((_('\t\tusers: {0}').format(', '.join(self.user_list))))
            if len(self.group_list) > 0:
                print((_('\t\tgroups: {0}').format(', '.join(self.group_list))))

        if print_resources:
            self.resource_print()


class ExceptionList:
    def __init__(self):
        self.config = ExceptionListConfig()
        try:
            self.config.load()
        except SnaecctlError as err:
            self.resource_list = list()

    def reinit(self):
        self.__init__()

    def find_resource(self, resource_name):
        for idx, resource in enumerate(self.config.resource_list):
            if resource_name == resource:
                return idx
        return -1

    def print_resources(self):
        print((_('Policy source: {0}').format(RemoteSourceConfig.Source(sources.exceptions))))
        print((_('Resources exception list:\n{0}').format('\n'.join(self.config.resource_list))))

    def add_resources(self, resource_list):
        if not RemoteSourceConfig.is_local(sources.exceptions):
            raise SnaecctlError(SnaecctlError.CCEnabled)

        added_resources = list(set(resource_list) - set(self.config.resource_list))
        self.config.resource_list.extend(added_resources)

        for resource_name in added_resources:
            resource = Resource(resource_name)
            resource.create_aide_db()
            resource.apply_to_kernel()
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                                       log_('resource `{0}` was added to the exception list'),
                                       resource_name)

    def rm_resources(self, removed_resource):
        if not RemoteSourceConfig.is_local(sources.exceptions):
            raise SnaecctlError(SnaecctlError.CCEnabled)

        try:
            self.config.resource_list = ListUtil.remove_list(self.config.resource_list,
                                                             removed_resource)
        except ArrayException as er:
            raise SnaecctlError(SnaecctlError.NoResourcesInRule, er.array,
                                _('in the exception list there are no resources: {0}'))

        for resource in removed_resource:
            idx = rule_management.find_resource(resource)

            if idx == -1:
                # remove resource from kernel if no rules for it
                AideConfig(Resource.get_uuid(resource)).remove()
                Resource.rm_from_kernel(resource)
            else:
                # remove whitelist flag
                rule_management.resource_list[idx].is_exception = False
                rule_management.resource_list[idx].apply_to_kernel()

            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                                       log_('resource `{0}` was removed from the exception list'),
                                       resource)

    def add_to_kernel(self, idx):
        Resource(self.config.resource_list[idx]).apply_to_kernel()

    def apply_to_kernel(self):
        for resource in self.config.resource_list:
            Resource(resource).apply_to_kernel()


class RuleManagement:
    def __init__(self):
        self.rule_list = RuleConfig.get_all_configs()

        self.resource_list = list()
        for rule in self.rule_list:
            for resource in rule.resource_list:
                idx = self.find_resource(resource)

                if idx == -1:
                    self.resource_list.append(Resource(resource, [rule.name]))
                else:
                    self.resource_list[idx].rule_list.append(rule.name)

            # заполнить словари пользователей и групп
            for user in rule.user_list:
                AllUserGroupList.get_uid(user)
            for group in rule.group_list:
                AllUserGroupList.get_gid(group)

    def reinit(self):
        self.__init__()

    def find_resource(self, name):
        for idx, resource in enumerate(self.resource_list):
            if resource.name == name:
                return idx
        return -1

    def enable_disable_rule_list(self, name_list, is_enable):
        resource_list = list()
        changed_rule_list = list()

        if not RemoteSourceConfig.is_local(sources.rules):
            raise SnaecctlError(SnaecctlError.CCEnabled)

        for name in name_list:
            rule = self.find_rule(name)

            if rule.system:
                print_msg(Color.white('{0}: {1}'.format(_('Warning'), SnaecctlError.error_desc[
                    SnaecctlError.SystemImmutableRule].format(name))))
                continue

            if rule.enable != is_enable:
                rule.enable = is_enable
                changed_rule_list.append(rule)
                resource_list = \
                    list(set(resource_list + rule.resource_list))

        if is_enable:
            message = log_('rule `{0}` was enabled')
            id_msg = SnSyslog.IdMsgEnableRule
        else:
            message = log_('rule `{0}` was disabled')
            id_msg = SnSyslog.IdMsgDisableRule

        for resource in resource_list:
            idx = self.find_resource(resource)
            self.resource_list[idx].apply_to_kernel()

        for rule in changed_rule_list:
            rule.save()
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, id_msg, message, rule.name)

        sn_syslog.apply_cache()

    def find_rule(self, name):
        for rule in self.rule_list:
            if name == rule.name:
                return rule
        raise SnaecctlError(SnaecctlError.RuleIsNotExist, {name})

    def add_rule(self, rule):
        # save rule config
        self.rule_list.append(rule)
        rule.save()

        Resource.add_rules_for_resources([rule.name], rule.resource_list)

    def remove_rule(self, rule):
        resource_list = rule.resource_list
        name = rule.name

        self.rule_list.remove(rule)

        # delete rule config
        RuleConfig.remove_config(name)

        # delete resources
        Resource.remove_rules_for_resources([name], resource_list)

    # remove all user rules
    def delete_user_rules(self):
        # save names of user rules
        user_rules_names = [rule.name for rule in self.rule_list if not rule.system]

        # save resources of user rules
        resource_list = set()
        for rule in self.rule_list:
            if rule.system:
                continue
            resource_list.update(rule.resource_list)

        # delete user rules
        self.rule_list = [rule for rule in self.rule_list if rule.system]

        # delete rule configs
        for name in user_rules_names:
            RuleConfig.remove_config(name)

        # delete resources
        Resource.remove_rules_for_resources(user_rules_names, list(resource_list))

    def apply_rules_to_kernel(self):
        for resource in self.resource_list:
            resource.apply_to_kernel()


class ResourceFile(FileConfig):
    def __init__(self, resource_list=None):
        self.resource_list = resource_list


def print_rules_for_user(user, rules, print_resources=False):
    group = NameHelper.get_group_by_username(user)
    print((_('\nRules for user `{0}`:').format(user)))
    for rule in rules:
        if rule.all_allowed or user in rule.user_list or group in rule.group_list:
            rule.print_rule()
            if print_resources:
                rule.resource_print()


def print_rules_for_group(group, rules, print_resources=False):
    print((_('\nRules for group `{0}`:').format(group)))
    for rule in rules:
        if rule.all_allowed or group in rule.group_list:
            rule.print_rule()
            if print_resources:
                rule.resource_print()


def operation_confirmation(string):
    if force_flag:
        return

    answer_yes = ['yes', 'y', 'да', 'д']
    answer = input(string + _(' (yes/no): '))
    if answer.lower() not in answer_yes:
        print_msg(_('Operation canceled'))
        sys.exit(SnaecctlError.OperationCanceled)


def recalc_aide_configs():
    for resource in rule_management.resource_list:
        AideConfig(Resource.get_uuid(resource.name)).create([resource.name])

    for resource_name in resource_exception.config.resource_list:
        Resource(resource_name).create_aide_db()

    sn_syslog.log(syslog.LOG_INFO, SnSyslog.IdMsgRecalcCrcRule,
                  log_('checksums for all rules were recalculated'))


# list_function: print all rule or one rule or list of trusted users and groups.
def list_function(arguments):
    AideConfig.check_common_db()

    if arguments.name and (arguments.user or arguments.group):
        raise InvalidArgument(_('--name can not be used with --user and --group'))

    # print white list of trusted users and groups
    if arguments.view_users or arguments.view_groups:
        white_list_users = WhiteListUsersConfig()

        if arguments.view_users > 0:
            white_list_users.print_users()
        if arguments.view_groups > 0:
            white_list_users.print_groups()

        print((_('Policy source: {0}').format(RemoteSourceConfig.Source(sources.whitelist))))

        return SnaecctlError.Success

    if arguments.view_white_resources:
        resource_exception.print_resources()
        return SnaecctlError.Success

    print(_('Policy source: {0}').format(RemoteSourceConfig.Source(sources.rules)))

    if arguments.user or arguments.group:
        # print all rules for user
        for user in arguments.user:
            print_rules_for_user(user, rule_management.rule_list, arguments.view_resources)

        # print all rules for group
        for group in arguments.group:
            print_rules_for_group(group, rule_management.rule_list, arguments.view_resources)

        print('')
        return SnaecctlError.Success

    if not arguments.name:
        for rule in rule_management.rule_list:
            if arguments.system and rule.system or not arguments.system and not rule.system:
                rule.print_full_rule(arguments.view_resources)
    else:
        rule = rule_management.find_rule(arguments.name)
        rule.print_full_rule(True)
    return SnaecctlError.Success


# add_function: add new rule or add new trusted users, groups, resources to existed rule.
# Or add trusted users and groups to white list.
def add_function(arguments):
    AideConfig.check_common_db()

    if not is_full_mode():
        raise SnaecctlError(SnaecctlError.LimitedMode)

    if arguments.name is None and not arguments.user and not arguments.group \
            and not arguments.resource:
        raise InvalidArgument(_('a rule name or the whitelist of users, groups '
                                'or the exception list of resources are required'))

    if not arguments.name and (arguments.enable is not None or arguments.extended is not None
                               or arguments.recursive or arguments.no_depend is False
                               or arguments.resource_file):
        raise InvalidArgument(_('a rule name required for `-D`, `-X`, `-R`, `-d`, '
                                'and `-f` options'))

    if not arguments.resource and not arguments.resource_file and arguments.no_depend is False:
        raise InvalidArgument(_('a resource required for `-d` option'))

    if not arguments.resource and arguments.recursive:
        raise InvalidArgument(_('a resource required for `-R` option'))

    if arguments.user:
        AllUserGroupList.check_user_list(arguments.user)

    if arguments.group:
        AllUserGroupList.check_group_list(arguments.group)

    # if add to user white list
    if arguments.name is None and (arguments.user or arguments.group):
        # add new users and group to white list
        white_list_users = WhiteListUsersConfig()
        if arguments.user:
            added_user_list = white_list_users.add_user(arguments.user)
            for user in added_user_list:
                sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                           log_('user `{0}` was added to the whitelist'), user)
        if arguments.group:
            added_group_list = white_list_users.add_group(arguments.group)
            for group in added_group_list:
                sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                           log_('group `{0}` was added to the whitelist'), group)
        white_list_users.apply_rules()
        return AideConfig.generate_common_db()

    # if add to resource whitelist
    if arguments.name is None and arguments.resource:
        resource_list = DirectoryHelper.get_full_resource_list(arguments.resource, False, False)

        if not resource_list:
            raise SnaecctlError(SnaecctlError.ResourceListEmpty)

        resource_exception.add_resources(resource_list)
        resource_exception.config.save()
        return AideConfig.generate_common_db()

    try:
        rule = rule_management.find_rule(arguments.name)
    except RuntimeError:
        rule = None

    if rule and rule.system:
        raise SnaecctlError(SnaecctlError.SystemImmutableRule, {arguments.name})

    if rule and not RemoteSourceConfig.is_local(sources.rules):
        raise SnaecctlError(SnaecctlError.CCEnabled)

    resource_list = arguments.resource
    if arguments.resource_file:
        resource_file = ResourceFile()
        resource_file.load(os.path.abspath(arguments.resource_file))
        resource_list = ListUtil.add_lists(resource_list, resource_file.resource_list)

    resource_list = DirectoryHelper.get_full_resource_list(resource_list,
                                                           arguments.no_depend,
                                                           arguments.recursive)

    if rule is None:
        if not RemoteSourceConfig.is_local(sources.rules):
            raise SnaecctlError(SnaecctlError.CCEnabled)

        if arguments.resource and not resource_list:
            raise SnaecctlError(SnaecctlError.ResourceNotExist, arguments.resource)

        # create new rule
        rule = RuleConfig(name=arguments.name, resource_list=resource_list,
                          user_list=arguments.user, group_list=arguments.group,
                          extended=arguments.extended, enable=arguments.enable)

        sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgCreateRule,
                                   log_('a new rule `{0}` was added'), rule.name)
        for user in arguments.user:
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                       log_('user `{0}` was added to a rule `{1}`'),
                                       user, arguments.name)
        for group in arguments.group:
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                       log_('group `{0}` was added to a rule `{1}`'),
                                       group, arguments.name)
        for resource in resource_list:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                log_('resource `{0}` was added to a rule `{1}`'), resource, arguments.name)

        # create new rule
        rule_management.add_rule(rule)
        rule.apply_to_kernel()
    else:
        if (arguments.user or arguments.group) and rule.all_allowed:
            raise SnaecctlError(SnaecctlError.RuleAllowedForAll, {arguments.name})

        if not arguments.user and not arguments.group and not arguments.resource \
                and not arguments.resource_file:
            raise SnaecctlError(SnaecctlError.RuleExist, {arguments.name})

        if arguments.enable is not None:
            print_msg(Color.white(
                _('Warning: `-D` option will not apply because rule `{0}` already exists. '
                  'Use the command `disable`').format(arguments.name)))

        if arguments.extended is not None:
            print_msg(Color.white(
                _('Warning: `-X` option will not apply because rule `{0}` already exists. '
                  'Use the command `update').format(arguments.name)))

        added_user_list = list()
        # add new user to rule
        if arguments.user:
            new_user_list = ListUtil.add_lists(rule.user_list, arguments.user)
            added_user_list = ListUtil.remove_list(new_user_list, rule.user_list)
            rule.user_list = new_user_list
            for user in added_user_list:
                sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                           log_('user `{0}` was added to a rule `{1}`'),
                                           user, arguments.name)

        added_group_list = list()
        # add new group to rule
        if arguments.group:
            new_group_list = ListUtil.add_lists(rule.group_list, arguments.group)
            added_group_list = ListUtil.remove_list(new_group_list, rule.group_list)
            rule.group_list = new_group_list
            for group in added_group_list:
                sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                           log_('group `{0}` was added to a rule `{1}`'),
                                           group, arguments.name)

        # add new resources to config
        added_resources = list()
        if resource_list:
            added_resources = rule.add_resources(resource_list)
            for resource in added_resources:
                sn_syslog.add_log_to_cache(
                    syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                    log_('resource `{0}` was added to a rule `{1}`'), resource, arguments.name)

        if not added_user_list and not added_group_list and not added_resources:
            print_msg(_('No new resources, users or groups to add to the rule'))

        # save config
        rule.save()

        if arguments.user or arguments.group:
            # if new users or group was added, apply rule for all resources
            rule.apply_to_kernel()
        else:
            # else apply only for new resources
            for resource in added_resources:
                idx_r = rule_management.find_resource(resource)
                rule_management.resource_list[idx_r].apply_to_kernel()

    return AideConfig.generate_common_db()


# update_function: update rule name and rule mode.
def update_function(arguments):
    AideConfig.check_common_db()

    if not is_full_mode():
        raise SnaecctlError(SnaecctlError.LimitedMode)

    if arguments.name is None:
        raise InvalidArgument(_('need rule name'))

    if arguments.all_users and (arguments.user or arguments.group):
        raise InvalidArgument(_('--all-users is can not be used with --user and --group'))

    if arguments.user:
        AllUserGroupList.check_user_list(arguments.user)

    if arguments.group:
        AllUserGroupList.check_group_list(arguments.group)

    rule = rule_management.find_rule(arguments.name)
    if rule.system:
        raise SnaecctlError(SnaecctlError.SystemImmutableRule, {arguments.name})

    if not RemoteSourceConfig.is_local(sources.rules):
        raise SnaecctlError(SnaecctlError.CCEnabled)

    # enable rule for all users
    if arguments.all_users:
        del rule.user_list[:]
        del rule.group_list[:]
        rule.all_allowed = True
        sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                   log_('rule `{0}` was applied to all users`'), arguments.name)

    if arguments.user or arguments.group:
        # disable rule for all users
        if rule.all_allowed:
            rule.all_allowed = False
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRmUser,
                                       log_('rule `{0}` was disabled for all users`'),
                                       arguments.name)

        added_users, removed_users = ListUtil.replace_list(rule.user_list, arguments.user)

        # log removed resources
        for user in removed_users:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgRmUser,
                log_('user `{0}` was removed from rule `{1}`'), user, arguments.name)

        # log added resources
        for user in added_users:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                log_('user `{0}` was added to rule `{1}`'), user, arguments.name)

        # replace user list
        rule.user_list = arguments.user

        added_groups, removed_groups = ListUtil.replace_list(rule.group_list, arguments.group)

        # log removed resources
        for group in removed_groups:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgRmUser,
                log_('group `{0}` was removed from rule `{1}`'), group, arguments.name)

        # log added resources
        for group in added_groups:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                log_('group `{0}` was added to rule `{1}`'), group, arguments.name)

        # replace group list
        rule.group_list = arguments.group

    added_resources = arguments.resource
    if arguments.resource_file:
        resource_file = ResourceFile()
        resource_file.load(os.path.abspath(arguments.resource_file))
        added_resources = ListUtil.add_lists(added_resources, resource_file.resource_list)

    if added_resources:
        resource_list = DirectoryHelper.get_full_resource_list(added_resources,
                                                               arguments.no_depend,
                                                               arguments.recursive)

        if not len(resource_list):
            raise SnaecctlError(SnaecctlError.ResourceListEmpty)

        added_resources, removed_resources = \
            ListUtil.replace_list(rule.resource_list, resource_list)

        # log removed resources
        for resource in removed_resources:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                log_('resource `{0}` was removed from rule `{1}`'), resource, arguments.name)

        # log added resources
        for resource in added_resources:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                log_('resource `{0}` was added to rule `{1}`'), resource, arguments.name)

        # replace resource list
        rule.remove_resources(removed_resources)
        rule.add_resources(added_resources)

    rule.save()

    if arguments.user or arguments.group or arguments.all_users:
        # if new users or group was added, apply rule for all resources
        rule.apply_to_kernel()
    else:
        # else apply only for new resources
        for resource in added_resources:
            idx_r = rule_management.find_resource(resource)
            rule_management.resource_list[idx_r].apply_to_kernel()

    return AideConfig.generate_common_db()


# sync whitelist of users and groups with kernel
def restart_whitelist():
    wl_config = WhiteListUsersConfig()
    wl_config.update_user_list_in_kernel()
    wl_config.update_group_list_in_kernel()

    return SnaecctlError.Success


# sync rules with kernel
def restart_rules():
    # remove all rules from kernel
    cmd = KernelCommand(KernelCommand.RM_ALL_RULES_CMD)
    cmd.send()

    resource_exception.apply_to_kernel()

    rule_management.apply_rules_to_kernel()

    return SnaecctlError.Success


# restart_function: apply configuration to kernel
def restart_function(arguments=None):
    del arguments

    check_code = AideConfig.check_common_db()
    if check_code:
        return check_code

    restart_whitelist()

    restart_rules()

    print_msg(_('rules are synchronized with the kernel'))
    return SnaecctlError.Success


# remove_function: remove rule or remove trusted users, groups, resources from rule.
# Or remove trusted users and groups from white list.
def remove_function(arguments):
    AideConfig.check_common_db()

    if not is_full_mode():
        raise SnaecctlError(SnaecctlError.LimitedMode)

    if arguments.name is None and not arguments.user and not arguments.group \
            and not arguments.resource and not arguments.all_rules:
        raise InvalidArgument(_('a rule name, username, groupname, resource or --all-rules '
                                'are required. '
                                'The username, groupname can be used to remove from the whitelist '
                                '(without `-n` option) or a rule (with `-n` option). '
                                'The resource can be used to remove from the exception list '
                                '(without `-n` option) or a rule (with `-n` option)'))

    if arguments.all_rules:
        if arguments.name or arguments.user or arguments.group:
            raise InvalidArgument(_('--all-rules can not be used with --name, --user and --group'))

        operation_confirmation(_('All user rules will be removed completely. Continue?').
                               format(arguments.name))

        rule_management.delete_user_rules()

        # generate aide config for white resource
        for resource_name in resource_exception.config.resource_list:
            Resource(resource_name).create_aide_db()

        # apply resource whitelist to kernel
        resource_exception.apply_to_kernel()

        sn_syslog.log(syslog.LOG_INFO, SnSyslog.IdMsgRmRule, log_('all user rules were removed'))

        return AideConfig.generate_common_db()

    if arguments.name is None and (arguments.user or arguments.group):
        # remove users and group from white list
        white_list_users = WhiteListUsersConfig()

        if arguments.user:
            white_list_users.rm_user(arguments.user)
            for user in arguments.user:
                sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRmUser,
                                           log_('user `{0}` was removed from the whitelist'), user)
        if arguments.group:
            white_list_users.rm_group(arguments.group)
            for group in arguments.group:
                sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRmUser,
                                           log_('group `{0}` was removed from the whitelist'),
                                           group)

        white_list_users.apply_rules()
        return AideConfig.generate_common_db()

    # if remove from resource exception list
    if arguments.name is None and arguments.resource:
        resource_exception.rm_resources(arguments.resource)
        resource_exception.config.save()
        return AideConfig.generate_common_db()

    rule = rule_management.find_rule(arguments.name)
    if rule.system:
        raise SnaecctlError(SnaecctlError.SystemImmutableRule, {arguments.name})

    if not RemoteSourceConfig.is_local(sources.rules):
        raise SnaecctlError(SnaecctlError.CCEnabled)

    # remove rule completely
    if not arguments.user and not arguments.group and not arguments.resource:
        operation_confirmation(_('Are you sure you want to delete the rule?').
                               format(arguments.name))

        rule_management.remove_rule(rule)
        sn_syslog.log(syslog.LOG_INFO, SnSyslog.IdMsgRmRule,
                      log_('rule `{0}` was removed'), arguments.name)
        return AideConfig.generate_common_db()

    # remove user from rule
    if arguments.user:
        try:
            rule.user_list = ListUtil.remove_list(rule.user_list, arguments.user)
        except ArrayException as er:
            raise SnaecctlError(SnaecctlError.NoUsersInRule, er.array)

        for user in arguments.user:
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRmUser,
                                       log_('user `{0}` was removed from rule `{1}`'),
                                       user, arguments.name)

    # remove group from rule
    if arguments.group:
        try:
            rule.group_list = ListUtil.remove_list(rule.group_list, arguments.group)
        except ArrayException as er:
            raise SnaecctlError(SnaecctlError.NoGroupsInRule, er.array)

        for group in arguments.group:
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRmUser,
                                       log_('group `{0}` was removed from rule `{1}`'),
                                       group, arguments.name)

    # remove rule if all trusted user and group are removed
    if rule.is_empty():
        operation_confirmation(_('All users and groups were removed from rule `{0}`. '
                                 'It will be removed completely. Continue?').
                               format(arguments.name))

        rule_management.remove_rule(rule)
        sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRmRule,
                                   log_('all users and groups were removed from rule `{0}`. '
                                        'It was completely removed'), arguments.name)
        return AideConfig.generate_common_db()

    # remove resources from config
    resource_list = list()
    for resource in arguments.resource:
        resource_list.append(FileDependencyHelper.resolve_symlink(os.path.abspath(resource)))

    rule.remove_resources(resource_list)
    for resource in resource_list:
        sn_syslog.log(syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                      log_('resource `{0}` was removed from rule `{1}`'), resource, arguments.name)

    rule.save()
    if arguments.user or arguments.group:
        rule.apply_to_kernel()

    return AideConfig.generate_common_db()


# export_function: export
def export_function(arguments):
    AideConfig.check_common_db()

    rule = rule_management.find_rule(arguments.name)

    resource_file = ResourceFile(rule.resource_list)
    resource_file.save(os.path.abspath(arguments.resource_file))

    return SnaecctlError.Success

STATE_WAIT_FOR_CONFIRMATION=1

# confirm_function: confirm hard mode
def confirm_function(arguments):
    AideConfig.check_common_db()

    request = XmlTree.Element("request")
    request.set('plugin', 'aec')
    cmd = XmlTree.Element("cmd")
    cmd.set('name', 'status_confirmation')
    request.append(cmd)

    snserv_conn = SnservConnection()
    snserv_conn.send(request)
    response = snserv_conn.recv()

    for status in response.findall('status_confirmation'):
        state = int(status.find('state').text)
        description = str(status.find('description').text)

    if arguments.status:
        print_msg(description)
        return SnaecctlError.Success

    if state is not STATE_WAIT_FOR_CONFIRMATION:
        print_msg(_('No confirmation required: {}').format(description))
        return SnaecctlError.Success

    request = XmlTree.Element("request")
    request.set('plugin', 'aec')
    cmd = XmlTree.Element("cmd")
    cmd.set('name', 'confirm_hard_mode')
    request.append(cmd)

    snserv_conn = SnservConnection()
    snserv_conn.send(request)
    response = snserv_conn.recv()
    for status in response.findall('confirm_hard_mode'):
        result = int(status.find('result').text)

    if not result:
        raise SnaecctlError(SnaecctlError.UnknownError,
                            description=_('Failed to confirm AEC hard mode'))

    print_msg(_('Successful confirmation of AEC hard mode.'))

    return SnaecctlError.Success


# enable_function: enable rules
def enable_function(arguments):
    AideConfig.check_common_db()

    if not is_full_mode():
        raise SnaecctlError(SnaecctlError.LimitedMode)

    rule_management.enable_disable_rule_list(arguments.name, True)
    return AideConfig.generate_common_db()


# disable_function: disable rules
def disable_function(arguments):
    AideConfig.check_common_db()

    if not is_full_mode():
        raise SnaecctlError(SnaecctlError.LimitedMode)

    rule_management.enable_disable_rule_list(arguments.name, False)
    return AideConfig.generate_common_db()


def reload_function(arguments):
    if not arguments.name and not arguments.whitelist:
        # recalculate all aide config
        AideConfig.set_db_crc_alg()
        recalc_aide_configs()

        if sources.old_version:
            sources.save()
    elif arguments.name:
        resource_list = list()
        for name in arguments.name:
            rule = rule_management.find_rule(name)
            resource_list = list(set(resource_list + rule.resource_list))
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRecalcCrcRule,
                                       log_('checksum for rule `{0}` was recalculated'), name)

        for resource in resource_list:
            AideConfig(Resource.get_uuid(resource)).create([resource])

    gen_code = AideConfig.generate_common_db()

    if not arguments.name and not arguments.whitelist:
        restart_function()
    elif arguments.name:
        restart_rules()
    elif arguments.whitelist:
        restart_whitelist()
        sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgRecalcCrcRule,
                                   log_('checksum for whitelist was recalculated'))

    sn_syslog.apply_cache()

    return gen_code


def rename_function(arguments):
    AideConfig.check_common_db()

    if not is_full_mode():
        raise SnaecctlError(SnaecctlError.LimitedMode)

    if arguments.new_name:
        try:
            rule_management.find_rule(arguments.new_name)
        except RuntimeError:
            pass
        else:
            raise SnaecctlError(SnaecctlError.RuleExist, {arguments.new_name})

    rule = rule_management.find_rule(arguments.name)

    if rule.system:
        raise SnaecctlError(SnaecctlError.SystemImmutableRule, {arguments.name})

    if not RemoteSourceConfig.is_local(sources.rules):
        raise SnaecctlError(SnaecctlError.CCEnabled)

    rule.name = arguments.new_name
    rule.save()
    RuleConfig.remove_config(arguments.name)

    sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgChangeRule,
                               log_('rule `{0}` was renamed to `{1}`'),
                               arguments.name, arguments.new_name)

    return AideConfig.generate_common_db()


datetime_formats = [
    '%d-%m-%YT%H:%M:%S',
    '%d-%m-%YT%H:%M',
    '%d-%m-%YT%H',
    '%d-%m-%Y'
]


def string_to_timestamp(string):
    for datetime_format in datetime_formats:
        try:
            datetime_obj = datetime.strptime(string, datetime_format)
            return time.mktime(datetime_obj.timetuple())
        except ValueError:
            pass

    now = datetime.now()

    raise SnaecctlError(SnaecctlError.InvalidDateTime, now.strftime(datetime_formats[0]))


def analyze_log(arguments):
    AideConfig.check_common_db()

    # get the start and end time of the log analysis
    initial_time = None
    final_time = None

    if arguments.name is None and arguments.resource_file is None \
            and (arguments.user or arguments.group):
        raise InvalidArgument(_('a rule name or resource filename '
                                'required for -u/--user and -g/--group options'))

    if (arguments.name or arguments.resource_file) and not arguments.user and not arguments.group:
        raise InvalidArgument(_('a user name or a group name required '
                                'for -n/--name and -f/--resource-file options'))

    if arguments.initial_time:
        initial_time = string_to_timestamp(arguments.initial_time)

    if arguments.final_time:
        final_time = string_to_timestamp(arguments.final_time)

    sql_condition = 'success=0 AND event_id >= 1050 AND event_id <= 1055'
    if initial_time or final_time:
        sql_condition += ' AND '
    if initial_time:
        sql_condition += 'date_time >= {0}'.format(initial_time)
    if initial_time and final_time:
        sql_condition += ' AND '
    if final_time:
        sql_condition += 'date_time < {0}'.format(final_time)

    if arguments.exec_file:
        sql_condition += ' AND app=\'{}\''.format(arguments.exec_file)

    # connect to log database
    conn = sqlite3.connect('/opt/secretnet/var/log/audit_logs.db')
    c = conn.cursor()

    # print list of users and groups
    if arguments.list:
        users_response = c.execute('SELECT DISTINCT username FROM logs WHERE {0}'.
                                   format(sql_condition))

        print((_('Users: {0}').format(', '.join([user[0] for user in users_response]))))

        group_response = c.execute('SELECT DISTINCT groupname FROM logs WHERE {0}'.
                                   format(sql_condition))

        print((_('Groups: {0}').format(', '.join([group[0] for group in group_response]))))

        sys.exit(SnaecctlError.Success)

    if not is_full_mode():
        raise SnaecctlError(SnaecctlError.LimitedMode)

    # analyze log
    if arguments.user:
        response = c.execute('SELECT app, object_name, event_id FROM logs WHERE {0} AND '
                             'username=\'{1}\''.format(sql_condition, arguments.user))
    elif arguments.group:
        response = c.execute('SELECT app, object_name, event_id FROM logs WHERE {0} AND '
                             'groupname=\'{1}\''.format(sql_condition, arguments.group))

    resource_list = list()
    for row in response:
        if row[2] != 1055 or arguments.add_non_exec:
            resource_list = ListUtil.add_lists(resource_list, [row[1].strip()[1:-1]])
        resource_list = ListUtil.add_lists(resource_list, [row[0].strip()])

    if not resource_list:
        print_msg(_('There are no events in the log that match the specified conditions.'))
        sys.exit(SnaecctlError.Success)

    resource_list = DirectoryHelper.get_full_resource_list(resource_list, print_warnings=False)

    if arguments.resource_file:
        resource_file = ResourceFile(resource_list)
        resource_file.save(os.path.abspath(arguments.resource_file))
        sys.exit(SnaecctlError.Success)

    # check whether a new rule is created or added to an existing one?
    try:
        rule = rule_management.find_rule(arguments.name)
    except RuntimeError:
        # create new rule
        sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgCreateRule,
                                   log_('a new rule `{0}` was added'), arguments.name)

        if arguments.user:
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                       log_('user `{0}` was added to a rule `{1}`'),
                                       arguments.user, arguments.name)
            rule = RuleConfig(name=arguments.name, user_list=[arguments.user],
                              resource_list=resource_list)

        if arguments.group:
            sn_syslog.add_log_to_cache(syslog.LOG_INFO, SnSyslog.IdMsgAddUser,
                                       log_('group `{0}` was added to a rule `{1}`'),
                                       arguments.group, arguments.name)
            rule = RuleConfig(name=arguments.name, group_list=[arguments.group],
                              resource_list=resource_list)

        rule_management.add_rule(rule)

        for resource in rule.resource_list:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                log_('resource `{0}` was added to a rule `{1}`'), resource, arguments.name)

        rule.apply_to_kernel()
    else:
        if rule.system:
            raise SnaecctlError(SnaecctlError.SystemImmutableRule, {arguments.name})

        if not RemoteSourceConfig.is_local(sources.rules):
            raise SnaecctlError(SnaecctlError.CCEnabled)

        # add resources in existing rules
        added_resources = rule.add_resources(resource_list)

        if added_resources:
            operation_confirmation(_('Do you want to add resources to an existing rule `{0}`?').
                                   format(arguments.name))
        else:
            print_msg(_('There are no new resources in the log to add to the rule `{}`.').
                      format(arguments.name))
            sys.exit(SnaecctlError.Success)

        for resource in added_resources:
            sn_syslog.add_log_to_cache(
                syslog.LOG_INFO, SnSyslog.IdMsgChangeRuleResources,
                log_('resource `{0}` was added to a rule `{1}`'), resource, arguments.name)

        # save config
        rule.save()

        # apply for new resources
        for resource in added_resources:
            idx_r = rule_management.find_resource(resource)
            rule_management.resource_list[idx_r].apply_to_kernel()

    return AideConfig.generate_common_db()


argument_dictionary = {
    '--resource':
        _('resource path'),
    '--recursive':
        _('add resources recursively. Options `-n` and `-r` are required'),
    '--no-depend':
        _('don\'t add dependencies of executable binary files. '
          'Option `-n` and options `-r` or `-f` are required'),
    '--resource-file':
        _('JSON file containing resource list. Used with `-n` option'),
    '--name':
        _('rule name'),
    '--extended':
        _('set rule mode to extended. Used with `-n` option'),
    '--standard':
        _('set rule mode to standard. Used with `-n` option'),
    '--force':
        _('do not ask for confirmation')
}


# инициализация глобальных переменных
# snaecctl_exec должна инициализроваться первой, т.к. происходит ожидание заверешения
# других экземпляров snaecctl. Иначе другие переменные могут быть инициализированы
# устаревшими или невалидными конфигами
snaecctl_exec = snaecctl_lock()
sn_syslog = SnSyslog()
is_module_load = is_module_load_func()
NO_VERBOSE_FLAG = False
force_flag = False
resource_exception = ExceptionList()
rule_management = RuleManagement()
sources = RemoteSourceConfig()


if __name__ == '__main__':
    if not snaecctl_exec:
        sys.exit(SnaecctlError.UnknownError)

    # parser for resources
    resource_parser = argparse.ArgumentParser(add_help=False)
    resource_parser.add_argument(
        '-r', '--resource', help=argument_dictionary['--resource'],
        nargs='+', type=str, default=list())

    # parser for recursively resource addition
    recursive_parser = argparse.ArgumentParser(add_help=False)
    recursive_parser.add_argument(
        '-R', '--recursive', help=argument_dictionary['--recursive'],
        action='store_true', default=False)

    # parser for force flag
    force_parser = argparse.ArgumentParser(add_help=False)
    force_parser.add_argument(
        '-C', '--force', help=argument_dictionary['--force'], action='store_true', default=False)

    hide_force_parser = argparse.ArgumentParser(add_help=False)
    hide_force_parser.add_argument(
        '-C', '--force', help=argparse.SUPPRESS, action='store_true', default=False)

    # parser for no-verbose flag
    # скрытый флаг. Нужен для успользование утилиты плагином. При установке этого режима
    # утилита не выводит никакие сообщения (ошибки, предупреждения).
    # Выводятся только ошибочные объекты - пользователи, группы, файлы, правила
    # (каждый на отдельной строчке)
    no_verbose_parser = argparse.ArgumentParser(add_help=False)
    no_verbose_parser.add_argument(
        '-v', '--no-verbose', help=argparse.SUPPRESS, action='store_true', default=False)

    # parser for executable file and its resources
    depend_parser = argparse.ArgumentParser(add_help=False)
    depend_parser.add_argument(
        '-d', '--no-depend', help=argument_dictionary['--no-depend'],
        action='store_false', default=True)

    resource_file_parser = argparse.ArgumentParser(add_help=False)
    resource_file_parser.add_argument(
        '-f', '--resource-file', help=argument_dictionary['--resource-file'],
        type=str, default=None)

    required_resource_file_parser = argparse.ArgumentParser(add_help=False)
    required_resource_file_parser.add_argument(
        '-f', '--resource-file', help=argument_dictionary['--resource-file'],
        type=str, required=True)

    # parser for rule name
    name_parser = argparse.ArgumentParser(add_help=False)
    name_parser.add_argument(
        '-n', '--name', help=argument_dictionary['--name'], type=str, default=None)

    # parser for required rule name
    required_name_parser = argparse.ArgumentParser(add_help=False)
    required_name_parser.add_argument(
        '-n', '--name', help=argument_dictionary['--name'], type=str, required=True)

    # parser for rule names
    names_parser = argparse.ArgumentParser(add_help=False)
    names_parser.add_argument(
        '-n', '--name', help=argument_dictionary['--name'], type=str, default=list(), nargs='+')

    # parser for required rule names
    required_names_parser = argparse.ArgumentParser(add_help=False)
    required_names_parser.add_argument(
        '-n', '--name', help=argument_dictionary['--name'], type=str, required=True, nargs='+')

    # parser for user and groups list
    id_parser = argparse.ArgumentParser(add_help=False)
    id_parser.add_argument(
        '-u', '--user', help=_('user name'), nargs='+', type=str, default=list())
    id_parser.add_argument(
        '-g', '--group', help=_('group name'), nargs='+', type=str, default=list())

    # main parser of commands
    main_parser = argparse.ArgumentParser(
        description=_('Management by application execution control rules'), add_help=True)
    subparsers = main_parser.add_subparsers(metavar='<command>')

    # parser for viewing of rules and trusted users (groups)
    list_parser = subparsers.add_parser(
        'view', help=_('view rules, the whitelist of users and groups or resource exception list'),
        description=_('Prints a list of all user rules (without arguments), '
                      'all system (immutable) rules, '
                      'all rules for a specific user or group, a rule by name, '
                      'the whitelist of users, groups or exception list of resource'),
        parents=[name_parser, id_parser])
    list_parser.add_argument('-U', '--view-users', help=_('View the whitelist of users'),
                             action='store_true', default=False)
    list_parser.add_argument('-G', '--view-groups', help=_('View the whitelist of groups'),
                             action='store_true', default=False)
    list_parser.add_argument('-R', '--view-white-resources', action='store_true', default=False,
                             help=_('View the resource exception list'))
    list_parser.add_argument('-r', '--view-resources', help=_('View list of rule resources'),
                             action='store_true', default=False)
    list_parser.add_argument('-s', '--system', help=_('View list of system (immutable) rules'),
                             action='store_true', default=False)
    list_parser.set_defaults(func=list_function)

    # parser for adding new rule or new trusted users (groups)
    add_parser = subparsers.add_parser(
        'add', help=_('add new rules or trusted users and groups'),
        description=_('Adds a new rule, users, groups and resources to a existing rule, '
                      'users, groups, to the whitelist or resource to the exception list '),
        parents=[name_parser, id_parser, resource_parser, depend_parser, recursive_parser,
                 resource_file_parser, force_parser, no_verbose_parser])
    add_parser.add_argument('-D', '--disable',
                            help=_('create disabling rule. '
                                   'Used with `-n` option only for new rule'),
                            action='store_false', dest='enable', default=None)
    add_parser.add_argument('-X', '--extended',
                            help=argument_dictionary['--extended'] + _(' only for new rule'),
                            action='store_true', dest='extended', default=None)
    add_parser.set_defaults(func=add_function)

    # parser for removing rule or new trusted users (groups)
    remove_parser = subparsers.add_parser(
        'rm', help=_('remove rules or trusted users and groups'),
        description=_('Removes a rule by name, all rules, users, groups and resources '
                      'from a rule, users, groups from the whitelist '
                      'or resource from the exception list'),
        parents=[name_parser, resource_parser, id_parser, force_parser, no_verbose_parser])
    remove_parser.add_argument(
        '-A', '--all-rules', help=_('remove all user rules. Used without other options'),
        action='store_true', default=False)
    remove_parser.set_defaults(func=remove_function)

    # parser for updating rule
    update_parser = subparsers.add_parser(
        'update', help=_('update existing rule'),
        description=_('Replaces users, groups and resources in a rule and changes the rule mode'),
        parents=[required_name_parser, id_parser, resource_parser, depend_parser, recursive_parser,
                 resource_file_parser, force_parser, no_verbose_parser])
    update_parser.add_argument(
        '-a', '--all-users', help=_('enable rule for all users. Used with `-n` option '
                                    'and without `-u` and `-g` options'),
        action='store_true', default=False)
    update_parser.set_defaults(func=update_function)

    # parser for syncing rules with the kernel
    restart_parser = subparsers.add_parser('restart')
    restart_parser.set_defaults(func=restart_function, help=argparse.SUPPRESS, metavar='')

    # parser for recalculate rules checksum
    reload_parser = subparsers.add_parser(
        'reload', help=_('reload rules'),
        description=_('Reloads a rule by name or all rules or whitelist. Recalculates the checksum '
                      'of the rule resources and loads the rule to the kernel'),
        parents=[names_parser, hide_force_parser, no_verbose_parser])
    reload_parser.add_argument(
        '-w', '--whitelist', help=_('reload only whitelist'), action='store_true', default=False)
    reload_parser.set_defaults(func=reload_function)

    # parser for rule enabling
    enable_parser = subparsers.add_parser('enable', help=_('enable a rule'),
                                          description=_('Enables a rule'),
                                          parents=[required_names_parser, no_verbose_parser,
                                                   hide_force_parser])
    enable_parser.set_defaults(func=enable_function)

    # parser for rule disabling
    disable_parser = subparsers.add_parser('disable', help=_('disable a rule'),
                                           description=_('Disables a rule'),
                                           parents=[required_names_parser, no_verbose_parser,
                                                    hide_force_parser])
    disable_parser.set_defaults(func=disable_function)

    # parser for rule renaming
    rename_parser = subparsers.add_parser('rename', help=_('rename a rule'),
                                          description=_('Renames a rule'),
                                          parents=[required_name_parser, no_verbose_parser,
                                                   hide_force_parser])
    rename_parser.add_argument('-N', '--new-name', help=_('new rule name'), type=str, required=True)
    rename_parser.set_defaults(func=rename_function)

    # parser for resource export from rule
    export_parser = subparsers.add_parser(
        'export', help=_('export resource to file'),
        description=_('Exports resources from a rule to JSON file'),
        parents=[required_name_parser, required_resource_file_parser, no_verbose_parser,
                 hide_force_parser])
    export_parser.set_defaults(func=export_function)

    # parser for AEC hard mode confirmation
    confirm_parser = subparsers.add_parser(
        'confirm', help=_('confirm hard mode'),
        description=_('Confirm activation of AEC hard mode. '
                      'Confirmation is required after enabling hard mode and reboot. '
                      'Otherwise, after the next reboot, soft mode will be enabled. '
                      'If hard mode is enabled via remote control, no confirmation is required.'))
    confirm_parser.add_argument('-s', '--status',
                                help=_('view status of AEC hard mode confirmation'),
                                action='store_true', default=False)
    confirm_parser.set_defaults(func=confirm_function)

    # analyze audit journal
    log_parser = subparsers.add_parser(
        'log', help=_('generate a rule based on the log'),
        parents=[force_parser, no_verbose_parser],
        description=_('Analyzes the log and creates a new rule based on it '
                      'or adds resources to the existing rule. '
                      'It can also display all users and groups whose actions are registered. '
                      'Logs are filtered by username, groupname, executable file and by time. '
                      'If the initial time is not specified, '
                      'then the log is analyzed from the beginning. '
                      'If the final time is not specified, '
                      'then the log is analyzed to the ending. '
                      'Date/time format: dd-MM-yyyyThh:mm:ss. '
                      'Hour, minute and second are optional.'))
    action_log_group = log_parser.add_mutually_exclusive_group(required=True)
    action_log_group.add_argument('-l', '--list', help=_('view list of user and group'),
                                  action='store_true', default=False)
    action_log_group.add_argument('-n', '--name', help=argument_dictionary['--name'],
                                  type=str, default=None)
    action_log_group.add_argument('-f', '--resource-file', help=_('export resources to file'),
                                  type=str, default=None)
    id_group = log_parser.add_mutually_exclusive_group(required=False)
    id_group.add_argument('-u', '--user', help=_('user name'), type=str)
    id_group.add_argument('-g', '--group', help=_('group name'), type=str)
    log_parser.add_argument('-e', '--exec-file',
                            help=_('filter by executable file'), type=str)
    log_parser.add_argument('-I', '--initial-time',
                            help=_('date/time of the analysis beginning'), type=str)
    log_parser.add_argument('-F', '--final-time',
                            help=_('date/time of the analysis end. '
                                   'It is not included in the analysis range.'),
                            type=str)
    log_parser.add_argument('-A', '--add-non-exec',
                             help=_('add non-executable files (by default, '
                                    'only the executable and shared libraries are added)'),
                             action='store_true', default=False)
    log_parser.set_defaults(func=analyze_log)

    args = main_parser.parse_args()

    # установить флаг необходимости подтвреждения действия
    try:
        force_flag = args.force
    except AttributeError:
        pass

    # установить флаг
    try:
        NO_VERBOSE_FLAG = args.no_verbose
    except AttributeError:
        pass

    try:
        if len(sys.argv) < 2:
            raise InvalidArgument(_('no arguments'))
        ret_code = args.func(args)
        sn_syslog.apply_cache()
        sys.exit(ret_code)
    except InvalidArgument as err:
        print_msg(Color.red(
            _('Invalid argument: {0}. Use the argument --help for details.').format(err)))
        sys.exit(SnaecctlError.InvalidArgument)
    except SnaecctlError as err:
        err.print_message()
        sys.exit(err.code)
    except Exception as err:
        print_msg(Color.red(_('Error: {0}').format(err)))
        sys.exit(SnaecctlError.UnknownError)
