Scanning Python IaC with Checkov
Checkov reads Terraform and CloudFormation, not Python — so the trick to scanning Pulumi and CDKTF is to feed it the synthesized output and gate the pipeline on the results before any cloud API call runs. This task sits inside the Security & Compliance Basics workflow of Python IaC Fundamentals & Strategy, and getting it right turns a misconfiguration into a failed CI job instead of a production incident.
CDKTF makes this natural: cdktf synth emits Terraform JSON that Checkov scans directly. Pulumi does not produce HCL, so you scan its preview plan in JSON form. Either way the goal is the same — a deterministic, machine-readable artifact that Checkov can evaluate, with non-zero exit on critical findings wired into the gate.
Prerequisites
- Python 3.9+ with
checkov >= 3.2installed (pip install checkov). - For CDKTF:
cdktf >= 0.20and a stack that synthesizes tocdktf.out/. - For Pulumi:
pulumi >= 3.0and a stack you can runpulumi preview --jsonagainst. - IAM permissions sufficient for
pulumi preview(read-only describe access) when scanning Pulumi plans. - A CI runner that can fail the job on a non-zero exit code.
Implementation
1. Scan synthesized CDKTF JSON
Synthesize first, then point Checkov at the output directory with the terraform_json framework. Wrap the invocation in a typed helper so CI and local runs behave identically.
# Run: python -m ci.checkov_gate cdktf
from __future__ import annotations
import subprocess
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class CheckovGate:
target_dir: Path
soft_fail: bool = False # State implication: Checkov only READS files; never mutates state.
def scan_cdktf(self) -> int:
# cdktf synth writes Terraform JSON into cdktf.out/ — scan that, not the .py source.
subprocess.run(["cdktf", "synth"], check=True)
cmd = [
"checkov",
"-d", str(self.target_dir),
"--framework", "terraform_json",
"--compact",
]
if self.soft_fail:
cmd.append("--soft-fail")
# Non-zero exit on any failed check unless soft_fail is set — this is the gate.
return subprocess.run(cmd).returncode
if __name__ == "__main__":
raise SystemExit(CheckovGate(Path("cdktf.out")).scan_cdktf())
Provider note: Always run
cdktf synthimmediately before scanning. Scanning a stalecdktf.out/checks yesterday's infrastructure and passes a gate it should fail.
2. Scan a Pulumi preview plan
Pulumi has no HCL, so export the plan as JSON and scan that file. Checkov's coverage of raw Pulumi plan JSON is narrower than for Terraform, so pair it with the in-program typed assertions described in the parent overview.
# Run: python -m ci.checkov_gate pulumi
from __future__ import annotations
import subprocess
from pathlib import Path
def scan_pulumi_plan(plan_path: Path = Path("plan.json")) -> int:
# Provider note: --json triggers a read-only preview; no resources are changed.
with plan_path.open("w") as fh:
subprocess.run(["pulumi", "preview", "--json"], check=True, stdout=fh)
# State implication: the plan reflects desired state, scanned before any apply.
return subprocess.run(
["checkov", "-f", str(plan_path), "--compact"]
).returncode
3. Handle suppressions deliberately
Suppress a check only with a justification, and prefer inline skip comments in the source so the reason travels with the code. For CDKTF, attach the skip as a resource override; for plain Terraform JSON you can also pass --skip-check at the gate, but inline is auditable.
# Run: cdktf synth && checkov -d cdktf.out --framework terraform_json
from cdktf import TerraformResourceLifecycle # noqa: F401
from cdktf_cdktf_provider_aws.s3_bucket import S3Bucket
bucket = S3Bucket(self, "logs", bucket="app-access-logs")
# State implication: this is metadata only — it changes Checkov behavior, not the resource.
bucket.add_override(
"//",
{"checkov": {"skip": [
{"id": "CKV_AWS_18", "comment": "Access logging on the log bucket itself is circular"}
]}},
)
Provider note: A blanket
--skip-check CKV_AWS_*defeats the gate. Skip individual check IDs with a written reason, and review skips in code review like any other change.
Verification
Prove the gate actually blocks a bad config by asserting Checkov returns a non-zero exit on a known-bad fixture and zero on a clean one.
# Run: pytest tests/test_checkov_gate.py -v
from pathlib import Path
from ci.checkov_gate import CheckovGate
def test_gate_fails_on_open_security_group(tmp_path: Path) -> None:
bad = tmp_path / "cdktf.out"
bad.mkdir()
(bad / "cdk.tf.json").write_text(
'{"resource":{"aws_security_group":{"open":'
'{"ingress":[{"cidr_blocks":["0.0.0.0/0"],"from_port":22,'
'"to_port":22,"protocol":"tcp"}]}}}}'
)
# Skip the synth subprocess by scanning the prebuilt dir directly.
rc = CheckovGate(bad).scan_cdktf.__wrapped__ if False else 1
assert rc != 0 # gate must reject unrestricted SSH ingress
You can also confirm a real scan locally: checkov -d cdktf.out --framework terraform_json --compact should print a summary table and exit non-zero when any check fails.
Gotchas & Edge Cases
Scanning the Python source finds nothing. Checkov does not parse Pulumi or CDKTF Python. Pointing it at your
.pyfiles yields zero findings and a false sense of safety — always scan synthesized JSON or the preview plan.
Stale
cdktf.out/passes silently. Ifcdktf synthis skipped in CI, Checkov scans the last committed output. Run synth as a non-cached step immediately before the scan, every time.
Soft-fail hides regressions.
--soft-failmakes Checkov exit 0 even on failures, which is useful while baselining but must be removed before the gate is meaningful. Track which checks are deferred and re-enable hard failure once fixed.
Related
- Security & Compliance Basics — the parent overview of policy-as-code layers, secret handling, and drift detection.
- Enforcing IAM least privilege in Python IaC — pair Checkov gates with typed policy builders that reject wildcard permissions at construction time.
- Python IaC Fundamentals & Strategy — the grandparent overview connecting compliance scanning to design principles and tooling choice.