Secure the PipelineIaCStep 19 of 31

tfsec Is Dead — Here Is the Migration

difficulty intermediatehands-on 25 min hands-on

61% complete

prereqs · checkov-terraform-misconfig

concepts · tool lifecycle · silent coverage loss · successor is not a superset · migration verification

Every pipeline accumulates tools that were the right answer once. Nothing tells you when that stops being true — a scanner that has quietly stopped receiving new rules keeps exiting zero and keeps looking like a control.

This lesson is about a real instance of that, and about the check you should run before trusting any migration path a vendor hands you.

Verified against the Range Terraform — tfsec 1.28.14, Trivy 0.72.0, Checkov 3.2.x. Project status read from the GitHub API the same day.

Step 1 — The tool tells you itself

bash
brew install tfsec
git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range

tfsec terraform/

Before any finding:

text
======================================================
tfsec is joining the Trivy family

tfsec will continue to remain available
for the time being, although our engineering
attention will be directed at Trivy going forward.

You can read more here:
https://github.com/aquasecurity/tfsec/discussions/1994
======================================================

Aqua Security acquired tfsec and moved development to Trivy. tfsec still runs, still finds things, and prints this on every invocation.

In CI, nobody sees it. It goes to a log nobody reads on a job that passes. A tool can announce its own retirement on every run for two years and still be in your pipeline, because the only thing anyone checks is the exit code.

Step 2 — Check the status yourself, for every scanner you run

Do not take a banner's word for it, and do not take mine. The GitHub API answers this in one call, and it is worth running against your whole toolchain once a quarter:

bash
for r in aquasecurity/tfsec tenable/terrascan bridgecrewio/checkov aquasecurity/trivy; do
  curl -s "https://api.github.com/repos/$r" | python3 -c "
import json,sys; d=json.load(sys.stdin)
print(f\"{d['full_name']:28} archived={str(d['archived']):5} last_push={d['pushed_at'][:10]}\")"
done
text
aquasecurity/tfsec           archived=False last_push=2026-03-25
tenable/terrascan            archived=True  last_push=2025-11-20
bridgecrewio/checkov         archived=False last_push=2026-08-13
aquasecurity/trivy           archived=False last_push=2026-08-13

Read those four lines carefully, because they say three different things.

Terrascan is genuinely over. archived=True — the repository is read-only. Tenable stopped. If Terrascan is in a pipeline, it is not a control; it is a job that passes. Remove it.

tfsec is not archived, and that is the more dangerous state. It looks alive. It has 7,000 stars and a repository that accepts issues. But its last commit is March, while Checkov and Trivy both pushed yesterday. New AWS services and new CVE classes arrive continuously, and a rules engine that has not been updated in five months is silently narrowing its coverage every week.

An archived project is safer than a frozen one. Archiving is a signal your tooling and your team can act on. "Still technically maintained" is what keeps a stale scanner in a pipeline for years.

The number to watch is last_push, not stars. Popularity is a lagging indicator by definition.

Step 3 — Run the old tool and the new one on the same Terraform

The migration path is official: Aqua says use Trivy. So run both.

bash
tfsec terraform/
trivy config terraform/
text
tfsec         14 potential problems   (3 critical, 8 high, 2 medium, 1 low) — 5 passed
trivy config  11 failures             (1 critical, 7 high, 1 medium, 2 low)

Fourteen versus eleven, on identical files, from a tool and its own designated successor.

Match them by rule ID rather than by count, because that is the only comparison that means anything:

tfsec ID Trivy ID Count
aws-ec2-no-public-ingress-sgr AWS-0107 2
aws-ec2-no-public-egress-sgr AWS-0104 1
aws-ec2-add-description-to-security-group-rule AWS-0124 1
aws-s3-block-public-acls AWS-0086 1
aws-s3-block-public-policy AWS-0087 1
aws-s3-ignore-public-acls AWS-0091 1
aws-s3-no-public-buckets AWS-0093 1
aws-s3-encryption-customer-key AWS-0132 1
aws-s3-enable-bucket-logging AWS-0089 1
aws-s3-enable-versioning AWS-0090 1
aws-iam-no-policy-wildcards — none — 2
aws-s3-enable-bucket-encryption — none — 1

Eleven map one-to-one. The rules genuinely moved across — same checks, renamed to Trivy's AVD scheme.

Three did not.

Step 4 — Confirm the gap is real before believing it

A missing finding usually means a differently-named rule, a different severity, or a check that passed. Rule those out — --include-non-failures prints everything Trivy evaluated, not just what failed:

bash
trivy config terraform/ -f json -q --include-non-failures \
  | python3 -c "
import json,sys
ids={m['ID'] for r in json.load(sys.stdin).get('Results',[]) for m in (r.get('Misconfigurations') or [])}
for c in ('AWS-0057','AWS-0088'): print(c, 'evaluated?', c in ids)"
text
AWS-0057 evaluated? False
AWS-0088 evaluated? False

Trivy assessed dozens of AWS, Azure and GCP checks against this Terraform — including neighbouring IAM rules like AWS-0345 (unrestricted S3 IAM policies) and AWS-0143 (policies attached directly to users), both of which passed.

But AWS-0057 — a general "IAM policy uses a wildcard action on a wildcard resource" check — is not in the ruleset. It did not pass. It did not get suppressed. It was never evaluated, because it does not exist.

Here is what Trivy is not looking at:

text
resource "aws_iam_role_policy" "admin_everything" {
  policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "*"        # every action
      Resource = "*"        # on every resource
    }]
  })
}

Action = "*" on Resource = "*" is full administrative access. tfsec rates it HIGH, twice. Checkov flags it. Trivy says nothing — not "low severity", not "ignored". Nothing.

Terminal showing tfsec finding 14 issues including a wildcard IAM policy, and Trivy finding 11 with no IAM wildcard check at all

The successor is not a superset. A migration recommended by the tool's own maintainer, followed exactly, deletes a finding for full-admin IAM from your pipeline — and every subsequent build passes, which is what makes it invisible.

They also disagree where they agree: tfsec rates unrestricted ingress CRITICAL; Trivy rates the same finding (AWS-0107) HIGH. If your gate is --severity CRITICAL, that check silently stops blocking too.

Step 5 — Migrate, but keep the coverage

Three findings and eleven, from a third tool, put the whole picture together — the Checkov and Trivy config lessons measured the same files:

Findings Flags wildcard IAM Status
Checkov 3.2 26 failed / 13 passed yes active (pushed yesterday)
tfsec 1.28.14 14 yes frozen since March
trivy config 0.72.0 11 no active (pushed yesterday)
Terrascan archived

The migration that actually preserves coverage is not tfsec → Trivy. It is tfsec → Checkov, or Trivy plus a custom policy for what Trivy does not check.

yaml
# .github/workflows/iac.yml
name: iac
on: [pull_request]

permissions:
  contents: read

jobs:
  iac:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # gate: broadest ruleset, and it has the IAM checks
      - uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          soft_fail: false

      # second opinion, non-blocking
      - name: Trivy config (audit)
        run: trivy config terraform/ --severity HIGH,CRITICAL --exit-code 0

If you must gate on Trivy, write the missing check as a Rego policy and load it with --config-policy. That is real work — which is the point. Migrating tools is not a version bump, and "the vendor said to" is not a verification.

Step 6 — Make this routine

The failure here was never tfsec. It was that nothing in the pipeline was watching whether tfsec was still alive.

  • Run the last_push check quarterly across every scanner you gate on. It is one API call each.
  • Diff rule IDs, not counts, whenever you change tools. Totals hide substitutions in both directions.
  • Record the finding count and tool version with each release. A count that drops without a fix landing means the ruleset changed, not the code.
  • Treat a frozen project as a finding. archived=False with a five-month-old commit is a risk to raise, not a reassurance.

What you learned

  • tfsec prints its own retirement notice on every run, into a log nobody reads on a job that passes.
  • Terrascan is archived (archived=True). tfsec is not — it is frozen since March while Checkov and Trivy pushed yesterday. The frozen one is more dangerous, because it looks alive.
  • 14 findings from tfsec, 11 from Trivy on identical Terraform. Eleven map one-to-one by rule ID.
  • AWS-0057 is not in Trivy's ruleset at all — verified with --include-non-failures. A full-admin Action="*" on Resource="*" policy goes unreported.
  • The successor is not a superset. Following the official migration exactly drops a wildcard-IAM finding, and every build still passes.
  • Severities diverge too: ingress is CRITICAL in tfsec, HIGH in Trivy. A --severity CRITICAL gate stops blocking it.
  • Check last_push on every scanner quarterly, and diff rule IDs, not totals, on any tool change.