Created
July 8, 2025 07:56
-
-
Save FlorinAsavoaie/442903ebc385fda98733e9d3e841ebf8 to your computer and use it in GitHub Desktop.
CloudFormation Drift Detection Coverage Analyzer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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