#!/opt/alt/python35/bin/python3
"""Fix file ownership/permissions for /var/ossec.

Usage:

  $ imunify360-ossec-fix-var-ossec [--log-level <level>] <var-ossec-dir>

It supports cases when [orphaned/alian] files in /var/ossec are owned
by non-existing/unrelated to root/ossec* users (DEF-12616).

It replicates ownership/permissions specified in the %files sections
of the ossec spec file. Remaining [orphaned/alian] files are renamed
(+.rpmsave suffix is appended)

"""
import argparse
import logging
import shutil
import stat
import sys
from contextlib import suppress
from pathlib import Path


__all__ = ["main"]

#: File attributes are generated by the `inv get-permissions-ownership` command
#   based on the ossec spec's %attr data
# !!! DO NOT EDIT BY HAND !!!
FILE_ATTRS = [
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/active-response",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": True,
        "glob": "%{_localstatedir}/ossec/active-response/bin",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": True,
        "glob": "%{_localstatedir}/ossec/agentless",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/bin",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/etc",
    },
    {
        "mode": "0o770",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/etc/shared",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/etc/templates",
    },
    {
        "mode": "0o640",
        "user": "ossec",
        "group": "ossec",
        "recursive": True,
        "glob": "%{_localstatedir}/ossec/etc/templates/*",
    },
    {
        "mode": "0o770",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/logs",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue",
    },
    {
        "mode": "0o770",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/ossec",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/diff",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/var",
    },
    {
        "mode": "0o770",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/var/run",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "root",
        "recursive": True,
        "glob": "%{_localstatedir}/ossec/bin/ossec-lua*",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/tmp",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "root",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/lua/*",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/logs",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/queue/alerts",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/queue/diff",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/queue/ossec",
    },
    {
        "mode": "0o775",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/queue/rids",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/queue/syscheck",
    },
    {
        "mode": "0o1750",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/tmp",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/var",
    },
    {
        "mode": "0o770",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/ossec-agent/var/run",
    },
    {
        "mode": "0o600",
        "user": "root",
        "group": "root",
        "recursive": True,
        "glob": "%{_sysconfdir}/ossec-init.conf",
    },
    {
        "mode": "0o640",
        "user": "ossec",
        "group": "ossec",
        "recursive": True,
        "glob": "%{_localstatedir}/ossec/etc/shared/agent.conf",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/logs/archives",
    },
    {
        "mode": "0o770",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/logs/alerts",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/logs/firewall",
    },
    {
        "mode": "0o755",
        "user": "ossecr",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/agent-info",
    },
    {
        "mode": "0o755",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/agentless",
    },
    {
        "mode": "0o770",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/alerts",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/fts",
    },
    {
        "mode": "0o755",
        "user": "ossecr",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/rids",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/rootcheck",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/queue/syscheck",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/rules",
    },
    {
        "mode": "0o750",
        "user": "ossec",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/stats",
    },
    {
        "mode": "0o550",
        "user": "root",
        "group": "ossec",
        "recursive": False,
        "glob": "%{_localstatedir}/ossec/agentless",
    },
]
OSSEC_DIR_TEMPLATE = "%{_localstatedir}/ossec"
OSSEC_USER = "ossec"
OSSEC_GROUP = OSSEC_USER


logger = logging.getLogger("imunify360-ossec-fix-var-ossec")


def main(argv=None):
    "Entry point for the script."
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--log-level",
        default="INFO",
        choices=[
            "CRITICAL",
            "FATAL",
            "ERROR",
            "WARN",
            "WARNING",
            "INFO",
            "DEBUG",
            "NOTSET",
        ],
    )
    parser.add_argument("ossec_dir")
    args = parser.parse_args(args=argv[1:] if argv is not None else None)
    logging.basicConfig(
        level=getattr(logging, args.log_level),
        format=parser.prog + ": %(message)s",
    )
    ossec_path = Path(args.ossec_dir)
    logger.info("Processing %s", ossec_path)
    if not ossec_path.is_dir():
        return  # nothing to fix

    success = True
    native_users = set()  # users known from %files
    for d in FILE_ATTRS:
        if not d["glob"].startswith(OSSEC_DIR_TEMPLATE):
            continue  # ignore non /var/ossec files
        glob_path = ossec_path / Path(d["glob"]).relative_to(
            OSSEC_DIR_TEMPLATE
        )
        success &= set_ownership_permissions(
            glob_path,
            mode=int(d["mode"], 0),
            user=d["user"],
            group=d["group"],
            recursive=d["recursive"],
        )
        if d["user"]:
            native_users.add(d["user"])
    # workaround from DEF-12616
    success &= set_ownership_permissions(
        ossec_path / "queue/fts/*", user=OSSEC_USER, group=OSSEC_GROUP
    )

    success &= move_alians(ossec_path, native_users)
    return not success


def set_ownership_permissions(
    glob_path, *, mode=None, user=None, group=None, recursive=False
) -> bool:
    """
    Set ownership to *user*/*group* and permissions to *mode* for *glob_path.*

    Apply recursively to all files/dirs if *recursive*.

    Return whether setting ownership/permissions was successful.
    """
    success = True
    if not glob_path.parent.exists():
        return success  # skip non-existent paths

    # set given ownership/permissions (recursively if necessary)
    for path in glob_path.parent.glob(glob_path.name):
        with suppress(OSError):  # ignore I/O errors
            if mode is not None:
                logger.info(
                    "Setting %s permissions for %s", stat.filemode(mode), path
                )
                try:
                    path.chmod(mode)
                except OSError as e:
                    success = False
                    logger.warning("chmod failed: %s", str(e))

            if user or group:
                logger.info(
                    "Setting ownership to %s:%s for %s",
                    user or "",
                    group or "",
                    path,
                )
                try:
                    shutil.chown(str(path), user=user, group=group)
                except (LookupError, OSError) as e:
                    success = False
                    logger.warning("chown failed: %s", str(e))

            if recursive and path.is_dir():
                for p in path.iterdir():
                    success &= set_ownership_permissions(
                        p,
                        mode=mode,
                        user=user,
                        group=group,
                        recursive=recursive,
                    )
    return success


def move_alians(ossec_path, native_users, suffix=".rpmsave") -> bool:
    """
    Move files&dirs from *ossec_path* if they are not owned by *native_users*
    """
    success = True
    for path in ossec_path.iterdir():
        if path.name.endswith(suffix):
            continue  # do nothing if the path already has the suffix
        try:
            alian = path.owner() not in native_users
        except KeyError:
            alian = True
        if not alian:
            with suppress(OSError):
                if path.is_dir():
                    success &= move_alians(path, native_users, suffix)
            continue  # keep it

        # move path
        new_path = path.with_name(path.name + suffix)
        logger.info("Renaming %s -> %s", path, new_path)
        for last in range(2):
            try:
                path.replace(new_path)
            except OSError as err:
                if not last:  # remove target & retry
                    logger.info("Removing %s", new_path)
                    if isinstance(err, IsADirectoryError):
                        shutil.rmtree(str(new_path), ignore_errors=True)
                    else:  # assume file
                        with suppress(OSError):
                            new_path.unlink()
                else:  # last
                    success = False
                    logger.warning(
                        "Can't rename %s, reason: %s", path, str(err)
                    )
            else:
                break

    return success


if __name__ == "__main__":
    sys.exit(main())
