# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Gather the currently logged-on / console users.

This feeds the server's console-user attribution (e.g. the SigmaAIShield AI
Usage report's "User" dimension). Attribution is host -> logged-on-user
granularity: network flows carry no per-socket uid, so the server attributes a
host's activity to whoever is interactively logged on to that host.

Table columns:

- username     -- the logged-on account name
- tty          -- terminal / session line (e.g. tty1, pts/0, console, :0)
- login_time   -- login timestamp as reported by the OS (free text, host-local)
- source_host  -- remote host for a remote session; empty for local/console
- session_type -- console | graphical | remote | tty | unknown

Unix/Linux/macOS parse `who`; Windows reads the interactive console user from
WMI Win32_ComputerSystem.UserName (the same source gather_command already uses).

Example (Linux `who`):

    root     tty1         2026-07-24 09:15
    alice    pts/0        2026-07-24 09:20 (10.0.0.5)

produces:

    'root',  'tty1',  '2026-07-24 09:15', '',          'tty'
    'alice', 'pts/0', '2026-07-24 09:20', '10.0.0.5',  'remote'
"""

import os
import sys
import logging

if __name__ == "__main__":
    try:
        import sigma
    except ImportError:
        sys.path.append(os.path.join(os.pardir, os.pardir))

logger = logging.getLogger("gather.loggedonuser")

_COLUMNS = ["username", "tty", "login_time", "source_host", "session_type"]


def _classify(tty, source_host):
    if source_host:
        return "remote"
    if tty.startswith(":"):
        return "graphical"
    if tty in ("console",):
        return "console"
    return "tty"


def _gather_unix(t):
    # `who` is portable across Linux/macOS/BSD. We keep the OS-reported
    # login time as free text (Linux emits "YYYY-MM-DD HH:MM"; macOS/BSD
    # emit "Mon DD HH:MM") rather than parsing a format that varies by OS.
    try:
        f = os.popen("who", "r")
    except (IOError, OSError):
        logger.exception("who")
        return
    try:
        lines = f.readlines()
    finally:
        f.close()
    for line in lines:
        words = line.split()
        if len(words) < 2:
            continue
        username = words[0]
        tty = words[1]
        rest = words[2:]
        # The remote host, when present, is the final token wrapped in
        # parentheses — e.g. "(10.0.0.5)", "(:0)", or a nested "(tmux(1).%0)".
        # Take it as a whole token so nested parens don't leak into login_time.
        source_host = ""
        if rest and rest[-1].startswith("(") and rest[-1].endswith(")"):
            source_host = rest[-1][1:-1].strip()
            rest = rest[:-1]
        login_time = " ".join(rest).strip()
        t.add([username, tty, login_time, source_host, _classify(tty, source_host)])


def _gather_windows(t):
    try:
        from win32com.client import GetObject
    except ImportError:
        return
    try:
        items = GetObject("WinMgMts://./root/cimv2").InstancesOf("Win32_ComputerSystem")
        for item in items:
            username = item.Properties_("UserName").Value
            if username:
                # WMI returns "DOMAIN\\user" for the interactive console user.
                t.add([username, "console", "", "", "console"])
    except Exception:
        logger.exception("Win32_ComputerSystem")


def Gather(collector):
    t = collector.table("loggedonuser", _COLUMNS)
    if sys.platform == "win32":
        _gather_windows(t)
    else:
        _gather_unix(t)


def _test():
    from sigma.gather import gathering
    gathering.test(Gather)


if __name__ == "__main__":
    _test()
