Detect additive drift and orphaned resources in CloudFormation-managed AWS accounts
Project description
๐ก๏ธ cfn-drift-extended
Detect additive drift in CloudFormation-managed resources that native drift detection misses.
๐ Table of Contents
- The Problem
- Supported Services
- Installation
- Quick Start
- Orphaned Resource Detection
- IAM Permissions
- Exit Codes
- Example Output
- JSON Report Format
- GitHub Action
- Architecture
- Design Principles
- Performance
- Troubleshooting
- Development
- Contributing
- License
๐ The Problem
CloudFormation drift detection only catches modifications or deletions to resources it manages. It completely misses additive changes โ for example:
- ๐ A manually attached IAM policy on a CDK-managed role
- ๐ An extra security group ingress rule opening SSH to the world
- ๐จ An unauthorized SNS subscription exfiltrating data
- ๐ An extra SQS policy statement granting public access
- โก A rogue EventBridge rule routing events to unintended targets
CloudFormation says "IN_SYNC" for all of these. This tool catches them.
Real-world example: A reconciliation job failed in QA but worked in Dev. Root cause: someone had manually attached a broader IAM policy to the orchestrator role in Dev. CloudFormation showed "IN_SYNC" because the manual addition wasn't a modification โ it was an extra policy CFN didn't know about.
๐ฏ Supported Services
| Service | Drift Detected | Severity |
|---|---|---|
| ๐ IAM Roles | Extra inline policies, extra managed policies, modified policy documents | HIGH |
| ๐ Security Groups | Extra ingress rules (attack surface), extra egress rules (exfiltration) | HIGH / MEDIUM |
| ๐จ SNS Topics | Extra policy statements, extra subscriptions | HIGH / MEDIUM |
| ๐ SQS Queues | Extra resource policy statements | HIGH |
| โก EventBridge | Extra rules on CFN-managed event buses | MEDIUM |
| ๐ง Lambda | Extra environment variables, extra layers, extra resource-based permissions | HIGH / MEDIUM |
| ๐ชฃ S3 | Extra bucket policy statements, extra lifecycle rules, extra CORS rules | HIGH / MEDIUM / LOW |
| ๐๏ธ DynamoDB | Extra Global Secondary Indexes, extra auto-scaling targets/policies | MEDIUM |
๐ฆ Installation
pip install cfn-drift-extended
Requirements: Python 3.11+
๐ Quick Start
# Audit all stacks starting with "my-app"
cfn-drift-extended audit --stack-prefix my-app --region us-east-1
# Audit specific stacks by name
cfn-drift-extended audit --stack-name my-stack-prod --region us-east-1
# Filter by tags
cfn-drift-extended audit --stack-prefix my-app --tag Environment=Production --region us-east-1
# Write JSON report for CI/CD
cfn-drift-extended audit --stack-prefix my-app --output-json report.json
# Don't fail on drift (just report)
cfn-drift-extended audit --stack-prefix my-app --no-fail-on-drift
# Audit only specific services
cfn-drift-extended audit --stack-prefix my-app --services iam,sg
# Verbose mode for debugging
cfn-drift-extended audit --stack-prefix my-app -v
# Control concurrency (default: 10 parallel workers)
cfn-drift-extended audit --stack-prefix my-app --max-workers 5
๐ Orphaned Resource Detection
Detect resources that exist in your account but aren't managed by any CloudFormation stack โ manually created resources that were never cleaned up.
# Detect orphaned resources across all services
cfn-drift-extended orphans --region us-east-1
# Scope the managed index to specific stacks
cfn-drift-extended orphans --stack-prefix my-app --region us-east-1
# Scan only specific services
cfn-drift-extended orphans --services sqs,sns --region us-east-1
# Fail in CI if orphans found
cfn-drift-extended orphans --stack-prefix my-app --fail-on-orphans
# Write JSON report
cfn-drift-extended orphans --stack-prefix my-app --output-json orphans.json
Supported orphan detection services: iam, sg, lambda, sqs, sns
Exclusion filters applied automatically:
- AWS service-linked roles (
/aws-service-role/) and AWS-reserved roles (/aws-reserved/) - CDK bootstrap roles (name contains
cdk-) OrganizationAccountAccessRole- Default security groups (cannot be deleted)
- CDK custom resource Lambda handlers (
LogRetention,Custom::) - FIFO DLQ queues (
-dlq.fifo,-deadletter.fifo)
Provenance classification
Each orphan finding is classified by how the resource came to exist, so you can triage by cleanup priority instead of treating every leaked resource the same:
provenance |
Meaning | Severity |
|---|---|---|
cfn_orphan_deleted_stack |
Resource was retained when its CloudFormation stack was deleted (DeletionPolicy: Retain). Most actionable โ high-priority cleanup. |
HIGH (always) |
cfn_orphan_active_stack |
Resource appears tied to a still-active stack that the managed-index missed. Logged as a tool warning and not reported โ usually a cross-region or stack-prefix gap. | n/a (skipped) |
non_iac |
No CloudFormation record of the resource. Created via console / CLI / SDK directly. | service default |
unknown |
Tag tier indicated nothing and the CFN API was unavailable; we won't claim NON_IAC without evidence. | service default |
Provenance is resolved by two complementary signals:
- Managed-index lookup including
DELETE_COMPLETEstacks within CloudFormation's ~90-day retention window. The authoritative source for the deleted-stack-residue case (resources whose status wasDELETE_SKIPPED). cloudformation:DescribeStackResources --physical-resource-idas a fallback for active-stack resolution, plus a bulkresourcegroupstaggingapi:GetResourcescall for resource types where the reservedaws:cloudformation:stack-nametag does propagate (CloudWatch log groups, S3 buckets, SSM parameters โ note that IAM roles, SQS queues, SG, Lambda, and SNS topics do not carry the reserved tag, verified empirically).
originating_stack_name is populated on every CFN-orphan finding so you can trace each resource back to the stack that left it behind.
Live verification
A comprehensive end-to-end harness lives at scripts/live-provenance-test.sh. It deploys a CFN stack with one Retain'd resource per supported service plus a CLI-only resource per service plus exclusion-filter fixtures, deletes the stack, and asserts every classification path. Refuses to run against profiles or roles that look like production. Always tears down on success, failure, or interrupt.
scripts/live-provenance-test.sh --profile dev-account --region us-east-1
๐ Required IAM Permissions (Least Privilege)
This tool uses read-only AWS API calls exclusively. No write operations are performed.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CfnDriftExtendedReadOnly",
"Effect": "Allow",
"Action": [
"cloudformation:ListStacks",
"cloudformation:GetTemplate",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackResource",
"cloudformation:DescribeStackResources",
"cloudformation:ListStackResources",
"tag:GetResources",
"iam:ListRoles",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListRoleTags",
"iam:ListRolePolicies",
"iam:ListAttachedRolePolicies",
"ec2:DescribeVpcs",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSecurityGroupRules",
"sqs:ListQueues",
"sqs:GetQueueAttributes",
"sqs:ListQueueTags",
"sns:ListTopics",
"sns:GetTopicAttributes",
"sns:ListSubscriptionsByTopic",
"events:DescribeEventBus",
"events:ListRules",
"events:ListTargetsByRule",
"lambda:ListFunctions",
"lambda:GetFunctionConfiguration",
"lambda:GetPolicy",
"cloudwatch:GetMetricStatistics",
"s3:GetBucketPolicy",
"s3:GetBucketLifecycleConfiguration",
"s3:GetBucketCors",
"dynamodb:DescribeTable",
"application-autoscaling:DescribeScalableTargets",
"application-autoscaling:DescribeScalingPolicies",
"sts:GetCallerIdentity"
],
"Resource": "*"
}
]
}
๐ก For tighter scoping, restrict
Resourceto specific stack ARNs, role ARNs, security group IDs, queue ARNs, topic ARNs, and event bus ARNs.
๐ Exit Codes
| Code | Meaning |
|---|---|
0 |
โ
No drift detected (or --no-fail-on-drift used) |
1 |
โ ๏ธ Additive drift detected |
2 |
โ Error (permission denied, invalid input, unexpected failure) |
๐ Example Output
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
cfn-drift-extended โ Additive Drift Report
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Stacks scanned: 2
Resources scanned: 9
Resources drifted: 7
โ Found 10 drift finding(s) across 7 resource(s):
[HIGH] my-orchestrator-role (my-app-stack)
Managed policy 'arn:aws:iam::123456789012:policy/ManualBroadAccess'
is attached to role but is not declared in the CloudFormation template
+ arn:aws:iam::123456789012:policy/ManualBroadAccess
[HIGH] sg-0b7a2542ddb09edd6 (my-app-stack)
Ingress rule (tcp 22-22 0.0.0.0/0) exists on security group
but is not declared in the CloudFormation template
+ ('tcp', 22, 22, '0.0.0.0/0', None, None, None)
[MEDIUM] my-event-bus (my-app-stack)
Rule 'sneaky-exfil-rule' exists on event bus but is not declared
in the CloudFormation template
+ sneaky-exfil-rule
๐ JSON Report Format
{
"tool_version": "0.1.0",
"account_id": "123456789012",
"region": "us-east-1",
"timestamp": "2026-05-20T14:30:00+00:00",
"stacks_scanned": 3,
"resources_scanned": 12,
"resources_with_drift": 2,
"findings": [
{
"resource_type": "AWS::IAM::Role",
"resource_id": "my-role",
"stack_name": "my-stack",
"drift_type": "managed_policy_attached",
"severity": "high",
"description": "Managed policy 'arn:...' is attached but not in template",
"expected": ["arn:aws:iam::aws:policy/AWSLambdaBasicExecutionRole"],
"actual": ["arn:aws:iam::aws:policy/AWSLambdaBasicExecutionRole", "arn:aws:iam::aws:policy/AdministratorAccess"],
"extra": "arn:aws:iam::aws:policy/AdministratorAccess"
}
],
"errors": []
}
โ๏ธ GitHub Action Usage
- uses: mopyle4/cfn-drift-extended@v0.1
with:
stack-prefix: "my-app"
region: "us-east-1"
services: "iam,sg,sns,sqs,eventbridge" # optional, default: all
fail-on-drift: "true"
output-json: "drift-report.json"
Outputs:
drift-detectedโtrueorfalsefindings-countโ number of drift findings
๐๏ธ Architecture
graph TD
CLI[๐ฅ๏ธ CLI - Click] --> Auditor[๐ฏ Auditor - Orchestrator]
Auditor --> CfnCollector[๐ CfnCollector - Expected State]
Auditor --> ServiceCollectors[๐ Service Collectors - Actual State]
Auditor --> Comparators[โ๏ธ Comparators - Set Diff]
Auditor --> Reporters[๐ Reporters]
CfnCollector --> CfnSgExtractor[SG Extractor]
CfnCollector --> CfnSnsSqsExtractor[SNS/SQS Extractor]
CfnCollector --> CfnEventBridgeExtractor[EventBridge Extractor]
ServiceCollectors --> IamCollector[๐ IAM Collector]
ServiceCollectors --> SgCollector[๐ SG Collector]
ServiceCollectors --> SnsSqsCollector[๐จ SNS/SQS Collector]
ServiceCollectors --> EventBridgeCollector[โก EventBridge Collector]
Comparators --> IamComparator[IAM Comparator]
Comparators --> SgComparator[SG Comparator]
Comparators --> SnsSqsComparator[SNS/SQS Comparator]
Comparators --> EventBridgeComparator[EventBridge Comparator]
Reporters --> Console[Console Report]
Reporters --> JSON[JSON Report]
Reporters --> GitHubChecks[GitHub Checks]
| Component | Responsibility |
|---|---|
| CLI | Argument parsing, output formatting, exit codes |
| Auditor | Orchestrates the pipeline with parallel execution |
| CfnCollector | Extracts expected state from CloudFormation templates |
| Service Collectors | Fetches actual state from AWS APIs (IAM, EC2, SQS, SNS, Events) |
| CfnExtractors | Resolves intrinsics (Ref, GetAtt, Sub) in template resources |
| Comparators | Diffs expected vs actual using set operations (O(n)) |
| Reporters | Formats results for console, JSON, or GitHub Checks |
๐ง Design Principles
| Principle | Implementation |
|---|---|
| ๐ Least Privilege | Read-only API calls only; no write operations |
| ๐ SOLID | Single responsibility per module; dependency injection via constructor |
| ๐ง Immutable Models | Frozen Pydantic models and frozen dataclasses prevent mutation |
| ๐ Graceful Degradation | Individual resource failures don't crash the audit |
| โก Performance | Parallel auditing via ThreadPoolExecutor; set operations for O(n) comparison |
| ๐ Adaptive Retry | Exponential backoff with jitter (boto3 adaptive mode, 5 max attempts) |
| ๐ญ CI/CD Ready | Exit codes, JSON output, --services filter, and --fail-on-drift flag |
โก Performance Characteristics
| Metric | Value |
|---|---|
| Time complexity | O(S ร R) where S = stacks, R = resources per stack |
| Comparison | O(n) set-based diff per resource |
| Concurrency | Configurable thread pool (default 10 workers) |
| Memory | Frozen dataclasses with __slots__ (~40% less per instance) |
| Network | Adaptive retry with exponential backoff prevents throttling |
| Validated | 10 true findings, 0 false positives on live Isengard stack |
๐ง Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Exit code 2 with "Permission denied" | Missing IAM permissions | Add the required permissions from the policy above |
| No stacks found | Prefix doesn't match or stacks are in non-terminal state | Check stack names with aws cloudformation list-stacks |
| Slow execution | Many resources across many stacks | Increase --max-workers or narrow --stack-prefix |
| False positives on CDK stacks | CDK generates AWS::IAM::Policy resources separately |
Already handled โ external policies are associated with their target roles |
| Intrinsic resolution failures | Template uses complex Fn::Sub or nested intrinsics | File an issue โ we handle Ref, GetAtt, and Sub but edge cases may exist |
๐ ๏ธ Development
# Clone and install in dev mode
cd cfn-drift-extended
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# Run unit tests (249 tests, mocked AWS via moto)
pytest --cov=cfn_drift_extended --cov-report=term-missing
# Lint
ruff check src/ tests/
# Type check
mypy src/
# Run drift integration tests (requires AWS credentials)
cd integration-tests
./deploy.sh
./introduce-drift.sh
./validate.sh
./cleanup.sh
# Run orphan-detection live test (requires AWS credentials)
# Refuses to run against profiles or roles that look like production.
scripts/live-provenance-test.sh --profile dev-account --region us-east-1
๐ค Contributing
See CONTRIBUTING.md for guidelines.
Adding a new service collector: Follow the pattern in the design doc. Each service needs:
- Collector (frozen dataclass + boto3 client class)
- CfnExtractor (template โ expected state)
- Comparator (set-based diff โ findings)
- Tests (happy path, drift detected, not found, permission denied, edge cases)
๐ License
MIT โ see LICENSE for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cfn_drift_extended-1.0.0.tar.gz.
File metadata
- Download URL: cfn_drift_extended-1.0.0.tar.gz
- Upload date:
- Size: 106.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2914bcc7c8de114f2d5d955cfe465385c24af0ff9d2edb1d1e6fffa7b8a403b
|
|
| MD5 |
d5bfb6921f1de5306c29fa1db224bd15
|
|
| BLAKE2b-256 |
4b226af5f58a1f80bd7f7a71fd14835f8f312b857b701e93122c74ca2812f484
|
Provenance
The following attestation bundles were made for cfn_drift_extended-1.0.0.tar.gz:
Publisher:
ci.yml on mopyle4/cfn-drift-extended
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cfn_drift_extended-1.0.0.tar.gz -
Subject digest:
d2914bcc7c8de114f2d5d955cfe465385c24af0ff9d2edb1d1e6fffa7b8a403b - Sigstore transparency entry: 1704581170
- Sigstore integration time:
-
Permalink:
mopyle4/cfn-drift-extended@69906ee424ec99aeba0e5c2eacb31e799794c2e7 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/mopyle4
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@69906ee424ec99aeba0e5c2eacb31e799794c2e7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cfn_drift_extended-1.0.0-py3-none-any.whl.
File metadata
- Download URL: cfn_drift_extended-1.0.0-py3-none-any.whl
- Upload date:
- Size: 92.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7763608a63174da05cfb0342461d66dccd40fc2c6cbdfc2500b5ebc140a63c7d
|
|
| MD5 |
419508e5ac64a0634c33032b582bf22e
|
|
| BLAKE2b-256 |
4c5e10405c7fb5c505f17b92b70286011e4ad124bf03863ed39e2894e5f5768c
|
Provenance
The following attestation bundles were made for cfn_drift_extended-1.0.0-py3-none-any.whl:
Publisher:
ci.yml on mopyle4/cfn-drift-extended
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cfn_drift_extended-1.0.0-py3-none-any.whl -
Subject digest:
7763608a63174da05cfb0342461d66dccd40fc2c6cbdfc2500b5ebc140a63c7d - Sigstore transparency entry: 1704581185
- Sigstore integration time:
-
Permalink:
mopyle4/cfn-drift-extended@69906ee424ec99aeba0e5c2eacb31e799794c2e7 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/mopyle4
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@69906ee424ec99aeba0e5c2eacb31e799794c2e7 -
Trigger Event:
push
-
Statement type: