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

"""Sigma agent certificate management tool.

Usage: %prog archive
       %prog list
       %prog check [-n]
       %prog renew
       %prog restore [cert serial number]
       %prog show 
       
"""

import os
import sys
import time
import logging
import optparse

from cryptography import x509
from cryptography.hazmat.primitives.serialization import Encoding

from sigma.protocol import rest
from sigma.util import avalog
from sigma.util import cert as certutil
from sigma.util.config import Options
from sigma.util.errors import usage

parser = optparse.OptionParser(usage=__doc__.strip())
parser.add_option("-n", "--no-renewal", action="store_false", dest="renew",
                  help="don't renew cert even if it is in the expiration zone",
                  default=True)

options = Options()
store = certutil.CertificateStore(ssl=True)
logger = logging.getLogger("cert-mgr")

def main(args=None):
    avalog.setupLogging()
    opts, args = parser.parse_args(args)
    if not args:
        parser.print_help()
        return 0
    p = Processor(opts, args[0], args[1:])
    return p.run()


class Processor(object):

    def __init__(self, opts, cmd, args):
        self.opts = opts
        self.cmd = cmd
        self.args = args


    def run(self):

        # check to see we have registered
        if store.cert is None:
            # Not-yet-registered is an expected transient state -- e.g. the
            # scheduled cert-renew task firing at startup a beat before
            # registration completes. It's a no-op, not a failure, so log at
            # info and exit 0 (returning 1 made the scheduler log a spurious
            # "Task cert-renew: exit status 1" ERROR on every boot).
            logger.info("Agent not registered yet; nothing for cert-mgr to do.")
            return 0
        
        method = getattr(self, "do_" + self.cmd, None)
        if method is None:
            return usage("unrecognized command %r", self.cmd)
        return method()

    def getConnection(self, ssl):
        return rest.Connection(options.central_server, ssl=ssl,
                               certstore=store, useTimeout=False)

    def renewCertificate(self):
        """Renew Agent's certificate.

        Archives the current agent certificate then opens a secure connection
        to the server and requests a new certificate for the agent.

        """

        try:
            store.archive() # archive the cert just in case
            rc = self.getConnection(True)
            rc.loadCertificate() # always reload the root certificate
            if rc.requestCertificate():
                logger.info("Agent certificate renewed successfully")
                return 0
            else:
                logger.error("Can't renew agent certificate")
                return 1
        except Exception as err:
            logger.error("Can't renew agent certificate: %s", err)
            return 1

    def do_archive(self):
        "Archives the current agent certificate"
        if self.args:
            return usage("'archive' does not take arguments or options")

        if store.archive():
            return 0
        else:
            return 1
        
    def do_list(self):
        "Lists all of the archived certs on the agent"
        if self.args:
            return usage("'list' does not take arguments or options")

        certs = []
        # check to make sure directory exists, then build list of certs
        if os.path.isdir(store.archivedir):
            for file in os.listdir(store.archivedir):
                if file.endswith('.cert'):
                    file = os.path.join(store.archivedir, file)
                    with open(file, "rb") as cert_file:
                        certs.append(x509.load_pem_x509_certificate(cert_file.read()))

        # sort the list of certs based on date issued
        certs.sort(key=lambda x: x.not_valid_before_utc)
        
        print("%d certs currently archived" % len(certs))
        for order, cert in enumerate(certs):
            certutil.print_cert(order + 1, cert)
        return 0

    def do_check(self):
        """Certificate renewal check.

        Checks to see if the Agent's certificate will expire within the
        expiration period configured in the cert-new-days property.  If so
        the certificate will be renewed.

        """

        if self.args:
            return usage("'check' takes no arguments")

        expire_date = certutil.get_expiration_date(store.cert)

        # if it has already expired log an error, we can't renew it
        if expire_date < time.time():
            logger.error("Agent certificate has already expired")
            return 1

        # check if it will expire within the configured renewal time
        end_window = time.time() + (options.cert_renew_days * 24 * 60 * 60)
        if expire_date < end_window:
            if self.opts.renew:
                logger.info("Renewing agent certificate")
                return self.renewCertificate()
            else:
                logger.info("Agent certificate should be renewed now")
                return 0

        logger.info("Agent certificate should be renewed %d days before %s",
                    options.cert_renew_days, store.cert.get_not_after())
        return 0

    def do_renew(self):
        """Forced certificate renewal.

        Forces a renewal of the Agent's certificate regardless of the
        expiration date of the current certificate.  This is only invoked
        when the script is run manually.

        """
        if self.args:
            return usage("'renew' takes no arguments")

        logger.info("Manual upgrade of agent certificate requested")
        return self.renewCertificate()

    def do_restore(self):
        """Restore an older certificate from archives.

        Archives the current Agent certificate and restores an older
        certificate (selected by certificate serial number) from the archives.

        """
        if len(self.args) != 1:
            return usage("'restore' takes a serial number argument.")

        # serialnum to restore
        serialnum = int(self.args[0])
        
        # archive the current certificate 
        if not store.archive():
            return 1

        if store.restore_from_archive(serialnum):
            logger.info("Certificate %d restored", serialnum)
            return 0
        else:
            logger.error("Certificate %d could not be restored", serialnum)
            return 1

    def do_show(self):
        """Show the current agent certificate.

        Prints out the details of the current agent certificate in a similar
        form as the archive command.

        """

        if self.args:
            return usage("'show' takes no arguments")

        certutil.print_cert("(Active)", store.cert)


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