#!/usr/bin/env python3
# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Print Sigma agent status summary.

Usage: %prog [-iREPEAT]

"""

# XXX This embeds too much knowledge gleaned from other utilities;
# we should define proper APIs to review the queues, log files, etc.

# Python imports
import os
import re
import sys
import glob
import time
import errno
import socket
import optparse
import textwrap

# Fuel imports
from sigma.util import config, ostypes, timeskew
from sigma.util.errors import usage, error
from sigma.sched import remote
from sigma.spool import qmgr
from sigma.gather import gathering

kbyte = 1024

parser = optparse.OptionParser(usage=__doc__.strip())
parser.add_option("-i", "--interval", dest="repeat", action="store",
                  type="float", help="repeat output every REPEAT seconds")

def main(args=None):
    # Parse command line arguments
    opts, args = parser.parse_args(args)
    if args:
        return usage("This program takes no arguments")

    # Get Sigma configuration
    try:
        options = config.Options()
    except IOError as err:
        avroot = os.getenv("AVROOT")
        if not avroot:
            avroot = config.defaultroot()
        avconf = os.getenv("AVCONF")
        if not avconf:
            avconf = os.path.join(avroot, "conf")
        configfile = os.path.join(avconf, "config")
        return error("Can't open config file %r\n(AVROOT=%r, AVCONF=%r)",
                     configfile, avroot, avconf)

    # Either produce output once, or (with -i) loop forever
    if not opts.repeat:
        doit(options)
    else:
        delay = opts.repeat
        while True:
            try:
                if os.name == "posix" and sys.stdout.isatty():
                    # Clear screen (on most terminals these days :-)
                    sys.stdout.write("\033[H\033[2J")
                format("Repeat Interval", str(delay))
                doit(options)
                time.sleep(delay)
            except KeyboardInterrupt:
                break

def doit(options):
    # Print current date and time
    ymd_hms = time.localtime()[:6]
    format("Current Time", "%4d-%02d-%02d %02d:%02d:%02d" % ymd_hms)

    skew = timeskew.get_skew(options)
    format("Time Skew", "%.6f" % skew)

    # Print relevant environment variables
    if not os.getenv("AVROOT"):
        format("Default Root", config.defaultroot())
    for n in ("AVROOT", "AVDATA", "AVCONF", "AVLOGS", "AVBIN", "AVLIB",
              "FUELPATH"):
        v = os.getenv(n)
        if v is not None:
            format(n, v)

    # Print agent software version and ostype
    vfn = os.path.join(options.agentactive, "version.txt")
    try:
        f = open(vfn)
    except IOError:
        version = None
    else:
        try:
            version = f.readline().strip()
        finally:
            f.close()
    if not version:
        version = "(unknown)"
    version = "%s on %s" % (version, ostypes.guess_ostype())
    format("Agent version", version)

    # Print Agent ID
    from sigma.util import cert
    store = cert.CertificateStore(ssl=False, options=options)
    if not os.path.isfile(store.idfile):
        aid = "(not registered)"
    else:
        try:
            f = open(store.idfile)
        except IOError as err:
            aid = "(IOError: %s)" % err
        else:
            try:
                aid = f.read().strip()
            finally:
                f.close()
    format("Agent ID", aid)

    # Print configuration essentials
    format("Central Server", options.central_server)
    format("Local Scheduler", options.scheduler)

    # Print scheduler tasks
    sched = remote.Scheduler()
    try:
        taskdict = sched.listtasks()
    except socket.error:
        schedsts = "(not running)"
    except Exception as err:
        schedsts = "(%s: %s)" % (err.__class__.__name__, err)
    else:
        if not taskdict:
            schedsts = "(no tasks)"
        else:
            keys = sorted(taskdict.keys())
            schedsts = ", ".join(keys)
    format("Scheduler Tasks", schedsts)

    # Print upload spool queue status
    qm = qmgr.QueueManager()
    count = 0
    totalsize = 0
    for qi in qm.list_items():
        count += 1
        kbd = (os.path.getsize(qi.filename)+kbyte-1)/kbyte
        kbm = (os.path.getsize(qi.metadatafilename)+kbyte-1)/kbyte
        totalsize += kbd + kbm
    spoolsts = "%d spool items (%d kbytes)" % (count, totalsize)
    format("Spool Queue", spoolsts)

    # Print policy directory
    policydir = options.datapath("policy")
    policypattern = os.path.join(policydir, "dir.*.xml")
    directives = glob.glob(policypattern)
    dirnames = []
    for directive in directives:
        directive = os.path.basename(directive)
        ml = directive.lower()
        if ml.startswith("dir."):
            directive = directive[4:]
        if ml.endswith(".xml"):
            directive = directive[:-4]
        dirnames.append(directive)
    format("Directives", ", ".join(dirnames) or "(none)")

    # Pring Gather Status
    dirname = gathering.get_gather_dir(options)
    pattern = os.path.join(dirname, "*.??????")
    files = glob.glob(pattern)
    gathersts = "(none)"
    if files:
        classes = []
        for file in files:
            file = os.path.basename(file)
            i = file.rindex(".")
            cls = file[:i]
            if cls not in classes:
                classes.append(cls)
        classes.sort()
        gathersts = ", ".join(classes)
    format("Gathered Data", gathersts)

    # Print poliwall status
    pwpidfile = options.datapath("pwpid.txt")
    pwsts = "(not running)"
    if os.path.isfile(pwpidfile):
        try:
            f = open(pwpidfile)
        except IOError as err:
            pwsts = "(%s: %s)" % (err.__class__.__name__, err)
        else:
            try:
                try:
                    pids = f.read().strip()
                    pid = int(pids)
                except (IOError, ValueError):
                    pwsts = "(no pid)"
                else:
                    pwsts = "pid %d" % pid
                    if os.name != "nt":
                        try:
                            os.kill(pid, 0)
                        except os.error as err:
                            if err.errno == errno.ESRCH:
                                pwsts = "(not running)"
            finally:
                f.close()
    pwstatefile = options.datapath("pwstate.txt")
    pwstate = "(no pwstate.txt file)"
    if os.path.isfile(pwstatefile):
        try:
            f = open(pwstatefile)
        except IOError as err:
            pwstate = "(%s: %s)" % (err.__class__.__name__, err)
        else:
            try:
                pwmodule = None
                pwkernel = None
                pwoptions = None
                for line in f:
                    if line.startswith("module="):
                        pwmodule = line[7:].strip()
                        if pwmodule.startswith(options.agentlib):
                            n = len(options.agentlib)
                            pwmodule = "$AVLIB" + pwmodule[n:]
                    elif line.startswith("options="):
                        pwoptions = line[8:].strip()
                    elif line.startswith("kernel="):
                        pwkernel = line[7:].strip().lower() == "true"
            finally:
                f.close()
            pwstate = "options=%r, kernel=%r, module=%r" % (pwoptions,
                                                            pwkernel,
                                                            pwmodule)
            
    format("Poliwall Status", pwsts + "; " + pwstate)

    # Print Log File Status
    import logtail
    ymd = time.localtime()[:3]
    logfile = os.path.join(logtail.logdir, logtail.pattern % ymd)
    if not os.path.isfile(logfile):
        crit = None
        err = None
        warn = None
    else:
        try:
            f = open(logfile)
        except IOError as err:
            logsts = "(%s: %s)" % (err.__class__.__name__, err)
        else:
            try:
                crit = None
                err = None
                warn = None
                for line in f:
                    if ":CRITICAL:" in line:
                        crit = line
                    if ":ERROR:" in line:
                        err = line
                    if ":WARNING:" in line:
                        warn = line
            finally:
                f.close()
            if crit:
                crit = crit.strip()
                crit = crit.replace(":CRITICAL:", ":CRITICAL:\n")
            if err:
                err = err.strip()
                err = err.replace(":ERROR:", ":ERROR:\n")
            if warn:
                warn = warn.strip()
                warn = warn.replace(":WARNING:", ":WARNING:\n")
    format("Last Critical", crit or "(none)")
    format("Last Error", err or "(none)")
    format("Last Warning", warn or "(none)")

    # Print Last Full Connection Time
    # use the mtime of the SSL good status file to indicate this
    stsfile = options.datapath("last_good_ssl.txt")
    if not os.path.isfile(stsfile):
        connect = "(none)"
    else:
        connect = time.localtime(os.path.getmtime(stsfile))
        connect = "%4d-%02d-%02d %02d:%02d:%02d" % connect[:6]

    format("Last Full Connect", connect)

def format(label, value):
    value = str(value)
    # Tricky: wrap each line, but don't run lines together
    lines = [textwrap.fill(line, 59) for line in value.splitlines()]
    for line in lines:
        line = line.replace("\n", "\n" + " "*20)
        if label:
            print("%-17s = %s" % (label, line))
            label = None
        else:
            print(" "*20 + line)

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