Repository: cdklabs/cdk-enterprise-iac (language: python)

Issue:
feat: Allow extraction of resources to separate stack
### Description

Would like the ability to provide a list of [Cloudformation resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) and have those automatically extracted into a separate stack to provide to another team with sufficient IAM permissions to deploy, and update the remaining resources to properly reference them without using CFN intrinsic functions that expect the reference to be in the same template

### Use Case

In many enterprises, there are separate teams with different IAM permissions than developers deploying CDK applications.

For example there might be a networking team with permissions to deploy [AWS::EC2::SecurityGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html) and [AWS::EC2::EIP](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-eip.html), or a security team with permissions to deploy [AWS::IAM::Role](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html) and [AWS::IAM::Policy](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html), but the developers deploying the CDK _don't_ have those permissions

When a developer doesn't have permissions to deploy necessary resources in their CDK application, writing good code becomes difficult to manage when a `cdk deploy` will quickly error due to not being able to deploy something like an [AWS::IAM::Role](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html) which is foundational to any project deployed into AWS

### Proposed Solution

An Aspect that takes a list if Cloudformation resources and pulls those resources out of the CDK application into a separate stack, then updates any references to those extracted resources to use an [Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) or similar method for sharing

### Other information

_No response_

### Acknowledge

- [X] I may be able to implement this feature request
- [ ] This feature might incur a breaking change


Discussion / hints:

Fix patch:
diff --git a/.projen/deps.json b/.projen/deps.json
index 6e22784..76fcd69 100644
--- a/.projen/deps.json
+++ b/.projen/deps.json
@@ -126,6 +126,10 @@
       "name": "typescript",
       "type": "build"
     },
+    {
+      "name": "aws-sdk",
+      "type": "bundled"
+    },
     {
       "name": "@types/prettier",
       "version": "2.6.0",
@@ -150,6 +154,11 @@
       "name": "constructs",
       "version": "^10.0.5",
       "type": "peer"
+    },
+    {
+      "name": "aws-sdk",
+      "version": "^2.1230.0",
+      "type": "runtime"
     }
   ],
   "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"."
diff --git a/.projenrc.js b/.projenrc.js
index f16a03d..19ca633 100644
--- a/.projenrc.js
+++ b/.projenrc.js
@@ -15,6 +15,8 @@ const project = new awscdk.AwsCdkConstructLibrary({
     '@aws-cdk/integ-tests-alpha@2.41.0-alpha.0',
     '@aws-cdk/integ-runner@^2',
   ],
+  deps: ['aws-sdk@^2.1230.0'],
+  bundledDeps: ['aws-sdk'],
   gitignore: [
     '.vscode/',
     '*.d.ts',
diff --git a/API.md b/API.md
index dda04c0..dd49db8 100644
--- a/API.md
+++ b/API.md
@@ -865,6 +865,88 @@ Name of the tag property to remove from the resource.
 
 ---
 
+### ResourceExtractorProps <a name="ResourceExtractorProps" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps"></a>
+
+#### Initializer <a name="Initializer" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.Initializer"></a>
+
+```typescript
+import { ResourceExtractorProps } from '@cdklabs/cdk-enterprise-iac'
+
+const resourceExtractorProps: ResourceExtractorProps = { ... }
+```
+
+#### Properties <a name="Properties" id="Properties"></a>
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.extractDestinationStack">extractDestinationStack</a></code> | <code>aws-cdk-lib.Stack</code> | Stack to move found extracted resources into. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.resourceTypesToExtract">resourceTypesToExtract</a></code> | <code>string[]</code> | List of resource types to extract, ex: `AWS::IAM::Role`. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.stackArtifacts">stackArtifacts</a></code> | <code>aws-cdk-lib.cx_api.CloudFormationStackArtifact[]</code> | Synthed stack artifacts from your CDK app. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.additionalTransforms">additionalTransforms</a></code> | <code>{[ key: string ]: string}</code> | Additional resource transformations. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.valueShareMethod">valueShareMethod</a></code> | <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod">ResourceExtractorShareMethod</a></code> | The sharing method to use when passing exported resources from the "Extracted Stack" into the original stack(s). |
+
+---
+
+##### `extractDestinationStack`<sup>Required</sup> <a name="extractDestinationStack" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.extractDestinationStack"></a>
+
+```typescript
+public readonly extractDestinationStack: Stack;
+```
+
+- *Type:* aws-cdk-lib.Stack
+
+Stack to move found extracted resources into.
+
+---
+
+##### `resourceTypesToExtract`<sup>Required</sup> <a name="resourceTypesToExtract" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.resourceTypesToExtract"></a>
+
+```typescript
+public readonly resourceTypesToExtract: string[];
+```
+
+- *Type:* string[]
+
+List of resource types to extract, ex: `AWS::IAM::Role`.
+
+---
+
+##### `stackArtifacts`<sup>Required</sup> <a name="stackArtifacts" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.stackArtifacts"></a>
+
+```typescript
+public readonly stackArtifacts: CloudFormationStackArtifact[];
+```
+
+- *Type:* aws-cdk-lib.cx_api.CloudFormationStackArtifact[]
+
+Synthed stack artifacts from your CDK app.
+
+---
+
+##### `additionalTransforms`<sup>Optional</sup> <a name="additionalTransforms" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.additionalTransforms"></a>
+
+```typescript
+public readonly additionalTransforms: {[ key: string ]: string};
+```
+
+- *Type:* {[ key: string ]: string}
+
+Additional resource transformations.
+
+---
+
+##### `valueShareMethod`<sup>Optional</sup> <a name="valueShareMethod" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.valueShareMethod"></a>
+
+```typescript
+public readonly valueShareMethod: ResourceExtractorShareMethod;
+```
+
+- *Type:* <a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod">ResourceExtractorShareMethod</a>
+
+The sharing method to use when passing exported resources from the "Extracted Stack" into the original stack(s).
+
+---
+
 ### SetApiGatewayEndpointConfigurationProps <a name="SetApiGatewayEndpointConfigurationProps" id="@cdklabs/cdk-enterprise-iac.SetApiGatewayEndpointConfigurationProps"></a>
 
 #### Initializer <a name="Initializer" id="@cdklabs/cdk-enterprise-iac.SetApiGatewayEndpointConfigurationProps.Initializer"></a>
@@ -1390,6 +1472,79 @@ All aspects can visit an IConstruct.
 
 
 
+### ResourceExtractor <a name="ResourceExtractor" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor"></a>
+
+- *Implements:* aws-cdk-lib.IAspect
+
+This Aspect takes a CDK application, all synthesized CloudFormationStackArtifact, a value share method, and a list of Cloudformation resources that should be pulled out of the main CDK application, which should be synthesized to a cloudformation template that an external team (e.g. security team) to deploy, and adjusting the CDK application to reference pre-created resources already pulled out.
+
+*Example*
+
+```typescript
+ const app = App()
+ const stack = new Stack(app, 'MyStack');
+ extractedStack = new Stack(app, 'ExtractedStack');
+ const synthedApp = app.synth();
+ Aspects.of(app).add(new ResourceExtractor({
+   extractDestinationStack: extractedStack,
+   stackArtifacts: synthedApp.stacks,
+   valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+   resourceTypesToExtract: [
+     'AWS::IAM::Role',
+     'AWS::IAM::Policy',
+     'AWS::IAM::ManagedPolicy',
+     'AWS::IAM::InstanceProfile',
+   ],
+ });
+ app.synth({ force: true });
+```
+
+
+#### Initializers <a name="Initializers" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.Initializer"></a>
+
+```typescript
+import { ResourceExtractor } from '@cdklabs/cdk-enterprise-iac'
+
+new ResourceExtractor(props: ResourceExtractorProps)
+```
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractor.Initializer.parameter.props">props</a></code> | <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps">ResourceExtractorProps</a></code> | *No description.* |
+
+---
+
+##### `props`<sup>Required</sup> <a name="props" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.Initializer.parameter.props"></a>
+
+- *Type:* <a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps">ResourceExtractorProps</a>
+
+---
+
+#### Methods <a name="Methods" id="Methods"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractor.visit">visit</a></code> | Entrypoint. |
+
+---
+
+##### `visit` <a name="visit" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.visit"></a>
+
+```typescript
+public visit(node: IConstruct): void
+```
+
+Entrypoint.
+
+###### `node`<sup>Required</sup> <a name="node" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.visit.parameter.node"></a>
+
+- *Type:* constructs.IConstruct
+
+---
+
+
+
+
 ### SetApiGatewayEndpointConfiguration <a name="SetApiGatewayEndpointConfiguration" id="@cdklabs/cdk-enterprise-iac.SetApiGatewayEndpointConfiguration"></a>
 
 - *Implements:* aws-cdk-lib.IAspect
@@ -1473,3 +1628,53 @@ Whether an http-proxy or https-proxy.
 
 ---
 
+
+### ResourceExtractorShareMethod <a name="ResourceExtractorShareMethod" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod"></a>
+
+The available value sharing methods to pass values from the extracted stack onto the original stack(s).
+
+#### Members <a name="Members" id="Members"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.CFN_OUTPUT">CFN_OUTPUT</a></code> | *No description.* |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.SSM_PARAMETER">SSM_PARAMETER</a></code> | *No description.* |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.API_LOOKUP">API_LOOKUP</a></code> | *No description.* |
+
+---
+
+##### `CFN_OUTPUT` <a name="CFN_OUTPUT" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.CFN_OUTPUT"></a>
+
+---
+
+
+##### `SSM_PARAMETER` <a name="SSM_PARAMETER" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.SSM_PARAMETER"></a>
+
+---
+
+
+##### `API_LOOKUP` <a name="API_LOOKUP" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.API_LOOKUP"></a>
+
+---
+
+
+### ResourceTransform <a name="ResourceTransform" id="@cdklabs/cdk-enterprise-iac.ResourceTransform"></a>
+
+#### Members <a name="Members" id="Members"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceTransform.STACK_NAME">STACK_NAME</a></code> | *No description.* |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceTransform.LOGICAL_ID">LOGICAL_ID</a></code> | *No description.* |
+
+---
+
+##### `STACK_NAME` <a name="STACK_NAME" id="@cdklabs/cdk-enterprise-iac.ResourceTransform.STACK_NAME"></a>
+
+---
+
+
+##### `LOGICAL_ID` <a name="LOGICAL_ID" id="@cdklabs/cdk-enterprise-iac.ResourceTransform.LOGICAL_ID"></a>
+
+---
+
diff --git a/README.md b/README.md
index 7ad92f7..fa9f914 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,273 @@ pip install cdklabs.cdk-enterprise-iac
 
 ## Usage
 
+There are many tools available, all detailed in [`API.md`](./API.md).
+
+A few examples of these tools below
+
+### Resource extraction
+
+:warning: Resource extraction is in an experimental phase. Test and validate before using in production. Please open any issues found [here](https://github.com/cdklabs/cdk-enterprise-iac/)issues
+
+In many enterprises, there are separate teams with different IAM permissions than developers deploying CDK applications.
+
+For example there might be a networking team with permissions to deploy `AWS::EC2::SecurityGroup` and `AWS::EC2::EIP`, or a security team with permissions to deploy `AWS::IAM::Role` and `AWS::IAM::Policy`, but the developers deploying the CDK don't have those permissions
+
+When a developer doesn't have permissions to deploy necessary resources in their CDK application, writing good code becomes difficult to manage when a cdk deploy will quickly error due to not being able to deploy something like an `AWS::IAM::Role` which is foundational to any project deployed into AWS.
+
+Using the `ResourceExtractor` Aspect, developers can write their CDK code as though they had sufficient IAM permissions, but extract those resources into a separate stack for an external team to deploy on their behalf.
+
+Take the following example stack
+
+```ts
+import { App, Aspects, RemovalPolicy, Stack } from 'aws-cdk-lib';
+import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
+import { Bucket } from 'aws-cdk-lib/aws-s3';
+
+const app = new App();
+const appStack = new Stack(app, 'MyAppStack');
+
+const func = new Function(appStack, 'TestLambda', {
+  code: Code.fromAsset(path.join(__dirname, 'lambda-handler')),
+  handler: 'index.handler',
+  runtime: Runtime.PYTHON_3_9,
+});
+const bucket = new Bucket(appStack, 'TestBucket', {
+  autoDeleteObjects: true,
+  removalPolicy: RemovalPolicy.DESTROY,
+});
+bucket.grantReadWrite(func);
+
+app.synth()
+```
+
+The synthesized Cloudformation would include _all_ AWS resources required, including resources a developer might not have permissions to deploy
+
+The above example would include the following snippet in the synthesized Cloudformation
+
+```yaml
+TestLambdaServiceRoleC28C2D9C:
+  Type: 'AWS::IAM::Role'
+  Properties:
+    AssumeRolePolicyDocument:
+      Statement:
+        - Action: 'sts:AssumeRole'
+          Effect: Allow
+          Principal:
+            Service: lambda.amazonaws.com
+      Version: 2012-10-17
+    # excluding remaining properties
+  TestLambda2F70C45E:
+    Type: 'AWS::Lambda::Function'
+    Properties:
+      Role: !GetAtt
+        - TestLambdaServiceRoleC28C2D9C
+        - Arn
+      # excluding remaining properties
+```
+
+While including `bucket.grantReadWrite(func)` in the CDK application ensures an IAM role with least privilege IAM policies for the application, the creation of IAM resources such as Roles and Policies may be restricted to a security team, resulting in the synthesized Cloudformation template not being deployable by a developer.
+
+Using the `ResourceExtractor`, we can pull out an arbitrary list of Cloudformation resources that a developer _doesn't_ have permissions to provision, and create a separate stack that can be sent to a security team.
+
+```ts
+import { App, Aspects, RemovalPolicy, Stack } from 'aws-cdk-lib';
+import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
+import { Bucket } from 'aws-cdk-lib/aws-s3';
+// Import ResourceExtractor
+import { ResourceExtractor } from '@cdklabs/cdk-enterprise-iac';
+
+const app = new App();
+const appStack = new Stack(app, 'MyAppStack');
+// Set up a destination stack to extract resources to
+const extractedStack = new Stack(app, 'ExtractedStack');
+
+const func = new Function(appStack, 'TestLambda', {
+  code: Code.fromAsset(path.join(__dirname, 'lambda-handler')),
+  handler: 'index.handler',
+  runtime: Runtime.PYTHON_3_9,
+});
+const bucket = new Bucket(appStack, 'TestBucket', {
+  autoDeleteObjects: true,
+  removalPolicy: RemovalPolicy.DESTROY,
+});
+bucket.grantReadWrite(func);
+
+// Capture the output of app.synth()
+const synthedApp = app.synth();
+// Apply the ResourceExtractor Aspect
+Aspects.of(app).add(
+  new ResourceExtractor({
+    // synthesized stacks to examine
+    stackArtifacts: synthedApp.stacks,
+    // Array of Cloudformation resources to extract
+    resourceTypesToExtract: [
+      'AWS::IAM::Role',
+      'AWS::IAM::Policy',
+      'AWS::IAM::ManagedPolicy',
+      'AWS::IAM::InstanceProfile',
+    ],
+    // Destination stack for extracted resources
+    extractDestinationStack: extractedStack,
+  })
+);
+// Resynthing since ResourceExtractor has modified the app
+app.synth({ force: true });
+```
+
+In the example above, _all_ resources are created in the `appStack`, and an empty `extractedStack` is also created.
+
+We apply the `ResourceExtractor` Aspect, specifying the Cloudformation resource types the developer is unable to deploy due to insufficient IAM permissions.
+
+Now when we list stacks in the CDK project, we can see an added stack
+
+```zsh
+$ cdk ls
+MyAppStack
+ExtractedStack
+```
+
+Taking a look at these synthesized stacks, in the `ExtractedStack` we'll find:
+
+```yaml
+Resources:
+  TestLambdaServiceRoleC28C2D9C:
+    Type: 'AWS::IAM::Role'
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action: 'sts:AssumeRole'
+            Effect: Allow
+            Principal:
+              Service: lambda.amazonaws.com
+        Version: 2012-10-17
+      # excluding remaining properties
+Outputs:
+  ExportAppStackTestLambdaServiceRoleC28C2D9C:
+    Value:
+      'Fn::GetAtt':
+        - TestLambdaServiceRoleC28C2D9C
+        - Arn
+    Export:
+      Name: 'AppStack:TestLambdaServiceRoleC28C2D9C'  # Exported name
+```
+
+And inside the synthesized `MyAppStack` template:
+
+```yaml
+Resources:
+  TestLambda2F70C45E:
+    Type: 'AWS::Lambda::Function'
+    Properties:
+      Role: !ImportValue 'AppStack:TestLambdaServiceRoleC28C2D9C'  # Using ImportValue instrinsic function to use pre-created IAM role
+      # excluding remaining properties
+```
+
+In this scenario, a developer is able to provide an external security team with sufficient IAM privileges to deploy the `ExtractedStack`.
+
+Once deployed, a developer can run `cdk deploy MyAppStack` without errors due to insufficient IAM privileges
+
+
+#### Value Sharing methods
+
+When resources are extracted from a stack, there must be a method to reference the resources that have been extracted.
+
+There are three methods (see `ResourceExtractorShareMethod` enum)
+
+- `CFN_OUTPUT`
+- `SSM_PARAMETER`
+- `API_LOOKUP`
+
+##### `CFN_OUTPUT`
+
+The default sharing method is `CFN_OUTPUT`, which uses Cloudformation Export/Import to Export values in the extracted stack (see [Outputs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/outputs-section-structure.html)), and use the [Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html) intrinsic function to reference those values.
+
+This works fine, but some teams may prefer a looser coupling between the extracted stack deployed by an external team and the rest of the CDK infrastructure.
+
+##### `SSM_PARAMETER`
+
+In this method, the extracted stack generates Parameters in AWS Systems Manager Parameter Store, and modifies the CDK application to look up the generated parameter using [`aws_ssm.StringParameter.valueFromLookup()`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ssm.StringParameter.html#static-valuewbrfromwbrlookupscope-parametername) at synthesis time.
+
+Example on using this method:
+
+```ts
+import { ResourceExtractor, ResourceExtractorShareMethod } from '@cdklabs/cdk-enterprise-iac';
+
+Aspects.of(app).add(
+  new ResourceExtractor({
+    stackArtifacts: synthedApp.stacks,
+    resourceTypesToExtract: [
+      'AWS::IAM::Role',
+      'AWS::IAM::Policy',
+      'AWS::IAM::ManagedPolicy',
+      'AWS::IAM::InstanceProfile',
+    ],
+    extractDestinationStack: extractedStack,
+    valueShareMethod: ResourceExtractorShareMethod.SSM_PARAMETER,  // Specify SSM_PARAMETER Method
+  });
+);
+```
+
+##### `API_LOOKUP`
+
+The `API_LOOKUP` sharing method is similar to `CFN_OUTPUT`, but rather than modifying the CDK application to use [Fn::ImportValue](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-importvalue.html), the `Outputs` section of the Extracted stack is looked up using the nodejs SDK, and modifies the CDK application to use the resolved value of the Output
+
+#### Resource Partials
+
+Some resources that get extracted might reference resources that aren't yet created.
+
+In our example CDK application we include the line
+
+```ts
+bucket.grantReadWrite(func);
+```
+
+This creates an `AWS::IAM::Policy` that includes the necessary Actions scoped down to the S3 bucket.
+
+When the `AWS::IAM::Policy` is extracted, it's unable to use `Ref` or `Fn::GetAtt` to reference the S3 bucket since the S3 bucket wasn't extracted.
+
+In this case we substitute the reference with a "partial ARN" that makes a best effort to scope the resources in the IAM policy statement to the ARN of the yet-to-be created S3 bucket.
+
+There are multiple resource types supported out of the box (found in [`createDefaultTransforms`](src/patches/resource-extractor/resourceTransformer.ts)). In the event you have a resource not yet supported, you'll receive a `MissingTransformError`. In this case you can either open an [issue](https://github.com/cdklabs/cdk-enterprise-iac/issues) with the resource in question, or you can include the `additionalTransforms` property.
+
+Consider the following:
+
+```ts
+const vpc = new Vpc(stack, 'TestVpc');
+const db = new DatabaseInstance(stack, 'TestDb', {
+  vpc,
+  engine: DatabaseInstanceEngine.POSTGRES,
+})
+const func = new Function(stack, 'TestLambda', {
+  code: Code.fromAsset(path.join(__dirname, 'lambda-handler')),
+  handler: 'index.handler',
+  runtime: Runtime.PYTHON_3_9,
+});
+db.secret?.grantRead(func)
+
+const synthedApp = app.synth();
+Aspects.of(app).add(
+  new ResourceExtractor({
+    extractDestinationStack: extractedStack,
+    stackArtifacts: synthedApp.stacks,
+    valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+    resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+    additionalTransforms: {
+      'AWS::SecretsManager::SecretTargetAttachment': `arn:${Aws.PARTITION}:secretsmanager:${Aws.REGION}:${Aws.ACCOUNT_ID}:secret:some-expected-value*`,
+    },
+  });
+);
+app.synth({ force: true });
+```
+
+In this case, there is a `AWS::SecretsManager::SecretTargetAttachment` generated to complete the final link between a Secrets Manager secret and the associated database by adding the database connection information to the secret JSON, which returns the ARN of the generated secret.
+
+In the context of extracting the IAM policy, we want to tell the `ResourceExtractor` how to handle the resource section of the IAM policy statement so that it is scoped down sufficiently.
+
+In this case rather than using a `Ref: LogicalIdForTheSecretTargetAttachment` we construct the ARN we want to use.
+
+### Adding permissions boundaries to all generated IAM roles
+
 Example for `AddPermissionBoundary` in Typescript project.
 
 ```ts
@@ -534,7 +801,7 @@ public readonly proxyCredentials: ISecret;
 
 JSON secret containing `user` and `password` properties to use if your proxy requires credentials `http://user:password@host:port` could contain sensitive data, so using a Secret.
 
-Note that while the `user` and `password` won't be visible in the cloudformation tempalte
+Note that while the `user` and `password` won't be visible in the cloudformation template
 they **will** be in plain text inside your `UserData`
 
 ---
@@ -570,6 +837,7 @@ Proxy Type.
 ProxyType.HTTPS
 ```
 
+
 ### AddPermissionBoundaryProps <a name="AddPermissionBoundaryProps" id="@cdklabs/cdk-enterprise-iac.AddPermissionBoundaryProps"></a>
 
 Properties to pass to the AddPermissionBoundary.
@@ -941,6 +1209,88 @@ Name of the tag property to remove from the resource.
 
 ---
 
+### ResourceExtractorProps <a name="ResourceExtractorProps" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps"></a>
+
+#### Initializer <a name="Initializer" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.Initializer"></a>
+
+```typescript
+import { ResourceExtractorProps } from '@cdklabs/cdk-enterprise-iac'
+
+const resourceExtractorProps: ResourceExtractorProps = { ... }
+```
+
+#### Properties <a name="Properties" id="Properties"></a>
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.extractDestinationStack">extractDestinationStack</a></code> | <code>aws-cdk-lib.Stack</code> | Stack to move found extracted resources into. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.resourceTypesToExtract">resourceTypesToExtract</a></code> | <code>string[]</code> | List of resource types to extract, ex: `AWS::IAM::Role`. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.stackArtifacts">stackArtifacts</a></code> | <code>aws-cdk-lib.cx_api.CloudFormationStackArtifact[]</code> | Synthed stack artifacts from your CDK app. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.additionalTransforms">additionalTransforms</a></code> | <code>{[ key: string ]: string}</code> | Additional resource transformations. |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.valueShareMethod">valueShareMethod</a></code> | <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod">ResourceExtractorShareMethod</a></code> | The sharing method to use when passing exported resources from the "Extracted Stack" into the original stack(s). |
+
+---
+
+##### `extractDestinationStack`<sup>Required</sup> <a name="extractDestinationStack" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.extractDestinationStack"></a>
+
+```typescript
+public readonly extractDestinationStack: Stack;
+```
+
+- *Type:* aws-cdk-lib.Stack
+
+Stack to move found extracted resources into.
+
+---
+
+##### `resourceTypesToExtract`<sup>Required</sup> <a name="resourceTypesToExtract" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.resourceTypesToExtract"></a>
+
+```typescript
+public readonly resourceTypesToExtract: string[];
+```
+
+- *Type:* string[]
+
+List of resource types to extract, ex: `AWS::IAM::Role`.
+
+---
+
+##### `stackArtifacts`<sup>Required</sup> <a name="stackArtifacts" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.stackArtifacts"></a>
+
+```typescript
+public readonly stackArtifacts: CloudFormationStackArtifact[];
+```
+
+- *Type:* aws-cdk-lib.cx_api.CloudFormationStackArtifact[]
+
+Synthed stack artifacts from your CDK app.
+
+---
+
+##### `additionalTransforms`<sup>Optional</sup> <a name="additionalTransforms" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.additionalTransforms"></a>
+
+```typescript
+public readonly additionalTransforms: {[ key: string ]: string};
+```
+
+- *Type:* {[ key: string ]: string}
+
+Additional resource transformations.
+
+---
+
+##### `valueShareMethod`<sup>Optional</sup> <a name="valueShareMethod" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorProps.property.valueShareMethod"></a>
+
+```typescript
+public readonly valueShareMethod: ResourceExtractorShareMethod;
+```
+
+- *Type:* <a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod">ResourceExtractorShareMethod</a>
+
+The sharing method to use when passing exported resources from the "Extracted Stack" into the original stack(s).
+
+---
+
 ### SetApiGatewayEndpointConfigurationProps <a name="SetApiGatewayEndpointConfigurationProps" id="@cdklabs/cdk-enterprise-iac.SetApiGatewayEndpointConfigurationProps"></a>
 
 #### Initializer <a name="Initializer" id="@cdklabs/cdk-enterprise-iac.SetApiGatewayEndpointConfigurationProps.Initializer"></a>
@@ -1156,7 +1506,6 @@ SubnetType | `aws-cdk:subnet-type` tag value
 
 ## Classes <a name="Classes" id="Classes"></a>
 
-
 ### AddCfnInitProxy <a name="AddCfnInitProxy" id="@cdklabs/cdk-enterprise-iac.AddCfnInitProxy"></a>
 
 - *Implements:* aws-cdk-lib.IAspect
@@ -1205,6 +1554,9 @@ All aspects can visit an IConstruct.
 
 ---
 
+
+
+
 ### AddLambdaEnvironmentVariables <a name="AddLambdaEnvironmentVariables" id="@cdklabs/cdk-enterprise-iac.AddLambdaEnvironmentVariables"></a>
 
 - *Implements:* aws-cdk-lib.IAspect
@@ -1464,6 +1816,79 @@ All aspects can visit an IConstruct.
 
 
 
+### ResourceExtractor <a name="ResourceExtractor" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor"></a>
+
+- *Implements:* aws-cdk-lib.IAspect
+
+This Aspect takes a CDK application, all synthesized CloudFormationStackArtifact, a value share method, and a list of Cloudformation resources that should be pulled out of the main CDK application, which should be synthesized to a cloudformation template that an external team (e.g. security team) to deploy, and adjusting the CDK application to reference pre-created resources already pulled out.
+
+*Example*
+
+```typescript
+ const app = App()
+ const stack = new Stack(app, 'MyStack');
+ extractedStack = new Stack(app, 'ExtractedStack');
+ const synthedApp = app.synth();
+ Aspects.of(app).add(new ResourceExtractor({
+   extractDestinationStack: extractedStack,
+   stackArtifacts: synthedApp.stacks,
+   valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+   resourceTypesToExtract: [
+     'AWS::IAM::Role',
+     'AWS::IAM::Policy',
+     'AWS::IAM::ManagedPolicy',
+     'AWS::IAM::InstanceProfile',
+   ],
+ });
+ app.synth({ force: true });
+```
+
+
+#### Initializers <a name="Initializers" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.Initializer"></a>
+
+```typescript
+import { ResourceExtractor } from '@cdklabs/cdk-enterprise-iac'
+
+new ResourceExtractor(props: ResourceExtractorProps)
+```
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractor.Initializer.parameter.props">props</a></code> | <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps">ResourceExtractorProps</a></code> | *No description.* |
+
+---
+
+##### `props`<sup>Required</sup> <a name="props" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.Initializer.parameter.props"></a>
+
+- *Type:* <a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorProps">ResourceExtractorProps</a>
+
+---
+
+#### Methods <a name="Methods" id="Methods"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractor.visit">visit</a></code> | Entrypoint. |
+
+---
+
+##### `visit` <a name="visit" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.visit"></a>
+
+```typescript
+public visit(node: IConstruct): void
+```
+
+Entrypoint.
+
+###### `node`<sup>Required</sup> <a name="node" id="@cdklabs/cdk-enterprise-iac.ResourceExtractor.visit.parameter.node"></a>
+
+- *Type:* constructs.IConstruct
+
+---
+
+
+
+
 ### SetApiGatewayEndpointConfiguration <a name="SetApiGatewayEndpointConfiguration" id="@cdklabs/cdk-enterprise-iac.SetApiGatewayEndpointConfiguration"></a>
 
 - *Implements:* aws-cdk-lib.IAspect
@@ -1515,6 +1940,10 @@ All aspects can visit an IConstruct.
 
 ---
 
+
+
+
+
 ## Enums <a name="Enums" id="Enums"></a>
 
 ### ProxyType <a name="ProxyType" id="@cdklabs/cdk-enterprise-iac.ProxyType"></a>
@@ -1543,4 +1972,54 @@ Whether an http-proxy or https-proxy.
 
 ---
 
+
+### ResourceExtractorShareMethod <a name="ResourceExtractorShareMethod" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod"></a>
+
+The available value sharing methods to pass values from the extracted stack onto the original stack(s).
+
+#### Members <a name="Members" id="Members"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.CFN_OUTPUT">CFN_OUTPUT</a></code> | *No description.* |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.SSM_PARAMETER">SSM_PARAMETER</a></code> | *No description.* |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.API_LOOKUP">API_LOOKUP</a></code> | *No description.* |
+
+---
+
+##### `CFN_OUTPUT` <a name="CFN_OUTPUT" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.CFN_OUTPUT"></a>
+
+---
+
+
+##### `SSM_PARAMETER` <a name="SSM_PARAMETER" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.SSM_PARAMETER"></a>
+
+---
+
+
+##### `API_LOOKUP` <a name="API_LOOKUP" id="@cdklabs/cdk-enterprise-iac.ResourceExtractorShareMethod.API_LOOKUP"></a>
+
+---
+
+
+### ResourceTransform <a name="ResourceTransform" id="@cdklabs/cdk-enterprise-iac.ResourceTransform"></a>
+
+#### Members <a name="Members" id="Members"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceTransform.STACK_NAME">STACK_NAME</a></code> | *No description.* |
+| <code><a href="#@cdklabs/cdk-enterprise-iac.ResourceTransform.LOGICAL_ID">LOGICAL_ID</a></code> | *No description.* |
+
+---
+
+##### `STACK_NAME` <a name="STACK_NAME" id="@cdklabs/cdk-enterprise-iac.ResourceTransform.STACK_NAME"></a>
+
+---
+
+
+##### `LOGICAL_ID` <a name="LOGICAL_ID" id="@cdklabs/cdk-enterprise-iac.ResourceTransform.LOGICAL_ID"></a>
+
+---
+
 </details>
diff --git a/package.json b/package.json
index 94b527c..c94195f 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,12 @@
     "aws-cdk-lib": "^2.41.0",
     "constructs": "^10.0.5"
   },
+  "dependencies": {
+    "aws-sdk": "^2.1230.0"
+  },
+  "bundledDependencies": [
+    "aws-sdk"
+  ],
   "keywords": [
     "cdk"
   ],
diff --git a/src/index.ts b/src/index.ts
index 8ca909f..8f20ced 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,3 +10,5 @@ export * from './patches/setApiGatewayEndpointConfiguration';
 export * from './patches/addCfnInitProxy';
 export * from './constructs/populateProvidedVpc/populateProvidedVpc';
 export * from './constructs/ecsIsoServiceAutoscaler/ecsIsoServiceAutoscaler';
+export * from './patches/resource-extractor/resourceExtractor';
+export { ResourceTransform } from './patches/resource-extractor/resourceTransformer';
diff --git a/src/patches/resource-extractor/cfnStore.ts b/src/patches/resource-extractor/cfnStore.ts
new file mode 100644
index 0000000..beac6f8
--- /dev/null
+++ b/src/patches/resource-extractor/cfnStore.ts
@@ -0,0 +1,157 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import * as cp from 'child_process';
+import { CfnResource } from 'aws-cdk-lib';
+import { CloudFormationStackArtifact } from 'aws-cdk-lib/cx-api';
+import { CloudFormation } from 'aws-sdk';
+import { Flattener } from './flattener';
+import { ResourceExtractorShareMethod } from './resourceExtractor';
+import { FlatJson, Json } from './types';
+
+export interface CfnStoreProps {
+  readonly stackArtifacts: CloudFormationStackArtifact[];
+  readonly valueShareMethod: ResourceExtractorShareMethod;
+  readonly extractedStackName: string;
+  readonly region: string;
+}
+
+export class CfnStore {
+  public readonly templates: Json = {};
+  public readonly flatTemplates: FlatJson;
+  public readonly extractedStackExports: FlatJson = {};
+
+  constructor(props: CfnStoreProps) {
+    /** Save CloudFormation templates for future lookup */
+    for (const item of props.stackArtifacts) {
+      this.templates[item.stackName] = item.template;
+    }
+    this.flatTemplates = Flattener.flattenObject(this.templates);
+
+    /** Save Value Map if we are using the `API_LOOKUP` sharing method */
+    if (props.valueShareMethod == ResourceExtractorShareMethod.API_LOOKUP) {
+      const stack = this.describeStack(props.extractedStackName, props.region);
+      if (stack) {
+        this.extractedStackExports = this.createExportMap(stack);
+      }
+    }
+  }
+
+  /**
+   * Determine what the export value should be, for example if it should be a
+   * `Ref` or `Fn::GetAtt`
+   *
+   * @param resource
+   * @param flattendKey
+   * @returns
+   */
+  public determineExportValue(resource: CfnResource, flattendKey: string) {
+    const splitKey = flattendKey.split('.');
+
+    if (splitKey.slice(-1)[0] == 'Ref') {
+      return resource.ref;
+    } else if (splitKey.slice(-2)[0] == 'Fn::GetAtt') {
+      const attToGet = Flattener.getValueByPath(
+        this.templates,
+        splitKey.slice(0, -1).concat(['1']).join('.')
+      );
+      return resource.getAtt(attToGet);
+    } else if (splitKey.slice(-2)[0] == 'DependsOn') {
+      return false;
+    } else {
+      throw new Error(`Can't determine export value for ${flattendKey}`);
+    }
+  }
+
+  /**
+   * Retrieve a Stack Name from a given Logical ID
+   *
+   * @param logicalId the Logical ID of the Resource to find
+   * @returns the Stack Name that the Logical ID is found to be in
+   */
+  public getStackNameFromLogicalId(logicalId: string): string {
+    const stackTypeKey = Object.keys(this.flatTemplates).filter(
+      (key) => key.indexOf(`${logicalId}.Type`) > -1
+    )[0];
+    const stackName = stackTypeKey.split('.')[0];
+    return stackName;
+  }
+
+  /**
+   * Retrieve a Resource Type from a given Logical ID
+   *
+   * @param logicalId the logical ID of the Resource to find
+   * @returns the Resource Type of the provided Logical ID
+   */
+  public getResourceTypeFromLogicalId(logicalId: string): string {
+    const typeKey = Object.keys(this.flatTemplates).filter(
+      (key) => key.indexOf(`${logicalId}.Type`) > -1
+    )[0];
+
+    const resourceType = this.flatTemplates[typeKey];
+    return resourceType;
+  }
+
+  /**
+   * Performs a Describe Stack API call with the AWS SDK to determine what
+   * the CloudFormation Exports are for a given Stack Name.
+   *
+   * @param stackName the CloudFormation stack name to query against
+   * @param region the AWS region to target
+   * @returns CloudFormation Stack object
+   */
+  private describeStack(
+    stackName: string,
+    region: string
+  ): CloudFormation.Stack | undefined {
+    let stack: CloudFormation.Stack | undefined;
+    try {
+      const output = cp.spawnSync(
+        'node',
+        [
+          '-e',
+          `
+            const sdk = require('aws-sdk');
+            const cfn = new sdk.CloudFormation({
+              apiVersion: '2016-11-15',
+              region: '${region}'
+            });
+            cfn.describeStacks({StackName: '${stackName}'})
+                .promise()
+                .then((data) => {
+                  const j = JSON.stringify(data);
+                  console.log(j);
+                }
+            );
+          `,
+        ],
+        { encoding: 'utf8' }
+      );
+      const response = JSON.parse(output.stdout);
+      const stacks: CloudFormation.Stack[] = response.Stacks;
+
+      stack = stacks[0];
+    } catch {}
+
+    return stack;
+  }
+
+  /**
+   * Builds an Export lookup table from the provided AWS SDK CloudFormation
+   * stack object. This will be in the form of `{ 'MyExport': 'foobar' }`.
+   *
+   * @param stack
+   * @returns
+   */
+  private createExportMap(stack: CloudFormation.Stack): FlatJson {
+    const exports: FlatJson = {};
+    const outputs = stack.Outputs ?? [];
+    for (const output of outputs) {
+      if (output.ExportName && output.OutputValue) {
+        exports[output.ExportName] = output.OutputValue;
+      }
+    }
+    return exports;
+  }
+}
diff --git a/src/patches/resource-extractor/flattener.ts b/src/patches/resource-extractor/flattener.ts
new file mode 100644
index 0000000..632bdea
--- /dev/null
+++ b/src/patches/resource-extractor/flattener.ts
@@ -0,0 +1,67 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { Json, FlatJson } from './types';
+
+/**
+ * Contains static helper functions to deal with flattening objects,
+ * retrieving values from flat objects and setting values on flat objects.
+ */
+export class Flattener {
+  /**
+   * Flattens a provided Json object, moving all nested objects to the top
+   * level and separating with dots, example:
+   * ```
+   * {
+   *   "Stack.Resources.Foo.Properties.Tags.0.Value": "bar"
+   * }
+   * ```
+   *
+   * @param obj complex object to flatten
+   * @returns key-value pairs of object separated by dots, and the value
+   */
+  public static flattenObject = (obj: Json): FlatJson => {
+    var toReturn: { [key: string]: string } = {};
+    for (let i in obj) {
+      if (!obj.hasOwnProperty(i)) continue;
+
+      if (typeof obj[i] == 'object' && obj[i] !== null) {
+        let flatObject = Flattener.flattenObject(obj[i]);
+        for (let x in flatObject) {
+          if (!flatObject.hasOwnProperty(x)) continue;
+          toReturn[i + '.' + x] = flatObject[x];
+        }
+      } else {
+        toReturn[i] = obj[i];
+      }
+    }
+    return toReturn;
+  };
+
+  /**
+   * Retrieves a value from a provided Json object by using the provided flat
+   * path to look it up
+   *
+   * @param obj object to search through
+   * @param path string of where in the object to get, separated by dots
+   * @returns found value
+   */
+  public static getValueByPath = (obj: Json, path: string): any =>
+    path.split('.').reduce((acc, part) => (acc ? acc[part] : undefined), obj);
+
+  /**
+   * Updates a value in a provided Json object by using the provided flat
+   * path to look it up and the provided value to set
+   *
+   * @param obj object to update
+   * @param path string of path to key to update (separated by dots)
+   * @param value value to set in object path
+   */
+  public static setToValue = (obj: Json, path: string, value: any): void => {
+    let i;
+    const pathSplit = path.split('.');
+    for (i = 0; i < pathSplit.length - 1; i++) obj = obj[pathSplit[i]];
+    obj[pathSplit[i]] = value;
+  };
+}
diff --git a/src/patches/resource-extractor/resourceExtractor.ts b/src/patches/resource-extractor/resourceExtractor.ts
new file mode 100644
index 0000000..d5548dd
--- /dev/null
+++ b/src/patches/resource-extractor/resourceExtractor.ts
@@ -0,0 +1,457 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { Annotations, CfnResource, Fn, IAspect, Stack } from 'aws-cdk-lib';
+import { StringParameter } from 'aws-cdk-lib/aws-ssm';
+import { CloudFormationStackArtifact } from 'aws-cdk-lib/cx-api';
+import { IConstruct } from 'constructs';
+import { CfnStore } from './cfnStore';
+import { Flattener } from './flattener';
+import { ResourceTransformer } from './resourceTransformer';
+import { FlatJson, Json } from './types';
+
+interface overrideFoundRefWithImportValueProps {
+  readonly foundRefNode: CfnResource;
+  readonly importValue: string;
+  readonly flattenedKey: string;
+}
+
+interface deletionOverrideDependsOnProps {
+  readonly foundRefNode: CfnResource;
+  readonly flattenedKey: string;
+}
+
+interface modifyExtractedResourcePropertiesProps {
+  readonly props: Json;
+  readonly propsToAdjust: string[];
+}
+
+/**
+ * The available value sharing methods to pass values from the extracted stack
+ * onto the original stack(s).
+ */
+export enum ResourceExtractorShareMethod {
+  'CFN_OUTPUT' = 'CFN_OUTPUT',
+  'SSM_PARAMETER' = 'SSM_PARAMETER',
+  'API_LOOKUP' = 'API_LOOKUP',
+}
+
+export interface ResourceExtractorProps {
+  /** Stack to move found extracted resources into. */
+  readonly extractDestinationStack: Stack;
+
+  /** Synthed stack artifacts from your CDK app. */
+  readonly stackArtifacts: CloudFormationStackArtifact[];
+
+  /** The sharing method to use when passing exported resources from the "Extracted Stack" into the original stack(s). */
+  readonly valueShareMethod?: ResourceExtractorShareMethod;
+
+  /** List of resource types to extract, ex: `AWS::IAM::Role`. */
+  readonly resourceTypesToExtract: string[];
+
+  /** Additional resource transformations. */
+  readonly additionalTransforms?: { [key: string]: string };
+}
+
+/**
+ * This Aspect takes a CDK application, all synthesized CloudFormationStackArtifact,
+ * a value share method, and a list of Cloudformation resources that should be
+ * pulled out of the main CDK application, which should be synthesized to a
+ * cloudformation template that an external team (e.g. security team) to deploy,
+ * and adjusting the CDK application to reference pre-created resources already pulled out
+ *
+ * @example
+    const app = App()
+    const stack = new Stack(app, 'MyStack');
+    extractedStack = new Stack(app, 'ExtractedStack');
+    const synthedApp = app.synth();
+
+    Aspects.of(app).add(new ResourceExtractor({
+      extractDestinationStack: extractedStack,
+      stackArtifacts: synthedApp.stacks,
+      valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+      resourceTypesToExtract: [
+        'AWS::IAM::Role',
+        'AWS::IAM::Policy',
+        'AWS::IAM::ManagedPolicy',
+        'AWS::IAM::InstanceProfile',
+      ],
+    });
+    app.synth({ force: true });
+ */
+export class ResourceExtractor implements IAspect {
+  private readonly extractDestinationStack: Stack;
+  private readonly resourceTypesToExtract: string[];
+  private readonly valueShareMethod: ResourceExtractorShareMethod =
+    ResourceExtractorShareMethod.CFN_OUTPUT;
+  private readonly logicalIdsNotGettingExtracted: FlatJson;
+  private readonly cfn: CfnStore;
+  private readonly resourceTransformer: ResourceTransformer;
+
+  constructor(props: ResourceExtractorProps) {
+    /** Save props */
+    this.extractDestinationStack = props.extractDestinationStack;
+    this.resourceTypesToExtract = props.resourceTypesToExtract;
+    if (props.valueShareMethod) {
+      this.valueShareMethod = props.valueShareMethod;
+    }
+    Annotations.of(this.extractDestinationStack).addWarning(
+      '❗ ResourceExtractor is in experimental mode. \
+      Please be sure to validate synthesized assets prior to deploy. \
+      Please open any issues found at https://github.com/cdklabs/cdk-enterprise-iac/issues'
+    );
+
+    /** Initialize CfnStore to save templates and mappings */
+    this.cfn = new CfnStore({
+      extractedStackName: props.extractDestinationStack.stackName,
+      region: props.extractDestinationStack.region,
+      stackArtifacts: props.stackArtifacts,
+      valueShareMethod: this.valueShareMethod,
+    });
+
+    /** Initialize resource transformer */
+    this.resourceTransformer = new ResourceTransformer({
+      cfnStore: this.cfn,
+      additionalTransforms: props.additionalTransforms,
+    });
+
+    /** Save resources that are not getting extracted */
+    this.logicalIdsNotGettingExtracted =
+      this.getLogicalIdsNotGettingExtracted();
+  }
+
+  /** Entrypoint */
+  public visit(node: IConstruct): void {
+    // Ignore the extracted stack
+    const anyNode = node as any;
+    if (
+      'stack' in anyNode &&
+      anyNode.stack.toString() === this.extractDestinationStack.stackName
+    ) {
+      return;
+    }
+
+    // Process all resources
+    if (node instanceof CfnResource) {
+      if (this.resourceTypesToExtract.includes(node.cfnResourceType)) {
+        this.processCfnResource(node);
+      }
+    }
+  }
+
+  /**
+   * Takes in a CfnResource object and processes it by recreating it in
+   * the Security Stack and deleting it from the stack in which it came
+   * from.
+   *
+   * @param node the CfnResource to process
+   */
+  private processCfnResource(node: CfnResource): void {
+    const stackName = node.stack.stackName;
+    const logicalId = node.stack.resolve(node.logicalId);
+    const props = this.cfn.templates[stackName].Resources[logicalId].Properties;
+
+    /** Recreate resource in extracted stack and export value */
+    if (
+      !this.extractDestinationStack.node.tryFindChild(
+        node.stack.resolve(logicalId)
+      )
+    ) {
+      // Check if props include references to resources that _aren't_ being
+      // extracted
+      const flattenedProps = Flattener.flattenObject(props);
+      const propsToAdjust = Object.keys(flattenedProps).filter((key) =>
+        Object.keys(this.logicalIdsNotGettingExtracted).includes(
+          flattenedProps[key]
+        )
+      );
+
+      // If properties being extracted to another stack contain
+      // references to resources in the original stack, we need to adjust
+      let modifiedProps: Json = {};
+      if (propsToAdjust) {
+        modifiedProps = this.modifyExtractedResourceProperties({
+          props,
+          propsToAdjust,
+        });
+      }
+
+      // Add to extracted stack
+      const res = new CfnResource(this.extractDestinationStack, logicalId, {
+        type: node.cfnResourceType,
+        properties: modifiedProps || props,
+      });
+
+      // Look in all existing stacks for any refs to the resource being
+      // extracted
+      const foundRefs = Object.keys(this.cfn.flatTemplates).filter(
+        (key) => this.cfn.flatTemplates[key] === logicalId
+      );
+
+      // loop through resources in stack with Cfn intrinsic functions
+      // referencing extracted resources
+      for (const foundRef of foundRefs) {
+        // See if the found ref is also getting extracted. Ignore if so.
+        if (this.isRefAlsoGettingExtracted(foundRef)) continue;
+
+        // Get the CfnResource that is referencing this extracted resource
+        const foundRefNode = this.getResourceFromFoundRef(
+          node,
+          foundRef
+        ) as CfnResource;
+
+        // Figure out the pattern of how extracted resource is being referenced
+        // e.g. using `Fn::GetAtt` or `Ref`
+        const exportValue = this.cfn.determineExportValue(node, foundRef);
+        if (exportValue) {
+          // Generate export in ExportedStack, and return the `Fn::ImportValue`
+          // method to use when referencing in the App stack
+          const importValue = this.exportValue(
+            node.stack,
+            this.extractDestinationStack.resolve(res.logicalId),
+            exportValue
+          );
+
+          // Override any ref to extracted resource
+          this.overrideFoundRefWithImportValue({
+            foundRefNode,
+            importValue,
+            flattenedKey: foundRef,
+          });
+        }
+
+        // Remove any DependsOn references
+        this.deletionOverrideDependsOn({
+          foundRefNode,
+          flattenedKey: foundRef,
+        });
+      }
+    }
+
+    /** Delete from original stack */
+    const removed = node.stack.node.tryRemoveChild(node.node.id);
+    if (!removed) {
+      const parent = node.node.scope?.node;
+      parent?.tryRemoveChild(node.node.id);
+    }
+  }
+
+  /**
+   * This will adjust properties in extracted resources that have
+   * CloudFormation intrinsic functions referencing resources in the original
+   * stack.
+   *
+   * Most commonly found in IAM policies that scope IAM actions to specific
+   * resources in the Stack the policy is being extracted from. In this case
+   * it will take a look at the resource type and transform into a partial ARN.
+   *
+   * @param props modifyExtractedResourcePropertiesProps
+   * @returns Json of modified properties, including partial wildcard matchers
+   */
+  private modifyExtractedResourceProperties(
+    props: modifyExtractedResourcePropertiesProps
+  ): Json {
+    let modifiedProps = props.props;
+    for (const key in props.propsToAdjust) {
+      if (Object.prototype.hasOwnProperty.call(props.propsToAdjust, key)) {
+        const resourceLogicalIdToReplace = Flattener.getValueByPath(
+          props.props,
+          props.propsToAdjust[key]
+        );
+        const partial = this.resourceTransformer.toPartial(
+          resourceLogicalIdToReplace
+        );
+
+        const splitKey = props.propsToAdjust[key].split('.');
+        if (splitKey.slice(-1)[0] == 'Ref') {
+          Flattener.setToValue(
+            modifiedProps,
+            splitKey.slice(0, -1).join('.'),
+            partial
+          );
+        } else if (splitKey.slice(-2)[0] == 'Fn::GetAtt') {
+          Flattener.setToValue(
+            modifiedProps,
+            splitKey.slice(0, -2).join('.'),
+            partial
+          );
+        }
+      }
+    }
+    return modifiedProps;
+  }
+
+  /**
+   * This takes the flattened key of the resource that's referencing an
+   * extracted resource via Cfn intrinsic function, the CfnResource itself
+   * (e.g. CfnFunction) as well as the import value (from
+   * determineExportValue), and adds a Property override.
+   *
+   * @param props overrideFoundRefWithImportValueProps
+   */
+  private overrideFoundRefWithImportValue(
+    props: overrideFoundRefWithImportValueProps
+  ): void {
+    // find property to override
+    const splitKey = props.flattenedKey.split('.');
+    let propertyOverridePath: string;
+    if (splitKey.slice(-1)[0] == 'Ref') {
+      propertyOverridePath = splitKey.slice(4, -1).join('.');
+    } else if (splitKey.slice(-2)[0] == 'Fn::GetAtt') {
+      propertyOverridePath = splitKey.slice(4, -2).join('.');
+    } else {
+      propertyOverridePath = 'notFound';
+    }
+    if (this.valueShareMethod == ResourceExtractorShareMethod.CFN_OUTPUT) {
+      props.foundRefNode.addPropertyOverride(
+        propertyOverridePath,
+        props.foundRefNode.stack.resolve(props.importValue)
+      );
+    } else if (
+      this.valueShareMethod == ResourceExtractorShareMethod.SSM_PARAMETER
+    ) {
+      props.foundRefNode.addPropertyOverride(
+        propertyOverridePath,
+        StringParameter.valueFromLookup(
+          props.foundRefNode.stack,
+          props.importValue
+        )
+      );
+    } else if (
+      this.valueShareMethod == ResourceExtractorShareMethod.API_LOOKUP
+    ) {
+      const importValue = props.foundRefNode.stack.resolve(props.importValue)[
+        'Fn::ImportValue'
+      ];
+      props.foundRefNode.addPropertyOverride(
+        propertyOverridePath,
+        this.cfn.extractedStackExports[importValue] ||
+          `dummy-value-for-${importValue}`
+      );
+    }
+  }
+
+  /**
+   * Remove a `DependsOn` reference when not needed
+   *
+   * @param props
+   */
+  private deletionOverrideDependsOn(
+    props: deletionOverrideDependsOnProps
+  ): void {
+    const splitKey = props.flattenedKey.split('.');
+    if (splitKey.slice(-2)[0] == 'DependsOn') {
+      props.foundRefNode.addDeletionOverride(
+        `DependsOn.${splitKey.slice(-1)[0]}`
+      );
+    }
+  }
+
+  /**
+   * Finds the CDK resource for a given flattened key by looking it up in the
+   * context of the provided node's stack.
+   *
+   * @param node
+   * @param flattendKey
+   * @returns CfnResource
+   */
+  private getResourceFromFoundRef(
+    node: CfnResource,
+    flattendKey: string
+  ): IConstruct | undefined {
+    const foundRefLogicalId = flattendKey.split('.')[2];
+    return node.stack.node.findAll().find((x: any) => {
+      if ('stack' in x && 'logicalId' in x) {
+        return x.stack.resolve(x.logicalId) == foundRefLogicalId;
+      } else {
+        return undefined;
+      }
+    });
+  }
+
+  /**
+   * Given a flattened key, determine if the resouce is also getting
+   * extracted so we know whether or not to adjust CloudFormation Intrinsic
+   * functions.
+   *
+   * @param flattendKey
+   * @returns boolean
+   */
+  private isRefAlsoGettingExtracted(flattendKey: string): boolean {
+    const splitKey = flattendKey.split('.');
+    const pathToType = Flattener.getValueByPath(
+      this.cfn.templates,
+      splitKey.slice(0, 3).concat(['Type']).join('.')
+    );
+    return this.resourceTypesToExtract.includes(pathToType);
+  }
+
+  /**
+   * Get a list of logical ids of resources _not_ getting exported.
+   *
+   * @returns flat json in the form of `{ LogicalId: 'ResourceType' }`
+   */
+  private getLogicalIdsNotGettingExtracted(): FlatJson {
+    const logicalIds: FlatJson = {};
+    for (const key in this.cfn.flatTemplates) {
+      if (Object.prototype.hasOwnProperty.call(this.cfn.flatTemplates, key)) {
+        const splitKey = key.split('.');
+        if (
+          splitKey.slice(-1)[0] == 'Type' &&
+          splitKey.slice(1, 2)[0] == 'Resources'
+        ) {
+          if (
+            !this.resourceTypesToExtract.includes(this.cfn.flatTemplates[key])
+          ) {
+            logicalIds[splitKey.slice(-2)[0]] = this.cfn.flatTemplates[key];
+          }
+        }
+      }
+    }
+    return logicalIds;
+  }
+
+  /**
+   * Exports a value using a consistent standard for the different value
+   * share methods.
+   *
+   * @param stack - the CDK `Stack` object for the resource to import
+   * @param name - the name of the export, normally the LogicalId of the
+   * resource being exported
+   * @param value - the value to export
+   */
+  private exportValue(stack: Stack, name: string, value: any): string {
+    const stackName = stack.stackName;
+    const shareName = `${stackName}:${name}`;
+
+    if (this.valueShareMethod == ResourceExtractorShareMethod.SSM_PARAMETER) {
+      const paramName = `/${shareName.replace(':', '/')}`;
+      const paramLogicalId = `p${name}`;
+      if (!this.extractDestinationStack.node.tryFindChild(paramLogicalId)) {
+        new CfnResource(this.extractDestinationStack, paramLogicalId, {
+          type: 'AWS::SSM::Parameter',
+          properties: {
+            Name: paramName,
+            Type: 'String',
+            Value: stack.resolve(value),
+          },
+        });
+      }
+      return paramName;
+    } else {
+      // CFN_OUTPUT and API_LOOKUP share the same method of exporting values
+      if (
+        !this.extractDestinationStack.node.tryFindChild(
+          `Export${stackName}:${name}`
+        )
+      ) {
+        return this.extractDestinationStack.exportValue(stack.resolve(value), {
+          name: shareName,
+        });
+      } else {
+        return Fn.importValue(shareName);
+      }
+    }
+  }
+}
diff --git a/src/patches/resource-extractor/resourceTransformer.ts b/src/patches/resource-extractor/resourceTransformer.ts
new file mode 100644
index 0000000..498444d
--- /dev/null
+++ b/src/patches/resource-extractor/resourceTransformer.ts
@@ -0,0 +1,188 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { Aws } from 'aws-cdk-lib';
+import { CfnApiKey } from 'aws-cdk-lib/aws-apigateway';
+import { CfnTable } from 'aws-cdk-lib/aws-dynamodb';
+import { CfnCluster, CfnTaskDefinition } from 'aws-cdk-lib/aws-ecs';
+import { CfnDomain as CfnDomainEss } from 'aws-cdk-lib/aws-elasticsearch';
+import { CfnFunction } from 'aws-cdk-lib/aws-lambda';
+import { CfnLogGroup } from 'aws-cdk-lib/aws-logs';
+import { CfnDomain as CfnDomainOss } from 'aws-cdk-lib/aws-opensearchservice';
+import { CfnBucket } from 'aws-cdk-lib/aws-s3';
+import { CfnTopic } from 'aws-cdk-lib/aws-sns';
+import { CfnQueue } from 'aws-cdk-lib/aws-sqs';
+import { CfnParameter } from 'aws-cdk-lib/aws-ssm';
+import { CfnStateMachine } from 'aws-cdk-lib/aws-stepfunctions';
+import { CfnStore } from './cfnStore';
+import { FlatJson } from './types';
+
+type Transformer = (stackName: string, logicalId: string) => string;
+
+export class MissingTransformError extends Error {
+  constructor(resourceType: string) {
+    super(`
+    Unable to find transform for resource type: ${resourceType}.
+    You can provide an additional transformation to the Aspect to resolve
+    this and/or open a GitHub Issue to get the resource transformation
+    added at https://github.com/cdklabs/cdk-enterprise-iac/issues.
+    `);
+  }
+}
+
+export enum ResourceTransform {
+  STACK_NAME = 'ResourceTransform.STACK_NAME',
+  LOGICAL_ID = 'ResourceTransform.LOGICAL_ID',
+}
+
+export interface ResourceTransformerProps {
+  readonly cfnStore: CfnStore;
+  readonly additionalTransforms?: FlatJson;
+}
+
+export class ResourceTransformer {
+  private readonly cfn: CfnStore;
+  private readonly defaultTransforms: { [key: string]: Transformer };
+  private readonly additionalTransforms?: FlatJson;
+
+  constructor(props: ResourceTransformerProps) {
+    this.cfn = props.cfnStore;
+    this.defaultTransforms = this.createDefaultTransforms();
+    this.additionalTransforms = props.additionalTransforms;
+  }
+
+  /**
+   * Helper function that generates the beginning portion of an AWS Arn.
+   */
+  private generateArnPreamble(
+    service: string,
+    includeRegion: boolean = true,
+    includeAccount: boolean = true
+  ): string {
+    const region = includeRegion ? `${Aws.REGION}` : '';
+    const account = includeAccount ? `${Aws.ACCOUNT_ID}` : '';
+
+    return `arn:${Aws.PARTITION}:${service}:${region}:${account}`;
+  }
+
+  /**
+   * Creates the Default Transformation function table that contains transform
+   * functions for each CloudFormation Resource Type.
+   *
+   * This should be updated to support additional resource types.
+   * In addition, it may need to be fixed if some of the transformations are
+   * found to be incorrect or inconsistent.
+   *
+   * @returns object in the form of { 'AWS::S3::Bucket': transform function }
+   */
+  private createDefaultTransforms(): { [key: string]: Transformer } {
+    return {
+      /** Standard */
+      [CfnTable.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}-${logicalId}*`;
+        const preamble = this.generateArnPreamble('dynamodb');
+        return `${preamble}:table/${partial}`;
+      },
+      [CfnDomainEss.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+        const partial = `${logicalId.substring(0, 15).toLowerCase()}*`;
+        const preamble = this.generateArnPreamble('es');
+        return `${preamble}:domain/${partial}`;
+      },
+      [CfnDomainOss.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+        const partial = `${logicalId.substring(0, 15).toLowerCase()}*`;
+        const preamble = this.generateArnPreamble('es');
+        return `${preamble}:domain/${partial}`;
+      },
+      [CfnTaskDefinition.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}${logicalId}*:*`.replace('-', '');
+        const preamble = this.generateArnPreamble('ecs');
+        return `${preamble}:task-definition/${partial}`;
+      },
+      [CfnCluster.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}-${logicalId}*`;
+        const preamble = this.generateArnPreamble('ecs');
+        return `${preamble}:cluster/${partial}`;
+      },
+      /** Colon-resource name grouping */
+      [CfnLogGroup.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}-${logicalId}*:*`;
+        const preamble = this.generateArnPreamble('logs');
+        return `${preamble}:log-group:${partial}`;
+      },
+      [CfnFunction.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}-${logicalId}`.substring(0, 50) + '*';
+        const preamble = this.generateArnPreamble('lambda');
+        return `${preamble}:function:${partial}`;
+      },
+      [CfnStateMachine.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+        const partial = `${logicalId}*`;
+        const preamble = this.generateArnPreamble('states');
+        return `${preamble}:stateMachine:${partial}`;
+      },
+      /** No resource name grouping */
+      [CfnQueue.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}-${logicalId}*`;
+        const preamble = this.generateArnPreamble('sqs');
+        return `${preamble}:${partial}`;
+      },
+      [CfnBucket.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}-${logicalId}*`.toLowerCase();
+        const preamble = this.generateArnPreamble('s3', false, false);
+        return `${preamble}:${partial}`;
+      },
+      [CfnTopic.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName}-${logicalId}*`;
+        const preamble = this.generateArnPreamble('sns');
+        return `${preamble}:${partial}`;
+      },
+      /** Non-standard */
+      [CfnParameter.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+        const partial = `CFN-${logicalId}*`;
+        return partial;
+      },
+      [CfnApiKey.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+        const partial = `${stackName.substring(0, 6)}-${logicalId.substring(
+          0,
+          5
+        )}*`;
+        return partial;
+      },
+    };
+  }
+
+  /**
+   * Transforms resource names to partial values (primarily ARNs) using
+   * wildcards.
+   *
+   * Takes in a generated resource name from CDK and transforms it to a
+   * partial value that is used to replace resource references that use
+   * `Fn::GetAtt` from the generated resource name. This is mainly used to
+   * avoid cyclical dependencies within CDK and ensure that Policies can be
+   * correctly created without knowing the future value of a resource. This
+   * relies on the assumption that the developer does NOT input the `name`
+   * of the resource they are creating. In other words, you must let CDK
+   * generate the resource name.
+   *
+   * @param logicalId the Logical ID of the Resource generated by CDK
+   * @returns string of partial match value to use in IAM Policies
+   */
+  public toPartial(logicalId: string) {
+    const stackName = this.cfn.getStackNameFromLogicalId(logicalId);
+    const resourceType = this.cfn.getResourceTypeFromLogicalId(logicalId);
+
+    if (
+      this.additionalTransforms &&
+      resourceType in this.additionalTransforms
+    ) {
+      return this.additionalTransforms[resourceType]
+        .replace(ResourceTransform.STACK_NAME, stackName)
+        .replace(ResourceTransform.LOGICAL_ID, logicalId);
+    } else if (resourceType in this.defaultTransforms) {
+      const transformResourceToPartial = this.defaultTransforms[resourceType];
+      return transformResourceToPartial(stackName, logicalId);
+    } else {
+      throw new MissingTransformError(resourceType);
+    }
+  }
+}
diff --git a/src/patches/resource-extractor/types.ts b/src/patches/resource-extractor/types.ts
new file mode 100644
index 0000000..553dec7
--- /dev/null
+++ b/src/patches/resource-extractor/types.ts
@@ -0,0 +1,6 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+export type Json = { [key: string]: any };
+export type FlatJson = { [key: string]: string };
diff --git a/yarn.lock b/yarn.lock
index 9daadcd..9d2039d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1262,6 +1262,11 @@ at-least-node@^1.0.0:
   resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
   integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
 
+available-typed-arrays@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
+  integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
+
 aws-cdk-lib@2.41.0:
   version "2.41.0"
   resolved "https://registry.yarnpkg.com/aws-cdk-lib/-/aws-cdk-lib-2.41.0.tgz#5bcff1b075dd8eb3df6346ba11940a1d75d993d5"
@@ -1284,6 +1289,22 @@ aws-cdk@2.50.0:
   optionalDependencies:
     fsevents "2.3.2"
 
+aws-sdk@^2.1230.0:
+  version "2.1244.0"
+  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1244.0.tgz#1cb889c0fdcaa28b66b912eacddf87f59403727a"
+  integrity sha512-V7uXdz3Koti2FfU07Xj6SGnv4UVIGcw03aEGLiidieilQYYVrny828pn2Pl2X9AWJJczE/iLzge/ITTJ7ZPwZQ==
+  dependencies:
+    buffer "4.9.2"
+    events "1.1.1"
+    ieee754 "1.1.13"
+    jmespath "0.16.0"
+    querystring "0.2.0"
+    sax "1.2.1"
+    url "0.10.3"
+    util "^0.12.4"
+    uuid "8.0.0"
+    xml2js "0.4.19"
+
 babel-jest@^27.5.1:
   version "27.5.1"
   resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.5.1.tgz#a1bf8d61928edfefd21da27eb86a695bfd691444"
@@ -1350,6 +1371,11 @@ balanced-match@^1.0.0:
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
   integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
 
+base64-js@^1.0.2:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 boxen@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/boxen/-/boxen-7.0.0.tgz#9e5f8c26e716793fc96edcf7cf754cdf5e3fbf32"
@@ -1420,6 +1446,15 @@ buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
   integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
+buffer@4.9.2:
+  version "4.9.2"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
+  integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
+  dependencies:
+    base64-js "^1.0.2"
+    ieee754 "^1.1.4"
+    isarray "^1.0.0"
+
 builtins@^5.0.0:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9"
@@ -2221,7 +2256,7 @@ error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
-es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5:
+es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5, es-abstract@^1.20.0:
   version "1.20.4"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.4.tgz#1d103f9f8d78d4cf0713edcd6d0ed1a46eed5861"
   integrity sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==
@@ -2491,6 +2526,11 @@ esutils@^2.0.2:
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
+events@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+  integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==
+
 execa@^5.0.0:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -2640,6 +2680,13 @@ flatted@^3.1.0, flatted@^3.2.7:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
   integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
 
+for-each@^0.3.3:
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
+  integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==
+  dependencies:
+    is-callable "^1.1.3"
+
 form-data-encoder@^2.0.1:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.1.3.tgz#682cd821a8423605093992ff895e6b2ed5a9d429"
@@ -3125,6 +3172,16 @@ iconv-lite@^0.6.2:
   dependencies:
     safer-buffer ">= 2.1.2 < 3.0.0"
 
+ieee754@1.1.13:
+  version "1.1.13"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
+  integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
+
+ieee754@^1.1.4:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
 ignore-walk@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-6.0.0.tgz#1dd41c6eb4f661a49750a510a10c2cd934583fd8"
@@ -3220,6 +3277,14 @@ ip@^2.0.0:
   resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
   integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
 
+is-arguments@^1.0.4:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
+  integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
 is-arrayish@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -3240,7 +3305,7 @@ is-boolean-object@^1.1.0:
     call-bind "^1.0.2"
     has-tostringtag "^1.0.0"
 
-is-callable@^1.1.4, is-callable@^1.2.7:
+is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
   integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
@@ -3286,6 +3351,13 @@ is-generator-fn@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
   integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
 
+is-generator-function@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
+  integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
 is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
@@ -3389,6 +3461,17 @@ is-text-path@^1.0.1:
   dependencies:
     text-extensions "^1.0.0"
 
+is-typed-array@^1.1.3, is-typed-array@^1.1.9:
+  version "1.1.9"
+  resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.9.tgz#246d77d2871e7d9f5aeb1d54b9f52c71329ece67"
+  integrity sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==
+  dependencies:
+    available-typed-arrays "^1.0.5"
+    call-bind "^1.0.2"
+    es-abstract "^1.20.0"
+    for-each "^0.3.3"
+    has-tostringtag "^1.0.0"
+
 is-typedarray@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -3413,7 +3496,7 @@ is-yarn-global@^0.4.0:
   resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.4.0.tgz#714d94453327db9ea98fbf1a0c5f2b88f59ddd5c"
   integrity sha512-HneQBCrXGBy15QnaDfcn6OLoU8AQPAa0Qn0IeJR/QCo4E8dNZaGGwxpCwWyEBQC5QvFonP8d6t60iGpAHVAfNA==
 
-isarray@~1.0.0:
+isarray@^1.0.0, isarray@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
   integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
@@ -3885,6 +3968,11 @@ jju@^1.1.0:
   resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
   integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==
 
+jmespath@0.16.0:
+  version "0.16.0"
+  resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076"
+  integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==
+
 js-sdsl@^4.1.4:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.5.tgz#1ff1645e6b4d1b028cd3f862db88c9d887f26e2a"
@@ -3968,6 +4056,7 @@ jsii-diff@^1.70.0:
     log4js "^6.7.0"
     yargs "^16.2.0"
 
+
 jsii-docgen@^7.0.136:
   version "7.0.136"
   resolved "https://registry.yarnpkg.com/jsii-docgen/-/jsii-docgen-7.0.136.tgz#eaf0e45fad1a9140ac7da5ca8646c74e4794fbd4"
@@ -5085,6 +5174,7 @@ progress@^2.0.3:
   resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
   integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
 
+
 projen@^0.65.8:
   version "0.65.8"
   resolved "https://registry.yarnpkg.com/projen/-/projen-0.65.8.tgz#b436dfee700742ae188559296bee95d41a445cb5"
@@ -5152,6 +5242,11 @@ pump@^3.0.0:
     end-of-stream "^1.1.0"
     once "^1.3.1"
 
+punycode@1.3.2:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+  integrity sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==
+
 punycode@^2.1.0, punycode@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@@ -5169,6 +5264,11 @@ q@^1.5.1:
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
 
+querystring@0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+  integrity sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==
+
 querystringify@^2.1.1:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
@@ -5469,6 +5569,16 @@ safe-regex@^2.1.1:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+sax@1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
+  integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==
+
+sax@>=0.6.0:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
 saxes@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
@@ -6238,11 +6348,35 @@ url-parse@^1.5.3:
     querystringify "^2.1.1"
     requires-port "^1.0.0"
 
+url@0.10.3:
+  version "0.10.3"
+  resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
+  integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==
+  dependencies:
+    punycode "1.3.2"
+    querystring "0.2.0"
+
 util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
 
+util@^0.12.4:
+  version "0.12.5"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
+  integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
+  dependencies:
+    inherits "^2.0.3"
+    is-arguments "^1.0.4"
+    is-generator-function "^1.0.7"
+    is-typed-array "^1.1.3"
+    which-typed-array "^1.1.2"
+
+uuid@8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
+  integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
+
 uuid@^8.3.2:
   version "8.3.2"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
@@ -6335,6 +6469,18 @@ which-boxed-primitive@^1.0.2:
     is-string "^1.0.5"
     is-symbol "^1.0.3"
 
+which-typed-array@^1.1.2:
+  version "1.1.8"
+  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.8.tgz#0cfd53401a6f334d90ed1125754a42ed663eb01f"
+  integrity sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==
+  dependencies:
+    available-typed-arrays "^1.0.5"
+    call-bind "^1.0.2"
+    es-abstract "^1.20.0"
+    for-each "^0.3.3"
+    has-tostringtag "^1.0.0"
+    is-typed-array "^1.1.9"
+
 which@^2.0.1, which@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -6419,6 +6565,14 @@ xml-name-validator@^3.0.0:
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
   integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
 
+xml2js@0.4.19:
+  version "0.4.19"
+  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
+  integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
+  dependencies:
+    sax ">=0.6.0"
+    xmlbuilder "~9.0.1"
+
 xml@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
@@ -6440,6 +6594,11 @@ xmlbuilder@^15.1.1:
   resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5"
   integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==
 
+xmlbuilder@~9.0.1:
+  version "9.0.7"
+  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
+  integrity sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==
+
 xmlchars@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"


Test patch:
diff --git a/test/patches/resource-extractor/cfnStore.test.ts b/test/patches/resource-extractor/cfnStore.test.ts
new file mode 100644
index 0000000..1bb6d0d
--- /dev/null
+++ b/test/patches/resource-extractor/cfnStore.test.ts
@@ -0,0 +1,64 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { App, Stack } from 'aws-cdk-lib';
+import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
+import { Bucket } from 'aws-cdk-lib/aws-s3';
+import { CfnStore } from '../../../src/patches/resource-extractor/cfnStore';
+import { ResourceExtractorShareMethod } from '../../../src/patches/resource-extractor/resourceExtractor';
+
+let app: App;
+let stack: Stack;
+let extractedStack: Stack;
+
+const appStackName = 'AppStack';
+const extractedStackName = 'ExtractedStack';
+
+describe('CfnStore', () => {
+  const env = {
+    account: '111111111111',
+    region: 'us-east-2',
+  };
+
+  beforeEach(() => {
+    app = new App();
+    stack = new Stack(app, appStackName, { env });
+    extractedStack = new Stack(app, extractedStackName, { env });
+  });
+
+  test('Saving stack outputs from API lookup', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const bucket = new Bucket(stack, 'TestBucket');
+    bucket.grantReadWrite(func);
+    const synthedApp = app.synth();
+
+    const describeStackMock = jest.spyOn(
+      CfnStore.prototype as any,
+      'describeStack'
+    );
+    const mockStack = {
+      Outputs: [
+        {
+          ExportName: 'foo',
+          OutputValue: 'bar',
+        },
+      ],
+    };
+    describeStackMock.mockImplementation(() => mockStack);
+
+    const cfn = new CfnStore({
+      extractedStackName: extractedStack.stackName,
+      region: 'us-east-2',
+      stackArtifacts: synthedApp.stacks,
+      valueShareMethod: ResourceExtractorShareMethod.API_LOOKUP,
+    });
+    expect(describeStackMock).toBeCalled();
+    expect(cfn.templates).toHaveProperty('AppStack.Resources');
+    expect(cfn.extractedStackExports).toEqual({ foo: 'bar' });
+  });
+});
diff --git a/test/patches/resource-extractor/flattener.test.ts b/test/patches/resource-extractor/flattener.test.ts
new file mode 100644
index 0000000..2337cc5
--- /dev/null
+++ b/test/patches/resource-extractor/flattener.test.ts
@@ -0,0 +1,32 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { Flattener } from '../../../src/patches/resource-extractor/flattener';
+
+let complexObject: Object;
+
+describe('Flattener', () => {
+  beforeEach(() => {
+    complexObject = {
+      Foo: {
+        Bar: 'Baz',
+      },
+    };
+  });
+
+  test('Flatten an object', () => {
+    const flattened = Flattener.flattenObject(complexObject);
+    expect(flattened).toEqual({ 'Foo.Bar': 'Baz' });
+  });
+
+  test('Lookup flattened key', () => {
+    const lookup = Flattener.getValueByPath(complexObject, 'Foo.Bar');
+    expect(lookup).toEqual('Baz');
+  });
+
+  test('Set from flattened key', () => {
+    Flattener.setToValue(complexObject, 'Foo.Bar', 'newValue');
+    expect(complexObject).toEqual({ Foo: { Bar: 'newValue' } });
+  });
+});
diff --git a/test/patches/resource-extractor/resourceExtractor.test.ts b/test/patches/resource-extractor/resourceExtractor.test.ts
new file mode 100644
index 0000000..2cc7ab5
--- /dev/null
+++ b/test/patches/resource-extractor/resourceExtractor.test.ts
@@ -0,0 +1,487 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { App, Aspects, CfnElement, RemovalPolicy, Stack } from 'aws-cdk-lib';
+import { Match, Template, Annotations } from 'aws-cdk-lib/assertions';
+import {
+  Instance,
+  InstanceClass,
+  InstanceSize,
+  InstanceType,
+  MachineImage,
+  Vpc,
+} from 'aws-cdk-lib/aws-ec2';
+
+import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
+import { Key } from 'aws-cdk-lib/aws-kms';
+import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
+
+import { DatabaseInstance, DatabaseInstanceEngine } from 'aws-cdk-lib/aws-rds';
+import { Bucket, CfnBucket } from 'aws-cdk-lib/aws-s3';
+import { IConstruct } from 'constructs';
+import {
+  ResourceExtractor,
+  ResourceExtractorShareMethod,
+} from '../../../src/patches/resource-extractor/resourceExtractor';
+
+let app: App;
+let stack: Stack;
+let extractedStack: Stack;
+let resourceTypesToExtract = [
+  'AWS::IAM::Role',
+  'AWS::IAM::Policy',
+  'AWS::KMS::Key',
+  'AWS::KMS::Alias',
+  'AWS::IAM::ManagedPolicy',
+  'AWS::IAM::InstanceProfile',
+];
+
+const appStackName = 'AppStack';
+const extractedStackName = 'ExtractedStack';
+
+describe('Annotations for experimental mode', () => {
+  beforeEach(() => {
+    app = new App();
+    stack = new Stack(app, appStackName);
+    extractedStack = new Stack(app, extractedStackName);
+  });
+  test('warning shows for experimental mode', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const bucket = new Bucket(stack, 'TestBucket');
+    bucket.grantReadWrite(func);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+    Annotations.fromStack(extractedStack).hasWarning(
+      '/ExtractedStack',
+      '❗ ResourceExtractor is in experimental mode. \
+      Please be sure to validate synthesized assets prior to deploy. \
+      Please open any issues found at https://github.com/cdklabs/cdk-enterprise-iac/issues'
+    );
+  });
+});
+
+describe('Extracting resources from stack', () => {
+  beforeEach(() => {
+    app = new App();
+    stack = new Stack(app, appStackName);
+    extractedStack = new Stack(app, extractedStackName);
+  });
+
+  test('IAM resources pulled out of app', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const bucket = new Bucket(stack, 'TestBucket');
+    bucket.grantReadWrite(func);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    const appTemplate = Template.fromStack(stack);
+    // Extracted stack has IAM resources
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.resourceCountIs('AWS::IAM::Policy', 1);
+    // Non-IAM resources present in app stack
+    appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
+    appTemplate.resourceCountIs('AWS::Lambda::Function', 1);
+  });
+
+  test('Separate Role within the stack', () => {
+    const role = new Role(stack, 'TestRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const testFunc = new Function(stack, 'TestFunction', {
+      code: Code.fromInline("print('hello_world')"),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    testFunc.grantInvoke(role);
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    const appTemplate = Template.fromStack(stack);
+
+    // Extracted stack has IAM resources
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 2);
+    extractedTemplate.resourceCountIs('AWS::IAM::Policy', 1);
+    // Non-IAM resources present in app stack
+    appTemplate.resourceCountIs('AWS::Lambda::Function', 1);
+    appTemplate.hasResourceProperties('AWS::Lambda::Function', {
+      Role: {
+        'Fn::ImportValue': Match.anyValue(),
+        'Fn::GetAtt': Match.absent(),
+      },
+    });
+  });
+
+  test('Only appropriate DependsOn values are removed', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const bucket = new Bucket(stack, 'TestBucket');
+    bucket.grantReadWrite(func);
+    func.node.addDependency(bucket);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const appTemplate = Template.fromStack(stack);
+
+    const bucketId = bucket.node.findAll().find((x: IConstruct) => {
+      return x instanceof CfnBucket;
+    });
+
+    appTemplate.hasResource('AWS::Lambda::Function', {
+      DependsOn: [stack.getLogicalId(bucketId as CfnElement)],
+    });
+  });
+
+  test('Security groups work okay', () => {
+    resourceTypesToExtract = [
+      'AWS::IAM::Role',
+      'AWS::IAM::Policy',
+      'AWS::KMS::Key',
+      'AWS::KMS::Alias',
+      'AWS::IAM::ManagedPolicy',
+      'AWS::IAM::InstanceProfile',
+      'AWS::EC2::SecurityGroup',
+      'AWS::EC2::SecurityGroupEgress',
+      'AWS::EC2::SecurityGroupIngress',
+    ];
+
+    const vpc = new Vpc(stack, 'TestVpc');
+    const instance = new Instance(stack, 'TestInstance', {
+      machineImage: MachineImage.latestAmazonLinux(),
+      instanceType: InstanceType.of(InstanceClass.MEMORY5, InstanceSize.LARGE),
+      vpc,
+    });
+    const key = new Key(stack, 'TestKey');
+    key.addAlias('alias/TestKey');
+    const db = new DatabaseInstance(stack, 'TestDb', {
+      engine: DatabaseInstanceEngine.POSTGRES,
+      vpc,
+    });
+    db.connections.allowDefaultPortFrom(instance.connections);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+        additionalTransforms: {
+          'AWS::EC2::VPC': '*',
+          'AWS::RDS::DBInstance': '*',
+        },
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    const appTemplate = Template.fromStack(stack);
+    extractedTemplate.resourceCountIs('AWS::EC2::SecurityGroup', 2);
+    extractedTemplate.resourceCountIs('AWS::EC2::SecurityGroupIngress', 1);
+    appTemplate.resourceCountIs('AWS::EC2::Instance', 1);
+    appTemplate.resourceCountIs('AWS::RDS::DBInstance', 1);
+  });
+
+  describe('Sharing Methods - CFN_OUTPUT', () => {
+    beforeEach(() => {
+      app = new App();
+      stack = new Stack(app, appStackName);
+      extractedStack = new Stack(app, extractedStackName);
+    });
+
+    test('Simple case', () => {
+      const func = new Function(stack, 'TestLambda', {
+        code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+        handler: 'index.handler',
+        runtime: Runtime.PYTHON_3_9,
+      });
+      const key = new Key(stack, 'TestKey');
+      key.addAlias('alias/TestKey');
+      const bucket = new Bucket(stack, 'TestBucket', {
+        encryptionKey: key,
+      });
+      bucket.grantReadWrite(func);
+
+      const synthedApp = app.synth();
+      Aspects.of(app).add(
+        new ResourceExtractor({
+          extractDestinationStack: extractedStack,
+          stackArtifacts: synthedApp.stacks,
+          valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+          resourceTypesToExtract,
+        })
+      );
+      app.synth({ force: true });
+
+      const extractedTemplate = Template.fromStack(extractedStack);
+      const appTemplate = Template.fromStack(stack);
+      const roleLogicalId = Object.keys(
+        extractedTemplate.findResources('AWS::IAM::Role')
+      )[0];
+
+      extractedTemplate.hasOutput(`Export${appStackName}${roleLogicalId}`, {
+        Export: {
+          Name: `${appStackName}:${roleLogicalId}`,
+        },
+      });
+      appTemplate.hasResourceProperties('AWS::Lambda::Function', {
+        Role: {
+          'Fn::ImportValue': `${appStackName}:${roleLogicalId}`,
+        },
+      });
+    });
+
+    test('InstanceProfile case', () => {
+      const vpc = new Vpc(stack, 'TestVpc');
+      const instance = new Instance(stack, 'TestInstance', {
+        machineImage: MachineImage.latestAmazonLinux(),
+        instanceType: InstanceType.of(
+          InstanceClass.MEMORY5,
+          InstanceSize.LARGE
+        ),
+        vpc,
+      });
+      const key = new Key(stack, 'TestKey');
+      key.addAlias('alias/TestKey');
+      const bucket = new Bucket(stack, 'TestBucket', {
+        encryptionKey: key,
+      });
+      bucket.grantReadWrite(instance.role);
+
+      const synthedApp = app.synth();
+      Aspects.of(app).add(
+        new ResourceExtractor({
+          extractDestinationStack: extractedStack,
+          stackArtifacts: synthedApp.stacks,
+          valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+          resourceTypesToExtract,
+          additionalTransforms: {
+            'AWS::EC2::VPC': '*',
+          },
+        })
+      );
+      app.synth({ force: true });
+
+      const extractedTemplate = Template.fromStack(extractedStack);
+      const appTemplate = Template.fromStack(stack);
+      const instanceProfileLogicalId = Object.keys(
+        extractedTemplate.findResources('AWS::IAM::InstanceProfile')
+      )[0];
+
+      extractedTemplate.hasOutput(
+        `Export${appStackName}${instanceProfileLogicalId}`,
+        {
+          Export: {
+            Name: `${appStackName}:${instanceProfileLogicalId}`,
+          },
+        }
+      );
+      appTemplate.hasResourceProperties('AWS::EC2::Instance', {
+        IamInstanceProfile: {
+          'Fn::ImportValue': `${appStackName}:${instanceProfileLogicalId}`,
+        },
+      });
+    });
+
+    test("Ensure resources are't extracted more than once", () => {
+      const func = new Function(stack, 'TestLambda', {
+        code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+        handler: 'index.handler',
+        runtime: Runtime.PYTHON_3_9,
+      });
+      const bucket = new Bucket(stack, 'TestBucket', {
+        autoDeleteObjects: true,
+        removalPolicy: RemovalPolicy.DESTROY,
+      });
+      bucket.grantReadWrite(func);
+
+      const synthedApp = app.synth();
+      Aspects.of(app).add(
+        new ResourceExtractor({
+          extractDestinationStack: extractedStack,
+          stackArtifacts: synthedApp.stacks,
+          valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+          resourceTypesToExtract,
+        })
+      );
+      app.synth({ force: true });
+
+      const extractedTemplate = Template.fromStack(extractedStack);
+      const appTemplate = Template.fromStack(stack);
+      // Extracted stack has IAM resources
+      extractedTemplate.resourceCountIs('AWS::IAM::Role', 2);
+      extractedTemplate.resourceCountIs('AWS::IAM::Policy', 1);
+      // Non-IAM resources present in app stack
+      appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
+      appTemplate.resourceCountIs('AWS::Lambda::Function', 2);
+      // Custom resources are also using imports
+      appTemplate.resourcePropertiesCountIs(
+        'AWS::Lambda::Function',
+        {
+          Code: {
+            S3Bucket: {
+              'Fn::Sub':
+                'cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}',
+            },
+          },
+          Role: {
+            'Fn::ImportValue': Match.anyValue(),
+          },
+        },
+        1
+      );
+    });
+  });
+});
+
+describe('Sharing Methods - SSM_PARAMETER', () => {
+  const env = {
+    account: '111111111111',
+    region: 'us-east-2',
+  };
+
+  beforeEach(() => {
+    app = new App();
+    stack = new Stack(app, appStackName, { env });
+    extractedStack = new Stack(app, extractedStackName, { env });
+  });
+
+  test('ssm parameters are created in ExtractedStack, and used in App stack', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const bucket = new Bucket(stack, 'TestBucket');
+    bucket.grantReadWrite(func);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.SSM_PARAMETER,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    const appTemplate = Template.fromStack(stack);
+
+    const roleLogicalId = Object.keys(
+      extractedTemplate.findResources('AWS::IAM::Role')
+    )[0];
+
+    // Extracted stack has IAM resources
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.resourceCountIs('AWS::IAM::Policy', 1);
+    extractedTemplate.resourceCountIs('AWS::SSM::Parameter', 1);
+    // Non-IAM resources present in app stack
+    appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
+    appTemplate.resourceCountIs('AWS::Lambda::Function', 1);
+    appTemplate.hasResourceProperties('AWS::Lambda::Function', {
+      Role: `dummy-value-for-/${appStackName}/${roleLogicalId}`,
+    });
+  });
+});
+
+describe('Sharing Methods - API_LOOKUP', () => {
+  const env = {
+    account: '111111111111',
+    region: 'us-east-2',
+  };
+
+  beforeEach(() => {
+    app = new App();
+    stack = new Stack(app, appStackName, { env });
+    extractedStack = new Stack(app, extractedStackName, { env });
+  });
+
+  test('values are exported in ExtractedStack, and used in App stack', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const bucket = new Bucket(stack, 'TestBucket');
+    bucket.grantReadWrite(func);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.API_LOOKUP,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    const appTemplate = Template.fromStack(stack);
+
+    const roleLogicalId = Object.keys(
+      extractedTemplate.findResources('AWS::IAM::Role')
+    )[0];
+
+    // Extracted stack has IAM resources
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.resourceCountIs('AWS::IAM::Policy', 1);
+
+    // Non-IAM resources present in app stack
+    appTemplate.resourceCountIs('AWS::S3::Bucket', 1);
+    appTemplate.resourceCountIs('AWS::Lambda::Function', 1);
+    appTemplate.hasResourceProperties('AWS::Lambda::Function', {
+      Role: `dummy-value-for-${appStackName}:${roleLogicalId}`,
+    });
+  });
+});
diff --git a/test/patches/resource-extractor/resourceTransformer.test.ts b/test/patches/resource-extractor/resourceTransformer.test.ts
new file mode 100644
index 0000000..93fdc70
--- /dev/null
+++ b/test/patches/resource-extractor/resourceTransformer.test.ts
@@ -0,0 +1,796 @@
+/*
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: Apache-2.0
+*/
+import { App, Aspects, Aws, CfnElement, Duration, Stack } from 'aws-cdk-lib';
+import { Match, Template } from 'aws-cdk-lib/assertions';
+import { ApiKey, CfnApiKey } from 'aws-cdk-lib/aws-apigateway';
+import { UserPool } from 'aws-cdk-lib/aws-cognito';
+import { AttributeType, CfnTable, Table } from 'aws-cdk-lib/aws-dynamodb';
+import { InstanceType, Vpc } from 'aws-cdk-lib/aws-ec2';
+import {
+  CfnCluster,
+  CfnTaskDefinition,
+  Cluster,
+  Compatibility,
+  TaskDefinition,
+} from 'aws-cdk-lib/aws-ecs';
+import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
+import { CfnFunction, Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda';
+import { LogGroup } from 'aws-cdk-lib/aws-logs';
+import {
+  CfnDomain,
+  Domain,
+  EngineVersion,
+} from 'aws-cdk-lib/aws-opensearchservice';
+import { DatabaseInstance, DatabaseInstanceEngine } from 'aws-cdk-lib/aws-rds';
+
+import { Bucket, CfnBucket } from 'aws-cdk-lib/aws-s3';
+import { CfnTopic, Topic } from 'aws-cdk-lib/aws-sns';
+import { CfnQueue, Queue } from 'aws-cdk-lib/aws-sqs';
+import { CfnParameter, StringParameter } from 'aws-cdk-lib/aws-ssm';
+import {
+  CfnStateMachine,
+  StateMachine,
+  Wait,
+  WaitTime,
+} from 'aws-cdk-lib/aws-stepfunctions';
+import { IConstruct } from 'constructs';
+import {
+  ResourceExtractor,
+  ResourceExtractorShareMethod,
+} from '../../../src/patches/resource-extractor/resourceExtractor';
+import { MissingTransformError } from '../../../src/patches/resource-extractor/resourceTransformer';
+
+let app: App;
+let stack: Stack;
+let extractedStack: Stack;
+let resourceTypesToExtract = [
+  'AWS::IAM::Role',
+  'AWS::IAM::Policy',
+  'AWS::KMS::Key',
+  'AWS::KMS::Alias',
+  'AWS::IAM::ManagedPolicy',
+  'AWS::IAM::InstanceProfile',
+];
+
+const appStackName = 'AppStack';
+const extractedStackName = 'ExtractedStack';
+
+describe('toPartial scenarios', () => {
+  beforeEach(() => {
+    app = new App();
+    stack = new Stack(app, appStackName);
+    extractedStack = new Stack(app, extractedStackName);
+  });
+
+  test('cloudwatch CfnLogGroup', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const logGroup = new LogGroup(stack, 'TestLogGroup');
+    logGroup.grantWrite(func);
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+  });
+
+  test('sqs CfnQueue', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const queue = new Queue(stack, 'TestQueue');
+    queue.grantConsumeMessages(role);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const queueNode = queue.node.defaultChild as CfnQueue;
+    const queueLogicalID = stack.resolve(queueNode.logicalId);
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: {
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':sqs:',
+                  { Ref: 'AWS::Region' },
+                  ':',
+                  { Ref: 'AWS::AccountId' },
+                  `:${appStackName}-${queueLogicalID}*`,
+                ],
+              ],
+            },
+          },
+        ],
+      },
+    });
+  });
+
+  test('s3 CfnBucket', () => {
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    const bucket = new Bucket(stack, 'TestBucket');
+    bucket.grantReadWrite(func);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract,
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    const bucketId = bucket.node.findAll().find((x: IConstruct) => {
+      return x instanceof CfnBucket;
+    });
+
+    extractedTemplate.resourcePropertiesCountIs(
+      'AWS::IAM::Policy',
+      {
+        PolicyDocument: {
+          Statement: Match.arrayWith([
+            Match.objectLike({
+              Action: Match.arrayWith(['s3:GetObject*']),
+              Resource: Match.arrayWith([
+                // Make sure IAM policy isn't trying to !GetAtt: [BucketLogicalId, Arn]
+                Match.not({
+                  'Fn::GetAtt': [
+                    stack.getLogicalId(bucketId as CfnElement),
+                    'Arn',
+                  ],
+                }),
+              ]),
+            }),
+          ]),
+        },
+      },
+      1
+    );
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: Match.arrayWith([
+          Match.objectLike({
+            Action: Match.arrayWith(['s3:GetObject*']),
+            Resource: Match.arrayWith([
+              // Make sure IAM policy is using a partial generated resource Arn, derived from the logical id
+              Match.objectLike({
+                'Fn::Join': [
+                  '',
+                  [
+                    'arn:',
+                    { Ref: 'AWS::Partition' },
+                    `:s3:::${appStackName.toLowerCase()}-${stack
+                      .getLogicalId(bucketId as CfnElement)
+                      .toLowerCase()}*`,
+                  ],
+                ],
+              }),
+            ]),
+          }),
+        ]),
+      },
+    });
+  });
+
+  test('ssm CfnParameter', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const parameter = new StringParameter(stack, 'TestParameter', {
+      stringValue: 'someTestString',
+    });
+    parameter.grantRead(role);
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const parameterNode = parameter.node.defaultChild as CfnParameter;
+    const parameterLogicalID = stack.resolve(parameterNode.logicalId);
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: Match.objectLike({
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':ssm:',
+                  { Ref: 'AWS::Region' },
+                  ':',
+                  { Ref: 'AWS::AccountId' },
+                  ':parameter/',
+                  `CFN-${parameterLogicalID}*`,
+                ],
+              ],
+            }),
+          },
+        ],
+      },
+    });
+  });
+
+  test('dynamodb CfnTable', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const table = new Table(stack, 'TestDdbTable', {
+      partitionKey: {
+        name: 'id',
+        type: AttributeType.STRING,
+      },
+    });
+    table.grantReadData(role);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+    const extractedTemplate = Template.fromStack(extractedStack);
+    const tableNode = table.node.defaultChild as CfnTable;
+    const tableLogicalID = stack.resolve(tableNode.logicalId);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: Match.arrayWith([
+              Match.objectLike({
+                'Fn::Join': [
+                  '',
+                  [
+                    'arn:',
+                    { Ref: 'AWS::Partition' },
+                    ':dynamodb:',
+                    { Ref: 'AWS::Region' },
+                    ':',
+                    { Ref: 'AWS::AccountId' },
+                    `:table/${appStackName}-${tableLogicalID}*`,
+                  ],
+                ],
+              }),
+            ]),
+          },
+        ],
+      },
+    });
+  });
+
+  test('lambda CfnFunction', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const func = new Function(
+      stack,
+      'TestLambdaWithAVeryVeryVeryVeryAndIMeanVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongName',
+      {
+        code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+        handler: 'index.handler',
+        runtime: Runtime.PYTHON_3_9,
+      }
+    );
+    func.grantInvoke(role);
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const lambdaNode = func.node.defaultChild as CfnFunction;
+    const lambdaLogicalID = stack.resolve(lambdaNode.logicalId);
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 2);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: Match.arrayWith([
+              Match.objectLike({
+                'Fn::Join': [
+                  '',
+                  [
+                    'arn:',
+                    { Ref: 'AWS::Partition' },
+                    ':lambda:',
+                    { Ref: 'AWS::Region' },
+                    ':',
+                    { Ref: 'AWS::AccountId' },
+                    ':function:' +
+                      `${appStackName}-${lambdaLogicalID}`.substring(0, 50) +
+                      '*',
+                  ],
+                ],
+              }),
+            ]),
+          },
+        ],
+      },
+    });
+  });
+
+  test('lambda CfnFunction simple', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const func = new Function(stack, 'SimpleFunc', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    func.grantInvoke(role);
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const lambdaNode = func.node.defaultChild as CfnFunction;
+    const lambdaLogicalID = stack.resolve(lambdaNode.logicalId);
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 2);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: Match.arrayWith([
+              Match.objectLike({
+                'Fn::Join': [
+                  '',
+                  [
+                    'arn:',
+                    { Ref: 'AWS::Partition' },
+                    ':lambda:',
+                    { Ref: 'AWS::Region' },
+                    ':',
+                    { Ref: 'AWS::AccountId' },
+                    ':function:' +
+                      `${appStackName}-${lambdaLogicalID}`.substring(0, 50) +
+                      '*',
+                  ],
+                ],
+              }),
+            ]),
+          },
+        ],
+      },
+    });
+  });
+
+  test('sns CfnTopic', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const topic = new Topic(stack, 'TestTopic');
+    topic.grantPublish(role);
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const topicNode = topic.node.defaultChild as CfnTopic;
+    const topicLogicalID = stack.resolve(topicNode.logicalId);
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: {
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':sns:',
+                  { Ref: 'AWS::Region' },
+                  ':',
+                  { Ref: 'AWS::AccountId' },
+                  `:${appStackName}-${topicLogicalID}*`,
+                ],
+              ],
+            },
+          },
+        ],
+      },
+    });
+  });
+
+  test('apigw CfnApiKey', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const key = new ApiKey(stack, 'TestKey');
+    key.grantReadWrite(role);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const keyNode = key.node.defaultChild as CfnApiKey;
+    const keyLogicalID = stack.resolve(keyNode.logicalId);
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: {
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':apigateway:',
+                  { Ref: 'AWS::Region' },
+                  '::/apikeys/',
+                  `${appStackName.substring(0, 6)}-${keyLogicalID.substring(
+                    0,
+                    5
+                  )}*`,
+                ],
+              ],
+            },
+          },
+        ],
+      },
+    });
+  });
+
+  test('step functions CfnStateMachine', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const wait = new Wait(stack, 'TestWait', {
+      time: WaitTime.duration(Duration.seconds(1)),
+    });
+    const stateMachine = new StateMachine(stack, 'TestStateMachine', {
+      definition: wait,
+    });
+    stateMachine.grantStartExecution(role);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const stateMachineNode = stateMachine.node.defaultChild as CfnStateMachine;
+    const stateMachineLogicalID = stack.resolve(stateMachineNode.logicalId);
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 2);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: {
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':states:',
+                  { Ref: 'AWS::Region' },
+                  ':',
+                  { Ref: 'AWS::AccountId' },
+                  `:stateMachine:${stateMachineLogicalID}*`,
+                ],
+              ],
+            },
+          },
+        ],
+      },
+    });
+  });
+
+  test('opensearch CfnDomain', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('es.amazonaws.com'),
+    });
+    const domain = new Domain(stack, 'TestDomain', {
+      version: EngineVersion.OPENSEARCH_1_3,
+    });
+    const indexName = 'some-index';
+    domain.grantIndexReadWrite(indexName, role);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const domainNode = domain.node.defaultChild as CfnDomain;
+    const domainLogicalID = stack.resolve(domainNode.logicalId);
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 1);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: [
+          {
+            Resource: Match.arrayWith([
+              {
+                'Fn::Join': [
+                  '',
+                  [
+                    {
+                      'Fn::Join': [
+                        '',
+                        [
+                          'arn:',
+                          { Ref: 'AWS::Partition' },
+                          ':es:',
+                          { Ref: 'AWS::Region' },
+                          ':',
+                          { Ref: 'AWS::AccountId' },
+                          `:domain/${domainLogicalID
+                            .substring(0, 15)
+                            .toLowerCase()}*`,
+                        ],
+                      ],
+                    },
+                    `/${indexName}`,
+                  ],
+                ],
+              },
+            ]),
+          },
+        ],
+      },
+    });
+  });
+  test('ecs CfnTaskDefinition', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const task = new TaskDefinition(stack, 'Test-Task', {
+      compatibility: Compatibility.EC2,
+    });
+    task.grantRun(role);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    app.synth({ force: true });
+
+    const taskNode = task.node.defaultChild as CfnTaskDefinition;
+    const taskLogicalID = stack.resolve(taskNode.logicalId);
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 2);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: Match.arrayWith([
+          Match.objectLike({
+            Resource: {
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':ecs:',
+                  { Ref: 'AWS::Region' },
+                  ':',
+                  { Ref: 'AWS::AccountId' },
+                  `:task-definition/${appStackName}${taskLogicalID}*:*`,
+                ],
+              ],
+            },
+          }),
+        ]),
+      },
+    });
+  });
+
+  test('ecs CfnCluster', () => {
+    const cluster = new Cluster(stack, 'TestCluster');
+    cluster.addCapacity('TestAsgCapacity', {
+      instanceType: new InstanceType('t2.large'),
+      desiredCapacity: 2,
+    });
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+        additionalTransforms: {
+          'AWS::AutoScaling::AutoScalingGroup': '*',
+        },
+      })
+    );
+    app.synth({ force: true });
+
+    const clusterNode = cluster.node.defaultChild as CfnCluster;
+    const clusterLogicalID = stack.resolve(clusterNode.logicalId);
+    const extractedTemplate = Template.fromStack(extractedStack);
+    extractedTemplate.resourceCountIs('AWS::IAM::Role', 3);
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: Match.arrayWith([
+          Match.objectLike({
+            Resource: {
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':ecs:',
+                  { Ref: 'AWS::Region' },
+                  ':',
+                  { Ref: 'AWS::AccountId' },
+                  `:cluster/${appStackName}-${clusterLogicalID}*`,
+                ],
+              ],
+            },
+          }),
+        ]),
+      },
+    });
+  });
+
+  test('additional transforms', () => {
+    const vpc = new Vpc(stack, 'TestVpc');
+    const db = new DatabaseInstance(stack, 'TestDb', {
+      vpc,
+      engine: DatabaseInstanceEngine.POSTGRES,
+    });
+    const func = new Function(stack, 'TestLambda', {
+      code: Code.fromInline(`def handler(event, context)\n    print(event)`),
+      handler: 'index.handler',
+      runtime: Runtime.PYTHON_3_9,
+    });
+    db.secret?.grantRead(func);
+
+    const synthedApp = app.synth();
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+        additionalTransforms: {
+          'AWS::SecretsManager::SecretTargetAttachment': `arn:${Aws.PARTITION}:secretsmanager:${Aws.REGION}:${Aws.ACCOUNT_ID}:secret:some-expected-value*`,
+        },
+      })
+    );
+    app.synth({ force: true });
+
+    const extractedTemplate = Template.fromStack(extractedStack);
+
+    extractedTemplate.hasResourceProperties('AWS::IAM::Policy', {
+      PolicyDocument: {
+        Statement: Match.arrayWith([
+          Match.objectLike({
+            Resource: {
+              'Fn::Join': [
+                '',
+                [
+                  'arn:',
+                  { Ref: 'AWS::Partition' },
+                  ':secretsmanager:',
+                  { Ref: 'AWS::Region' },
+                  ':',
+                  { Ref: 'AWS::AccountId' },
+                  ':secret:some-expected-value*',
+                ],
+              ],
+            },
+          }),
+        ]),
+      },
+    });
+  });
+
+  test('unknown transform error', () => {
+    const userPool = new UserPool(stack, 'TestUserPool');
+    const role = new Role(stack, 'TestRole', {
+      assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
+    });
+    userPool.grant(role, 'cognito-idp:AdminCreateUser');
+
+    const synthedApp = app.synth();
+
+    Aspects.of(app).add(
+      new ResourceExtractor({
+        extractDestinationStack: extractedStack,
+        stackArtifacts: synthedApp.stacks,
+        valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT,
+        resourceTypesToExtract: ['AWS::IAM::Role', 'AWS::IAM::Policy'],
+      })
+    );
+    expect(() => {
+      app.synth({ force: true });
+    }).toThrowError(MissingTransformError);
+  });
+});