├── .gitignore ├── LICENSE ├── README.md ├── index.py ├── requirements.txt └── test.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ian Mckay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CloudFormation Remediate Drift 2 | 3 | **Update 2022: Check out https://github.com/WeAreCloudar/cfn-drift-remediation for a new, better way to achieve this** 4 | 5 | The following script will programmatically perform the following steps: 6 | 7 | * Check for drifted resources 8 | * Using CloudFormation outputs, extract any references to resources that have drifted and replace the references with the dereferenced values temporarily 9 | * Remove any *supported* drifted resources from the stack, whilst retaining the resource 10 | * Import the resources with their current state back into the stack 11 | * Perform an update on the stack back to its original template, effectively remediating the resources 12 | 13 | > :exclamation: This script is not thoroughly tested and you should attempt to use this on a non-critical resource before real-world usage as some resources refuse to re-import for a variety of reasons. I am not responsible for your data loss. 14 | 15 | ## Usage 16 | 17 | ``` 18 | python3 index.py MyStackName 19 | ``` 20 | 21 | or to specify a region 22 | 23 | ``` 24 | python3 index.py MyStackName us-east-1 25 | ``` 26 | 27 | ### Supported Resources 28 | 29 | The following resources are supported for import operations (other resources will be ignored, even if drift is detected): 30 | 31 | * AWS::ACMPCA::Certificate 32 | * AWS::ACMPCA::CertificateAuthority 33 | * AWS::ACMPCA::CertificateAuthorityActivation 34 | * AWS::AccessAnalyzer::Analyzer 35 | * AWS::ApiGateway::Authorizer 36 | * AWS::ApiGateway::Deployment 37 | * AWS::ApiGateway::Method 38 | * AWS::ApiGateway::Model 39 | * AWS::ApiGateway::RequestValidator 40 | * AWS::ApiGateway::Resource 41 | * AWS::ApiGateway::RestApi 42 | * AWS::ApiGateway::Stage 43 | * AWS::Athena::DataCatalog 44 | * AWS::Athena::NamedQuery 45 | * AWS::Athena::WorkGroup 46 | * AWS::AutoScaling::AutoScalingGroup 47 | * AWS::AutoScaling::LaunchConfiguration 48 | * AWS::AutoScaling::LifecycleHook 49 | * AWS::AutoScaling::ScalingPolicy 50 | * AWS::AutoScaling::ScheduledAction 51 | * AWS::CE::CostCategory 52 | * AWS::Cassandra::Keyspace 53 | * AWS::Cassandra::Table 54 | * AWS::Chatbot::SlackChannelConfiguration 55 | * AWS::CloudFormation::Stack 56 | * AWS::CloudTrail::Trail 57 | * AWS::CloudWatch::Alarm 58 | * AWS::CloudWatch::CompositeAlarm 59 | * AWS::CodeGuruProfiler::ProfilingGroup 60 | * AWS::CodeStarConnections::Connection 61 | * AWS::Config::ConformancePack 62 | * AWS::Config::OrganizationConformancePack 63 | * AWS::Detective::Graph 64 | * AWS::Detective::MemberInvitation 65 | * AWS::DynamoDB::Table 66 | * AWS::EC2::EIP 67 | * AWS::EC2::FlowLog 68 | * AWS::EC2::GatewayRouteTableAssociation 69 | * AWS::EC2::Instance 70 | * AWS::EC2::InternetGateway 71 | * AWS::EC2::LocalGatewayRoute 72 | * AWS::EC2::LocalGatewayRouteTableVPCAssociation 73 | * AWS::EC2::NatGateway 74 | * AWS::EC2::NetworkAcl 75 | * AWS::EC2::NetworkInterface 76 | * AWS::EC2::PrefixList 77 | * AWS::EC2::RouteTable 78 | * AWS::EC2::SecurityGroup 79 | * AWS::EC2::Subnet 80 | * AWS::EC2::VPC 81 | * AWS::EC2::Volume 82 | * AWS::ECS::CapacityProvider 83 | * AWS::ECS::Cluster 84 | * AWS::ECS::PrimaryTaskSet 85 | * AWS::ECS::Service 86 | * AWS::ECS::TaskDefinition 87 | * AWS::ECS::TaskSet 88 | * AWS::EFS::AccessPoint 89 | * AWS::EFS::FileSystem 90 | * AWS::ElasticLoadBalancing::LoadBalancer 91 | * AWS::ElasticLoadBalancingV2::Listener 92 | * AWS::ElasticLoadBalancingV2::ListenerRule 93 | * AWS::ElasticLoadBalancingV2::LoadBalancer 94 | * AWS::EventSchemas::RegistryPolicy 95 | * AWS::Events::Rule 96 | * AWS::FMS::NotificationChannel 97 | * AWS::FMS::Policy 98 | * AWS::GlobalAccelerator::Accelerator 99 | * AWS::GlobalAccelerator::EndpointGroup 100 | * AWS::GlobalAccelerator::Listener 101 | * AWS::ImageBuilder::Component 102 | * AWS::ImageBuilder::DistributionConfiguration 103 | * AWS::ImageBuilder::Image 104 | * AWS::ImageBuilder::ImagePipeline 105 | * AWS::ImageBuilder::ImageRecipe 106 | * AWS::ImageBuilder::InfrastructureConfiguration 107 | * AWS::IoT::ProvisioningTemplate 108 | * AWS::IoT::Thing 109 | * AWS::KinesisFirehose::DeliveryStream 110 | * AWS::Lambda::Alias 111 | * AWS::Lambda::Function 112 | * AWS::Lambda::Version 113 | * AWS::Logs::LogGroup 114 | * AWS::Logs::MetricFilter 115 | * AWS::Logs::SubscriptionFilter 116 | * AWS::Macie::CustomDataIdentifier 117 | * AWS::Macie::FindingsFilter 118 | * AWS::Macie::Session 119 | * AWS::NetworkManager::CustomerGatewayAssociation 120 | * AWS::NetworkManager::Device 121 | * AWS::NetworkManager::GlobalNetwork 122 | * AWS::NetworkManager::Link 123 | * AWS::NetworkManager::LinkAssociation 124 | * AWS::NetworkManager::Site 125 | * AWS::NetworkManager::TransitGatewayRegistration 126 | * AWS::QLDB::Stream 127 | * AWS::RDS::DBCluster 128 | * AWS::RDS::DBInstance 129 | * AWS::RDS::DBProxy 130 | * AWS::RDS::DBProxyTargetGroup 131 | * AWS::ResourceGroups::Group 132 | * AWS::Route53::HostedZone 133 | * AWS::S3::AccessPoint 134 | * AWS::S3::Bucket 135 | * AWS::SES::ConfigurationSet 136 | * AWS::SNS::Topic 137 | * AWS::SQS::Queue 138 | * AWS::SSM::Association 139 | * AWS::ServiceCatalog::CloudFormationProvisionedProduct 140 | * AWS::Synthetics::Canary 141 | * AWS::WAFv2::IPSet 142 | * AWS::WAFv2::RegexPatternSet 143 | * AWS::WAFv2::RuleGroup 144 | * AWS::WAFv2::WebACL 145 | * AWS::WAFv2::WebACLAssociation 146 | * AWS::IAM::Group 147 | * AWS::IAM::InstanceProfile 148 | * AWS::IAM::Role 149 | * AWS::IAM::User 150 | * AWS::IAM::ManagedPolicy 151 | 152 | ### Known Issues 153 | 154 | * Templates with a high amount of drifted resources may cause an error regarding too many outputs 155 | * Drifted resources referenced within a `Fn::Sub` string may cause the process to fail 156 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import sys 3 | import json 4 | import time 5 | from collections import OrderedDict 6 | from cfn_flip import flip, to_yaml, to_json 7 | 8 | resolve_matches = {} 9 | 10 | def resolvePropertyValue(prop, match_resources, replace_values): 11 | if isinstance(prop, dict): 12 | if 'Ref' in prop: 13 | if prop['Ref'] in match_resources: 14 | if replace_values: 15 | return resolve_matches['Ref' + prop['Ref']] 16 | else: 17 | resolve_matches['Ref' + prop['Ref']] = { 18 | 'Ref': prop['Ref'] 19 | } 20 | elif 'Fn::GetAtt' in prop: 21 | if prop['Fn::GetAtt'][0] in match_resources: 22 | if replace_values: 23 | return resolve_matches['GetAtt' + prop['Fn::GetAtt'][0] + prop['Fn::GetAtt'][1]] 24 | else: 25 | resolve_matches['GetAtt' + prop['Fn::GetAtt'][0] + prop['Fn::GetAtt'][1]] = { 26 | 'Fn::GetAtt': prop['Fn::GetAtt'] 27 | } 28 | elif 'Fn::Sub' in prop: 29 | pass # TODO 30 | 31 | ret = {} 32 | for k, v in prop.items(): 33 | ret[k] = resolvePropertyValue(v, match_resources, replace_values) 34 | return ret 35 | elif isinstance(prop, list) and not isinstance(prop, str): 36 | ret = [] 37 | for listitem in prop: 38 | ret.append(resolvePropertyValue(listitem, match_resources, replace_values)) 39 | return ret 40 | else: 41 | return prop 42 | 43 | empty_template = { 44 | "Conditions": { 45 | "FalseCondition": { 46 | "Fn::Equals": [1, 2] 47 | } 48 | }, 49 | "Resources": { 50 | "PlaceholderResource": { 51 | "Condition": "FalseCondition", 52 | "Type": "AWS::S3::Bucket" 53 | } 54 | } 55 | } 56 | 57 | eligible_import_resources = { # from Former2 58 | "AWS::ACMPCA::Certificate": { 59 | "importProperties": [ 60 | "Arn", 61 | "CertificateAuthorityArn" 62 | ] 63 | }, 64 | "AWS::ACMPCA::CertificateAuthority": { 65 | "importProperties": [ 66 | "Arn" 67 | ] 68 | }, 69 | "AWS::ACMPCA::CertificateAuthorityActivation": { 70 | "importProperties": [ 71 | "CertificateAuthorityArn" 72 | ] 73 | }, 74 | "AWS::AccessAnalyzer::Analyzer": { 75 | "importProperties": [ 76 | "Arn" 77 | ] 78 | }, 79 | "AWS::ApiGateway::Authorizer": { 80 | "importProperties": [ 81 | "RestApiId", 82 | "AuthorizerId" 83 | ] 84 | }, 85 | "AWS::ApiGateway::Deployment": { 86 | "importProperties": [ 87 | "RestApiId", 88 | "DeploymentId" 89 | ] 90 | }, 91 | "AWS::ApiGateway::Method": { 92 | "importProperties": [ 93 | "RestApiId", 94 | "ResourceId", 95 | "HttpMethod" 96 | ] 97 | }, 98 | "AWS::ApiGateway::Model": { 99 | "importProperties": [ 100 | "RestApiId", 101 | "Name" 102 | ] 103 | }, 104 | "AWS::ApiGateway::RequestValidator": { 105 | "importProperties": [ 106 | "RestApiId", 107 | "RequestValidatorId" 108 | ] 109 | }, 110 | "AWS::ApiGateway::Resource": { 111 | "importProperties": [ 112 | "RestApiId", 113 | "ResourceId" 114 | ] 115 | }, 116 | "AWS::ApiGateway::RestApi": { 117 | "importProperties": [ 118 | "RestApiId" 119 | ] 120 | }, 121 | "AWS::ApiGateway::Stage": { 122 | "importProperties": [ 123 | "RestApiId", 124 | "StageName" 125 | ] 126 | }, 127 | "AWS::Athena::DataCatalog": { 128 | "importProperties": [ 129 | "Name" 130 | ] 131 | }, 132 | "AWS::Athena::NamedQuery": { 133 | "importProperties": [ 134 | "NamedQueryId" 135 | ] 136 | }, 137 | "AWS::Athena::WorkGroup": { 138 | "importProperties": [ 139 | "Name" 140 | ] 141 | }, 142 | "AWS::AutoScaling::AutoScalingGroup": { 143 | "importProperties": [ 144 | "AutoScalingGroupName" 145 | ] 146 | }, 147 | "AWS::AutoScaling::LaunchConfiguration": { 148 | "importProperties": [ 149 | "LaunchConfigurationName" 150 | ] 151 | }, 152 | "AWS::AutoScaling::LifecycleHook": { 153 | "importProperties": [ 154 | "AutoScalingGroupName", 155 | "LifecycleHookName" 156 | ] 157 | }, 158 | "AWS::AutoScaling::ScalingPolicy": { 159 | "importProperties": [ 160 | "PolicyName" 161 | ] 162 | }, 163 | "AWS::AutoScaling::ScheduledAction": { 164 | "importProperties": [ 165 | "ScheduledActionName" 166 | ] 167 | }, 168 | "AWS::CE::CostCategory": { 169 | "importProperties": [ 170 | "Arn" 171 | ] 172 | }, 173 | "AWS::Cassandra::Keyspace": { 174 | "importProperties": [ 175 | "KeyspaceName" 176 | ] 177 | }, 178 | "AWS::Cassandra::Table": { 179 | "importProperties": [ 180 | "KeyspaceName", 181 | "TableName" 182 | ] 183 | }, 184 | "AWS::Chatbot::SlackChannelConfiguration": { 185 | "importProperties": [ 186 | "Arn" 187 | ] 188 | }, 189 | "AWS::CloudFormation::Stack": { 190 | "importProperties": [ 191 | "StackId" 192 | ] 193 | }, 194 | "AWS::CloudTrail::Trail": { 195 | "importProperties": [ 196 | "TrailName" 197 | ] 198 | }, 199 | "AWS::CloudWatch::Alarm": { 200 | "importProperties": [ 201 | "AlarmName" 202 | ] 203 | }, 204 | "AWS::CloudWatch::CompositeAlarm": { 205 | "importProperties": [ 206 | "AlarmName" 207 | ] 208 | }, 209 | "AWS::CodeGuruProfiler::ProfilingGroup": { 210 | "importProperties": [ 211 | "ProfilingGroupName" 212 | ] 213 | }, 214 | "AWS::CodeStarConnections::Connection": { 215 | "importProperties": [ 216 | "ConnectionArn" 217 | ] 218 | }, 219 | "AWS::Config::ConformancePack": { 220 | "importProperties": [ 221 | "ConformancePackName" 222 | ] 223 | }, 224 | "AWS::Config::OrganizationConformancePack": { 225 | "importProperties": [ 226 | "OrganizationConformancePackName" 227 | ] 228 | }, 229 | "AWS::Detective::Graph": { 230 | "importProperties": [ 231 | "Arn" 232 | ] 233 | }, 234 | "AWS::Detective::MemberInvitation": { 235 | "importProperties": [ 236 | "GraphArn", 237 | "MemberId" 238 | ] 239 | }, 240 | "AWS::DynamoDB::Table": { 241 | "importProperties": [ 242 | "TableName" 243 | ] 244 | }, 245 | "AWS::EC2::EIP": { 246 | "importProperties": [ 247 | "PublicIp" 248 | ] 249 | }, 250 | "AWS::EC2::FlowLog": { 251 | "importProperties": [ 252 | "Id" 253 | ] 254 | }, 255 | "AWS::EC2::GatewayRouteTableAssociation": { 256 | "importProperties": [ 257 | "GatewayId" 258 | ] 259 | }, 260 | "AWS::EC2::Instance": { 261 | "importProperties": [ 262 | "InstanceId" 263 | ] 264 | }, 265 | "AWS::EC2::InternetGateway": { 266 | "importProperties": [ 267 | "InternetGatewayId" 268 | ] 269 | }, 270 | "AWS::EC2::LocalGatewayRoute": { 271 | "importProperties": [ 272 | "DestinationCidrBlock", 273 | "LocalGatewayRouteTableId" 274 | ] 275 | }, 276 | "AWS::EC2::LocalGatewayRouteTableVPCAssociation": { 277 | "importProperties": [ 278 | "LocalGatewayRouteTableVpcAssociationId" 279 | ] 280 | }, 281 | "AWS::EC2::NatGateway": { 282 | "importProperties": [ 283 | "NatGatewayId" 284 | ] 285 | }, 286 | "AWS::EC2::NetworkAcl": { 287 | "importProperties": [ 288 | "NetworkAclId" 289 | ] 290 | }, 291 | "AWS::EC2::NetworkInterface": { 292 | "importProperties": [ 293 | "NetworkInterfaceId" 294 | ] 295 | }, 296 | "AWS::EC2::PrefixList": { 297 | "importProperties": [ 298 | "PrefixListId" 299 | ] 300 | }, 301 | "AWS::EC2::RouteTable": { 302 | "importProperties": [ 303 | "RouteTableId" 304 | ] 305 | }, 306 | "AWS::EC2::SecurityGroup": { 307 | "importProperties": [ 308 | "GroupId" 309 | ] 310 | }, 311 | "AWS::EC2::Subnet": { 312 | "importProperties": [ 313 | "SubnetId" 314 | ] 315 | }, 316 | "AWS::EC2::VPC": { 317 | "importProperties": [ 318 | "VpcId" 319 | ] 320 | }, 321 | "AWS::EC2::Volume": { 322 | "importProperties": [ 323 | "VolumeId" 324 | ] 325 | }, 326 | "AWS::ECS::CapacityProvider": { 327 | "importProperties": [ 328 | "Name" 329 | ] 330 | }, 331 | "AWS::ECS::Cluster": { 332 | "importProperties": [ 333 | "ClusterName" 334 | ] 335 | }, 336 | "AWS::ECS::PrimaryTaskSet": { 337 | "importProperties": [ 338 | "Cluster", 339 | "Service" 340 | ] 341 | }, 342 | "AWS::ECS::Service": { 343 | "importProperties": [ 344 | "ServiceArn", 345 | "Cluster" 346 | ] 347 | }, 348 | "AWS::ECS::TaskDefinition": { 349 | "importProperties": [ 350 | "TaskDefinitionArn" 351 | ] 352 | }, 353 | "AWS::ECS::TaskSet": { 354 | "importProperties": [ 355 | "Cluster", 356 | "Service", 357 | "Id" 358 | ] 359 | }, 360 | "AWS::EFS::AccessPoint": { 361 | "importProperties": [ 362 | "AccessPointId" 363 | ] 364 | }, 365 | "AWS::EFS::FileSystem": { 366 | "importProperties": [ 367 | "FileSystemId" 368 | ] 369 | }, 370 | "AWS::ElasticLoadBalancing::LoadBalancer": { 371 | "importProperties": [ 372 | "LoadBalancerName" 373 | ] 374 | }, 375 | "AWS::ElasticLoadBalancingV2::Listener": { 376 | "importProperties": [ 377 | "ListenerArn" 378 | ] 379 | }, 380 | "AWS::ElasticLoadBalancingV2::ListenerRule": { 381 | "importProperties": [ 382 | "RuleArn" 383 | ] 384 | }, 385 | "AWS::ElasticLoadBalancingV2::LoadBalancer": { 386 | "importProperties": [ 387 | "LoadBalancerArn" 388 | ] 389 | }, 390 | "AWS::EventSchemas::RegistryPolicy": { 391 | "importProperties": [ 392 | "Id" 393 | ] 394 | }, 395 | "AWS::Events::Rule": { 396 | "importProperties": [ 397 | "Name" 398 | ] 399 | }, 400 | "AWS::FMS::NotificationChannel": { 401 | "importProperties": [ 402 | "SnsTopicArn" 403 | ] 404 | }, 405 | "AWS::FMS::Policy": { 406 | "importProperties": [ 407 | "Id" 408 | ] 409 | }, 410 | "AWS::GlobalAccelerator::Accelerator": { 411 | "importProperties": [ 412 | "AcceleratorArn" 413 | ] 414 | }, 415 | "AWS::GlobalAccelerator::EndpointGroup": { 416 | "importProperties": [ 417 | "EndpointGroupArn" 418 | ] 419 | }, 420 | "AWS::GlobalAccelerator::Listener": { 421 | "importProperties": [ 422 | "ListenerArn" 423 | ] 424 | }, 425 | "AWS::ImageBuilder::Component": { 426 | "importProperties": [ 427 | "Arn" 428 | ] 429 | }, 430 | "AWS::ImageBuilder::DistributionConfiguration": { 431 | "importProperties": [ 432 | "Arn" 433 | ] 434 | }, 435 | "AWS::ImageBuilder::Image": { 436 | "importProperties": [ 437 | "Arn" 438 | ] 439 | }, 440 | "AWS::ImageBuilder::ImagePipeline": { 441 | "importProperties": [ 442 | "Arn" 443 | ] 444 | }, 445 | "AWS::ImageBuilder::ImageRecipe": { 446 | "importProperties": [ 447 | "Arn" 448 | ] 449 | }, 450 | "AWS::ImageBuilder::InfrastructureConfiguration": { 451 | "importProperties": [ 452 | "Arn" 453 | ] 454 | }, 455 | "AWS::IoT::ProvisioningTemplate": { 456 | "importProperties": [ 457 | "TemplateName" 458 | ] 459 | }, 460 | "AWS::IoT::Thing": { 461 | "importProperties": [ 462 | "ThingName" 463 | ] 464 | }, 465 | "AWS::KinesisFirehose::DeliveryStream": { 466 | "importProperties": [ 467 | "DeliveryStreamName" 468 | ] 469 | }, 470 | "AWS::Lambda::Alias": { 471 | "importProperties": [ 472 | "AliasArn" 473 | ] 474 | }, 475 | "AWS::Lambda::Function": { 476 | "importProperties": [ 477 | "FunctionName" 478 | ] 479 | }, 480 | "AWS::Lambda::Version": { 481 | "importProperties": [ 482 | "FunctionArn" 483 | ] 484 | }, 485 | "AWS::Logs::LogGroup": { 486 | "importProperties": [ 487 | "LogGroupName" 488 | ] 489 | }, 490 | "AWS::Logs::MetricFilter": { 491 | "importProperties": [ 492 | "FilterName" 493 | ] 494 | }, 495 | "AWS::Logs::SubscriptionFilter": { 496 | "importProperties": [ 497 | "LogGroupName", 498 | "FilterName" 499 | ] 500 | }, 501 | "AWS::Macie::CustomDataIdentifier": { 502 | "importProperties": [ 503 | "Id" 504 | ] 505 | }, 506 | "AWS::Macie::FindingsFilter": { 507 | "importProperties": [ 508 | "Id" 509 | ] 510 | }, 511 | "AWS::Macie::Session": { 512 | "importProperties": [ 513 | "AwsAccountId" 514 | ] 515 | }, 516 | "AWS::NetworkManager::CustomerGatewayAssociation": { 517 | "importProperties": [ 518 | "GlobalNetworkId", 519 | "CustomerGatewayArn" 520 | ] 521 | }, 522 | "AWS::NetworkManager::Device": { 523 | "importProperties": [ 524 | "GlobalNetworkId", 525 | "DeviceId" 526 | ] 527 | }, 528 | "AWS::NetworkManager::GlobalNetwork": { 529 | "importProperties": [ 530 | "Id" 531 | ] 532 | }, 533 | "AWS::NetworkManager::Link": { 534 | "importProperties": [ 535 | "GlobalNetworkId", 536 | "LinkId" 537 | ] 538 | }, 539 | "AWS::NetworkManager::LinkAssociation": { 540 | "importProperties": [ 541 | "GlobalNetworkId", 542 | "DeviceId", 543 | "LinkId" 544 | ] 545 | }, 546 | "AWS::NetworkManager::Site": { 547 | "importProperties": [ 548 | "GlobalNetworkId", 549 | "SiteId" 550 | ] 551 | }, 552 | "AWS::NetworkManager::TransitGatewayRegistration": { 553 | "importProperties": [ 554 | "GlobalNetworkId", 555 | "TransitGatewayArn" 556 | ] 557 | }, 558 | "AWS::QLDB::Stream": { 559 | "importProperties": [ 560 | "LedgerName", 561 | "Id" 562 | ] 563 | }, 564 | "AWS::RDS::DBCluster": { 565 | "importProperties": [ 566 | "DBClusterIdentifier" 567 | ] 568 | }, 569 | "AWS::RDS::DBInstance": { 570 | "importProperties": [ 571 | "DBInstanceIdentifier" 572 | ] 573 | }, 574 | "AWS::RDS::DBProxy": { 575 | "importProperties": [ 576 | "DBProxyName" 577 | ] 578 | }, 579 | "AWS::RDS::DBProxyTargetGroup": { 580 | "importProperties": [ 581 | "TargetGroupArn" 582 | ] 583 | }, 584 | "AWS::ResourceGroups::Group": { 585 | "importProperties": [ 586 | "Name" 587 | ] 588 | }, 589 | "AWS::Route53::HostedZone": { 590 | "importProperties": [ 591 | "HostedZoneId" 592 | ] 593 | }, 594 | "AWS::S3::AccessPoint": { 595 | "importProperties": [ 596 | "Name" 597 | ] 598 | }, 599 | "AWS::S3::Bucket": { 600 | "importProperties": [ 601 | "BucketName" 602 | ] 603 | }, 604 | "AWS::SES::ConfigurationSet": { 605 | "importProperties": [ 606 | "Name" 607 | ] 608 | }, 609 | "AWS::SNS::Topic": { 610 | "importProperties": [ 611 | "TopicArn" 612 | ] 613 | }, 614 | "AWS::SQS::Queue": { 615 | "importProperties": [ 616 | "QueueUrl" 617 | ] 618 | }, 619 | "AWS::SSM::Association": { 620 | "importProperties": [ 621 | "AssociationId" 622 | ] 623 | }, 624 | "AWS::ServiceCatalog::CloudFormationProvisionedProduct": { 625 | "importProperties": [ 626 | "ProvisionedProductId" 627 | ] 628 | }, 629 | "AWS::Synthetics::Canary": { 630 | "importProperties": [ 631 | "Name" 632 | ] 633 | }, 634 | "AWS::WAFv2::IPSet": { 635 | "importProperties": [ 636 | "Name", 637 | "Id", 638 | "Scope" 639 | ] 640 | }, 641 | "AWS::WAFv2::RegexPatternSet": { 642 | "importProperties": [ 643 | "Name", 644 | "Id", 645 | "Scope" 646 | ] 647 | }, 648 | "AWS::WAFv2::RuleGroup": { 649 | "importProperties": [ 650 | "Name", 651 | "Id", 652 | "Scope" 653 | ] 654 | }, 655 | "AWS::WAFv2::WebACL": { 656 | "importProperties": [ 657 | "Name", 658 | "Id", 659 | "Scope" 660 | ] 661 | }, 662 | "AWS::WAFv2::WebACLAssociation": { 663 | "importProperties": [ 664 | "ResourceArn", 665 | "WebACLArn" 666 | ] 667 | }, 668 | "AWS::CloudFormation::Stack": { 669 | "importProperties": [ 670 | "StackId" 671 | ], 672 | "capabilities": [ 673 | "CAPABILITY_NAMED_IAM" 674 | ] 675 | }, 676 | "AWS::IAM::Group": { 677 | "importProperties": [ 678 | "GroupName" 679 | ], 680 | "capabilities": [ 681 | "CAPABILITY_NAMED_IAM" 682 | ] 683 | }, 684 | "AWS::IAM::InstanceProfile": { 685 | "importProperties": [ 686 | "InstanceProfileName" 687 | ], 688 | "capabilities": [ 689 | "CAPABILITY_NAMED_IAM" 690 | ] 691 | }, 692 | "AWS::IAM::Role": { 693 | "importProperties": [ 694 | "RoleName" 695 | ], 696 | "capabilities": [ 697 | "CAPABILITY_NAMED_IAM" 698 | ] 699 | }, 700 | "AWS::IAM::User": { 701 | "importProperties": [ 702 | "UserName" 703 | ], 704 | "capabilities": [ 705 | "CAPABILITY_NAMED_IAM" 706 | ] 707 | }, 708 | "AWS::IAM::ManagedPolicy": { 709 | "importProperties": [ 710 | "PolicyArn" 711 | ], 712 | "capabilities": [ 713 | "CAPABILITY_NAMED_IAM" 714 | ] 715 | } 716 | } 717 | 718 | if len(sys.argv) == 3: 719 | cfnclient = boto3.client('cloudformation', region_name=sys.argv[2]) 720 | elif len(sys.argv) == 2: 721 | cfnclient = boto3.client('cloudformation') 722 | else: 723 | print("Inconsistent arguments") 724 | quit() 725 | 726 | try: 727 | stacks = cfnclient.describe_stacks( 728 | StackName=sys.argv[1] 729 | )['Stacks'] 730 | except: 731 | print("Could not find stack") 732 | quit() 733 | 734 | original_stack_id = stacks[0]['StackId'] 735 | stack_name = stacks[0]['StackName'] 736 | stack_params = [] 737 | if 'Parameters' in stacks[0]: 738 | stack_params = stacks[0]['Parameters'] 739 | 740 | original_template = cfnclient.get_template( 741 | StackName=original_stack_id, 742 | TemplateStage='Processed' 743 | )['TemplateBody'] 744 | 745 | if not isinstance(original_template, str): 746 | original_template = json.dumps(dict(original_template)) # OrderedDict 747 | 748 | print("Found stack, detecting drift...") 749 | 750 | stack_drift_detection_id = cfnclient.detect_stack_drift( 751 | StackName=original_stack_id 752 | )['StackDriftDetectionId'] 753 | 754 | stack_drift_detection_status = cfnclient.describe_stack_drift_detection_status( # no waiter :( 755 | StackDriftDetectionId=stack_drift_detection_id 756 | ) 757 | while stack_drift_detection_status['DetectionStatus'] == "DETECTION_IN_PROGRESS": 758 | time.sleep(5) 759 | stack_drift_detection_status = cfnclient.describe_stack_drift_detection_status( 760 | StackDriftDetectionId=stack_drift_detection_id 761 | ) 762 | 763 | if stack_drift_detection_status['DetectionStatus'] != "DETECTION_COMPLETE" or stack_drift_detection_status['StackDriftStatus'] != "DRIFTED": 764 | if stack_drift_detection_status['StackDriftStatus'] == "IN_SYNC": 765 | print("Could not find any drifted resources") 766 | else: 767 | print("Could not determine drift results") 768 | quit() 769 | 770 | resource_drifts = [] 771 | resource_drifts_result = cfnclient.describe_stack_resource_drifts( 772 | StackName=original_stack_id, 773 | StackResourceDriftStatusFilters=[ 774 | 'MODIFIED', 775 | 'DELETED' 776 | ], 777 | MaxResults=100 778 | ) 779 | resource_drifts += resource_drifts_result['StackResourceDrifts'] 780 | while 'NextToken' in resource_drifts_result: 781 | resource_drifts_result = cfnclient.describe_stack_resource_drifts( 782 | StackName=original_stack_id, 783 | StackResourceDriftStatusFilters=[ 784 | 'MODIFIED', 785 | 'DELETED' 786 | ], 787 | NextToken=resource_drifts_result['NextToken'], 788 | MaxResults=100 789 | ) 790 | resource_drifts += resource_drifts_result['StackResourceDrifts'] 791 | 792 | for i in range(len(resource_drifts)): # filter non-importable resources 793 | if resource_drifts[i]['ResourceType'] not in eligible_import_resources.keys(): 794 | del resource_drifts[i] 795 | 796 | template = json.loads(to_json(original_template)) 797 | 798 | for k, v in template['Resources'].items(): 799 | template['Resources'][k]['DeletionPolicy'] = 'Retain' 800 | 801 | print("Drift detected, setting resource retention...") 802 | 803 | cfnclient.update_stack( 804 | StackName=original_stack_id, 805 | TemplateBody=json.dumps(template), 806 | Capabilities=[ 807 | 'CAPABILITY_NAMED_IAM', 808 | 'CAPABILITY_AUTO_EXPAND' 809 | ], 810 | Parameters=stack_params 811 | ) 812 | 813 | waiter = cfnclient.get_waiter('stack_update_complete') 814 | waiter.wait( 815 | StackName=original_stack_id, 816 | WaiterConfig={ 817 | 'Delay': 10, 818 | 'MaxAttempts': 360 819 | } 820 | ) 821 | 822 | # De-ref 823 | match_resources = [] 824 | for drifted_resource in resource_drifts: 825 | match_resources.append(drifted_resource['LogicalResourceId']) 826 | resolvePropertyValue(template, match_resources, False) 827 | 828 | if len(resolve_matches) > 0: 829 | print("Temporarily dereferencing removed resources...") 830 | 831 | if not 'Outputs' in template: 832 | template['Outputs'] = {} 833 | 834 | for k, v in resolve_matches.items(): 835 | template['Outputs']['Drift' + str(k)] = { 836 | 'Value': v 837 | } 838 | 839 | cfnclient.update_stack( 840 | StackName=original_stack_id, 841 | TemplateBody=json.dumps(template), 842 | Capabilities=[ 843 | 'CAPABILITY_NAMED_IAM', 844 | 'CAPABILITY_AUTO_EXPAND' 845 | ], 846 | Parameters=stack_params 847 | ) 848 | 849 | waiter = cfnclient.get_waiter('stack_update_complete') 850 | waiter.wait( 851 | StackName=original_stack_id, 852 | WaiterConfig={ 853 | 'Delay': 10, 854 | 'MaxAttempts': 360 855 | } 856 | ) 857 | 858 | stack_outputs = cfnclient.describe_stacks( 859 | StackName=original_stack_id 860 | )['Stacks'][0]['Outputs'] 861 | 862 | for k, v in resolve_matches.items(): 863 | for output in stack_outputs: 864 | if output['OutputKey'] == 'Drift' + str(k): 865 | resolve_matches[k] = output['OutputValue'] 866 | 867 | template = resolvePropertyValue(template, match_resources, True) 868 | 869 | print("Removing drifted resources (whilst retaining resources!)...") 870 | 871 | for drifted_resource in resource_drifts: 872 | del template['Resources'][drifted_resource['LogicalResourceId']] 873 | 874 | if len(template['Resources']) == 0: # last ditch effort to retain stack 875 | template = empty_template 876 | 877 | cfnclient.update_stack( 878 | StackName=original_stack_id, 879 | TemplateBody=json.dumps(template), 880 | Capabilities=[ 881 | 'CAPABILITY_NAMED_IAM', 882 | 'CAPABILITY_AUTO_EXPAND' 883 | ] 884 | ) 885 | 886 | waiter = cfnclient.get_waiter('stack_update_complete') 887 | waiter.wait( 888 | StackName=original_stack_id, 889 | WaiterConfig={ 890 | 'Delay': 10, 891 | 'MaxAttempts': 360 892 | } 893 | ) 894 | 895 | import_resources = [] 896 | for drifted_resource in resource_drifts: 897 | resource_identifier = {} 898 | 899 | import_properties = eligible_import_resources[drifted_resource['ResourceType']]['importProperties'].copy() 900 | if 'PhysicalResourceIdContext' in drifted_resource: 901 | for prop in drifted_resource['PhysicalResourceIdContext']: 902 | if prop['Key'] in import_properties: 903 | resource_identifier[prop['Key']] = prop['Value'] 904 | import_properties.remove(prop['Key']) 905 | 906 | if len(import_properties) > 1: 907 | print("ERROR: Unexpected additional importable keys required, aborting...") 908 | quit() 909 | elif len(import_properties) == 1: 910 | resource_identifier[import_properties[0]] = drifted_resource['PhysicalResourceId'] 911 | 912 | template['Resources'][drifted_resource['LogicalResourceId']] = { 913 | 'DeletionPolicy': 'Retain', 914 | 'Type': drifted_resource['ResourceType'], 915 | 'Properties': json.loads(drifted_resource['ActualProperties']) 916 | } 917 | 918 | import_resources.append({ 919 | 'ResourceType': drifted_resource['ResourceType'], 920 | 'LogicalResourceId': drifted_resource['LogicalResourceId'], 921 | 'ResourceIdentifier': resource_identifier 922 | }) 923 | 924 | print("Recreating drifted resources with current state...") 925 | 926 | change_set_name = 'Drift-Remediation-' + str(int(time.time())) 927 | new_stack_id = cfnclient.create_change_set( 928 | StackName=stack_name, 929 | ChangeSetName=change_set_name, 930 | TemplateBody=json.dumps(template), 931 | ChangeSetType='IMPORT', 932 | Capabilities=[ 933 | 'CAPABILITY_NAMED_IAM', 934 | 'CAPABILITY_AUTO_EXPAND' 935 | ], 936 | ResourcesToImport=import_resources 937 | )['StackId'] 938 | 939 | waiter = cfnclient.get_waiter('change_set_create_complete') 940 | waiter.wait( 941 | StackName=new_stack_id, 942 | ChangeSetName=change_set_name, 943 | WaiterConfig={ 944 | 'Delay': 10, 945 | 'MaxAttempts': 360 946 | } 947 | ) 948 | 949 | cfnclient.execute_change_set( 950 | ChangeSetName=change_set_name, 951 | StackName=new_stack_id 952 | ) 953 | 954 | waiter = cfnclient.get_waiter('stack_import_complete') 955 | waiter.wait( 956 | StackName=new_stack_id, 957 | WaiterConfig={ 958 | 'Delay': 10, 959 | 'MaxAttempts': 360 960 | } 961 | ) 962 | 963 | print("Remediating drift...") 964 | 965 | cfnclient.update_stack( 966 | StackName=stack_name, 967 | TemplateBody=original_template, 968 | Capabilities=[ 969 | 'CAPABILITY_NAMED_IAM', 970 | 'CAPABILITY_AUTO_EXPAND' 971 | ], 972 | Parameters=stack_params 973 | ) 974 | 975 | waiter = cfnclient.get_waiter('stack_update_complete') 976 | waiter.wait( 977 | StackName=new_stack_id, 978 | WaiterConfig={ 979 | 'Delay': 10, 980 | 'MaxAttempts': 360 981 | } 982 | ) 983 | 984 | print("Succcessfully remediated drift") 985 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | cfn-flip 3 | -------------------------------------------------------------------------------- /test.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: Contains IAM Role and policies to support drift remediation demo 3 | Parameters: 4 | RoleName: 5 | Type: String 6 | Resources: 7 | AutomaticDriftRemediationRole: 8 | Type: AWS::IAM::Role 9 | Properties: 10 | RoleName: !Ref RoleName 11 | Path: / 12 | AssumeRolePolicyDocument: 13 | Version: 2012-10-17 14 | Statement: 15 | - Effect: Allow 16 | Principal: 17 | Service: 18 | - ec2.amazonaws.com 19 | Action: 20 | - 'sts:AssumeRole' 21 | ManagedPolicyArns: 22 | - arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess 23 | - arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess 24 | Policies: 25 | - PolicyDocument: 26 | Statement: 27 | - Action: 28 | - s3:Get* 29 | - s3:List* 30 | Effect: Allow 31 | Resource: '*' 32 | - Action: 33 | - glacier:DescribeJob 34 | - glacier:DescribeVault 35 | - glacier:GetDataRetrievalPolicy 36 | - glacier:GetJobOutput 37 | - glacier:GetVaultAccessPolicy 38 | - glacier:GetVaultLock 39 | - glacier:GetVaultNotifications 40 | - glacier:ListJobs 41 | - glacier:ListMultipartUploads 42 | - glacier:ListParts 43 | - glacier:ListTagsForVault 44 | - glacier:ListVaults 45 | Effect: Allow 46 | Resource: '*' 47 | PolicyName: 'AutomaticDriftRemediationPolicyOne' 48 | - PolicyDocument: 49 | Statement: 50 | - Action: 51 | - ecs:ListClusters 52 | - ecs:DescribeContainerInstances 53 | Effect: Allow 54 | Resource: 55 | - arn:aws:ecs:us-east-1:123456789012:service/exampleClusterOne* 56 | - arn:aws:ecs:us-east-1:123456789012:service/exampleClusterTwo* 57 | PolicyName: 'AutomaticDriftRemediationPolicyTwo' 58 | --------------------------------------------------------------------------------