#!/usr/bin/env python
from __future__ import annotations

import atexit
import itertools
import json
import logging
import os
import shutil
import signal
import sys
import platform
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING

import argcomplete
import configargparse
from urllib3.exceptions import MaxRetryError

import checkov.logging_init  # noqa  # should be imported before the others to ensure correct logging setup

from checkov.ansible.runner import Runner as ansible_runner
from checkov.argo_workflows.runner import Runner as argo_workflows_runner
from checkov.arm.runner import Runner as arm_runner
from checkov.azure_pipelines.runner import Runner as azure_pipelines_runner
from checkov.bitbucket.runner import Runner as bitbucket_configuration_runner
from checkov.bitbucket_pipelines.runner import Runner as bitbucket_pipelines_runner
from checkov.cloudformation.runner import Runner as cfn_runner
from checkov.common.bridgecrew.bc_source import SourceTypes, BCSourceType, get_source_type, SourceType
from checkov.common.bridgecrew.integration_features.features.policy_metadata_integration import \
    integration as policy_metadata_integration
from checkov.common.bridgecrew.integration_features.features.repo_config_integration import \
    integration as repo_config_integration
from checkov.common.bridgecrew.integration_features.features.suppressions_integration import \
    integration as suppressions_integration
from checkov.common.bridgecrew.integration_features.features.custom_policies_integration import \
    integration as custom_policies_integration
from checkov.common.bridgecrew.integration_features.integration_feature_registry import integration_feature_registry
from checkov.common.bridgecrew.platform_integration import bc_integration
from checkov.common.bridgecrew.integration_features.features.licensing_integration import integration as licensing_integration
from checkov.common.bridgecrew.severities import BcSeverities
from checkov.common.goget.github.get_git import GitGetter
from checkov.common.output.baseline import Baseline
from checkov.common.bridgecrew.check_type import checkov_runners, CheckType
from checkov.common.resource_code_logger_filter import add_resource_code_filter_to_logger
from checkov.common.runners.runner_registry import RunnerRegistry
from checkov.common.util import prompt
from checkov.common.util.banner import banner as checkov_banner, tool as checkov_tool
from checkov.common.util.config_utils import get_default_config_paths
from checkov.common.util.consts import CHECKOV_RUN_SCA_PACKAGE_SCAN_V2
from checkov.common.util.ext_argument_parser import ExtArgumentParser
from checkov.common.util.runner_dependency_handler import RunnerDependencyHandler
from checkov.common.util.type_forcers import convert_str_to_bool
from checkov.contributor_metrics import report_contributor_metrics
from checkov.dockerfile.runner import Runner as dockerfile_runner
from checkov.docs_generator import print_checks
from checkov.github.runner import Runner as github_configuration_runner
from checkov.github_actions.runner import Runner as github_actions_runner
from checkov.gitlab.runner import Runner as gitlab_configuration_runner
from checkov.gitlab_ci.runner import Runner as gitlab_ci_runner
from checkov.helm.runner import Runner as helm_runner
from checkov.json_doc.runner import Runner as json_runner
from checkov.kubernetes.runner import Runner as k8_runner
from checkov.kustomize.runner import Runner as kustomize_runner
from checkov.runner_filter import RunnerFilter
from checkov.sca_image.runner import Runner as sca_image_runner
from checkov.sca_package.runner import Runner as sca_package_runner
from checkov.sca_package_2.runner import Runner as sca_package_runner_2
from checkov.secrets.runner import Runner as secrets_runner
from checkov.serverless.runner import Runner as sls_runner
from checkov.terraform.plan_runner import Runner as tf_plan_runner
from checkov.terraform.runner import Runner as tf_graph_runner
from checkov.terraform_json.runner import TerraformJsonRunner
from checkov.version import version
from checkov.yaml_doc.runner import Runner as yaml_runner
from checkov.bicep.runner import Runner as bicep_runner
from checkov.openapi.runner import Runner as openapi_runner
from checkov.circleci_pipelines.runner import Runner as circleci_pipelines_runner
from checkov.logging_init import log_stream as logs_stream

if TYPE_CHECKING:
    from checkov.common.output.report import Report
    from configargparse import Namespace
    from typing_extensions import Literal
    from igraph import Graph
    from networkx import DiGraph

signal.signal(signal.SIGINT, lambda x, y: sys.exit(''))

outer_registry = None

logger = logging.getLogger(__name__)
add_resource_code_filter_to_logger(logger)

# sca package runner added during the run method
DEFAULT_RUNNERS = [
    tf_graph_runner(),
    cfn_runner(),
    k8_runner(),
    sls_runner(),
    arm_runner(),
    tf_plan_runner(),
    helm_runner(),
    dockerfile_runner(),
    secrets_runner(),
    json_runner(),
    yaml_runner(),
    github_configuration_runner(),
    gitlab_configuration_runner(),
    gitlab_ci_runner(),
    bitbucket_configuration_runner(),
    bitbucket_pipelines_runner(),
    kustomize_runner(),
    github_actions_runner(),
    bicep_runner(),
    openapi_runner(),
    sca_image_runner(),
    argo_workflows_runner(),
    circleci_pipelines_runner(),
    azure_pipelines_runner(),
    ansible_runner(),
    TerraformJsonRunner(),
]


class Checkov:
    def __init__(self, argv: list[str] = sys.argv[1:]) -> None:
        self.config: "Namespace"  # set in 'parse_config()'
        self.parser: "ExtArgumentParser"  # set in 'parse_config()'
        self.runners = DEFAULT_RUNNERS
        self.scan_reports: "list[Report]" = []
        self.run_metadata: dict[str, str | list[str]] = {}
        self.graphs: dict[str, DiGraph | Graph] = {}
        self.url: str | None = None

        self.parse_config(argv=argv)

    def _parse_mask_to_resource_attributes_to_omit(self) -> None:
        resource_attributes_to_omit = defaultdict(set)
        for entry in self.config.mask:
            splitted_entry = entry.split(':')
            # if we have 2 entries, this is resource & variable to mask
            splitted_entry_len = len(splitted_entry)
            if 2 == splitted_entry_len:
                resource = splitted_entry[0]
                variables_to_mask = splitted_entry[1].split(',')
                resource_attributes_to_omit[resource].update(variables_to_mask)
            # ToDo: Uncomment if we want to support universal masking
            # elif 1 == splitted_entry_len:
            #     variables_to_mask = splitted_entry[0].split(',')
            #     resource_attributes_to_omit[RESOURCE_ATTRIBUTES_TO_OMIT_UNIVERSAL_MASK].update(variables_to_mask)

        self.config.mask = resource_attributes_to_omit

    def parse_config(self, argv: list[str] = sys.argv[1:]) -> None:
        """Parses the user defined config via CLI flags"""

        default_config_paths = get_default_config_paths(sys.argv[1:])
        self.parser = ExtArgumentParser(
            description='Infrastructure as code static analysis',
            default_config_files=default_config_paths,
            config_file_parser_class=configargparse.YAMLConfigFileParser,
            add_env_var_help=True,
        )
        self.parser.add_parser_args()
        argcomplete.autocomplete(self.parser)

        self.config = self.parser.parse_args(argv)
        self.normalize_config()

    def normalize_config(self) -> None:
        if self.config.no_guide:
            logger.warning(
                '--no-guide is deprecated and will be removed in a future release. Use --skip-download instead'
            )
            self.config.skip_download = True
        if self.config.skip_suppressions:
            logger.warning(
                '--skip-suppressions is deprecated and will be removed in a future release. Use --skip-download instead'
            )
            self.config.skip_download = True
        if self.config.skip_policy_download:
            logger.warning(
                '--skip-policy-download is deprecated and will be removed in a future release. Use --skip-download instead'
            )
            self.config.skip_download = True

        elif not self.config.bc_api_key and not self.config.include_all_checkov_policies:
            # makes it easier to pick out policies later if we can just always rely on this flag without other context
            logger.debug('No API key present; setting include_all_checkov_policies to True')
            self.config.include_all_checkov_policies = True

        if self.config.use_enforcement_rules and not self.config.bc_api_key:
            self.parser.error('Must specify an API key with --use-enforcement-rules')

        if self.config.policy_metadata_filter and not (self.config.bc_api_key and self.config.prisma_api_url):
            logger.warning(
                '--policy-metadata-filter flag was used without a Prisma Cloud API key. Policy filtering will be skipped.'
            )

        # Parse mask into json with default dict. If self.config.mask is empty list, default dict will be assigned
        self._parse_mask_to_resource_attributes_to_omit()

        if self.config.file:
            # it is passed as a list of lists
            self.config.file = list(itertools.chain.from_iterable(self.config.file))

    def run(self, banner: str = checkov_banner, tool: str = checkov_tool, source_type: SourceType | None = None) -> int | None:
        self.run_metadata = {
            "checkov_version": version,
            "python_executable": sys.executable,
            "python_version": sys.version,
            "checkov_executable": sys.argv[0],
            "args": self.parser.format_values(sanitize=True).split('\n'),
            "OS_system_info": platform.platform(),
            "CPU_architecture": platform.processor(),
            "Python_implementation": platform.python_implementation()
        }

        logger.debug(f'Run metadata: {json.dumps(self.run_metadata, indent=2)}')
        try:
            if self.config.add_check:
                resp = prompt.Prompt()
                check = prompt.Check(resp.responses)
                check.action()
                return None

            # Check if --output value is None. If so, replace with ['cli'] for default cli output.
            if self.config.output is None:
                self.config.output = ['cli']

            if self.config.support:
                bc_integration.support_flag_enabled = True

            if self.config.bc_api_key and not self.config.include_all_checkov_policies:
                if self.config.skip_download and not self.config.external_checks_dir:
                    print('You are using an API key along with --skip-download but not --include-all-checkov-policies or --external-checks-dir. '
                          'With these arguments, Checkov cannot fetch metadata to determine what is a local Checkov-only '
                          'policy and what is a platform policy, so no policies will be evaluated. Please re-run Checkov '
                          'and either remove the --skip-download option, or use the --include-all-checkov-policies and / or '
                          '--external-checks-dir options.',
                          file=sys.stderr)
                    self.exit_run()
                elif self.config.skip_download:
                    print('You are using an API key along with --skip-download but not --include-all-checkov-policies. '
                          'With these arguments, Checkov cannot fetch metadata to determine what is a local Checkov-only '
                          'policy and what is a platform policy, so only local custom policies loaded with --external-checks-dir '
                          'will be evaluated.',
                          file=sys.stderr)
                else:
                    logger.debug('Using API key and not --include-all-checkov-policies - only running platform policies '
                                 '(this is the default behavior, and this message is just for debugging purposes)')

            # bridgecrew uses both the urllib3 and requests libraries, while checkov uses the requests library.
            # Allow the user to specify a CA bundle to be used by both libraries.
            bc_integration.setup_http_manager(self.config.ca_certificate, self.config.no_cert_verify)

            # if a repo is passed in it'll save it.  Otherwise a default will be created based on the file or dir
            self.config.repo_id = bc_integration.persist_repo_id(self.config)
            # if a bc_api_key is passed it'll save it.  Otherwise it will check ~/.bridgecrew/credentials
            self.config.bc_api_key = bc_integration.persist_bc_api_key(self.config)

            if not self.config.bc_api_key:
                # check, if someone tries to use a severity filter without an API key
                severities = {severity for severity in BcSeverities.__dict__.values() if isinstance(severity, str)}
                if (
                    (self.config.check and any(check in severities for check in self.config.check))
                    or (self.config.skip_check and any(check in severities for check in self.config.skip_check))
                ):
                    logging.warning("Filtering checks by severity is only possible with an API key")

            excluded_paths = self.config.skip_path or []

            if self.config.var_file:
                self.config.var_file = [os.path.abspath(f) for f in self.config.var_file]

            runner_filter = RunnerFilter(
                framework=self.config.framework,
                skip_framework=self.config.skip_framework,
                checks=self.config.check,
                skip_checks=self.config.skip_check,
                include_all_checkov_policies=self.config.include_all_checkov_policies,
                download_external_modules=bool(convert_str_to_bool(self.config.download_external_modules)),
                external_modules_download_path=self.config.external_modules_download_path,
                evaluate_variables=bool(convert_str_to_bool(self.config.evaluate_variables)),
                runners=checkov_runners,
                excluded_paths=excluded_paths,
                all_external=self.config.run_all_external_checks,
                var_files=self.config.var_file,
                skip_cve_package=self.config.skip_cve_package,
                show_progress_bar=not self.config.quiet,
                use_enforcement_rules=self.config.use_enforcement_rules,
                enable_secret_scan_all_files=bool(convert_str_to_bool(self.config.enable_secret_scan_all_files)),
                block_list_secret_scan=self.config.block_list_secret_scan,
                deep_analysis=self.config.deep_analysis,
                repo_root_for_plan_enrichment=self.config.repo_root_for_plan_enrichment,
                resource_attr_to_omit=self.config.mask,
                enable_git_history_secret_scan=self.config.scan_secrets_history,
                git_history_timeout=self.config.secrets_history_timeout
            )

            source_env_val = os.getenv('BC_SOURCE', 'cli')
            source = source_type if source_type else get_source_type(source_env_val)
            if source == SourceTypes[BCSourceType.DISABLED]:
                logger.warning(
                    f'Received unexpected value for BC_SOURCE: {source_env_val}; Should be one of {{{",".join(SourceTypes.keys())}}} setting source to DISABLED')
            source_version = os.getenv('BC_SOURCE_VERSION', version)
            logger.debug(f'BC_SOURCE = {source.name}, version = {source_version}')

            if self.config.list:
                # This speeds up execution by not setting up upload credentials (since we won't upload anything anyways)
                logger.debug('Using --list; setting source to DISABLED')
                source = SourceTypes[BCSourceType.DISABLED]

            if CHECKOV_RUN_SCA_PACKAGE_SCAN_V2:
                self.runners.append(sca_package_runner_2())
            else:
                self.runners.append(sca_package_runner())

            if outer_registry:
                runner_registry = outer_registry
                runner_registry.runner_filter = runner_filter
                runner_registry.filter_runner_framework()
            else:
                runner_registry = RunnerRegistry(banner, runner_filter, *self.runners, tool=tool)

            runnerDependencyHandler = RunnerDependencyHandler(runner_registry)
            runnerDependencyHandler.validate_runner_deps()

            if self.config.show_config:
                print(self.parser.format_values(sanitize=True))
                return None

            if self.config.bc_api_key == '':
                self.parser.error(
                    'The --bc-api-key flag was specified but the value was blank. If this value was passed as a '
                    'secret, you may need to double check the mapping.'
                )
            elif self.config.bc_api_key:
                logger.debug(f'Using API key ending with {self.config.bc_api_key[-8:]}')

                if not bc_integration.is_token_valid(self.config.bc_api_key):
                    raise Exception('The provided API key does not appear to be a valid Bridgecrew API key or Prisma Cloud '
                                    'access key and secret key. For Prisma, the value must be in the form '
                                    'ACCESS_KEY::SECRET_KEY. For Bridgecrew, make sure to copy the token value from when you '
                                    'created it, not the token ID visible later on. If you are using environment variables, '
                                    'make sure they are properly set and exported.')

                if not self.config.list:
                    # if you are only listing policies, then the API key will be used to fetch policies, but that's it,
                    # so the repo is not required and ignored
                    if self.config.repo_id is None:
                        self.parser.error("--repo-id argument is required when using --bc-api-key")
                    else:
                        repo_id_sections = self.config.repo_id.split('/')
                        if len(repo_id_sections) < 2 or any(len(section) == 0 for section in repo_id_sections):
                            self.parser.error(
                                "--repo-id argument format should be 'organization/repository_name' E.g "
                                "bridgecrewio/checkov"
                            )

                try:
                    bc_integration.bc_api_key = self.config.bc_api_key
                    bc_integration.setup_bridgecrew_credentials(repo_id=self.config.repo_id,
                                                                skip_fixes=self.config.skip_fixes,
                                                                skip_download=self.config.skip_download,
                                                                source=source,
                                                                source_version=source_version,
                                                                repo_branch=self.config.branch,
                                                                prisma_api_url=self.config.prisma_api_url)

                except MaxRetryError:
                    self.exit_run()
                except Exception:
                    if bc_integration.prisma_api_url:
                        message = 'An error occurred setting up the Prisma Cloud platform integration. ' \
                                  'Please check your Prisma Cloud API token and URL and try again.'
                    else:
                        message = 'An error occurred setting up the Bridgecrew platform integration. ' \
                                  'Please check your API token and try again.'
                    if logger.isEnabledFor(logging.DEBUG):
                        logger.debug(message, exc_info=True)
                    else:
                        logger.error(message)
                        logger.error('Please try setting the environment variable LOG_LEVEL=DEBUG and re-running the command, and provide the output to support', exc_info=True)
                    self.exit_run()
            else:
                if self.config.support:
                    logger.warning("--bc-api-key argument is required when using --support")
                logger.debug('No API key found. Scanning locally only.')
                self.config.include_all_checkov_policies = True

            if self.config.check and self.config.skip_check:
                if any(item in runner_filter.checks for item in runner_filter.skip_checks):
                    self.parser.error("The check ids specified for '--check' and '--skip-check' must be mutually exclusive.")
                    return None

            BC_SKIP_MAPPING = os.getenv("BC_SKIP_MAPPING", "FALSE")
            if self.config.skip_download or BC_SKIP_MAPPING.upper() == "TRUE":
                bc_integration.skip_download = True

            try:
                bc_integration.get_platform_run_config()
            except Exception:
                if not self.config.include_all_checkov_policies:
                    # stack trace gets printed in the exception handlers above
                    # include_all_checkov_policies will always be set when there is no API key, so we don't need to worry about it here
                    print('An error occurred getting data from the platform, including policy metadata. Because --include-all-checkov-policies '
                          'was not used, Checkov cannot differentiate Checkov-only policies from platform policies, and no '
                          'policies will get evaluated. Please resolve the error above or re-run with the --include-all-checkov-policies argument '
                          '(but note that this will not include any custom platform configurations or policy metadata).',
                          file=sys.stderr)
                    self.exit_run()

            bc_integration.get_prisma_build_policies(self.config.policy_metadata_filter)

            # set config to make it usable inside the integration features
            integration_feature_registry.config = self.config
            integration_feature_registry.run_pre_scan()

            policy_level_suppression = suppressions_integration.get_policy_level_suppressions()
            bc_cloned_checks = custom_policies_integration.bc_cloned_checks
            runner_filter.bc_cloned_checks = bc_cloned_checks
            custom_policies_integration.policy_level_suppression = list(policy_level_suppression.keys())

            if any(framework in runner_filter.framework for framework in ("all", CheckType.SCA_IMAGE)):
                # only run image referencer, when sca_image framework is enabled
                runner_filter.run_image_referencer = licensing_integration.should_run_image_referencer()

            runner_filter.filtered_policy_ids = policy_metadata_integration.filtered_policy_ids
            logger.debug(f"Filtered list of policies: {runner_filter.filtered_policy_ids}")

            runner_filter.excluded_paths = runner_filter.excluded_paths + list(repo_config_integration.skip_paths)
            policy_level_suppression = suppressions_integration.get_policy_level_suppressions()
            bc_cloned_checks = custom_policies_integration.bc_cloned_checks
            runner_filter.bc_cloned_checks = bc_cloned_checks
            custom_policies_integration.policy_level_suppression = list(policy_level_suppression.keys())
            runner_filter.set_suppressed_policies(list(policy_level_suppression.values()))

            if self.config.use_enforcement_rules:
                runner_filter.apply_enforcement_rules(repo_config_integration.code_category_configs)

            if self.config.list:
                print_checks(frameworks=self.config.framework, use_bc_ids=self.config.output_bc_ids,
                             include_all_checkov_policies=self.config.include_all_checkov_policies,
                             filtered_policy_ids=runner_filter.filtered_policy_ids)
                return None

            baseline = None
            if self.config.baseline:
                baseline = Baseline(self.config.output_baseline_as_skipped)
                baseline.from_json(self.config.baseline)

            external_checks_dir = self.get_external_checks_dir()
            created_baseline_path = None

            default_github_dir_path = os.getcwd() + '/' + os.getenv('CKV_GITHUB_CONF_DIR_NAME', 'github_conf')
            git_configuration_folders = [os.getenv("CKV_GITHUB_CONF_DIR_PATH", default_github_dir_path),
                                         os.getcwd() + '/' + os.getenv('CKV_GITLAB_CONF_DIR_NAME', 'gitlab_conf')]

            if self.config.directory:
                exit_codes = []
                for root_folder in self.config.directory:
                    absolute_root_folder = os.path.abspath(root_folder)
                    if not os.path.exists(root_folder):
                        logger.error(f'Directory {root_folder} does not exist; skipping it')
                        continue
                    file = self.config.file
                    self.scan_reports = runner_registry.run(
                        root_folder=root_folder,
                        external_checks_dir=external_checks_dir,
                        files=file,
                    )
                    self.graphs = runner_registry.check_type_to_graph
                    if runner_registry.is_error_in_reports(self.scan_reports):
                        self.exit_run()
                    if baseline:
                        baseline.compare_and_reduce_reports(self.scan_reports)

                    if bc_integration.is_integration_configured() \
                            and bc_integration.bc_source \
                            and bc_integration.bc_source.upload_results \
                            and not self.config.skip_results_upload:
                        included_paths = [self.config.external_modules_download_path]
                        for r in runner_registry.runners:
                            included_paths.extend(r.included_paths())
                        self.upload_results(
                            root_folder=root_folder,
                            absolute_root_folder=absolute_root_folder,
                            excluded_paths=runner_filter.excluded_paths,
                            included_paths=included_paths,
                            git_configuration_folders=git_configuration_folders,
                        )

                    if self.config.create_baseline:
                        overall_baseline = Baseline()
                        for report in self.scan_reports:
                            overall_baseline.add_findings_from_report(report)
                        created_baseline_path = os.path.join(os.path.abspath(root_folder), '.checkov.baseline')
                        with open(created_baseline_path, 'w') as f:
                            json.dump(overall_baseline.to_dict(), f, indent=4)
                    exit_codes.append(self.print_results(
                        runner_registry=runner_registry,
                        url=self.url,
                        created_baseline_path=created_baseline_path,
                        baseline=baseline,
                    ))

                # this needs to run after the upload (otherwise the repository does not exist)
                should_run_contributor_metrics = bc_integration.bc_api_key and self.config.repo_id and self.config.prisma_api_url
                logger.debug(f"Should run contributor metrics report: {should_run_contributor_metrics}")
                if should_run_contributor_metrics:
                    try:  # collect contributor info and upload
                        report_contributor_metrics(self.config.repo_id, source.name, bc_integration)
                    except Exception as e:
                        logger.warning(f"Unable to report contributor metrics due to: {e}")

                exit_code = 1 if 1 in exit_codes else 0
                return exit_code
            elif self.config.docker_image:
                if self.config.bc_api_key is None:
                    self.parser.error("--bc-api-key argument is required when using --docker-image")
                    return None
                if self.config.dockerfile_path is None:
                    self.parser.error("--dockerfile-path argument is required when using --docker-image")
                    return None
                if self.config.branch is None:
                    self.parser.error("--branch argument is required when using --docker-image")
                    return None
                files = [os.path.abspath(self.config.dockerfile_path)]
                runner = sca_image_runner()
                result = runner.run(
                    root_folder='',
                    image_id=self.config.docker_image,
                    dockerfile_path=self.config.dockerfile_path,
                    runner_filter=runner_filter,
                )
                self.scan_reports = result if isinstance(result, list) else [result]
                if runner_registry.is_error_in_reports(self.scan_reports):
                    self.exit_run()
                if len(self.scan_reports) > 1:
                    # this shouldn't happen, but if it happens, then it is intended or something is broke
                    logger.error(f"SCA image runner returned {len(self.scan_reports)} reports; expected 1")

                integration_feature_registry.run_post_runner(self.scan_reports[0])

                if not self.config.skip_results_upload:
                    bc_integration.persist_repository(os.path.dirname(self.config.dockerfile_path), files=files)
                    bc_integration.persist_scan_results(self.scan_reports)
                    bc_integration.persist_image_scan_results(runner.raw_report, self.config.dockerfile_path,
                                                              self.config.docker_image,
                                                              self.config.branch)

                    bc_integration.persist_run_metadata(self.run_metadata)
                    if bc_integration.enable_persist_graphs:
                        bc_integration.persist_graphs(self.graphs)
                    self.url = self.commit_repository()

                should_run_contributor_metrics = bc_integration.bc_api_key and self.config.repo_id and self.config.prisma_api_url
                logger.debug(f"Should run contributor metrics report: {should_run_contributor_metrics}")
                if should_run_contributor_metrics:
                    try:  # collect contributor info and upload
                        report_contributor_metrics(self.config.repo_id, source.name, bc_integration)
                    except Exception as e:
                        logger.warning(f"Unable to report contributor metrics due to: {e}")

                exit_code = self.print_results(runner_registry=runner_registry, url=self.url)
                return exit_code
            elif self.config.file:
                runner_registry.filter_runners_for_files(self.config.file)
                self.scan_reports = runner_registry.run(
                    external_checks_dir=external_checks_dir,
                    files=self.config.file,
                    repo_root_for_plan_enrichment=self.config.repo_root_for_plan_enrichment,
                )
                self.graphs = runner_registry.check_type_to_graph
                if runner_registry.is_error_in_reports(self.scan_reports):
                    self.exit_run()
                if baseline:
                    baseline.compare_and_reduce_reports(self.scan_reports)
                if self.config.create_baseline:
                    overall_baseline = Baseline()
                    for report in self.scan_reports:
                        overall_baseline.add_findings_from_report(report)
                    created_baseline_path = os.path.join(os.path.abspath(os.path.commonprefix(self.config.file)),
                                                         '.checkov.baseline')
                    with open(created_baseline_path, 'w') as f:
                        json.dump(overall_baseline.to_dict(), f, indent=4)

                if bc_integration.is_integration_configured() \
                        and bc_integration.bc_source \
                        and bc_integration.bc_source.upload_results \
                        and not self.config.skip_results_upload:
                    files = [os.path.abspath(file) for file in self.config.file]
                    root_folder = os.path.split(os.path.commonprefix(files))[0]
                    absolute_root_folder = os.path.abspath(root_folder)

                    self.upload_results(
                        root_folder=root_folder,
                        absolute_root_folder=absolute_root_folder,
                        files=files,
                        excluded_paths=runner_filter.excluded_paths,
                        git_configuration_folders=git_configuration_folders,
                    )

                should_run_contributor_metrics = bc_integration.bc_api_key and self.config.repo_id and self.config.prisma_api_url
                logger.debug(f"Should run contributor metrics report: {should_run_contributor_metrics}")
                if should_run_contributor_metrics:
                    try:  # collect contributor info and upload
                        report_contributor_metrics(self.config.repo_id, source.name, bc_integration)
                    except Exception as e:
                        logger.warning(f"Unable to report contributor metrics due to: {e}")

                exit_code = self.print_results(
                    runner_registry=runner_registry,
                    url=self.url,
                    created_baseline_path=created_baseline_path,
                    baseline=baseline,
                )
                return exit_code
            elif not self.config.quiet:
                print(f"{banner}")

                bc_integration.onboarding()
            return None
        except BaseException:
            logging.error("Exception traceback:", exc_info=True)
            raise

        finally:
            if self.config.support:
                bc_integration.persist_logs_stream(logs_stream)

    def exit_run(self) -> None:
        exit(0) if self.config.no_fail_on_crash else exit(2)

    def commit_repository(self) -> str | None:
        try:
            return bc_integration.commit_repository(self.config.branch)
        except Exception:
            logging.debug("commit_repository failed, exiting", exc_info=True)
            self.exit_run()
            return ""

    def get_external_checks_dir(self) -> list[str]:
        external_checks_dir: "list[str]" = self.config.external_checks_dir
        if self.config.external_checks_git:
            git_getter = GitGetter(url=self.config.external_checks_git[0])
            external_checks_dir = [git_getter.get()]
            atexit.register(shutil.rmtree, str(Path(external_checks_dir[0]).parent))
        return external_checks_dir

    def upload_results(
            self,
            root_folder: str,
            absolute_root_folder: str,
            files: list[str] | None = None,
            excluded_paths: list[str] | None = None,
            included_paths: list[str] | None = None,
            git_configuration_folders: list[str] | None = None,
    ) -> None:
        """Upload scan results and other relevant files"""

        bc_integration.persist_repository(
            root_dir=root_folder,
            files=files,
            excluded_paths=excluded_paths,
            included_paths=included_paths,
        )
        if git_configuration_folders:
            bc_integration.persist_git_configuration(os.getcwd(), git_configuration_folders)
        bc_integration.persist_scan_results(self.scan_reports)
        bc_integration.persist_run_metadata(self.run_metadata)
        if bc_integration.enable_persist_graphs:
            bc_integration.persist_graphs(self.graphs, absolute_root_folder=absolute_root_folder)
        self.url = self.commit_repository()

    def print_results(
            self,
            runner_registry: RunnerRegistry,
            url: str | None = None,
            created_baseline_path: str | None = None,
            baseline: Baseline | None = None,
    ) -> Literal[0, 1]:
        """Print scan results to stdout"""

        if convert_str_to_bool(os.getenv("CHECKOV_NO_OUTPUT", "False")):
            # this is mainly used for testing, where the report output is not needed
            return 0

        return runner_registry.print_reports(
            scan_reports=self.scan_reports,
            config=self.config,
            url=url,
            created_baseline_path=created_baseline_path,
            baseline=baseline,
        )


# the flag/arg parsing moved to checkov/common/util/ext_argument_parser.py


if __name__ == '__main__':
    ckv = Checkov()
    sys.exit(ckv.run())
