False positives · context reasoning · benchmark

Why audytx has 36× fewer false positives than Checkov

Most Terraform scanners check each resource in isolation against a checklist. That produces hundreds of findings on clean infrastructure — because a Lambda without a DLQ is always flagged, regardless of whether anything async ever invokes it. audytx reasons across how your resources actually connect, applies 17 suppression axes, and only flags what the context can't explain away. Every suppressed finding is preserved with its rationale — never silently dropped.

The numbers

36×
fewer false positives than Checkov
33 vs 1,193 on 21 clean modules
17
cross-resource reasoning axes
pre-computed once per scan
fewer false positives than Trivy
33 vs 175 on same corpus

Why single-resource scanners produce so many false positives

A scanner that checks each Terraform resource in isolation has no choice but to apply every rule that could apply — it can't see the context that makes the rule irrelevant.

1. Lambda DLQ pattern

Rule: "Lambda function has no dead-letter queue"
Checkov: aws_lambda_function.api — Lambda DLQ missing ⚠
audytx: suppressed — this Lambda is only invoked synchronously via API Gateway. A DLQ only fires on async invocations; there are none here.

2. DLQ-needs-a-DLQ pattern

Rule: "SQS queue has no dead-letter queue"
Checkov: aws_sqs_queue.jobs_dlq — Queue has no DLQ of its own ⚠
audytx: suppressed — this queue is the dead-letter target of aws_sqs_queue.jobs. Requiring a DLQ to have its own DLQ is infinite regress.

3. Ephemeral data pattern

Rule: "DynamoDB table does not have point-in-time recovery enabled"
Checkov: aws_dynamodb_table.sessions — PITR not enabled ⚠
audytx: suppressed — TTL is configured on this table; the data is intentionally ephemeral. Point-in-time recovery is mismatched for data that self-expires.

4. Internal-load-balancer pattern

Rule: "Load balancer does not have deletion protection enabled"
Checkov: aws_lb.internal_admin — Deletion protection disabled ⚠
audytx: suppressed — this is an internal load balancer with no internet route. Deletion protection matters most for public-facing LBs; internal-only LBs carry lower risk.

5. IMDSv2 inheritance pattern

Rule: "EC2 instance does not enforce IMDSv2"
Checkov: aws_instance.worker — IMDSv2 not enforced ⚠
audytx: suppressed — IMDSv2 is enforced at the account level; this instance inherits that setting. No per-instance override is needed.

The 17 reasoning checks

Each check represents a class of relationship that proves a finding benign in context — computed once per scan across your full resource graph.

Queue is already a dead-letter target
A queue that is itself a dead-letter destination doesn't need its own DLQ — requiring one is infinite regress.
Lambda is invoked synchronously
Dead-letter queues only fire on async invocations. When a Lambda is only invoked synchronously (API Gateway, ALB, Step Functions), a DLQ would never receive an event.
Data is intentionally short-lived
Tables, queues, and buckets with a configured expiry policy are ephemeral by design — point-in-time recovery and versioning are mismatched for data that self-expires.
Resource has no internet exposure
Internal load balancers and VPC-only resources are lower-risk for exposure-related findings — hardening advice aimed at public-facing resources doesn't always apply.
Encryption method satisfies the rule's intent
Server-side encryption with a managed key satisfies at-rest encryption findings even when the rule nominally demands a customer-managed KMS key — the goal is protection, which is met.
Role's trust is narrowly scoped
An IAM role whose trust policy limits assumption to a specific service or account has a reduced blast radius — findings about its permissions are weighted accordingly.
Role's actual permissions, after expansion
Wildcard actions are expanded to concrete permissions before risk scoring. A role with a broad action wildcard is treated differently from one with a handful of specific read actions.
Instance inherits account-level settings
When a metadata security setting (like requiring IMDSv2) is enforced at the account or launch-template level, individual instances inherit it without needing a per-instance override.
Resource is tagged as non-production
Resources explicitly tagged as development or staging may be suppressed for production-hardening findings that don't apply at that lifecycle stage.
Queue handles errors at the source-mapping level
When an SQS event source mapping is configured with its own error handling, the Lambda function DLQ is redundant — failures are already caught before they'd reach the function.
Secret is automatically rotated
A Secrets Manager secret with rotation configured is not a static credential — findings about long-lived secrets don't apply when the secret regularly changes itself.
Public access is blocked at a higher level
When public access is blocked at the account level, individual bucket public-access findings are already covered — the stricter setting wins regardless of bucket-level configuration.
Encryption key scope doesn't cover sensitive data
A KMS key used exclusively by non-sensitive services may be suppressed for rotation requirements written on the assumption the key protects sensitive customer data.
Database runs across multiple availability zones
Multi-AZ database deployments already address the single-point-of-failure concern that some backup-window findings are trying to prevent.
Resource emits to a connected log group
When a resource is wired to a CloudWatch log group, logging findings are satisfied even if the per-resource logging flag isn't explicitly set — the logs are flowing.
Network traffic is logged at the VPC level
When flow logs are enabled on the VPC a resource belongs to, network visibility findings on that resource are already addressed by the parent network's logging.
Container uses managed runtime (Fargate)
Fargate containers run on managed infrastructure that doesn't support custom host networking or privileged mode — findings about those EC2-specific risks don't apply.

How a suppression looks in the PR comment

Suppressed findings are never silently dropped. They appear in a collapsible block in the PR comment, each with a plain-English rationale. You can audit every call audytx made.

🧠 audytx reasoned about 4 findings and suppressed them
· aws_lambda_function.api — DLQ not needed: this Lambda is only invoked synchronously via API Gateway; a DLQ would never receive an event
· aws_sqs_queue.jobs_dlqthis queue is itself the dead-letter target for jobs_queue; requiring a DLQ to have its own DLQ is infinite regress
· aws_dynamodb_table.sessions — PITR skipped: TTL is configured — this table is intentionally ephemeral; point-in-time recovery is mismatched for data that self-expires
· aws_lb.internal_admin — deletion protection lower risk: this is an internal load balancer with no internet route; the finding targets public-facing LBs

Every suppression shows its reasoning in plain English. If you disagree with a call, you can override it in a .audytx-baseline.yaml file in your repo.

The benchmark: 21 clean production modules

The false-positive comparison ran all five tools against 21 well-maintained AWS community Terraform modules with an expected HIGH finding count of 0. Lower is better — every finding here is noise. Full data and methodology: the benchmark page.

ModuleaudytxCheckovTrivyKICS
terraform-aws-iam328710
terraform-aws-ecs086162
terraform-aws-lambda6112237
terraform-aws-rds212471
terraform-aws-alb454132
terraform-aws-eks688381
terraform-aws-s3-bucket3129184
+ 14 more modules93135917
Total (21 modules)331,19317534
KICS reaches 34 (close to audytx's 33) only by detecting 3% of IAM privilege-escalation paths in Table 1 of the benchmark (1 of 31), to audytx's 100%. audytx achieves its low false-positive count without sacrificing IAM recall.

FAQ

Doesn't suppression hide real issues?

No — suppressed findings are never removed. They appear in a dedicated collapsible block in every PR comment, each with a plain-English explanation of why it was dismissed. You can always see what was reasoned away and why, and override any suppression in your .audytx-baseline.yaml.

How do I override a suppression I disagree with?

Add an entry to .audytx-baseline.yaml in your repo root specifying the rule ID and resource. audytx will stop suppressing that finding — it'll surface as a real finding on the next PR that touches the resource. You can set an expiry date so the override is time-bounded.

What if audytx is wrong about the context?

The reasoning checks are conservative — audytx only suppresses when the relationship in your Terraform configuration is unambiguous. If the configuration is incomplete or the connection can't be proven (e.g. the DLQ reference is behind a variable), the finding is kept. Incomplete evidence = no suppression.

Why does Checkov have 1,193 false positives on clean modules?

Checkov applies every rule that matches a resource type, without checking whether the finding is warranted by the resource's actual role in the system. It has more rules (breadth) and less context reasoning (precision). Both tools detect 100% of IAM privesc paths — the difference is entirely in clean-infrastructure precision.

Does audytx support suppression files like Checkov's .checkov.yaml?

Yes, via .audytx-baseline.yaml. You list rule IDs to suppress, optionally scoped to specific files or resources, with an expiry date. This is for cases the reasoning axes can't cover — team-specific decisions, known acceptable risks, or third-party modules you can't change.

Stop triaging false positives

Install audytx on one repo. Open a PR that touches your ECS, Lambda, or IAM resources. See the reasoning block — and how many findings don't make it through.

Install audytx free →

See the full benchmark: audytx vs Checkov, Trivy, KICS, Terrascan →