#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
from defusedxml.ElementTree import parse
import json
import os
import shutil
import subprocess
import sys
import tarfile
import tempfile

import oras.client
from quart import Quart, request
from rich.panel import Panel
from rich.terminal_theme import DEFAULT_TERMINAL_THEME, MONOKAI
from vdb.lib import config
from vdb.lib import db as db_lib
from vdb.lib.config import data_dir
from vdb.lib.gha import GitHubSource
from vdb.lib.nvd import NvdSource
from vdb.lib.osv import OSVSource
from vdb.lib.utils import parse_purl

from depscan.lib import explainer, github, utils
from depscan.lib.analysis import (
    PrepareVdrOptions,
    analyse_licenses,
    analyse_pkg_risks,
    find_purl_usages,
    jsonl_report,
    prepare_vdr,
    suggest_version,
    summary_stats,
)
from depscan.lib.audit import audit, risk_audit, risk_audit_map, type_audit_map
from depscan.lib.bom import (
    create_bom,
    get_pkg_by_type,
    get_pkg_list,
    submit_bom,
)
from depscan.lib.config import (
    UNIVERSAL_SCAN_TYPE,
    license_data_dir,
    spdx_license_list,
    vdb_database_url,
    vdb_rafs_database_url,
)
from depscan.lib.csaf import export_csaf, write_toml
from depscan.lib.license import build_license_data, bulk_lookup
from depscan.lib.logger import DEBUG, LOG, console

try:
    os.environ["PYTHONIOENCODING"] = "utf-8"
except Exception:
    pass

LOGO = """
██████╗ ███████╗██████╗ ███████╗ ██████╗ █████╗ ███╗   ██╗
██╔══██╗██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗████╗  ██║
██║  ██║█████╗  ██████╔╝███████╗██║     ███████║██╔██╗ ██║
██║  ██║██╔══╝  ██╔═══╝ ╚════██║██║     ██╔══██║██║╚██╗██║
██████╔╝███████╗██║     ███████║╚██████╗██║  ██║██║ ╚████║
╚═════╝ ╚══════╝╚═╝     ╚══════╝ ╚═════╝╚═╝  ╚═╝╚═╝  ╚═══╝
"""


app = Quart(__name__)
app.config.from_prefixed_env()


def build_args():
    """
    Constructs command line arguments for the depscan tool
    """
    parser = argparse.ArgumentParser(
        description="Fully open-source security and license audit for "
        "application dependencies and container images based on "
        "known vulnerabilities and advisories.",
        epilog="Visit https://github.com/owasp-dep-scan/dep-scan to learn more.",
    )
    parser.add_argument(
        "--no-banner",
        action="store_true",
        default=False,
        dest="no_banner",
        help="Do not display banner",
    )
    parser.add_argument(
        "--cache",
        action="store_true",
        default=False,
        dest="cache",
        help="Cache vulnerability information in platform specific "
        "user_data_dir",
    )
    parser.add_argument(
        "--csaf",
        action="store_true",
        default=False,
        dest="csaf",
        help="Generate a OASIS CSAF VEX document",
    )
    parser.add_argument(
        "--sync",
        action="store_true",
        default=False,
        dest="sync",
        help="Sync to receive the latest vulnerability data. Should have "
        "invoked cache first.",
    )
    parser.add_argument(
        "--profile",
        default="generic",
        choices=(
            "appsec",
            "research",
            "operational",
            "threat-modeling",
            "license-compliance",
            "generic",
        ),
        dest="profile",
        help="Profile to use while generating the BOM.",
    )
    parser.add_argument(
        "--no-suggest",
        action="store_false",
        default="True",
        dest="suggest",
        help="Disable suggest mode",
    )
    parser.add_argument(
        "--risk-audit",
        action="store_true",
        default=os.getenv("ENABLE_OSS_RISK", "") in ("true", "1"),
        dest="risk_audit",
        help="Perform package risk audit (slow operation). Npm only.",
    )
    parser.add_argument(
        "--private-ns",
        dest="private_ns",
        default=os.getenv("PKG_PRIVATE_NAMESPACE"),
        help="Private namespace to use while performing oss risk audit. "
        "Private packages should not be available in public registries "
        "by default. Comma separated values accepted.",
    )
    parser.add_argument(
        "-t",
        "--type",
        dest="project_type",
        default=os.getenv("DEPSCAN_PROJECT_TYPE"),
        help="Override project type if auto-detection is incorrect",
    )
    parser.add_argument(
        "--bom",
        dest="bom",
        help="Examine using the given Software Bill-of-Materials (SBOM) file "
        "in CycloneDX format. Use cdxgen command to produce one.",
    )
    parser.add_argument(
        "-i",
        "--src",
        dest="src_dir_image",
        help="Source directory or container image or binary file",
    )
    parser.add_argument(
        "-o",
        "--report_file",
        dest="report_file",
        help="DEPRECATED. Use reports directory since multiple files are "
        "created. Report filename with directory",
    )
    parser.add_argument(
        "--reports-dir",
        default=os.getenv(
            "DEPSCAN_REPORTS_DIR", os.path.join(os.getcwd(), "reports")
        ),
        dest="reports_dir",
        help="Reports directory",
    )
    parser.add_argument(
        "--no-error",
        action="store_true",
        default=False,
        dest="noerror",
        help="UNUSED: Continue on error to prevent build from breaking",
    )
    parser.add_argument(
        "--no-license-scan",
        action="store_true",
        default=False,
        dest="no_license_scan",
        help="UNUSED: dep-scan doesn't perform license scanning by default",
    )
    parser.add_argument(
        "--deep",
        action="store_true",
        default=False,
        dest="deep_scan",
        help="Perform deep scan by passing this --deep argument to cdxgen. "
        "Useful while scanning docker images and OS packages.",
    )
    parser.add_argument(
        "--no-universal",
        action="store_true",
        default=False,
        dest="non_universal_scan",
        help="Depscan would attempt to perform a single universal scan "
        "instead of individual scans per language type.",
    )
    parser.add_argument(
        "--no-vuln-table",
        action="store_true",
        default=False,
        dest="no_vuln_table",
        help="Do not print the table with the full list of vulnerabilities. "
        "This can help reduce console output.",
    )
    parser.add_argument(
        "--threatdb-server",
        default=os.getenv("THREATDB_SERVER_URL"),
        dest="threatdb_server",
        help="ThreatDB server url. Eg: https://api.sbom.cx",
    )
    parser.add_argument(
        "--threatdb-username",
        default=os.getenv("THREATDB_USERNAME"),
        dest="threatdb_username",
        help="ThreatDB username",
    )
    parser.add_argument(
        "--threatdb-password",
        default=os.getenv("THREATDB_PASSWORD"),
        dest="threatdb_password",
        help="ThreatDB password",
    )
    parser.add_argument(
        "--threatdb-token",
        default=os.getenv("THREATDB_ACCESS_TOKEN"),
        dest="threatdb_token",
        help="ThreatDB token for token based submission",
    )
    parser.add_argument(
        "--server",
        action="store_true",
        default=False,
        dest="server_mode",
        help="Run depscan as a server",
    )
    parser.add_argument(
        "--server-host",
        default=os.getenv("DEPSCAN_HOST", "127.0.0.1"),
        dest="server_host",
        help="depscan server host",
    )
    parser.add_argument(
        "--server-port",
        default=os.getenv("DEPSCAN_PORT", "7070"),
        dest="server_port",
        help="depscan server port",
    )
    parser.add_argument(
        "--cdxgen-server",
        default=os.getenv("CDXGEN_SERVER_URL"),
        dest="cdxgen_server",
        help="cdxgen server url. Eg: http://cdxgen:9090",
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        default=False,
        dest="enable_debug",
        help="Run depscan in debug mode.",
    )
    parser.add_argument(
        "--explain",
        action="store_true",
        default=False,
        dest="explain",
        help="Makes depscan to explain the various analysis. Useful for creating detailed reports.",
    )
    parser.add_argument(
        "--reachables-slices-file",
        dest="reachables_slices_file",
        help="Path for the reachables slices file created by atom.",
    )
    parser.add_argument(
        "-v",
        "--version",
        help="Display the version",
        action="version",
        version="%(prog)s " + utils.get_version(),
    )
    return parser.parse_args()


def scan(db, project_type, pkg_list, suggest_mode):
    """
    Method to search packages in our vulnerability database

    :param db: Reference to db
    :param project_type: Project Type
    :param pkg_list: List of packages
    :param suggest_mode: True if package fix version should be normalized across
            findings
    :returns: A list of package issue objects or dictionaries.
              A dictionary mapping package names to their aliases.
              A dictionary mapping packages to their suggested fix versions.
              A dictionary mapping package URLs to their aliases.
    """
    if not pkg_list:
        LOG.debug("Empty package search attempted!")
    else:
        LOG.debug("Scanning %d oss dependencies for issues", len(pkg_list))
    results, pkg_aliases, purl_aliases = utils.search_pkgs(
        db, project_type, pkg_list
    )
    # pkg_aliases is a dict that can be used to find the original vendor and
    # package name This way we consistently use the same names used by the
    # caller irrespective of how the result was obtained
    sug_version_dict = {}
    if suggest_mode:
        # From the results identify optimal max version
        sug_version_dict = suggest_version(results, pkg_aliases, purl_aliases)
        if sug_version_dict:
            LOG.debug(
                "Adjusting fix version based on the initial suggestion %s",
                sug_version_dict,
            )
            # Recheck packages
            sug_pkg_list = []
            for k, v in sug_version_dict.items():
                if not v:
                    continue
                vendor = ""
                version = v
                # Key is already a purl
                if k.startswith("pkg:"):
                    try:
                        purl_obj = parse_purl(k)
                        vendor = purl_obj.get("namespace")
                        if not vendor:
                            vendor = purl_obj.get("type")
                        name = purl_obj.get("name")
                        version = purl_obj.get("version")
                        sug_pkg_list.append(
                            {
                                "vendor": vendor,
                                "name": name,
                                "version": version,
                                "purl": k,
                            }
                        )
                        continue
                    except Exception:
                        pass
                tmp_a = k.split(":")
                if len(tmp_a) == 3:
                    vendor = tmp_a[0]
                    name = tmp_a[1]
                else:
                    name = tmp_a[0]
                # De-alias the vendor and package name
                full_pkg = f"{vendor}:{name}:{version}"
                full_pkg = pkg_aliases.get(full_pkg, full_pkg)
                vendor, name, version = full_pkg.split(":")
                sug_pkg_list.append(
                    {"vendor": vendor, "name": name, "version": version}
                )
            LOG.debug(
                "Re-checking our suggestion to ensure there are no further "
                "vulnerabilities"
            )
            override_results, _, _ = utils.search_pkgs(
                db, project_type, sug_pkg_list
            )
            if override_results:
                new_sug_dict = suggest_version(override_results)
                LOG.debug("Received override results: %s", new_sug_dict)
                for nk, nv in new_sug_dict.items():
                    sug_version_dict[nk] = nv
    return results, pkg_aliases, sug_version_dict, purl_aliases


def summarise(
    project_type,
    results,
    pkg_aliases,
    purl_aliases,
    sug_version_dict,
    scoped_pkgs,
    report_file,
    bom_file,
    no_vuln_table=False,
    direct_purls=None,
    reached_purls=None,
):
    """
    Method to summarise the results
    :param project_type: Project type
    :param results: Scan or audit results
    :param pkg_aliases: Package aliases used
    :param purl_aliases: Package URL to package name aliase
    :param sug_version_dict: Dictionary containing version suggestions
    :param scoped_pkgs: Dict containing package scopes
    :param report_file: Output report file
    :param bom_file: SBOM file
    :param no_vuln_table: Boolean to indicate if the results should get printed
            to the console
    :return: A dict of vulnerability and severity summary statistics
    """
    if not results:
        LOG.info("No oss vulnerabilities detected for type %s ✅", project_type)
        return None
    if report_file:
        jsonl_report(
            project_type,
            results,
            pkg_aliases,
            purl_aliases,
            sug_version_dict,
            scoped_pkgs,
            report_file,
            direct_purls=direct_purls,
            reached_purls=reached_purls,
        )
    options = PrepareVdrOptions(
        project_type,
        results,
        pkg_aliases,
        purl_aliases,
        sug_version_dict,
        scoped_pkgs=scoped_pkgs,
        no_vuln_table=no_vuln_table,
        bom_file=bom_file,
        direct_purls=direct_purls,
        reached_purls=reached_purls,
    )
    pkg_vulnerabilities, pkg_group_rows = prepare_vdr(options)
    vdr_file = bom_file.replace(".json", ".vdr.json")
    if pkg_vulnerabilities and bom_file:
        try:
            with open(bom_file, encoding="utf-8") as fp:
                bom_data = json.load(fp)
                if bom_data:
                    # Add depscan information as metadata
                    metadata = bom_data.get("metadata", {})
                    tools = metadata.get("tools", {})
                    bom_version = str(bom_data.get("version", 1))
                    # Update the version
                    if bom_version.isdigit():
                        bom_version = int(bom_version) + 1
                        bom_data["version"] = bom_version
                    # Update the tools section
                    if isinstance(tools, dict):
                        components = tools.get("components", [])
                        ds_version = utils.get_version()
                        ds_purl = f"pkg:pypi/owasp-depscan@{ds_version}"
                        components.append(
                            {
                                "type": "application",
                                "name": "owasp-depscan",
                                "version": ds_version,
                                "purl": ds_purl,
                                "bom-ref": ds_purl,
                            }
                        )
                        tools["components"] = components
                        metadata["tools"] = tools
                        bom_data["metadata"] = metadata

                    bom_data["vulnerabilities"] = pkg_vulnerabilities
                    with open(vdr_file, mode="w", encoding="utf-8") as vdrfp:
                        json.dump(bom_data, vdrfp, indent=4)
                        LOG.debug(
                            "VDR file %s generated successfully", vdr_file
                        )
        except Exception:
            LOG.warning("Unable to generate VDR file for this scan")
    summary = summary_stats(results)
    return summary, vdr_file, pkg_vulnerabilities, pkg_group_rows


def download_rafs_based_image():
    rafs_image_downloaded, paths_list = False, None
    nydus_image_command = shutil.which("nydus-image", mode=os.X_OK)
    if nydus_image_command is not None:
        LOG.info(
            "About to download the vulnerability database from %s. This might take a while ...",
            vdb_rafs_database_url,
        )

        try:
            oras_client = oras.client.OrasClient()
            rafs_data_dir = tempfile.TemporaryDirectory()
            paths_list = oras_client.pull(
                target=vdb_rafs_database_url, outdir=rafs_data_dir.name
            )

            if os.path.exists(
                os.path.join(rafs_data_dir.name, "data.rafs")
            ) and os.path.exists(os.path.join(rafs_data_dir.name, "meta.rafs")):
                nydus_download_command = [
                    f"{nydus_image_command}",
                    "unpack",
                    "--blob",
                    os.path.join(rafs_data_dir.name, "data.rafs"),
                    "--output",
                    os.path.join(data_dir, "vdb.tar"),
                    "--bootstrap",
                    os.path.join(rafs_data_dir.name, "meta.rafs"),
                ]
                _ = subprocess.run(
                    nydus_download_command,
                    check=True,
                    stdout=subprocess.DEVNULL,
                )
                if os.path.exists(os.path.join(data_dir, "vdb.tar")):
                    rafs_image_downloaded = True
                    with tarfile.open(
                        os.path.join(data_dir, "vdb.tar"), "r"
                    ) as tar:
                        tar.extractall(path=data_dir)
                    os.remove(os.path.join(data_dir, "vdb.tar"))
                else:
                    raise FileNotFoundError("vdb.tar not found")
            else:
                raise FileNotFoundError("data.rafs or meta.rafs not found")

        except Exception as e:
            LOG.info(f"Error: {e}")
            LOG.info(
                f"Unable to pull the vulnerability database (rafs image) from {vdb_rafs_database_url}. Trying to pull the non-rafs-based VDB image."
            )
            rafs_image_downloaded = False

    return rafs_image_downloaded, data_dir


@app.get("/")
async def index():
    """

    :return: An empty dictionary
    """
    return {}


@app.get("/cache")
async def cache():
    """

    :return: a JSON response indicating the status of the caching operation.
    """
    db = db_lib.get()
    if not db_lib.index_count(db["index_file"]):
        rafs_image_downloaded, _ = download_rafs_based_image()
        if not rafs_image_downloaded:
            LOG.info(
                "About to download the vulnerability database from %s. This might take a while ...",
                vdb_database_url,
            )
            oras_client = oras.client.OrasClient()
            oras_client.pull(target=vdb_database_url, outdir=data_dir)
            return {
                "error": "false",
                "message": "vulnerability database cached successfully",
            }
    return {
        "error": "false",
        "message": "vulnerability database already exists",
    }


@app.route("/scan", methods=["GET", "POST"])
async def run_scan():
    """
    :return: A JSON response containing the SBOM file path and a list of
    vulnerabilities found in the scanned packages
    """
    q = request.args
    params = await request.get_json()
    uploaded_bom_file = await request.files

    url = None
    path = None
    multi_project = None
    project_type = None
    results = []
    db = db_lib.get()
    profile = "generic"
    if q.get("url"):
        url = q.get("url")
    if q.get("path"):
        path = q.get("path")
    if q.get("multiProject"):
        multi_project = q.get("multiProject", "").lower() in ("true", "1")
    if q.get("type"):
        project_type = q.get("type")
    if q.get("profile"):
        profile = q.get("profile")
    if params is not None:
        if not url and params.get("url"):
            url = params.get("url")
        if not path and params.get("path"):
            path = params.get("path")
        if not multi_project and params.get("multiProject"):
            multi_project = params.get("multiProject", "").lower() in (
                "true",
                "1",
            )
        if not project_type and params.get("type"):
            project_type = params.get("type")
        if not profile and params.get("profile"):
            profile = params.get("profile")

    if not path and not url and (uploaded_bom_file.get("file", None) is None):
        return {
            "error": "true",
            "message": "path or url or a bom file upload is required",
        }, 400
    if not project_type:
        return {"error": "true", "message": "project type is required"}, 400

    if not db_lib.index_count(db["index_file"]):
        return (
            {
                "error": "true",
                "message": "Vulnerability database is empty. Prepare the "
                "vulnerability database by invoking /cache endpoint "
                "before running scans.",
            },
            500,
            {"Content-Type": "application/json"},
        )

    cdxgen_server = app.config.get("CDXGEN_SERVER_URL")
    bom_file_path = None

    if uploaded_bom_file.get("file", None) is not None:
        bom_file = uploaded_bom_file["file"]
        bom_file_content = bom_file.read().decode("utf-8")
        try:
            if str(bom_file.filename).endswith(".json"):
                _ = json.loads(bom_file_content)
            else:
                _ = parse(bom_file_content)
        except Exception as e:
            LOG.info(e)
            return (
                {
                    "error": "true",
                    "message": "The uploaded file must be a valid JSON or XML.",
                },
                400,
                {"Content-Type": "application/json"},
            )

        LOG.debug("Processing uploaded file")
        bom_file_suffix = str(bom_file.filename).split(".")[-1]
        tmp_bom_file = tempfile.NamedTemporaryFile(
            delete=False, suffix=f".bom.{bom_file_suffix}"
        )
        with open(tmp_bom_file.name, "w") as f:
            f.write(bom_file_content)
        path = tmp_bom_file.name

    # Path points to a project directory
    if os.path.isdir(path):
        with tempfile.NamedTemporaryFile(
            delete=False, suffix=".bom.json"
        ) as bfp:
            bom_status = create_bom(
                project_type,
                bfp.name,
                path,
                True,
                {
                    "url": url,
                    "path": path,
                    "type": project_type,
                    "multiProject": multi_project,
                    "cdxgen_server": cdxgen_server,
                    "profile": profile,
                },
            )
            if bom_status:
                LOG.debug("BOM file was generated successfully at %s", bfp.name)
                bom_file_path = bfp.name

    # Path points to a SBOM file
    else:
        if os.path.exists(path):
            bom_file_path = path

    if bom_file_path is not None:
        pkg_list = get_pkg_list(bom_file_path)
        if not pkg_list:
            return {}
        if project_type in type_audit_map:
            audit_results = audit(project_type, pkg_list)
            if audit_results:
                results = results + audit_results
        vdb_results, pkg_aliases, sug_version_dict, purl_aliases = scan(
            db, project_type, pkg_list, True
        )
        if vdb_results:
            results += vdb_results
        results = [r.to_dict() for r in results]
        bom_data = None
        with open(bom_file_path, encoding="utf-8") as fp:
            bom_data = json.load(fp)
        if not bom_data:
            return (
                {
                    "error": "true",
                    "message": "Unable to generate SBOM. Check your input path or url.",
                },
                400,
                {"Content-Type": "application/json"},
            )
        options = PrepareVdrOptions(
            project_type,
            results,
            pkg_aliases,
            purl_aliases,
            sug_version_dict,
            scoped_pkgs={},
            no_vuln_table=True,
            bom_file=bom_file_path,
            direct_purls=None,
            reached_purls=None,
        )
        pkg_vulnerabilities, _ = prepare_vdr(options)
        if pkg_vulnerabilities:
            bom_data["vulnerabilities"] = pkg_vulnerabilities
        return json.dumps(bom_data), 200, {"Content-Type": "application/json"}

    else:
        return (
            {
                "error": "true",
                "message": "Unable to generate SBOM. Check your input path or url.",
            },
            500,
            {"Content-Type": "application/json"},
        )


def run_server(args):
    """
    Run depscan as server

    :param args: Command line arguments passed to the function.
    """
    print(LOGO)
    console.print(
        f"Depscan server running on {args.server_host}:{args.server_port}"
    )
    app.config["CDXGEN_SERVER_URL"] = args.cdxgen_server
    app.run(
        host=args.server_host,
        port=args.server_port,
        debug=os.getenv("SCAN_DEBUG_MODE") == "debug"
        or os.getenv("AT_DEBUG_MODE") == "debug",
        use_reloader=False,
    )


def main():
    """
    Detects the project type, performs various scans and audits,
    and generates reports based on the results.
    """
    args = build_args()
    # Should we turn on the debug mode
    if args.enable_debug:
        os.environ["AT_DEBUG_MODE"] = "debug"
        LOG.setLevel(DEBUG)
    if args.server_mode:
        return run_server(args)
    if not args.no_banner:
        print(LOGO)
    src_dir = args.src_dir_image
    if not src_dir or src_dir == ".":
        src_dir = os.getcwd()
    reports_dir = args.reports_dir
    if args.csaf:
        toml_file_path = os.getenv(
            "DEPSCAN_CSAF_TEMPLATE", os.path.join(src_dir, "csaf.toml")
        )
        if not os.path.exists(toml_file_path):
            LOG.info("CSAF toml not found, creating template in %s", src_dir)
            write_toml(toml_file_path)
            LOG.info(
                "Please fill out the toml with your details and rerun depscan."
            )
            LOG.info(
                "Check out our CSAF documentation for an explanation of "
                "this feature. https://github.com/owasp-dep-scan/dep-scan"
                "/blob/master/contrib/CSAF_README.md"
            )
            LOG.info(
                "If you're just checking out how our generator works, "
                "feel free to skip filling out the toml and just rerun "
                "depscan."
            )
            sys.exit(0)
    # Detect the project types and perform the right type of scan
    if args.project_type:
        project_types_list = args.project_type.split(",")
    elif args.bom and not args.project_type:
        project_types_list = ["bom"]
    elif not args.non_universal_scan:
        project_types_list = [UNIVERSAL_SCAN_TYPE]
    else:
        project_types_list = utils.detect_project_type(src_dir)
    db = db_lib.get()
    run_cacher = args.cache
    areport_file = (
        args.report_file
        if args.report_file
        else os.path.join(reports_dir, "depscan.json")
    )
    html_file = areport_file.replace(".json", ".html")
    pdf_file = areport_file.replace(".json", ".pdf")
    # Create reports directory
    if reports_dir and not os.path.exists(reports_dir):
        os.makedirs(reports_dir, exist_ok=True)
    if len(project_types_list) > 1:
        LOG.debug("Multiple project types found: %s", project_types_list)
    # Enable license scanning
    if "license" in project_types_list or "license" in args.profile:
        os.environ["FETCH_LICENSE"] = "true"
        project_types_list.remove("license")
        console.print(
            Panel(
                "License audit is enabled for this scan. This would increase "
                "the time by up to 10 minutes.",
                title="License Audit",
                expand=False,
            )
        )
    for project_type in project_types_list:
        results = []
        report_file = areport_file.replace(".json", f"-{project_type}.json")
        risk_report_file = areport_file.replace(
            ".json", f"-risk.{project_type}.json"
        )
        if args.bom and os.path.exists(args.bom):
            bom_file = args.bom
            creation_status = True
        else:
            if args.profile in ("appsec", "research"):
                # The bom file has to be called bom.json for atom reachables to work :(
                bom_file = os.path.join(src_dir, "bom.json")
            else:
                bom_file = report_file.replace("depscan-", "sbom-")
            creation_status = create_bom(
                project_type,
                bom_file,
                src_dir,
                args.deep_scan,
                {"cdxgen_server": args.cdxgen_server, "profile": args.profile},
            )
        if not creation_status:
            LOG.debug("Bom file %s was not created successfully", bom_file)
            continue
        LOG.debug("Scanning using the bom file %s", bom_file)
        if not args.bom:
            LOG.info(
                "To improve performance, cache the bom file and invoke "
                "depscan with --bom %s instead of -i",
                bom_file,
            )
        pkg_list = get_pkg_list(bom_file)
        if not pkg_list:
            LOG.debug("No packages found in the project!")
            continue
        scoped_pkgs = utils.get_pkgs_by_scope(pkg_list)
        if (
            os.getenv("FETCH_LICENSE", "") in (True, "1", "true")
            or "license" in args.profile
        ):
            licenses_results = bulk_lookup(
                build_license_data(license_data_dir, spdx_license_list),
                pkg_list=pkg_list,
            )
            license_report_file = os.path.join(
                reports_dir, "license-" + project_type + ".json"
            )
            analyse_licenses(
                project_type, licenses_results, license_report_file
            )
        if project_type in risk_audit_map:
            if args.risk_audit:
                console.print(
                    Panel(
                        f"Performing OSS Risk Audit for packages from "
                        f"{src_dir}\nNo of packages [bold]{len(pkg_list)}"
                        f"[/bold]. This will take a while ...",
                        title="OSS Risk Audit",
                        expand=False,
                    )
                )
                try:
                    risk_results = risk_audit(
                        project_type,
                        scoped_pkgs,
                        args.private_ns,
                        pkg_list,
                    )
                    analyse_pkg_risks(
                        project_type,
                        scoped_pkgs,
                        risk_results,
                        risk_report_file,
                    )
                except Exception as e:
                    LOG.error(e)
                    LOG.error("Risk audit was not successful")
            else:
                console.print(
                    Panel(
                        "Depscan supports OSS Risk audit for this "
                        "project.\nTo enable set the environment variable ["
                        "bold]ENABLE_OSS_RISK=true[/bold]",
                        title="Risk Audit Capability",
                        expand=False,
                    )
                )
        if project_type in type_audit_map:
            LOG.debug(
                "Performing remote audit for %s of type %s",
                src_dir,
                project_type,
            )
            LOG.debug("No of packages %d", len(pkg_list))
            try:
                audit_results = audit(project_type, pkg_list)
                if audit_results:
                    LOG.debug(
                        "Remote audit yielded %d results", len(audit_results)
                    )
                    results = results + audit_results
            except Exception as e:
                LOG.error("Remote audit was not successful")
                LOG.error(e)
                results = []
        # In case of docker, bom, or universal type, check if there are any
        # npm packages that can be audited remotely
        if project_type in (
            "podman",
            "docker",
            "oci",
            "container",
            "bom",
            "universal",
        ):
            npm_pkg_list = get_pkg_by_type(pkg_list, "npm")
            if npm_pkg_list:
                LOG.debug("No of npm packages %d", len(npm_pkg_list))
                try:
                    audit_results = audit("nodejs", npm_pkg_list)
                    if audit_results:
                        LOG.debug(
                            "Remote audit yielded %d results",
                            len(audit_results),
                        )
                        results = results + audit_results
                except Exception as e:
                    LOG.error("Remote audit was not successful")
                    LOG.error(e)
        if not db_lib.index_count(db["index_file"]):
            run_cacher = True
        else:
            LOG.debug(
                "Vulnerability database loaded from %s", config.vdb_bin_file
            )

        sources_list = [OSVSource(), NvdSource()]
        github_token = os.environ.get("GITHUB_TOKEN")
        if github_token and os.getenv("CI"):
            try:
                github_client = github.GitHub(github_token)

                if not github_client.can_authenticate():
                    LOG.error(
                        "The GitHub personal access token supplied appears to be invalid or expired. Please see: https://github.com/owasp-dep-scan/dep-scan#github-security-advisory"
                    )
                else:
                    sources_list.insert(0, GitHubSource())
                    scopes = github_client.get_token_scopes()
                    if scopes:
                        LOG.warning(
                            "The GitHub personal access token was granted more permissions than is necessary for depscan to operate, including the scopes of: %s. It is recommended to use a dedicated token with only the minimum scope necesary for depscan to operate. Please see: https://github.com/owasp-dep-scan/dep-scan#github-security-advisory",
                            ", ".join(scopes),
                        )
            except Exception:
                pass
        if run_cacher:
            rafs_image_downloaded, paths_list = download_rafs_based_image()
            if not rafs_image_downloaded:
                LOG.info(
                    "About to download the vulnerability database from %s. This might take a while ...",
                    vdb_database_url,
                )
                oras_client = oras.client.OrasClient()
                paths_list = oras_client.pull(
                    target=vdb_database_url, outdir=data_dir
                )

            LOG.debug("VDB data is stored at: %s", paths_list)
            run_cacher = False
            db = db_lib.get()
        elif args.sync:
            for s in sources_list:
                LOG.debug("Syncing %s", s.__class__.__name__)
                try:
                    s.download_recent()
                except NotImplementedError:
                    pass
                run_cacher = False
        LOG.info(
            "Performing regular scan for %s using plugin %s",
            src_dir,
            project_type,
        )
        vdb_results, pkg_aliases, sug_version_dict, purl_aliases = scan(
            db, project_type, pkg_list, args.suggest
        )
        if vdb_results:
            results += vdb_results
        results = [r.to_dict() for r in results]
        direct_purls, reached_purls = find_purl_usages(
            bom_file, src_dir, args.reachables_slices_file
        )
        # Summarise and print results
        _, vdr_file, pkg_vulnerabilities, pkg_group_rows = summarise(
            project_type,
            results,
            pkg_aliases,
            purl_aliases,
            sug_version_dict,
            scoped_pkgs=scoped_pkgs,
            report_file=report_file,
            bom_file=bom_file,
            no_vuln_table=args.no_vuln_table,
            direct_purls=direct_purls,
            reached_purls=reached_purls,
        )
        # Explain the results
        if args.explain:
            explainer.explain(
                project_type,
                src_dir,
                args.reachables_slices_file,
                vdr_file,
                pkg_vulnerabilities,
                pkg_group_rows,
                direct_purls,
                reached_purls,
            )
        # CSAF VEX export
        if args.csaf:
            export_csaf(
                results,
                src_dir,
                reports_dir,
                vdr_file,
                direct_purls=direct_purls,
                reached_purls=reached_purls,
            )
    console.save_html(
        html_file,
        theme=MONOKAI
        if os.getenv("USE_DARK_THEME")
        else DEFAULT_TERMINAL_THEME,
    )
    utils.export_pdf(html_file, pdf_file)
    # Submit vdr/vex files to threatdb server
    if args.threatdb_server and (args.threatdb_username or args.threatdb_token):
        submit_bom(
            reports_dir,
            {
                "threatdb_server": args.threatdb_server,
                "threatdb_username": args.threatdb_username,
                "threatdb_password": args.threatdb_password,
                "threatdb_token": args.threatdb_token,
            },
        )


if __name__ == "__main__":
    main()
