432 lines
9.9 KiB
Markdown
432 lines
9.9 KiB
Markdown
# Checkov Suppression and Exception Handling Guide
|
|
|
|
Best practices for suppressing false positives and managing policy exceptions in Checkov.
|
|
|
|
## Suppression Methods
|
|
|
|
### Inline Suppression (Recommended)
|
|
|
|
#### Terraform
|
|
|
|
```hcl
|
|
# Single check suppression with justification
|
|
resource "aws_s3_bucket" "public_site" {
|
|
# checkov:skip=CKV_AWS_18:Public bucket for static website hosting
|
|
bucket = "my-public-website"
|
|
acl = "public-read"
|
|
}
|
|
|
|
# Multiple checks suppression
|
|
resource "aws_security_group" "legacy" {
|
|
# checkov:skip=CKV_AWS_23:Legacy app requires open access
|
|
# checkov:skip=CKV_AWS_24:IPv6 not supported by application
|
|
name = "legacy-sg"
|
|
|
|
ingress {
|
|
from_port = 0
|
|
to_port = 0
|
|
protocol = "-1"
|
|
cidr_blocks = ["0.0.0.0/0"]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Kubernetes
|
|
|
|
```yaml
|
|
# Annotation-based suppression
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: legacy-app
|
|
annotations:
|
|
checkov.io/skip: CKV_K8S_16=Legacy application requires elevated privileges
|
|
spec:
|
|
containers:
|
|
- name: app
|
|
image: myapp:1.0
|
|
securityContext:
|
|
privileged: true
|
|
```
|
|
|
|
#### CloudFormation
|
|
|
|
```yaml
|
|
Resources:
|
|
PublicBucket:
|
|
Type: AWS::S3::Bucket
|
|
Metadata:
|
|
checkov:
|
|
skip:
|
|
- id: CKV_AWS_18
|
|
comment: "Public bucket for CDN origin"
|
|
Properties:
|
|
BucketName: my-public-bucket
|
|
PublicAccessBlockConfiguration:
|
|
BlockPublicAcls: false
|
|
```
|
|
|
|
### Configuration File Suppression
|
|
|
|
#### .checkov.yaml
|
|
|
|
```yaml
|
|
# .checkov.yaml (project root)
|
|
skip-check:
|
|
- CKV_AWS_8 # Ensure CloudWatch log groups encrypted
|
|
- CKV_K8S_43 # Image pull policy Always
|
|
|
|
# Skip specific paths
|
|
skip-path:
|
|
- .terraform/
|
|
- node_modules/
|
|
- vendor/
|
|
|
|
# Severity-based soft fail
|
|
soft-fail-on:
|
|
- LOW
|
|
- MEDIUM
|
|
|
|
# Hard fail on critical/high only
|
|
hard-fail-on:
|
|
- CRITICAL
|
|
- HIGH
|
|
```
|
|
|
|
### CLI-Based Suppression
|
|
|
|
```bash
|
|
# Skip specific checks
|
|
checkov -d ./terraform --skip-check CKV_AWS_8,CKV_AWS_21
|
|
|
|
# Skip entire frameworks
|
|
checkov -d ./infra --skip-framework secrets
|
|
|
|
# Skip paths
|
|
checkov -d ./terraform --skip-path .terraform/ --skip-path vendor/
|
|
```
|
|
|
|
## Suppression Governance
|
|
|
|
### Approval Workflow
|
|
|
|
```yaml
|
|
# .github/workflows/checkov-review.yml
|
|
name: Review Checkov Suppressions
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- '**.tf'
|
|
- '**.yaml'
|
|
- '**.yml'
|
|
|
|
jobs:
|
|
check-suppressions:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Check for New Suppressions
|
|
run: |
|
|
# Count suppressions in PR
|
|
SUPPRESSIONS=$(git diff origin/main | grep -c "checkov:skip" || true)
|
|
|
|
if [ "$SUPPRESSIONS" -gt 0 ]; then
|
|
echo "::warning::PR contains $SUPPRESSIONS new suppression(s)"
|
|
echo "Security team review required"
|
|
# Request review from security team
|
|
fi
|
|
```
|
|
|
|
### Suppression Documentation Template
|
|
|
|
```hcl
|
|
resource "aws_security_group" "example" {
|
|
# checkov:skip=CKV_AWS_23:TICKET-1234 - Business justification here
|
|
# Approved by: security-team@example.com
|
|
# Review date: 2024-01-15
|
|
# Expiration: 2024-06-15 (review quarterly)
|
|
#
|
|
# Compensating controls:
|
|
# - WAF rule blocks malicious traffic
|
|
# - Application-level authentication required
|
|
# - IP allow-listing at load balancer
|
|
# - 24/7 monitoring and alerting
|
|
|
|
name = "approved-exception"
|
|
# ... configuration
|
|
}
|
|
```
|
|
|
|
## Suppression Best Practices
|
|
|
|
### 1. Always Provide Justification
|
|
|
|
```hcl
|
|
# ❌ BAD: No justification
|
|
resource "aws_s3_bucket" "example" {
|
|
# checkov:skip=CKV_AWS_18
|
|
bucket = "my-bucket"
|
|
}
|
|
|
|
# ✅ GOOD: Clear business justification
|
|
resource "aws_s3_bucket" "example" {
|
|
# checkov:skip=CKV_AWS_18:Public bucket required for static website hosting.
|
|
# Content is non-sensitive marketing materials. CloudFront restricts direct access.
|
|
bucket = "marketing-website"
|
|
}
|
|
```
|
|
|
|
### 2. Document Compensating Controls
|
|
|
|
```hcl
|
|
resource "aws_security_group" "app" {
|
|
# checkov:skip=CKV_AWS_23:Office IP range access required for developers
|
|
#
|
|
# Compensating controls:
|
|
# 1. IP range limited to corporate /24 subnet (203.0.113.0/24)
|
|
# 2. MFA required for VPN access to corporate network
|
|
# 3. Additional application-level authentication
|
|
# 4. Session timeout of 15 minutes
|
|
# 5. All access logged to SIEM
|
|
|
|
ingress {
|
|
from_port = 22
|
|
to_port = 22
|
|
protocol = "tcp"
|
|
cidr_blocks = ["203.0.113.0/24"]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Set Expiration Dates
|
|
|
|
```hcl
|
|
resource "aws_instance" "temp" {
|
|
# checkov:skip=CKV_AWS_8:Temporary instance for POC
|
|
# EXPIRES: 2024-03-31
|
|
# After expiration: Remove or apply encryption
|
|
|
|
ami = "ami-12345678"
|
|
instance_type = "t3.micro"
|
|
}
|
|
```
|
|
|
|
### 4. Use Granular Suppressions
|
|
|
|
```hcl
|
|
# ❌ BAD: Suppress entire file or directory
|
|
# checkov:skip=* (Don't do this!)
|
|
|
|
# ✅ GOOD: Suppress specific checks on specific resources
|
|
resource "aws_s3_bucket" "example" {
|
|
# checkov:skip=CKV_AWS_18:Specific reason for this resource only
|
|
bucket = "specific-bucket"
|
|
}
|
|
```
|
|
|
|
## Exception Categories
|
|
|
|
### Legitimate Exceptions
|
|
|
|
#### 1. Public Resources by Design
|
|
|
|
```hcl
|
|
resource "aws_s3_bucket" "website" {
|
|
# checkov:skip=CKV_AWS_18:Public bucket for static website
|
|
# checkov:skip=CKV_AWS_93:Public access required by design
|
|
# Content: Marketing materials (non-sensitive)
|
|
# Access: Read-only via CloudFront
|
|
|
|
bucket = "company-website"
|
|
}
|
|
```
|
|
|
|
#### 2. Legacy System Constraints
|
|
|
|
```yaml
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: legacy-app
|
|
annotations:
|
|
checkov.io/skip: CKV_K8S_16=Legacy app built before containers, requires host access
|
|
# Migration plan: TICKET-5678
|
|
# Target date: Q2 2024
|
|
spec:
|
|
hostNetwork: true
|
|
containers:
|
|
- name: legacy
|
|
image: legacy-app:1.0
|
|
```
|
|
|
|
#### 3. Development/Testing Environments
|
|
|
|
```hcl
|
|
resource "aws_db_instance" "dev_db" {
|
|
# checkov:skip=CKV_AWS_17:Dev environment - backups not required
|
|
# checkov:skip=CKV_AWS_61:Dev environment - encryption overhead not needed
|
|
# Environment: Non-production only
|
|
# Data: Synthetic test data (no PII/PHI)
|
|
|
|
identifier = "dev-database"
|
|
backup_retention_period = 0
|
|
storage_encrypted = false
|
|
|
|
tags = {
|
|
Environment = "development"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Temporary Exceptions
|
|
|
|
```hcl
|
|
resource "aws_rds_cluster" "temp_unencrypted" {
|
|
# checkov:skip=CKV_AWS_96:Temporary exception during migration
|
|
# TICKET: INFRA-1234
|
|
# EXPIRES: 2024-02-15
|
|
# PLAN: Enable encryption at rest in Phase 2 migration
|
|
# OWNER: platform-team@example.com
|
|
|
|
cluster_identifier = "migration-temp"
|
|
storage_encrypted = false
|
|
}
|
|
```
|
|
|
|
## Suppression Anti-Patterns
|
|
|
|
### ❌ Don't: Blanket Suppressions
|
|
|
|
```yaml
|
|
# BAD: Suppress all checks
|
|
skip-check:
|
|
- "*"
|
|
```
|
|
|
|
### ❌ Don't: Suppress Without Documentation
|
|
|
|
```hcl
|
|
# BAD: No explanation
|
|
resource "aws_s3_bucket" "example" {
|
|
# checkov:skip=CKV_AWS_18
|
|
bucket = "my-bucket"
|
|
}
|
|
```
|
|
|
|
### ❌ Don't: Permanent Suppressions for Production
|
|
|
|
```hcl
|
|
# BAD: Permanent suppression of critical security control
|
|
resource "aws_rds_cluster" "prod" {
|
|
# checkov:skip=CKV_AWS_96:Too expensive
|
|
# ^ This is unacceptable for production!
|
|
|
|
cluster_identifier = "production-db"
|
|
storage_encrypted = false
|
|
}
|
|
```
|
|
|
|
### ❌ Don't: Suppress High/Critical Without Review
|
|
|
|
```hcl
|
|
# DANGEROUS: Suppressing critical finding without security review
|
|
resource "aws_security_group" "prod" {
|
|
# checkov:skip=CKV_AWS_23:Need access from anywhere
|
|
# ^ Requires security team approval!
|
|
|
|
ingress {
|
|
cidr_blocks = ["0.0.0.0/0"]
|
|
}
|
|
}
|
|
```
|
|
|
|
## Monitoring Suppressions
|
|
|
|
### Track Suppression Metrics
|
|
|
|
```bash
|
|
# Count suppressions by type
|
|
grep -r "checkov:skip" ./terraform | \
|
|
sed 's/.*checkov:skip=\([^:]*\).*/\1/' | \
|
|
sort | uniq -c | sort -rn
|
|
|
|
# Find suppressions without justification
|
|
grep -r "checkov:skip=" ./terraform | \
|
|
grep -v "checkov:skip=.*:.*"
|
|
```
|
|
|
|
### Suppression Audit Report
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
"""Generate suppression audit report."""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
def find_suppressions(directory):
|
|
"""Find all Checkov suppressions."""
|
|
suppressions = []
|
|
|
|
for file_path in Path(directory).rglob('*.tf'):
|
|
with open(file_path) as f:
|
|
content = f.read()
|
|
|
|
# Find suppressions
|
|
matches = re.findall(
|
|
r'#\s*checkov:skip=([^:]+):(.*)',
|
|
content
|
|
)
|
|
|
|
for check_id, reason in matches:
|
|
suppressions.append({
|
|
'file': str(file_path),
|
|
'check_id': check_id.strip(),
|
|
'reason': reason.strip()
|
|
})
|
|
|
|
return suppressions
|
|
|
|
def generate_report(suppressions):
|
|
"""Generate markdown report."""
|
|
print("# Checkov Suppression Audit Report")
|
|
print(f"\nGenerated: {datetime.now().isoformat()}")
|
|
print(f"\nTotal Suppressions: {len(suppressions)}\n")
|
|
|
|
print("## Suppressions by Check")
|
|
check_counts = {}
|
|
for s in suppressions:
|
|
check_counts[s['check_id']] = check_counts.get(s['check_id'], 0) + 1
|
|
|
|
for check_id, count in sorted(check_counts.items(), key=lambda x: -x[1]):
|
|
print(f"- {check_id}: {count}")
|
|
|
|
print("\n## All Suppressions")
|
|
for s in suppressions:
|
|
print(f"\n### {s['file']}")
|
|
print(f"**Check:** {s['check_id']}")
|
|
print(f"**Reason:** {s['reason'] or '(no justification provided)'}")
|
|
|
|
if __name__ == '__main__':
|
|
directory = sys.argv[1] if len(sys.argv) > 1 else './terraform'
|
|
suppressions = find_suppressions(directory)
|
|
generate_report(suppressions)
|
|
```
|
|
|
|
## Quarterly Review Process
|
|
|
|
1. **Generate Suppression Report**: List all active suppressions
|
|
2. **Review Expirations**: Check for expired temporary suppressions
|
|
3. **Validate Justifications**: Ensure reasons still apply
|
|
4. **Verify Compensating Controls**: Confirm controls are still in place
|
|
5. **Update or Remove**: Update suppressions or fix underlying issues
|
|
|
|
## Additional Resources
|
|
|
|
- [Checkov Suppression Documentation](https://www.checkov.io/2.Basics/Suppressing%20and%20Skipping%20Policies.html)
|
|
- [Security Exception Management Best Practices](https://owasp.org/www-community/Security_Exception_Management)
|