#!/usr/bin/env python3

"""
check_gitlab_security_scan -- Monitoring plugin to check if a Gitlab scan job report
has security vulnerabilities.

Author: Tunui Franken <tfranken@easter-eggs.com>

Copyright (C) 2026 Easter-eggs

check_gitlab_security_scan is free software; you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import json
import os
import sys
from argparse import ArgumentParser
from datetime import datetime, timedelta
from enum import IntEnum
from getpass import getpass

from gitlab import Gitlab
from gitlab.exceptions import GitlabError
from requests import Session
from requests.exceptions import ConnectionError


def main() -> None:
    multiline = ""
    status = "OK"
    message = "No security vulnerabilities have been found"
    code = ExitCode.OK

    args = parse_args()

    report = get_report(
        url=args.url,
        proxy=args.https_proxy,
        token=args.token,
        project=args.project,
        ref_name=args.ref_name,
        job=args.job,
        report_path=args.report_path,
        doc_url=args.doc_url,
    )

    str_job_date = report["scan"]["end_time"]
    job_date = datetime.strptime(str_job_date, "%Y-%m-%dT%H:%M:%S")
    limit_date = datetime.today() - timedelta(days=args.limit_old)
    if job_date < limit_date:
        status = "CRITICAL"
        message = f"""Job {args.job} with the reference {args.ref_name} in the {args.project} project has been
        executed for the  last time on {str_job_date} which is older than {args.limit_old} days ago."""
        code = ExitCode.CRITICAL
    elif report["vulnerabilities"]:
        vulnerabilities = [0, 0, 0, 0, 0]
        if isinstance(report["vulnerabilities"], list):
            vulns = report["vulnerabilities"]
        elif isinstance(report["vulnerabilities"], dict):
            vulns = report["vulnerabilities"].values()
        else:
            clean_exit(
                code=ExitCode.UNKNOWN,
                status="UNKNOWN",
                message="Vulnerabilities are in unknown format",
                multiline="",
                doc_url=args.doc_url,
            )
        for vuln in vulns:
            vulnerabilities[Severity[vuln.get("severity").upper()]] += 1
        for i in range(Severity[args.limit_warning], Severity[args.limit_critical]):
            if vulnerabilities[Severity(i)] > 0:
                status = "WARNING"
                code = ExitCode.WARNING
                break
        for i in range(Severity[args.limit_critical], len(Severity)):
            if vulnerabilities[Severity(i)] > 0:
                status = "CRITICAL"
                code = ExitCode.CRITICAL
                break
        message = f"""{len(report["vulnerabilities"])} security vulnerabilities have been found.
        Find a full report in {args.url}/{args.project}/-/artifacts"""
        multiline += f"Number of critical vulnerabilities: {vulnerabilities[Severity.CRITICAL]}\n"
        multiline += (
            f"Number of high vulnerabilities: {vulnerabilities[Severity.HIGH]}\n"
        )
        multiline += (
            f"Number of medium vulnerabilities: {vulnerabilities[Severity.MEDIUM]}\n"
        )
        multiline += f"Number of low vulnerabilities: {vulnerabilities[Severity.LOW]}\n"
        multiline += (
            f"Number of unknown vulnerabilities: {vulnerabilities[Severity.UNKNOWN]}\n"
        )

    clean_exit(code, status, message, multiline, args.doc_url)


class ExitCode(IntEnum):
    """Enum for exit codes"""

    OK = 0
    WARNING = 1
    CRITICAL = 2
    UNKNOWN = 3


class Severity(IntEnum):
    """Vuln severity"""

    UNKNOWN = 0
    LOW = 1
    INFO = 1
    MEDIUM = 2
    MODERATE = 2
    HIGH = 3
    CRITICAL = 4


def get_report(
    url: str,
    proxy: str,
    token: str,
    project: str,
    ref_name: str,
    job: str,
    report_path: str,
    doc_url: str,
) -> dict:
    """Use Gitlab API to retrieve the scan report"""

    session = Session()
    session.proxies.update({"https": proxy})

    try:
        with Gitlab(url=url, session=session, private_token=token) as gl:
            p = gl.projects.get(project)
            j = [
                j
                for j in p.jobs.list(iterator=True)
                if j.name == job and j.ref == ref_name
            ][0]
            report = j.artifact(path=report_path)
    except (GitlabError, ConnectionError) as err:
        clean_exit(code=ExitCode.UNKNOWN, status="UNKNOWN", message=err, multiline="", doc_url=doc_url)

    return json.loads(report.decode())


def clean_exit(code: ExitCode, status: str, message: str, multiline: str, doc_url: str) -> None:
    """Clean exit"""

    doc = f"Documentation can be found at {doc_url}"

    output = f"{status}: {message}\n{multiline}{doc}"
    print(output)
    sys.exit(code)


def parse_args():
    parser = ArgumentParser(
        description="Monitoring plugin to check if a Gitlab scan job report has security vulnerabilities."
    )

    parser.add_argument(
        "-d",
        "--doc-url",
        help="Documentation URL",
        default="https://www.easter-eggs.fr/ee/gitlab/#job_de_scan_de_vulnerabilites_d_images_docker",
    )
    gitlab_args = parser.add_argument_group("Gitlab options")
    gitlab_args.add_argument(
        "-u",
        "--url",
        required=True,
        help="Gitlab URL",
    )
    gitlab_args.add_argument(
        "-t",
        "--token",
        help="Gitlab access token",
    )
    gitlab_args.add_argument(
        "-f",
        "--token-file",
        help="Gitlab access token file",
    )
    gitlab_args.add_argument(
        "-p",
        "--project",
        required=True,
        help="Gitlab project",
    )
    gitlab_args.add_argument(
        "-r", "--ref-name", help="Branch or tag name in repository", default="main"
    )
    gitlab_args.add_argument(
        "-j", "--job", help="Job name", default="container_scanning"
    )
    gitlab_args.add_argument(
        "-R",
        "--report-path",
        help="Path to report json inside artifacts archive",
        default="gl-container-scanning-report.json",
    )
    gitlab_args.add_argument(
        "-x",
        "--https-proxy",
        help="HTTPS proxy",
    )
    gitlab_args.add_argument(
        "-o",
        "--limit-old",
        help="If the job is older than this number of days, return a critical status",
        default="7",
        type=int,
    )
    gitlab_args.add_argument(
        "-C",
        "--limit-critical",
        help="If severities higher than limit-critical are found, return a critical status",
        default="CRITICAL",
        choices=["UNKNOWN", "LOW", "INFO", "MEDIUM", "MODERATE", "HIGH", "CRITICAL"],
    )
    gitlab_args.add_argument(
        "-W",
        "--limit-warning",
        help="If severities higher than limit-warning are found, return a warning status",
        default="HIGH",
        choices=["UNKNOWN", "LOW", "INFO", "MEDIUM", "MODERATE", "HIGH", "CRITICAL"],
    )

    args = parser.parse_args()

    if args.token:
        # token provided, we are done
        return args

    if args.token_file and os.path.exists(args.token_file):
        # token file provided, load it
        try:
            with open(args.token_file, "r") as token_file:
                token = token_file.read().strip()
        except Exception as err:  # pylint: disable=broad-exception-caught
            parser.exit(
                ExitCode.UNKNOWN,
                "UNKNOWN - Fail to load Gitlab access token from file "
                f"'{args.token_file}': {err}"
                "\n",
            )
        if not token:
            parser.exit(
                ExitCode.UNKNOWN,
                "UNKNOWN - Empty access token loaded from file "
                f" ({args.token_file}). Please check it or provide it "
                "using -t/--token parameter.\n",
            )
    else:
        # no token, ask for it
        token = getpass(f"Please enter token for project {args.project}: ")

    args.token = token

    return args


if __name__ == "__main__":
    main()
