diff --git a/checkov/common/checks/base_check_registry.py b/checkov/common/checks/base_check_registry.py index 06ca37d738..abbb971e74 100644 --- a/checkov/common/checks/base_check_registry.py +++ b/checkov/common/checks/base_check_registry.py @@ -145,7 +145,11 @@ def scan( report_type=report_type or self.report_type, file_origin_paths=[scanned_file] ): - result = self.run_check(check, entity_configuration, entity_name, entity_type, scanned_file, skip_info) + is_protected = runner_filter.protect_checks and runner_filter.check_matches( + check.id, check.bc_id, runner_filter.protect_checks + ) + effective_skip_info = {} if is_protected else skip_info + result = self.run_check(check, entity_configuration, entity_name, entity_type, scanned_file, effective_skip_info) results[check] = result return results diff --git a/checkov/common/util/ext_argument_parser.py b/checkov/common/util/ext_argument_parser.py index 75211b6b2d..47b2cec330 100644 --- a/checkov/common/util/ext_argument_parser.py +++ b/checkov/common/util/ext_argument_parser.py @@ -100,6 +100,8 @@ def write_config_file( config_items["check"] = config_items["check"][0].split(",") if "skip-check" in config_items.keys(): config_items["skip-check"] = config_items["skip-check"][0].split(",") + if "protect-check" in config_items.keys(): + config_items["protect-check"] = config_items["protect-check"][0].split(",") if "soft-fail-on" in config_items.keys(): config_items["soft-fail-on"] = config_items["soft-fail-on"][0].split(",") if "hard-fail-on" in config_items.keys(): @@ -290,6 +292,16 @@ def add_parser_args(self) -> None: default=None, env_var="CKV_SKIP_CHECK", ) + self.add( + "--protect-check", + help="Checks that will always be run, even if they are skipped via --skip-check, the config file, or " + "inline skip comments (e.g. #checkov:skip=). Enter one or more items separated by commas. " + "Each item may be a Checkov check ID (CKV_AWS_123) or a BC check ID (BC_AWS_GENERAL_123). " + "This option takes precedence over all skip mechanisms.", + action="append", + default=None, + env_var="CKV_PROTECT_CHECK", + ) self.add( "--run-all-external-checks", action="store_true", diff --git a/checkov/main.py b/checkov/main.py index f009ba95b1..c3cedca35b 100755 --- a/checkov/main.py +++ b/checkov/main.py @@ -329,6 +329,7 @@ def run(self, banner: str = checkov_banner, tool: str = default_tool, source_typ skip_framework=self.config.skip_framework, checks=self.config.check, skip_checks=self.config.skip_check, + protect_checks=self.config.protect_check, include_all_checkov_policies=self.config.include_all_checkov_policies, download_external_modules=convert_str_to_optional_bool(self.config.download_external_modules), external_modules_download_path=self.config.external_modules_download_path, diff --git a/checkov/runner_filter.py b/checkov/runner_filter.py index a7441a10ed..f7defe0e04 100644 --- a/checkov/runner_filter.py +++ b/checkov/runner_filter.py @@ -34,6 +34,7 @@ def __init__( framework: Optional[List[str]] = None, checks: Union[str, List[str], None] = None, skip_checks: Union[str, List[str], None] = None, + protect_checks: Union[str, List[str], None] = None, include_all_checkov_policies: bool = True, download_external_modules: Optional[bool] = False, external_modules_download_path: str = DEFAULT_EXTERNAL_MODULES_DIR, @@ -64,6 +65,9 @@ def __init__( checks = convert_csv_string_arg_to_list(checks) skip_checks = convert_csv_string_arg_to_list(skip_checks) + protect_checks = convert_csv_string_arg_to_list(protect_checks) + + self.protect_checks: List[str] = protect_checks or [] self.skip_invalid_secrets = skip_checks and any(skip_check.capitalize() == ValidationStatus.INVALID.value for skip_check in skip_checks) @@ -265,11 +269,17 @@ def should_run_check( (not bc_check_id and not self.include_all_checkov_policies and not is_external and not explicit_run) or (bc_check_id in self.suppressed_policies and bc_check_id not in self.bc_cloned_checks) ) + + is_protected = self.protect_checks and self.check_matches(check_id, bc_check_id, self.protect_checks) + logging.debug(f'skip_severity = {skip_severity}, explicit_skip = {explicit_skip}, regex_match = {regex_match}, suppressed_policies: {self.suppressed_policies}') logging.debug( - f'bc_check_id = {bc_check_id}, include_all_checkov_policies = {self.include_all_checkov_policies}, is_external = {is_external}, explicit_run: {explicit_run}') + f'bc_check_id = {bc_check_id}, include_all_checkov_policies = {self.include_all_checkov_policies}, is_external = {is_external}, explicit_run: {explicit_run}, is_protected: {is_protected}') - if should_skip_check: + if is_protected: + result = True + logging.debug(f'protect_check override {check_id}: {result}') + elif should_skip_check: result = False logging.debug(f'should_skip_check {check_id}: {should_skip_check}') elif should_run_check: diff --git a/docs/2.Basics/CLI Command Reference.md b/docs/2.Basics/CLI Command Reference.md index a08658c670..d9570b09cd 100644 --- a/docs/2.Basics/CLI Command Reference.md +++ b/docs/2.Basics/CLI Command Reference.md @@ -31,6 +31,8 @@ nav_order: 2 | `--run-all-external-checks` | Run all external checks (loaded via `--external-checks` options) even if the checks are not present in the `--check` list. This allows you to always ensure that new checks present in the external source are used. If an external check is included in `--skip-check`, it will still be skipped. | | `-s, --soft-fail` | Runs checks but always returns a 0 exit code. Using either `--soft-fail-on` and / or `--hard-fail-on` overrides this option, except for the case when a result does not match either of the soft fail or hard fail criteria, in which case this flag determines the result. | | `--soft-fail-on SOFT_FAIL_ON` | Exits with a 0 exit code if only the specified items fail. Enter one or more items separated by commas. Each item may be either a Checkov check ID(CKV_AWS_123), a BC check ID (BC_AWS_GENERAL_123), or a severity (LOW, MEDIUM, HIGH, CRITICAL). If you use a severity, then any severity equal to or less than the highest severity in the list will result in a soft fail. This option may be used with `--hard-fail-on`, using the same priority logic described in `--check` and `--skip-check` options above, with `--hard-fail-on` taking precedence in a tie. If a given result does not meet the `--soft-fail-on` nor the `--hard-fail-on` criteria, then the default is to hard fail. | +| `--protect-check PROTECT_CHECK` | Checks that will always be run, even if they are present in `--skip-check`, the YAML config file, or inline skip comments (`#checkov:skip=`). Enter one or more items separated by commas. Each item may be a Checkov check ID (CKV_AWS_123) or a BC check ID (BC_AWS_GENERAL_123). This option takes precedence over all skip mechanisms and is useful for enforcing mandatory security checks that developers must not be able to suppress. Can be repeated multiple times. [env var: CKV_PROTECT_CHECK] | +| `--protect-check PROTECT_CHECK` | Checks that will always be run, even if they are present in `--skip-check`, the YAML config file, or inline skip comments (`#checkov:skip=`). Enter one or more items separated by commas. Each item may be a Checkov check ID (CKV_AWS_123) or a BC check ID (BC_AWS_GENERAL_123). This option takes precedence over all skip mechanisms and is useful for enforcing mandatory security checks that developers must not be able to suppress. Can be repeated multiple times. [env var: CKV_PROTECT_CHECK] | | `--hard-fail-on HARD_FAIL_ON` | Exits with a non-zero exit code for specified checks. Enter one or more items separated by commas. Each item may be either a Checkov check ID (CKV_AWS_123), a BCcheck ID (BC_AWS_GENERAL_123), or a severity (LOW, MEDIUM, HIGH, CRITICAL). If you use a severity, then any severity equal to or greater than the lowest severity in the list will result in a hard fail. This option can be used with `--soft-fail-on`, using the same priority logic described in `--check` and `--skip-check` options above, with `--hard-fail-on` taking precedence in a tie. | | `--bc-api-key BC_API_KEY` | Prisma Cloud Access Key (see `--prisma-api-url`) [env var: BC_API_KEY] | | `--prisma-api-url PRISMA_API_URL` | The Prisma Cloud API URL (see:https://prisma.pan.dev/api/cloud/api-urls). Must be a `*.prismacloud.io`, `*.prismacloud.cn`, or `*.bridgecrew.cloud` domain. Requires `--bc-api-key` to be a Prisma Cloud Access Key in the following format: `access_key_id::secret_key` [env var: PRISMA_API_URL] | diff --git a/docs/2.Basics/Suppressing and Skipping Policies.md b/docs/2.Basics/Suppressing and Skipping Policies.md index 73789386e8..7e99cc98a3 100644 --- a/docs/2.Basics/Suppressing and Skipping Policies.md +++ b/docs/2.Basics/Suppressing and Skipping Policies.md @@ -347,6 +347,57 @@ kube-system namespace: checkov -d . --skip-check kube-system ``` +# Protecting checks from being skipped + +In some cases you may want to guarantee that certain checks are always executed, regardless of any skip configuration applied by developers. Use the `--protect-check` flag to mark checks as mandatory. A protected check will run even if it is present in `--skip-check`, in the YAML config file, or in an inline comment. + +This is especially useful in CI/CD pipelines where a security team wants to enforce that critical checks cannot be skipped by developers. + +## Usage + +Via CLI: +```sh +checkov -d . --protect-check CKV_AWS_20,CKV_AWS_57 +``` + +Via YAML config file: +```yaml +protect-check: + - CKV_AWS_20 + - CKV_AWS_57 +``` + +Via environment variable: +```sh +export CKV_PROTECT_CHECK=CKV_AWS_20,CKV_AWS_57 +checkov -d . +``` + +## Priority order + +When multiple skip mechanisms are in play, the resolution order from highest to lowest priority is: + +1. `--protect-check` — always runs the check, overrides everything below +2. `--skip-check` (CLI or YAML config) +3. Inline `#checkov:skip=` comment in code + +## Example + +Given a resource with an inline skip comment: + +```python +resource "aws_s3_bucket" "foo-bucket" { + #checkov:skip=CKV_AWS_20:The bucket is a public static content host + acl = "public-read" +} +``` + +Running with `--protect-check CKV_AWS_20` will **ignore** the inline comment and execute `CKV_AWS_20`, reporting a PASS or FAIL result instead of SKIPPED. + +```sh +checkov -d . --protect-check CKV_AWS_20 +``` + # Platform enforcement rules Checkov can download [enforcement rules](https://docs.prismacloud.io/en/enterprise-edition/content-collections/application-security/risk-management/monitor-and-manage-code-build/enforcement) that you configure in Prisma Cloud. This allows you to centralize the failure and check threshold configurations, instead of defining them in each pipeline. diff --git a/tests/common/test_runner_filter.py b/tests/common/test_runner_filter.py index 4811d26256..fbdadf2318 100644 --- a/tests/common/test_runner_filter.py +++ b/tests/common/test_runner_filter.py @@ -874,6 +874,45 @@ def test_scan_secrets_history_limits_to_secrets_framework(self): assert filter.enable_git_history_secret_scan is True assert filter.framework == [CheckType.SECRETS] + # --protect-check tests + + def test_protect_check_overrides_skip(self): + instance = RunnerFilter(skip_checks=["CHECK_1"], protect_checks=["CHECK_1"]) + self.assertTrue(instance.should_run_check(check_id="CHECK_1")) + + def test_protect_check_overrides_skip_bc_id(self): + instance = RunnerFilter(skip_checks=["BC_CHECK_1"], protect_checks=["BC_CHECK_1"]) + self.assertTrue(instance.should_run_check(check_id="CHECK_1", bc_check_id="BC_CHECK_1")) + + def test_protect_check_does_not_affect_other_checks(self): + instance = RunnerFilter(skip_checks=["CHECK_2"], protect_checks=["CHECK_1"]) + self.assertFalse(instance.should_run_check(check_id="CHECK_2")) + + def test_protect_check_does_not_force_run_non_skipped_check(self): + # protect_check on a check that is not skipped should still run normally + instance = RunnerFilter(protect_checks=["CHECK_1"]) + self.assertTrue(instance.should_run_check(check_id="CHECK_1")) + + def test_protect_check_wildcard_overrides_wildcard_skip(self): + # Wildcard skip of CHECK_AWS_*, but protect a specific one + instance = RunnerFilter(skip_checks=["CHECK_AWS_*"], protect_checks=["CHECK_AWS_1"]) + self.assertTrue(instance.should_run_check(check_id="CHECK_AWS_1")) + self.assertFalse(instance.should_run_check(check_id="CHECK_AWS_2")) + + def test_protect_check_overrides_skip_check_object(self): + from checkov.terraform.checks.resource.aws.LambdaEnvironmentCredentials import check + instance = RunnerFilter(skip_checks=[check.id], protect_checks=[check.id]) + self.assertTrue(instance.should_run_check(check=check)) + + def test_protect_check_csv_string(self): + # protect_checks accepts a comma-separated string like CLI input + instance = RunnerFilter(skip_checks="CHECK_1,CHECK_2", protect_checks="CHECK_1,CHECK_2") + self.assertTrue(instance.should_run_check(check_id="CHECK_1")) + self.assertTrue(instance.should_run_check(check_id="CHECK_2")) + + def test_protect_check_default_is_empty(self): + instance = RunnerFilter() + self.assertEqual(instance.protect_checks, []) if __name__ == '__main__': unittest.main()