tf-eu-guard
EU compliance security linter for IaC — scans Terraform (source + plan JSON) and Kubernetes manifests, mapping infrastructure misconfigurations to NIS2 (Directive 2022/2555) and GDPR (Regulation 2016/679) requirements.
Problem
The NIS2 Directive requires covered entities to implement specific cybersecurity measures by October 2024. Article 21(2) mandates risk management measures including access control, encryption, incident handling, backup policies, and secure authentication. GDPR Article 32 imposes similar technical and organizational safeguards for processing personal data.
Most organizations scan infrastructure-as-code with tools like Checkov or tfsec, but the output is generic security findings — not compliance mappings. Security teams must manually trace each finding to the relevant NIS2 or GDPR article, a slow and error-prone process that blocks audit preparation.
tf-eu-guard solves this: it wraps Checkov, enriches each failed check with the exact NIS2/GDPR articles it violates, and produces reports structured for developers, security engineers, and auditors.
What It Does
- Scans IaC using Checkov — Terraform source, Terraform plan JSON (
terraform show -json), and Kubernetes manifests (--iac-type) - Enriches findings with NIS2 Article 21(2) and GDPR Article 32(1) / 44 mappings from a curated registry
- Reports mapped and unmapped findings separately — Checkov failures with no current EU mapping are surfaced as a count in every report format ("52 mapped findings, 9 unmapped"), never silently dropped; see Unmapped findings
- Generates three report formats from one scan:
- Dev report (terminal/HTML): severity-sorted findings for engineers
- Security dashboard (HTML): CRITICAL/HIGH/MEDIUM/LOW breakdown with stats
- Auditor report (HTML): article-by-article view showing which NIS2/GDPR clauses have open findings
- Custom checks: Adds EU-specific rules (regions outside the EU Sovereign Cloud, hardcoded secrets) not in upstream Checkov
Note on flags:
--iac-typeselects what to scan (terraform, terraform_plan, kubernetes);--frameworkselects which compliance regime to report (nis2/gdpr). They are unrelated.
Quick Start
Installation
Requires Python ≥ 3.11 and uv. Installs Checkov 3.3.13 as a dependency.
uv tool install tf-eu-guard
(Also available on PyPI: tf-eu-guard if you prefer pip: pip install tf-eu-guard.)
For development (from a clone of this repo):
uv sync --extra dev
Basic Scan
# Scan a Terraform directory
tf-eu-guard scan ./terraform --output dev
# Generate all three HTML reports
tf-eu-guard scan ./terraform --output all
# Filter to GDPR-only findings and output JSON
tf-eu-guard scan ./terraform --output json --framework gdpr
# Fail the build (exit code 1) on HIGH or worse findings — for CI/CD
tf-eu-guard scan ./terraform --fail-on-severity HIGH
Try it: Run
tf-eu-guard scan examples/vulnerable-aws/ --output allto see ~52 mapped findings, ortf-eu-guard scan examples/compliant-aws/ --output devto see a clean scan.
Scan Terraform Plan JSON
Plan-mode scanning catches the configuration as it will actually be applied. Generate the plan JSON, then scan it:
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > plan.json
tf-eu-guard scan plan.json --iac-type terraform_plan --output json
Plan mode runs Checkov against the plan file itself (
checkov -f <plan.json> --framework terraform_plan). It reuses the sameCKV_AWS_*check IDs as source mode (verified: a plan ofexamples/vulnerable-awsmaps 31 findings, all to existing registry entries), minus lifecycle-block checks that plan JSON doesn't store. Findings reportfile_line_range: [0, 0](the whole plan is one JSON line) — reports omit the line reference in that case. A committed fixture lives atexamples/vulnerable-aws-plan/.Note:
--checkov-jsonis a different feature — it reuses Checkov JSON output you already generated (see below). Passing a rawterraform show -jsonplan file to--checkov-jsonis an error; scan the plan file directly as shown above.
Scan Kubernetes
tf-eu-guard scan ./k8s --iac-type kubernetes --output all
Try it:
tf-eu-guard scan examples/vulnerable-kubernetes/ --iac-type kubernetes --output jsonreturns ~24 mapped findings across pod security context, RBAC, network policy, secrets and resource limits. Kubernetes checks live in the separateCKV_K8S_*namespace, mapped inregistry-kubernetes.yaml.
Use Pre-Generated Checkov JSON
If you already run Checkov in CI or with custom checks, feed tf-eu-guard the
Checkov output JSON directly (not a raw Terraform plan file — that's what
--iac-type terraform_plan is for):
# From a file
checkov -d ./terraform --output json --framework terraform --quiet > checkov.json
tf-eu-guard scan --checkov-json checkov.json --output security
# From stdin
checkov -d ./terraform --output json --framework terraform --quiet \
| tf-eu-guard scan --checkov-json - --output auditor
Use
--framework terraformin Checkov to get a single JSON object. tf-eu-guard also handles the multi-check-type array and extracts theterraformresult automatically.
Use as a Claude Code Skill
This repo includes a Claude Code skill at .claude/skills/tf-eu-guard/. In Claude Code:
/tf-eu-guard ./terraform
Or just ask: "Check my Terraform for NIS2 and GDPR compliance" — Claude runs the scan and summarizes findings by framework and severity.
Usage
Developer Scan
Quick terminal output for engineers fixing issues:
tf-eu-guard scan ./terraform --output dev
Security Dashboard
HTML report with severity breakdown and compliance tags:
tf-eu-guard scan ./terraform --output security
Live HTML version:
docs/screenshots/scan-report.html
Auditor Compliance Matrix
Article-by-article view for audit preparation:
tf-eu-guard scan ./terraform --output auditor
Live HTML version:
docs/screenshots/auditor-report.html
JSON (CI/CD & Tooling)
Machine-readable output for pipelines:
tf-eu-guard scan ./terraform --output json --framework nis2
SARIF (GitHub Code Scanning)
tf-eu-guard scan ./terraform --output sarif > tf-eu-guard.sarif
Emits a SARIF 2.1.0
document — the format GitHub Code Scanning ingests directly. Each finding
becomes a result with ruleId = Checkov check ID, level derived from
severity (CRITICAL/HIGH → error, MEDIUM → warning, LOW/INFO → note),
and a physicalLocation from the finding's file and line range. The EU
regulatory context (NIS2/GDPR articles, severity, remediation) rides along
in properties on both the rule and the result, so Code Scanning shows the
article metadata with the finding. Unmapped Checkov findings are included
as note-level results tagged mapped: false.
The document is validated against the official OASIS SARIF 2.1.0 JSON
schema in the test suite (tests/test_sarif_report.py). Use
--output-file results.sarif to write the file directly instead of stdout.
Generate All Reports at Once
tf-eu-guard scan ./terraform --output all
CI/CD Gating
Exit codes make tf-eu-guard usable as a pipeline gate. They distinguish "the scan ran and found blocking issues" from "the tool itself failed to run" — so a pipeline can fail the build on findings while paging someone when the scanner is broken:
| Exit code | Meaning |
|---|---|
0 |
Scan ran; no findings met the --fail-on-* threshold |
1 |
Scan ran; blocking findings found (threshold met) |
2 |
Invalid invocation or configuration — bad flags, unusable --checkov-json input, missing target path |
3 |
Scanner/runtime failure — Checkov itself crashed, the mapping registry failed to load |
tf-eu-guard scan ./terraform --fail-on-severity HIGH # exit 1 on HIGH or CRITICAL
tf-eu-guard scan ./terraform --fail-on-any # exit 1 on any finding
A pipeline can gate on findings while treating tool failure separately:
tf-eu-guard scan ./terraform --fail-on-severity HIGH
case $? in
0) echo "clean" ;;
1) echo "blocking findings — failing the build"; exit 1 ;;
2|3) echo "tf-eu-guard itself failed — tooling problem, not a findings verdict"; exit 2 ;;
esac
Gating considers only mapped findings. Severity is authored in the mapping registry, so an unmapped Checkov finding has no severity to compare and never trips
--fail-on-severity/--fail-on-any— it is reported for visibility instead (a note is printed to stderr whenever unmapped findings exist during a gated run).
Unmapped findings
With 185 of Checkov's ~700 AWS checks mapped (plus Azure, GCP and Kubernetes),
a scan can produce Checkov failures that have no current EU regulatory mapping.
These are not silently dropped: every report format surfaces them as
"present-but-unmapped" — a count in the dev/security/auditor reports, a
stderr note alongside the JSON output, note-level results tagged
mapped: false in the SARIF output, and an explicit mention in each
report's caveats. Unmapped checks are those with no honest compliance link
(see docs/check-mapping-table.md for the
methodology); if you believe a check deserves a mapping,
contribute one.
Try it: tf-eu-guard scan examples/unmapped-aws/ --output dev — a fixture that
deliberately mixes mapped S3 findings with the unmapped password-policy check
family (CKV_AWS_10–15).
GitHub Action
- uses: 44aayush/tf-eu-guard@v1
with:
path: './infra'
iac-type: 'terraform' # terraform | terraform_plan | kubernetes
output: 'json' # dev | security | auditor | json | sarif | all
framework: 'all' # nis2 | gdpr | all
fail-on-severity: 'HIGH' # empty ('') disables gating
The Action runs the same CLI inside the repo's Docker image, so it supports
every --iac-type the CLI does — pass the terraform show -json plan file
as path with iac-type: terraform_plan, or point at a manifest directory
with iac-type: kubernetes.
Two optional extras:
upload-reports: true— uploads the generated HTML report(s) as atf-eu-guard-reportsworkflow artifact (useful when the severity gate fails the scan and you need the actual report).upload-sarif: true(withoutput: sarif) — uploads the SARIF to GitHub Code Scanning, so findings appear in the repo's Security tab with their NIS2/GDPR article metadata. The calling workflow needspermissions: security-events: write.
jobs:
compliance:
runs-on: ubuntu-latest
permissions:
security-events: write # for upload-sarif
steps:
- uses: actions/checkout@v4
- uses: 44aayush/tf-eu-guard@v1
with:
path: './infra'
output: sarif
upload-sarif: true
upload-reports: true
Pre-commit Hook
repos:
- repo: https://github.com/44aayush/tf-eu-guard
rev: v0.4.0
hooks:
- id: tf-eu-guard
The hook runs with pass_filenames: false — it invokes
tf-eu-guard scan --output dev --fail-on-severity HIGH once with no file
arguments, and the CLI scans the repository root (its default target).
Checkov needs directory-level context anyway (providers, variables, modules
declared in sibling files), so per-file invocation was never going to work.
Scope
| Regulation | Terraform source | Terraform plan | Kubernetes |
|---|---|---|---|
| NIS2 Article 21(2) | ✅ 155 cloud check mappings | ✅ same checks apply | ✅ 24 CKV_K8S_* mappings |
| GDPR Article 32(1) | ✅ 125 cloud check mappings | ✅ same checks apply | ✅ 13 CKV_K8S_* mappings |
| GDPR Art. 44 (data residency) | ✅ Custom check EUGUARD_GDPR_001 |
✅ provider config visible in plan | ❌ Out of scope — see note |
Mapping counts above are generated from the registry files and verified in CI (
tools/check_doc_counts.py) — they cannot drift from the registries.
Coverage is classified by requirement, not by check count. Not every NIS2/GDPR requirement can be verified from IaC — some need organisational evidence a linter structurally cannot see. See
docs/regulatory-coverage.mdfor the requirement-level classification (automated / partial / manual / not covered) of NIS2 Art. 21(2)(a)–(j) and GDPR Art. 32(1)(a)–(d) & 44 — also generated from the registries, so it cannot drift.
Data residency on Kubernetes: K8s manifests are cloud-agnostic; region is decided at the cluster/cloud boundary, not in the manifest. Enforce residency there (cluster placement policy, provider constraints) — tf-eu-guard does not claim GDPR Art. 44 coverage for Kubernetes. See
docs/DECISIONS.md.
How to read the tool's claims:
docs/limitations.mdconsolidates what tf-eu-guard can and cannot see — technical evidence vs. legal determination, per-article mapping confidence, structural blind spots (backup ≠ restore, logging ≠ monitoring), and the assumptions (personal data assumed present, registry-authored severity). Worth reading before citing a clean scan in an audit.
Note: CRA and DORA are out of scope for this tool — they address product lifecycle and financial-sector operational resilience respectively, not cloud infrastructure configuration.
Current Registry
185 Checkov checks mapped — AWS: 116 mappings | Azure: 23 | GCP: 22 | Kubernetes: 24 — covering encryption at rest/in transit, logging & detection, backup & recovery, secure development/supply chain, secrets in code, IAM/access control, network segmentation, pod security context, RBAC, and resource limits:
- IAM / access control: CKV_AWS_273, 287, 288, 62, 286, 63, 355, 289, 290, 274, 40, 9, 109, 111, 283, 356, 70, 79, 162, 359, CKV2_AWS_40, CKV2_AWS_41, CKV2_AWS_52
- Encryption (rest + transit): CKV_AWS_145, 3, 8, 96, 5, 247, 44, 347, 279, 280, 327, 136, 189, 186, 173, 58, 7, 127, 376, 228, 379, CKV2_AWS_2, CKV2_AWS_64, CKV2_AWS_69
- Logging / detection: CKV_AWS_18, 157, 101, 84, 317, 324, 325, 92, 37, 50, 126, 353, 158, 338, CKV2_AWS_11, CKV2_AWS_30, CKV2_AWS_62
- Backup / resilience: CKV_AWS_21, 144, 326, 361, 139, 115, 116, 135, 318, 313, 362, CKV2_AWS_8, CKV2_AWS_58, CKV2_AWS_59, CKV2_AWS_60, CKV2_AWS_61
- Secure development / secrets: CKV_AWS_226, 363, 272, 51, 163, 41, 45, 46
- S3 / RDS / network exposure: CKV_AWS_20, 53–56, 16, 17, 133, 129, 161, 293, 118, 24, 25, 260, 382, 137, 248, 38, 39, 117, 23, CKV2_AWS_6, CKV2_AWS_12, CKV2_AWS_5
- Custom: EUGUARD_GDPR_001 (regions outside the EU Sovereign Cloud), EUGUARD_NIS2_001 (hardcoded secrets — Terraform and Kubernetes variants)
- Azure (23):
tf_eu_guard/mapping/registry-azure.yaml— storage account encryption/public access, SQL firewall & public network access, Key Vault network rules, App Service HTTPS/auth/logging, NSG SSH rules, and more - GCP (22):
tf_eu_guard/mapping/registry-gcp.yaml— GCS bucket CMEK/public IAM, Cloud SQL public IP/SSL/CMEK, GKE private clusters/ABAC/authorized networks, VPC flow logs, and more - Kubernetes (24):
tf_eu_guard/mapping/registry-kubernetes.yaml— pod security context (privileged/root/capabilities), RBAC privilege escalation, missing NetworkPolicy, secrets as literals, resource requests/limits, image hygiene, health probes
The registry is split per namespace — registry-aws.yaml, registry-azure.yaml, registry-gcp.yaml, registry-kubernetes.yaml — and every entry is schema-validated in CI. The AWS registry covers both Terraform source and Terraform plan JSON (same CKV_AWS_* IDs fire in both modes).
Architecture
┌──────────────┐
│ Terraform │
│ files │
└──────┬───────┘
│
▼
┌──────────────────────────────────────────────┐
│ tf-eu-guard │
│ ┌─────────────────────────────────────────┐ │
│ │ 1. run_checkov() / load_checkov_json() │ │
│ │ → Checkov results (passed + failed) │ │
│ └──────────────┬──────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 2. extract_failed_checks() │ │
│ │ → Filter to failed checks only │ │
│ └──────────────┬──────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 3. load_registry() │ │
│ │ → NIS2/GDPR mapping registry │ │
│ └──────────────┬──────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 4. enrich_findings() │ │
│ │ → Join checks ⟷ compliance articles │ │
│ │ → Unmapped checks tracked + reported │ │
│ └──────────────┬──────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 5. Optional: filter by framework │ │
│ │ (--framework nis2 / gdpr) │ │
│ └──────────────┬──────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 6. generate_report() │ │
│ │ → dev / json / security / auditor │ │
│ └─────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ Reports: │
│ • Terminal rich table │
│ • dev-report.html │
│ • scan-report.html (dashboard) │
│ • auditor-report.html (by article) │
│ • JSON (for CI/tooling) │
└──────────────────────────────────────────┘
Key differentiator: The EU compliance mapping registry. Without it, this is just another Checkov wrapper. With it, it's the bridge from "S3 bucket not encrypted" to "violates NIS2 Art. 21(2)(h) and GDPR Art. 32(1)(a)."
A prose walkthrough of the pipeline (including where the unmapped-findings split happens and why Checkov is a swappable, pinned detection backend):
docs/architecture.md.
Contributing
See CONTRIBUTING.md for guidelines — especially on proposing new registry mappings. Release history lives in CHANGELOG.md. Contributions welcome:
- Registry expansion: Map more Checkov checks to NIS2/GDPR
- New custom checks: EU-specific patterns Checkov doesn't cover
- Report improvements: Better visualizations, export formats
License
MIT — see LICENSE
Acknowledgments
- Checkov (bridgecrewio/checkov) — detection engine
- terragoat (bridgecrewio/terragoat) — vulnerable infrastructure test cases (used in the end-to-end test suite; see tests/README.md)
- NIS2 Directive — Directive (EU) 2022/2555
- GDPR — Regulation (EU) 2016/679
Metadata
Release files for tf-eu-guard 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tf_eu_guard-0.4.0.tar.gz | 6.2 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tf_eu_guard-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 6.3 MB
Release files / tf_eu_guard-0.4.0.tar.gz
| Download URL | tf_eu_guard-0.4.0.tar.gz |
|---|---|
| Size | 6.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
affa6611bec316de53c76f789958f56cdceae16b89277a5e9060f9c8a8faa9c9
|
|
BLAKE2b-256 checksum How to use checksums |
be3d9f74a760c50a770d186f1013ecd95e448b53061c05d246059415e15d4f95
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / tf_eu_guard-0.4.0-py3-none-any.whl
| Download URL | tf_eu_guard-0.4.0-py3-none-any.whl |
|---|---|
| Size | 91.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2dcf3b548cb184f0424cb5e6873913d6495f5f682558d82c3843404d45757cea
|
|
BLAKE2b-256 checksum How to use checksums |
2e08152c6d11f0985df9af79d03ea528749c32c7a0c1c917b1a9042c9deb6283
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency log