Skip to content

Instantly share code, notes, and snippets.

@FlorinAsavoaie
Created July 8, 2025 07:56
Show Gist options
  • Save FlorinAsavoaie/442903ebc385fda98733e9d3e841ebf8 to your computer and use it in GitHub Desktop.
Save FlorinAsavoaie/442903ebc385fda98733e9d3e841ebf8 to your computer and use it in GitHub Desktop.
CloudFormation Drift Detection Coverage Analyzer
"""
CloudFormation Drift Detection Coverage Analyzer
This script analyzes your AWS CloudFormation stacks to identify resource types that do not
support drift detection. It cross-references the resources in your stacks against AWS's
official documentation to find resources that cannot be monitored for configuration drift.
Purpose:
- Identifies CloudFormation resource types in your AWS account that don't support drift detection
- Helps with compliance and infrastructure monitoring by highlighting blind spots
- Provides visibility into which resources cannot be automatically checked for configuration changes
How it works:
1. Scrapes the AWS CloudFormation documentation for supported drift detection resources
2. Lists all active CloudFormation stacks in your AWS account
3. Enumerates all resource types across those stacks
4. Compares your resources against the supported drift detection list
5. Returns resource types that don't support drift detection
Prerequisites:
- AWS credentials configured (via AWS CLI, environment variables, or IAM roles)
- Required Python packages: boto3, requests, beautifulsoup4
- Appropriate IAM permissions for CloudFormation read operations
Usage:
python cf-drift-coverage.py
Output:
Prints a list of resource types that don't support drift detection or an error message
if the drift coverage information cannot be retrieved.
Example output:
['AWS::S3::BucketPolicy', 'AWS::IAM::Role', 'Custom::MyCustomResource']
IAM Permissions Required:
- cloudformation:ListStacks
- cloudformation:ListStackResources
Note: This script makes web requests to AWS documentation and AWS API calls. Ensure you have
appropriate network connectivity and AWS permissions.
"""
from typing import Optional, Iterator
import boto3
import requests
from bs4 import BeautifulSoup, Tag
cloudformation = boto3.client("cloudformation")
def get_drift_coverage() -> Optional[set[str]]:
url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html"
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
tables = soup.find_all("table")
if not tables:
return None
table = tables[0]
if not isinstance(table, Tag):
return None
results = set()
headers = [th.get_text(strip=True) for th in table.find_all("th")]
resource_index = headers.index("Resource")
drift_index = headers.index("Drift detection")
for row in table.find_all("tr")[1:]:
if not isinstance(row, Tag):
continue
cells = [td.get_text(strip=True) for td in row.find_all(["td", "th"])]
if cells[drift_index] == "Yes":
results.add(cells[resource_index])
return results
def list_stacks() -> Iterator[str]:
paginator = cloudformation.get_paginator("list_stacks")
for page in paginator.paginate():
for stack in page.get("StackSummaries", []):
status = stack["StackStatus"]
if not status.startswith("DELETE_"):
yield stack["StackName"]
def list_stack_resources(stack_name: str) -> Iterator[str]:
paginator = cloudformation.get_paginator("list_stack_resources")
for page in paginator.paginate(StackName=stack_name):
for resource in page.get("StackResourceSummaries", []):
yield resource["ResourceType"]
def list_stack_resource_types(stack_names: Iterator[str]) -> Iterator[str]:
seen_resources = set()
for stack_name in stack_names:
for resource_type in list_stack_resources(stack_name):
if resource_type not in seen_resources:
seen_resources.add(resource_type)
yield resource_type
def list_non_drift_detectable_resource_types() -> Iterator[str]:
detectable_resource_types = get_drift_coverage()
if detectable_resource_types is None:
return None
stacks = list_stacks()
for resource_type in list_stack_resource_types(stacks):
if not resource_type in detectable_resource_types:
yield resource_type
return None
if __name__ == "__main__":
non_drift_detectable_resource_types = list_non_drift_detectable_resource_types()
if non_drift_detectable_resource_types is None:
print("Failed to retrieve drift coverage")
exit(1)
print(list(non_drift_detectable_resource_types))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment