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

Issue:
bug: Static Resource Arns
### What is the problem?

In particular, this is for working with S3. When setting a static S3 Bucket Name, it's arn is falsely represented in the extracted stack

### Reproduction Steps

```typescript
#!/usr/bin/env node
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.fromInline('def handler(event, context):\n    print(event)'),
  handler: 'index.handler',
  runtime: Runtime.PYTHON_3_9,
});
const bucket = new Bucket(appStack, 'TestBucket', {
  bucketName:"static-bucket-name",
  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 });
```

### What did you expect to happen?

Goal was to separate out IAM permissions with proper reference to the S3 bucket name

### What actually happened?

The ARN for the S3 bucket in the IAM Role is incorrect. (Should be "static-bucket-name" but got "myappstack-testbucket560b80bc" )
```yaml
TestLambdaServiceRoleDefaultPolicy0F2D5E78:
  Type: AWS::IAM::Policy
  Properties:
    PolicyDocument:
      Statement:
        - Action:
            - s3:GetObject*
            - s3:GetBucket*
            - s3:List*
            - s3:DeleteObject*
            - s3:PutObject
            - s3:PutObjectLegalHold
            - s3:PutObjectRetention
            - s3:PutObjectTagging
            - s3:PutObjectVersionTagging
            - s3:Abort*
          Effect: Allow
          Resource:
            - Fn::Join:
                - ""
                - - "arn:"
                  - Ref: AWS::Partition
                  - :s3:::myappstack-testbucket560b80bc*
            - Fn::Join:
                - ""
                - - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - :s3:::myappstack-testbucket560b80bc*
                  - /*
      Version: "2012-10-17"
```

### cdk-enterprise-iac version

0.0.126

### Language

Typescript

### Other information

This is likely caused as `bucket.bucketArn` and `bucket.bucketName` are both tokens.

If anyone is encountering this issue, there are a few workarounds:
- Don't specify the `bucketName` parameter, allowing CDK to set it's default
- Manually define IAM permissions in CDK, statically typing the resource arn there as well
- Add AWS::S3::Bucket to the extract list as well. Other stacks will reference the value via CFN_Output variables


Discussion / hints:

Fix patch:
diff --git a/src/patches/resource-extractor/cfnStore.ts b/src/patches/resource-extractor/cfnStore.ts
index beac6f8..d37f5a7 100644
--- a/src/patches/resource-extractor/cfnStore.ts
+++ b/src/patches/resource-extractor/cfnStore.ts
@@ -93,6 +93,19 @@ export class CfnStore {
     return resourceType;
   }
 
+  /**
+   *
+   * @param stackName Stack name
+   * @param logicalId the logical ID of the Resource to find
+   * @returns Json object of the Cloudformation resource properties
+   */
+  public getResourcePropertiesFromLogicalId(
+    stackName: string,
+    logicalId: string
+  ): Json {
+    return this.templates[stackName].Resources[logicalId].Properties;
+  }
+
   /**
    * Performs a Describe Stack API call with the AWS SDK to determine what
    * the CloudFormation Exports are for a given Stack Name.
diff --git a/src/patches/resource-extractor/resourceTransformer.ts b/src/patches/resource-extractor/resourceTransformer.ts
index 498444d..9d487db 100644
--- a/src/patches/resource-extractor/resourceTransformer.ts
+++ b/src/patches/resource-extractor/resourceTransformer.ts
@@ -16,9 +16,13 @@ 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';
+import { FlatJson, Json } from './types';
 
-type Transformer = (stackName: string, logicalId: string) => string;
+type Transformer = (
+  stackName: string,
+  logicalId: string,
+  resourceProperties: Json
+) => string;
 
 export class MissingTransformError extends Error {
   constructor(resourceType: string) {
@@ -79,69 +83,132 @@ export class ResourceTransformer {
   private createDefaultTransforms(): { [key: string]: Transformer } {
     return {
       /** Standard */
-      [CfnTable.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnTable.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName}-${logicalId}*`;
         const preamble = this.generateArnPreamble('dynamodb');
         return `${preamble}:table/${partial}`;
       },
-      [CfnDomainEss.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+      [CfnDomainEss.CFN_RESOURCE_TYPE_NAME]: (
+        _,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${logicalId.substring(0, 15).toLowerCase()}*`;
         const preamble = this.generateArnPreamble('es');
         return `${preamble}:domain/${partial}`;
       },
-      [CfnDomainOss.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+      [CfnDomainOss.CFN_RESOURCE_TYPE_NAME]: (
+        _,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${logicalId.substring(0, 15).toLowerCase()}*`;
         const preamble = this.generateArnPreamble('es');
         return `${preamble}:domain/${partial}`;
       },
-      [CfnTaskDefinition.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnTaskDefinition.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName}${logicalId}*:*`.replace('-', '');
         const preamble = this.generateArnPreamble('ecs');
         return `${preamble}:task-definition/${partial}`;
       },
-      [CfnCluster.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnCluster.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName}-${logicalId}*`;
         const preamble = this.generateArnPreamble('ecs');
         return `${preamble}:cluster/${partial}`;
       },
       /** Colon-resource name grouping */
-      [CfnLogGroup.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnLogGroup.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName}-${logicalId}*:*`;
         const preamble = this.generateArnPreamble('logs');
         return `${preamble}:log-group:${partial}`;
       },
-      [CfnFunction.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnFunction.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName}-${logicalId}`.substring(0, 50) + '*';
         const preamble = this.generateArnPreamble('lambda');
         return `${preamble}:function:${partial}`;
       },
-      [CfnStateMachine.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+      [CfnStateMachine.CFN_RESOURCE_TYPE_NAME]: (
+        _,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${logicalId}*`;
         const preamble = this.generateArnPreamble('states');
         return `${preamble}:stateMachine:${partial}`;
       },
       /** No resource name grouping */
-      [CfnQueue.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnQueue.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName}-${logicalId}*`;
         const preamble = this.generateArnPreamble('sqs');
         return `${preamble}:${partial}`;
       },
-      [CfnBucket.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
-        const partial = `${stackName}-${logicalId}*`.toLowerCase();
+      [CfnBucket.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        resourceProperties
+      ) => {
+        let partial: string;
+        if (
+          resourceProperties !== undefined &&
+          'BucketName' in resourceProperties
+        ) {
+          partial = `${resourceProperties.BucketName}*`;
+        } else {
+          partial = `${stackName}-${logicalId}*`.toLowerCase();
+        }
         const preamble = this.generateArnPreamble('s3', false, false);
         return `${preamble}:${partial}`;
       },
-      [CfnTopic.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnTopic.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName}-${logicalId}*`;
         const preamble = this.generateArnPreamble('sns');
         return `${preamble}:${partial}`;
       },
       /** Non-standard */
-      [CfnParameter.CFN_RESOURCE_TYPE_NAME]: (_, logicalId) => {
+      [CfnParameter.CFN_RESOURCE_TYPE_NAME]: (
+        _,
+        logicalId,
+        resourceProperties
+      ) => {
+        if ('Name' in resourceProperties) {
+          return resourceProperties.Name;
+        }
         const partial = `CFN-${logicalId}*`;
         return partial;
       },
-      [CfnApiKey.CFN_RESOURCE_TYPE_NAME]: (stackName, logicalId) => {
+      [CfnApiKey.CFN_RESOURCE_TYPE_NAME]: (
+        stackName,
+        logicalId,
+        _resourceProperties
+      ) => {
         const partial = `${stackName.substring(0, 6)}-${logicalId.substring(
           0,
           5
@@ -170,6 +237,10 @@ export class ResourceTransformer {
   public toPartial(logicalId: string) {
     const stackName = this.cfn.getStackNameFromLogicalId(logicalId);
     const resourceType = this.cfn.getResourceTypeFromLogicalId(logicalId);
+    const resourceProperties = this.cfn.getResourcePropertiesFromLogicalId(
+      stackName,
+      logicalId
+    );
 
     if (
       this.additionalTransforms &&
@@ -180,7 +251,11 @@ export class ResourceTransformer {
         .replace(ResourceTransform.LOGICAL_ID, logicalId);
     } else if (resourceType in this.defaultTransforms) {
       const transformResourceToPartial = this.defaultTransforms[resourceType];
-      return transformResourceToPartial(stackName, logicalId);
+      return transformResourceToPartial(
+        stackName,
+        logicalId,
+        resourceProperties
+      );
     } else {
       throw new MissingTransformError(resourceType);
     }


Test patch:
diff --git a/test/patches/resource-extractor/resourceTransformer.test.ts b/test/patches/resource-extractor/resourceTransformer.test.ts
index 93fdc70..c148375 100644
--- a/test/patches/resource-extractor/resourceTransformer.test.ts
+++ b/test/patches/resource-extractor/resourceTransformer.test.ts
@@ -204,6 +204,76 @@ describe('toPartial scenarios', () => {
     });
   });
 
+  test('s3 CfnBucket with set name', () => {
+    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 bucketName = 'my-fancy-test-bucket';
+    const bucket = new Bucket(stack, 'TestBucket', {
+      bucketName,
+    });
+    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:::${bucketName}*`],
+                ],
+              }),
+            ]),
+          }),
+        ]),
+      },
+    });
+  });
+
   test('ssm CfnParameter', () => {
     const role = new Role(stack, 'testRole', {
       assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
@@ -252,6 +322,58 @@ describe('toPartial scenarios', () => {
       },
     });
   });
+  test('ssm CfnParameter set name', () => {
+    const role = new Role(stack, 'testRole', {
+      assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
+    });
+    const parameterName = 'Some-Provided-Name';
+    const parameter = new StringParameter(stack, 'TestParameter', {
+      stringValue: 'someTestString',
+      parameterName,
+    });
+    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);
+    const appTemplate = Template.fromStack(stack);
+    console.log(appTemplate);
+    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/',
+                  parameterName,
+                ],
+              ],
+            }),
+          },
+        ],
+      },
+    });
+  });
 
   test('dynamodb CfnTable', () => {
     const role = new Role(stack, 'testRole', {