#!/usr/lib64/linuxfabrik-monitoring-plugins/venv/bin/python
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import sys

import lib.args
import lib.base
import lib.lftest
import lib.shell
from lib.globals import STATE_OK, STATE_UNKNOWN, STATE_WARN

__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026102403'

DESCRIPTION = """Verifies that the system-wide cryptographic policy (as reported by
update-crypto-policies) matches the expected setting (default: "DEFAULT"). Subpolicies
such as NO-SHA1 are listed, but only the base policy is compared, unless the expected
setting names subpolicies itself. Alerts when the policy in force is not the expected
one. Useful for ensuring consistent TLS and cipher configurations across a fleet of
servers."""

CMD = ['update-crypto-policies', '--show']
DEFAULT_CRYPTO_POLICY = 'DEFAULT'
DEFAULT_TIMEOUT = 8


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(
        description=DESCRIPTION,
        epilog=lib.args.epilog(__file__),
        formatter_class=lib.args.HelpFormatter,
    )

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--always-ok',
        help=lib.args.help('--always-ok'),
        dest='ALWAYS_OK',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--policy',
        default=DEFAULT_CRYPTO_POLICY,
        dest='CRYPTO_POLICY',
        help='Expected crypto policy name. '
        'Only the base policy is compared, unless this names subpolicies itself. '
        'Case-insensitive. '
        'Example: `--policy=FUTURE`, or `--policy=DEFAULT:NO-SHA1` to require that '
        'subpolicy. '
        'Default: %(default)s',
    )

    parser.add_argument(
        '--test',
        help=lib.args.help('--test'),
        dest='TEST',
        type=lib.args.csv,
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    args, _ = parser.parse_known_args()
    return args


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    # fetch data
    if args.TEST is None:
        success, result = lib.shell.shell_exec(CMD, timeout=args.TIMEOUT)
        if not success:
            if result.startswith('Timeout after'):
                lib.base.oao(
                    f'Timeout after {args.TIMEOUT}s while running `{" ".join(CMD)}`.',
                    STATE_WARN,
                    always_ok=args.ALWAYS_OK,
                )
            lib.base.cu('Crypto policies are not applicable to your system.')
        stdout, _stderr, _retc = result
    else:
        # do not call the command, put in test data
        stdout, _stderr, _retc = lib.lftest.test(args.TEST)

    # init some vars
    state = STATE_OK

    # analyze data
    # `update-crypto-policies --show` prints the base policy followed by the
    # subpolicies, separated by colons and upper-cased, for example
    # "DEFAULT:NO-SHA1:NO-CAMELLIA" (ProfileConfig in python/update-crypto-policies.py).
    # Subpolicies tighten or loosen the base policy and are added and removed for
    # local reasons, so only the base policy is compared, unless the admin names
    # subpolicies in --policy and thereby asks for the exact combination. That one
    # is a set: the tool keeps the subpolicies in the order they were set in
    # (ProfileConfig.parse_string() and __str__(), crypto-policies at 359ab16), which
    # says nothing about the policy in force.
    crypto_policy = stdout.strip()
    base_policy, *subpolicies = crypto_policy.split(':')
    expected_base, *expected_subpolicies = args.CRYPTO_POLICY.strip().split(':')
    matches = base_policy.upper() == expected_base.upper()
    if ':' in args.CRYPTO_POLICY:
        matches = matches and {item.upper() for item in subpolicies if item} == {
            item.upper() for item in expected_subpolicies if item
        }

    # build the message
    msg = f'Crypto policy is "{base_policy}"'
    if subpolicies:
        msg += f' with the subpolicies {", ".join(subpolicies)}'
    if matches:
        msg += ' (as expected).'
    else:
        msg += f', but supposed to be "{args.CRYPTO_POLICY}".'
        state = STATE_WARN

    # over and out
    lib.base.oao(msg, state, always_ok=args.ALWAYS_OK)


if __name__ == '__main__':
    try:
        main()
    except Exception:
        lib.base.cu()
