Repository: aws-cloudformation/rain
Pull request #177 (2023-08-08 23:05:32)

Forecast permissions checks
Arn generation and IAM action configuration for S3, IAM, EC2, and Lambda to enable the forecast command to verify if you have the permissions needed to deploy those resources.



Patch:
diff --git a/internal/aws/cfn/cfn.go b/internal/aws/cfn/cfn.go
index 471436a4..1133df31 100644
--- a/internal/aws/cfn/cfn.go
+++ b/internal/aws/cfn/cfn.go
@@ -55,6 +55,8 @@ var liveStatuses = []types.StackStatus{
 
 const WAIT_PERIOD_IN_SECONDS = 2
 
+var Schemas map[string]string
+
 func checkTemplate(template cft.Template) (string, error) {
 	templateBody := format.String(template, format.Options{})
 
@@ -758,19 +760,29 @@ func WaitUntilStackCreateComplete(stackName string) error {
 
 // Get the schema for a CloudFormation resource type
 func GetTypeSchema(name string) (string, error) {
-	res, err := getClient().DescribeType(context.Background(), &cloudformation.DescribeTypeInput{
-		Type: "RESOURCE", TypeName: &name,
-	})
-	if err != nil {
-		return "", nil
+	schema, exists := Schemas[name]
+	if exists {
+		config.Debugf("Already downloaded schema for %v", name)
+		return schema, nil
+	} else {
+		config.Debugf("Downloading schema for %v", name)
+		res, err := getClient().DescribeType(context.Background(), &cloudformation.DescribeTypeInput{
+			Type: "RESOURCE", TypeName: &name,
+		})
+		if err != nil {
+			config.Debugf("GetTypeSchema SDK error: %v", err)
+			return "", err
+		}
+		Schemas[name] = *res.Schema
+		return *res.Schema, nil
 	}
-	return *res.Schema, nil
 }
 
-// Get the list of action required to invoke a CloudFormation handler
+// Get the list of actions required to invoke a CloudFormation handler
 func GetTypePermissions(name string, handlerVerb string) ([]string, error) {
-	schema, err := GetTypeSchema(name)
 
+	// Get the schema, checking to see if we cached it
+	schema, err := GetTypeSchema(name)
 	if err != nil {
 		return nil, err
 	}
@@ -785,12 +797,160 @@ func GetTypePermissions(name string, handlerVerb string) ([]string, error) {
 	           "s3:PutBucketTagging",
 
 	*/
-	handlers := result["handlers"].(map[string]any)
-	handler := handlers[handlerVerb].(map[string]any)
+
+	config.Debugf("GetTypePermissions result: %v", result)
+
+	retval := make([]string, 0)
+
+	handlerMap, exists := result["handlers"]
+	if !exists {
+		// Resources that have not been fully migrated to the registry won't have this.
+		// This is a best guess.. don't think legacy resource permissions are documented anywhere
+		// This will become dead code as soon as the permissions are available from the registry.
+		if name == "AWS::EC2::Instance" {
+			handlerMap = map[string]any{
+				"create": map[string]any{
+					"permissions": []any{
+						"ec2:AttachVolume",
+						"ec2:CreateTags",
+						"ec2:RunInstances",
+						"ec2:StartInstances",
+					},
+				},
+				"read": map[string]any{
+					"permissions": []any{
+						"ec2:DescribeInstanceAttribute",
+						"ec2:DescribeInstanceStatus",
+						"ec2:DescribeInstances",
+						"ec2:DescribeTags",
+					},
+				},
+				"update": map[string]any{
+					"permissions": []any{
+						"ec2:AttachVolume",
+						"ec2:CreateTags",
+						"ec2:DeleteTags",
+						"ec2:DescribeInstanceAttribute",
+						"ec2:DescribeInstanceStatus",
+						"ec2:DescribeInstances",
+						"ec2:DescribeTags",
+						"ec2:DetachVolume",
+						"ec2:ModifyInstanceAttribute",
+						"ec2:StartInstances",
+						"ec2:StopInstances",
+						"ec2:TerminateInstances",
+					},
+				},
+				"delete": map[string]any{
+					"permissions": []any{
+						"ec2:DeleteTags",
+						"ec2:DescribeInstanceAttribute",
+						"ec2:DescribeInstanceStatus",
+						"ec2:DescribeInstances",
+						"ec2:DescribeTags",
+						"ec2:DetachVolume",
+						"ec2:StopInstances",
+						"ec2:TerminateInstances",
+					},
+				},
+			}
+		} else if name == "AWS::Lambda::Alias" {
+			handlerMap = map[string]any{
+				"create": map[string]any{
+					"permissions": []any{
+						"lambda:CreateAlias",
+						"lambda:GetAlias",
+						"lambda:GetFunctionConfiguration",
+					},
+				},
+				"read": map[string]any{
+					"permissions": []any{
+						"lambda:GetAlias",
+						"lambda:GetFunctionConfiguration",
+					},
+				},
+				"update": map[string]any{
+					"permissions": []any{
+						"lambda:CreateAlias",
+						"lambda:DeleteAlias",
+						"lambda:GetAlias",
+						"lambda:GetFunctionConfiguration",
+						"lambda:UpdateAlias",
+					},
+				},
+				"delete": map[string]any{
+					"permissions": []any{
+						"lambda:DeleteAlias",
+						"lambda:GetAlias",
+						"lambda:GetFunctionConfiguration",
+					},
+				},
+			}
+		} else if name == "AWS::Lambda::Version" {
+			handlerMap = map[string]any{
+				"create": map[string]any{
+					"permissions": []any{
+						"lambda:GetFunctionConfiguration",
+						"lambda:CreateFunction",
+						"lambda:GetFunction",
+						"lambda:PutFunctionConcurrency",
+						"lambda:GetCodeSigningConfig",
+						"lambda:GetFunctionCodeSigningConfig",
+						"lambda:GetRuntimeManagementConfig",
+						"lambda:PutRuntimeManagementConfig",
+					},
+				},
+				"read": map[string]any{
+					"permissions": []any{
+						"lambda:GetFunctionConfiguration",
+						"lambda:GetFunction",
+						"lambda:GetFunctionCodeSigningConfig",
+					},
+				},
+				"update": map[string]any{
+					"permissions": []any{
+						"lambda:GetFunctionConfiguration",
+						"lambda:DeleteFunctionConcurrency",
+						"lambda:GetFunction",
+						"lambda:PutFunctionConcurrency",
+						"lambda:ListTags",
+						"lambda:TagResource",
+						"lambda:UntagResource",
+						"lambda:UpdateFunctionConfiguration",
+						"lambda:UpdateFunctionCode",
+						"lambda:PutFunctionCodeSigningConfig",
+						"lambda:DeleteFunctionCodeSigningConfig",
+						"lambda:GetCodeSigningConfig",
+						"lambda:GetFunctionCodeSigningConfig",
+						"lambda:GetRuntimeManagementConfig",
+						"lambda:PutRuntimeManagementConfig",
+					},
+				},
+				"delete": map[string]any{
+					"permissions": []any{
+						"lambda:GetFunctionConfiguration",
+						"lambda:DeleteFunction",
+					},
+				},
+			}
+		} else {
+			// Return an empty array
+			config.Debugf("No data on what permissions are required for %v", name)
+			return retval, nil
+		}
+	}
+	config.Debugf("handlerMap: %v", handlerMap)
+	handlers := handlerMap.(map[string]any)
+	handlerVerbMap, exists := handlers[handlerVerb]
+	if !exists {
+		config.Debugf("handler verb is missing: %v", handlerVerb)
+		// Some resources can't be updated, for example
+		return retval, nil
+	}
+	handler := handlerVerbMap.(map[string]any)
 	config.Debugf("handler: %v", handler)
 	permissions := handler["permissions"].([]interface{})
 	config.Debugf("Got permissions for %v %v: %v", name, handlerVerb, permissions)
-	retval := make([]string, 0)
 	for _, p := range permissions {
 		if p == "iam:PassRole" {
 			// This will fail even for admin roles, and is not actually necessary
@@ -809,16 +969,30 @@ func GetTypeIdentifier(name string) ([]string, error) {
 	if err != nil {
 		return nil, err
 	}
+	if schema == "" {
+		return nil, errors.New("schema is empty")
+	}
+
 	var result map[string]any
 	json.Unmarshal([]byte(schema), &result)
 
-	pi := result["primaryIdentifier"].([]interface{})
-	retval := make([]string, 0)
-	for _, pid := range pi {
-		retval = append(retval, strings.Replace(fmt.Sprintf("%v", pid), "/properties/", "", 1))
+	config.Debugf("GetTypeIdentifier schema for %s: %v", name, result)
+
+	piNode, exists := result["primaryIdentifier"]
+	if !exists {
+		// The schema does not have a primary identifier.
+		// TODO
+		config.Debugf("GetTypeIdentifier %v does not have a primaryIdentifier", name)
+		return nil, errors.New("no primary identifier")
+	} else {
+		pi := piNode.([]interface{})
+		retval := make([]string, 0)
+		for _, pid := range pi {
+			retval = append(retval, strings.Replace(fmt.Sprintf("%v", pid), "/properties/", "", 1))
+		}
+		config.Debugf("GetTypeIdentifier for %v: %v", name, retval)
+		return retval, nil
 	}
-	config.Debugf("GetTypeIdentifier for %v: %v", name, retval)
-	return retval, nil
 }
 
 // Get the values specified for primary identifiers in the template.
@@ -910,7 +1084,8 @@ func ResourceAlreadyExists(
 	if !stackExists {
 		primaryIdentifiers, err := GetTypeIdentifier(typeName)
 		if err != nil {
-			fmt.Println("Unable to get primary identifier for ", err)
+			config.Debugf("Unable to get primary identifier for %v: %v", typeName, err)
+			return false
 		} else {
 			config.Debugf("PrimaryIdentifiers: %v", primaryIdentifiers)
 
@@ -938,3 +1113,7 @@ func ResourceAlreadyExists(
 
 	return false
 }
+
+func init() {
+	Schemas = make(map[string]string)
+}
diff --git a/internal/aws/iam/iam.go b/internal/aws/iam/iam.go
index 722ebaca..2743d2b0 100644
--- a/internal/aws/iam/iam.go
+++ b/internal/aws/iam/iam.go
@@ -22,7 +22,7 @@ func getClient() *iam.Client {
 }
 
 // Get the role arn of the caller based on the aws config
-func getCallerArn(config awsgo.Config, iamClient *iam.Client) (string, error) {
+func GetCallerArn(config awsgo.Config) (string, error) {
 	stsClient := sts.NewFromConfig(config)
 	stsRes, stsErr := stsClient.GetCallerIdentity(context.Background(),
 		&sts.GetCallerIdentityInput{})
@@ -47,7 +47,12 @@ func getCallerArn(config awsgo.Config, iamClient *iam.Client) (string, error) {
 
 // Simulate actions on a resource.
 // The role arg is optional, if not provided, the current aws config will be used.
-func Simulate(actions []string, resource string, roleArn string, spinnerCallback func(string)) (bool, []string) {
+func Simulate(
+	actions []string,
+	resource string,
+	roleArn string,
+	spinnerCallback func(string)) (bool, []string) {
+
 	awsConfig := aws.Config()
 	client := iam.NewFromConfig(awsConfig)
 	input := &iam.SimulatePrincipalPolicyInput{}
@@ -55,15 +60,6 @@ func Simulate(actions []string, resource string, roleArn string, spinnerCallback
 
 	messages := make([]string, 0)
 
-	var err error
-	if roleArn == "" {
-		roleArn, err = getCallerArn(awsConfig, client)
-		if err != nil {
-			messages = append(messages, fmt.Sprintf("Could not get caller arn: %v", err))
-			return false, messages
-		}
-	}
-	config.Debugf("Caller role arn: %v", roleArn)
 	input.PolicySourceArn = &roleArn
 
 	// Return value
diff --git a/internal/cmd/forecast/arns.go b/internal/cmd/forecast/arns.go
new file mode 100644
index 00000000..0d52afd8
--- /dev/null
+++ b/internal/cmd/forecast/arns.go
@@ -0,0 +1,477 @@
+package forecast
+
+import (
+	"fmt"
+
+	"github.com/aws-cloudformation/rain/internal/aws/cfn"
+	"github.com/aws-cloudformation/rain/internal/config"
+	"github.com/aws-cloudformation/rain/internal/node"
+	"github.com/aws-cloudformation/rain/internal/s11n"
+	"github.com/google/uuid"
+)
+
+// predictResourceArn returns an arn that matches the format for the resource.
+// Returns "" if we don't know how to make the arn, we haven't implemented it yet, or if
+// the resource does not have Arns that we can use to plug in to the simulator.
+// Supported services: S3, EC2
+func predictResourceArn(input PredictionInput) string {
+
+	// There's not a great way to do this in a truly generic fashion.
+	// There is no direct map between IAM resource ARNs and CloudFormation resource types.
+
+	// We considered trying to generate something based on the arn patterns in
+	// https://awspolicygen.s3.amazonaws.com/js/policies.js
+
+	// Make up an id if it doesn't exist yet
+	physicalId := fmt.Sprintf("rain-%v", uuid.New())
+
+	if input.stackExists {
+
+		config.Debugf("predictResourceArn stack exists")
+
+		res, err := cfn.GetStackResource(input.stackName, input.logicalId)
+		if err == nil {
+			// The resource exists
+			physicalId = *res.PhysicalResourceId
+
+			// This physical id is not super useful
+			// It's often an internal id that is not part of the arn
+
+			config.Debugf("predictResourceArn physicalId: %v", physicalId)
+		} else {
+			config.Debugf("predictResourceArn got an error trying to get stack resource: %v", err)
+		}
+	}
+
+	// There might be restrictions on the format of a physical id, so
+	// we might have to change it per resource type.
+
+	// Some resources have complex arns that will require us to inspect input.resource
+
+	// To add new resources here, a good way to start is by opening both the CloudFormation
+	// docs for the service, and the Service Authorization Reference (Resource Types section)
+	//
+	// for example
+	//
+	// https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_S3.html
+	// and
+	// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html#amazons3-resources-for-iam-policies
+
+	switch input.typeName {
+	case "AWS::S3::Bucket":
+		return fmt.Sprintf("arn:aws:s3:::%v", physicalId)
+	case "AWS::S3::BucketPolicy":
+		return ""
+	case "AWS::S3::AccessPoint":
+		// arn:${Partition}:s3:${Region}:${Account}:accesspoint/${AccessPointName}
+		return fmt.Sprintf("arn:%v:s3:%v:%v:accesspoint/%v",
+			input.env.partition, input.env.region, input.env.account, physicalId)
+	case "AWS::S3::MultiRegionAccessPoint":
+		// arn:${Partition}:s3::${Account}:accesspoint/${AccessPointAlias}
+		return fmt.Sprintf("arn:%v:s3::%v:accesspoint/%v",
+			input.env.partition, input.env.account, physicalId)
+	case "AWS::S3::MultiRegionAccessPointPolicy":
+		return ""
+	case "AWS::S3::StorageLens":
+		// arn:${Partition}:s3:${Region}:${Account}:storage-lens/${ConfigId}
+		return standardFormat("s3", "storage-lens", input, physicalId)
+	case "AWS::EC2::CapacityReservation":
+		// arn:${Partition}:ec2:${Region}:${Account}:capacity-reservation/${CapacityReservationId}
+		return standardFormat("ec2", "capacity-reservation", input, physicalId)
+	case "AWS::EC2::CapacityReservationFleet":
+		// arn:${Partition}:ec2:${Region}:${Account}:capacity-reservation-fleet/${CapacityReservationFleetId}
+		return standardFormat("ec2", "capacity-reservation-fleet", input, physicalId)
+	case "AWS::EC2::CarrierGateway":
+		// arn:${Partition}:ec2:${Region}:${Account}:carrier-gateway/${CarrierGatewayId}
+		return standardFormat("ec2", "carrier-gateway", input, physicalId)
+	case "AWS::EC2::ClientVpnAuthorizationRule":
+		return ""
+	case "AWS::EC2::ClientVpnEndpoint":
+		// arn:${Partition}:ec2:${Region}:${Account}:client-vpn-endpoint/${ClientVpnEndpointId}
+		return standardFormat("ec2", "client-vpn-endpoint", input, physicalId)
+	case "AWS::EC2::ClientVpnRoute":
+		return ""
+	case "AWS::EC2::ClientVpnTargetNetworkAssociation":
+		return ""
+	case "AWS::EC2::CustomerGateway":
+		// arn:${Partition}:ec2:${Region}:${Account}:customer-gateway/${CustomerGatewayId}
+		return standardFormat("ec2", "customer-gateway", input, physicalId)
+	case "AWS::EC2::DHCPOptions":
+		// arn:${Partition}:ec2:${Region}:${Account}:dhcp-options/${DhcpOptionsId}
+		return standardFormat("ec2", "dhcp-options", input, physicalId)
+	case "AWS::EC2::EC2Fleet":
+		return ""
+	case "AWS::EC2::EgressOnlyInternetGateway":
+		// arn:${Partition}:ec2:${Region}:${Account}:egress-only-internet-gateway/${EgressOnlyInternetGatewayId}
+		return standardFormat("ec2", "egress-only-internet-gateway", input, physicalId)
+	case "AWS::EC2::EIP":
+		// arn:${Partition}:ec2:${Region}:${Account}:elastic-ip/${AllocationId}
+		return standardFormat("ec2", "elastic-ip", input, physicalId)
+	case "AWS::EC2::EIPAssociation":
+		return ""
+	case "AWS::EC2::EnclaveCertificateIamRoleAssociation":
+		return ""
+	case "AWS::EC2::FlowLog":
+		return ""
+	case "AWS::EC2::GatewayRouteTableAssociation":
+		return ""
+	case "AWS::EC2::Host":
+		return ""
+	case "AWS::EC2::Instance":
+		// arn:${Partition}:ec2:${Region}:${Account}:instance/${InstanceId}
+		return standardFormat("ec2", "instance", input, physicalId)
+	case "AWS::EC2::InternetGateway":
+		// arn:${Partition}:ec2:${Region}:${Account}:internet-gateway/${InternetGatewayId}
+		return standardFormat("ec2", "internet-gateway", input, physicalId)
+	case "AWS::EC2::IPAM":
+		// arn:${Partition}:ec2::${Account}:ipam/${IpamId}
+		return standardFormat("ec2", "ipam", input, physicalId)
+	case "AWS::EC2::IPAMAllocation":
+		return ""
+	case "AWS::EC2::IPAMPool":
+		// arn:${Partition}:ec2::${Account}:ipam-pool/${IpamPoolId}
+		return standardFormat("ec2", "ipam-pool", input, physicalId)
+	case "AWS::EC2::IPAMPoolCidr":
+		return ""
+	case "AWS::EC2::IPAMResourceDiscovery":
+		// arn:${Partition}:ec2::${Account}:ipam-resource-discovery/${IpamResourceDiscoveryId}
+		return standardFormat("ec2", "ipam-resource-discovery", input, physicalId)
+	case "AWS::EC2::IPAMResourceDiscoveryAssociation":
+		// arn:${Partition}:ec2::${Account}:ipam-resource-discovery-association/${IpamResourceDiscoveryAssociationId}
+		return standardFormat("ec2", "ipam-resource-discovery-association", input, physicalId)
+	case "AWS::EC2::IPAMScope":
+		// arn:${Partition}:ec2::${Account}:ipam-scope/${IpamScopeId}
+		return standardFormat("ec2", "ipam-scope", input, physicalId)
+	case "AWS::EC2::KeyPair":
+		// arn:${Partition}:ec2:${Region}:${Account}:key-pair/${KeyPairName}
+		return standardFormat("ec2", "key-pair", input, physicalId)
+	case "AWS::EC2::LaunchTemplate":
+		// arn:${Partition}:ec2:${Region}:${Account}:launch-template/${LaunchTemplateId}
+		return standardFormat("ec2", "launch-template", input, physicalId)
+	case "AWS::EC2::LocalGatewayRoute":
+		return ""
+	case "AWS::EC2::LocalGatewayRouteTable":
+		// arn:${Partition}:ec2:${Region}:${Account}:local-gateway-route-table/${LocalGatewayRouteTableId}
+		return standardFormat("ec2", "local-gateway-route-table", input, physicalId)
+	case "AWS::EC2::LocalGatewayRouteTableVirtualInterfaceGroupAssociation":
+		// arn:${Partition}:ec2:${Region}:${Account}:local-gateway-route-table-virtual-interface-group-association/${LocalGatewayRouteTableVirtualInterfaceGroupAssociationId}
+		return standardFormat("ec2", "local-gateway-route-table-virtual-interface-group-association", input, physicalId)
+	case "AWS::EC2::LocalGatewayRouteTableVPCAssociation":
+		// arn:${Partition}:ec2:${Region}:${Account}:local-gateway-route-table-vpc-association/${LocalGatewayRouteTableVpcAssociationId}
+		return standardFormat("ec2", "local-gateway-route-table-vpc-association", input, physicalId)
+	case "AWS::EC2::NatGateway":
+		// arn:${Partition}:ec2:${Region}:${Account}:nat-gateway/${NatGatewayId}
+		return standardFormat("ec2", "nat-gateway", input, physicalId)
+	case "AWS::EC2::NetworkAcl":
+		// arn:${Partition}:ec2:${Region}:${Account}:network-acl/${NaclId}
+		return standardFormat("ec2", "network-acl", input, physicalId)
+	case "AWS::EC2::NetworkAclEntry":
+		return ""
+	case "AWS::EC2::NetworkInsightsAccessScope":
+		// arn:${Partition}:ec2:${Region}:${Account}:network-insights-access-scope/${NetworkInsightsAccessScopeId}
+		return standardFormat("ec2", "network-insights-access-scope", input, physicalId)
+	case "AWS::EC2::NetworkInsightsAccessScopeAnalysis":
+		// arn:${Partition}:ec2:${Region}:${Account}:network-insights-access-scope-analysis/${NetworkInsightsAccessScopeAnalysisId}
+		return standardFormat("ec2", "network-insights-access-scope-analysis", input, physicalId)
+	case "AWS::EC2::NetworkInsightsAnalysis":
+		// arn:${Partition}:ec2:${Region}:${Account}:network-insights-analysis/${NetworkInsightsAnalysisId}
+		return standardFormat("ec2", "network-insights-analysis", input, physicalId)
+	case "AWS::EC2::NetworkInsightsPath":
+		// arn:${Partition}:ec2:${Region}:${Account}:network-insights-path/${NetworkInsightsPathId}
+		return standardFormat("ec2", "network-insights-path", input, physicalId)
+	case "AWS::EC2::NetworkInterface":
+		// arn:${Partition}:ec2:${Region}:${Account}:network-interface/${NetworkInterfaceId}
+		return standardFormat("ec2", "network-interface", input, physicalId)
+	case "AWS::EC2::NetworkInterfaceAttachment":
+		return ""
+	case "AWS::EC2::NetworkInterfacePermission":
+		return ""
+	case "AWS::EC2::NetworkPerformanceMetricSubscription":
+		return ""
+	case "AWS::EC2::PlacementGroup":
+		// arn:${Partition}:ec2:${Region}:${Account}:placement-group/${PlacementGroupName}
+		return standardFormat("ec2", "placement-group", input, physicalId)
+	case "AWS::EC2::PrefixList":
+		// arn:${Partition}:ec2:${Region}:${Account}:prefix-list/${PrefixListId}
+		return standardFormat("ec2", "prefix-list", input, physicalId)
+	case "AWS::EC2::Route":
+		return ""
+	case "AWS::EC2::RouteTable":
+		// arn:${Partition}:ec2:${Region}:${Account}:route-table/${RouteTableId}
+		return standardFormat("ec2", "route-table", input, physicalId)
+	case "AWS::EC2::SecurityGroup":
+		// arn:${Partition}:ec2:${Region}:${Account}:security-group/${SecurityGroupId}
+		return standardFormat("ec2", "security-group", input, physicalId)
+	case "AWS::EC2::SecurityGroupEgress":
+		return ""
+	case "AWS::EC2::SecurityGroupIngress":
+		return ""
+	case "AWS::EC2::SpotFleet":
+		// arn:${Partition}:ec2:${Region}:${Account}:spot-fleet-request/${SpotFleetRequestId}
+		return standardFormat("ec2", "spot-fleet-request", input, physicalId)
+	case "AWS::EC2::Subnet":
+		// arn:${Partition}:ec2:${Region}:${Account}:subnet/${SubnetId}
+		return standardFormat("ec2", "subnet", input, physicalId)
+	case "AWS::EC2::SubnetCidrBlock":
+		return ""
+	case "AWS::EC2::SubnetNetworkAclAssociation":
+		return ""
+	case "AWS::EC2::SubnetRouteTableAssociation":
+		return ""
+	case "AWS::EC2::TrafficMirrorFilter":
+		// arn:${Partition}:ec2:${Region}:${Account}:traffic-mirror-filter/${TrafficMirrorFilterId}
+		return standardFormat("ec2", "traffic-mirror-filter", input, physicalId)
+	case "AWS::EC2::TrafficMirrorFilterRule":
+		// arn:${Partition}:ec2:${Region}:${Account}:traffic-mirror-filter-rule/${TrafficMirrorFilterRuleId}
+		return standardFormat("ec2", "traffic-mirror-filter-rule", input, physicalId)
+	case "AWS::EC2::TrafficMirrorSession":
+		// arn:${Partition}:ec2:${Region}:${Account}:traffic-mirror-session/${TrafficMirrorSessionId}
+		return standardFormat("ec2", "traffic-mirror-session", input, physicalId)
+	case "AWS::EC2::TrafficMirrorTarget":
+		// arn:${Partition}:ec2:${Region}:${Account}:traffic-mirror-target/${TrafficMirrorTargetId}
+		return standardFormat("ec2", "traffic-mirror-target", input, physicalId)
+	case "AWS::EC2::TransitGateway":
+		// arn:${Partition}:ec2:${Region}:${Account}:transit-gateway/${TransitGatewayId}
+		return standardFormat("ec2", "transit-gateway", input, physicalId)
+	case "AWS::EC2::TransitGatewayAttachment":
+		// arn:${Partition}:ec2:${Region}:${Account}:transit-gateway-attachment/${TransitGatewayAttachmentId}
+		return standardFormat("ec2", "transit-gateway-attachment", input, physicalId)
+	case "AWS::EC2::TransitGatewayConnect":
+		return ""
+	case "AWS::EC2::TransitGatewayMulticastDomain":
+		// arn:${Partition}:ec2:${Region}:${Account}:transit-gateway-multicast-domain/${TransitGatewayMulticastDomainId}
+		return standardFormat("ec2", "transit-gateway-multicast-domain", input, physicalId)
+	case "AWS::EC2::TransitGatewayMulticastDomainAssociation":
+		return ""
+	case "AWS::EC2::TransitGatewayMulticastGroupMember":
+		return ""
+	case "AWS::EC2::TransitGatewayMulticastGroupSource":
+		return ""
+	case "AWS::EC2::TransitGatewayPeeringAttachment":
+		return ""
+	case "AWS::EC2::TransitGatewayRoute":
+		return ""
+	case "AWS::EC2::TransitGatewayRouteTable":
+		// arn:${Partition}:ec2:${Region}:${Account}:transit-gateway-route-table/${TransitGatewayRouteTableId}
+		return standardFormat("ec2", "transit-gateway-route-table", input, physicalId)
+	case "AWS::EC2::TransitGatewayRouteTableAssociation":
+		return ""
+	case "AWS::EC2::TransitGatewayRouteTablePropagation":
+		return ""
+	case "AWS::EC2::TransitGatewayVpcAttachment":
+		return ""
+	case "AWS::EC2::VerifiedAccessEndpoint":
+		// arn:${Partition}:ec2:${Region}:${Account}:verified-access-endpoint/${VerifiedAccessEndpointId}
+		return standardFormat("ec2", "verified-access-endpoint", input, physicalId)
+	case "AWS::EC2::VerifiedAccessGroup":
+		// arn:${Partition}:ec2:${Region}:${Account}:verified-access-group/${VerifiedAccessGroupId}
+		return standardFormat("ec2", "verified-access-group", input, physicalId)
+	case "AWS::EC2::VerifiedAccessInstance":
+		// arn:${Partition}:ec2:${Region}:${Account}:verified-access-instance/${VerifiedAccessInstanceId}
+		return standardFormat("ec2", "verified-access-instance", input, physicalId)
+	case "AWS::EC2::VerifiedAccessTrustProvider":
+		// arn:${Partition}:ec2:${Region}:${Account}:verified-access-trust-provider/${VerifiedAccessTrustProviderId}
+		return standardFormat("ec2", "verified-access-trust-provider", input, physicalId)
+	case "AWS::EC2::Volume":
+		// arn:${Partition}:ec2:${Region}:${Account}:volume/${VolumeId}
+		return standardFormat("ec2", "volume", input, physicalId)
+	case "AWS::EC2::VolumeAttachment":
+		return ""
+	case "AWS::EC2::VPC":
+		// arn:${Partition}:ec2:${Region}:${Account}:vpc/${VpcId}
+		return standardFormat("ec2", "vpc", input, physicalId)
+	case "AWS::EC2::VPCCidrBlock":
+		return ""
+	case "AWS::EC2::VPCDHCPOptionsAssociation":
+		return ""
+	case "AWS::EC2::VPCEndpoint":
+		// arn:${Partition}:ec2:${Region}:${Account}:vpc-endpoint/${VpcEndpointId}
+		return standardFormat("ec2", "vpc-endpoint", input, physicalId)
+	case "AWS::EC2::VPCEndpointConnectionNotification":
+		return ""
+	case "AWS::EC2::VPCEndpointService":
+		// arn:${Partition}:ec2:${Region}:${Account}:vpc-endpoint-service/${VpcEndpointServiceId}
+		return standardFormat("ec2", "vpc-endpoint-service", input, physicalId)
+	case "AWS::EC2::VPCEndpointServicePermissions":
+		// arn:${Partition}:ec2:${Region}:${Account}:vpc-endpoint-service-permission/${VpcEndpointServicePermissionId}
+		return standardFormat("ec2", "vpc-endpoint-service-permission", input, physicalId)
+	case "AWS::EC2::VPCGatewayAttachment":
+		return ""
+	case "AWS::EC2::VPCPeeringConnection":
+		// arn:${Partition}:ec2:${Region}:${Account}:vpc-peering-connection/${VpcPeeringConnectionId}
+		return standardFormat("ec2", "vpc-peering-connection", input, physicalId)
+	case "AWS::EC2::VPNConnection":
+		// arn:${Partition}:ec2:${Region}:${Account}:vpn-connection/${VpnConnectionId}
+		return standardFormat("ec2", "vpn-connection", input, physicalId)
+	case "AWS::EC2::VPNConnectionRoute":
+		return ""
+	case "AWS::EC2::VPNGateway":
+		// arn:${Partition}:ec2:${Region}:${Account}:vpn-gateway/${VpnGatewayId}
+		return standardFormat("ec2", "vpn-gateway", input, physicalId)
+	case "AWS::EC2::VPNGatewayRoutePropagation":
+		return ""
+	case "AWS::Lambda::Alias":
+		// arn:${Partition}:lambda:${Region}:${Account}:function:${FunctionName}:${Alias}
+		// Get the function name and alias from the template
+		_, props := s11n.GetMapValue(input.resource, "Properties")
+		if props == nil {
+			config.Debugf("unexpected, AWS::Lambda::Alias props are nil: %v",
+				node.ToJson(input.resource))
+			return ""
+		}
+		_, functionName := s11n.GetMapValue(props, "FunctionName")
+		if functionName == nil {
+			config.Debugf("unexpected, AWS::Lambda::Alias FunctionName is nil: %v",
+				node.ToJson(input.resource))
+			return ""
+		}
+		_, name := s11n.GetMapValue(props, "Name")
+		if name == nil {
+			config.Debugf("unexpected, AWS::Lambda::Alias Name is nil: %v",
+				node.ToJson(input.resource))
+			return ""
+		}
+		fn := functionName.Value
+		if fn == "" {
+			fn = physicalId
+			if fn == "" {
+				fn = "function-name"
+			}
+		}
+		return fmt.Sprintf("arn:%v:lambda:%v:%v:function:%v:%v",
+			input.env.partition, input.env.region, input.env.account,
+			fn, name.Value)
+	case "AWS::Lambda::CodeSigningConfig":
+		// arn:${Partition}:lambda:${Region}:${Account}:code-signing-config:${CodeSigningConfigId}
+		return colonFormat("lambda", "code-signing-config", input, physicalId)
+	case "AWS::Lambda::EventInvokeConfig":
+		return ""
+	case "AWS::Lambda::EventSourceMapping":
+		// arn:${Partition}:lambda:${Region}:${Account}:event-source-mapping:${UUID}
+		return colonFormat("lambda", "event-source-mapping", input, physicalId)
+	case "AWS::Lambda::Function":
+		// arn:${Partition}:lambda:${Region}:${Account}:function:${FunctionName}
+		return colonFormat("lambda", "function", input, physicalId)
+	case "AWS::Lambda::LayerVersion":
+		// arn:${Partition}:lambda:${Region}:${Account}:layer:${LayerName}:${LayerVersion}
+		_, props := s11n.GetMapValue(input.resource, "Properties")
+		if props == nil {
+			config.Debugf("unexpected, AWS::Lambda::Alias props are nil: %v",
+				node.ToJson(input.resource))
+			return ""
+		}
+		_, layerNameProp := s11n.GetMapValue(props, "LayerName")
+		layerName := ""
+		if layerNameProp != nil {
+			layerName = layerNameProp.Value
+		}
+		if layerName == "" {
+			layerName = physicalId
+			if layerName == "" {
+				layerName = "layername"
+			}
+		}
+		return fmt.Sprintf("arn:%v:lambda:%v:%v:layer:%v:%v",
+			input.env.partition, input.env.region, input.env.account,
+			layerName, "1")
+	case "AWS::Lambda::LayerVersionPermission":
+		return ""
+	case "AWS::Lambda::Permission":
+		return ""
+	case "AWS::Lambda::Url":
+		return ""
+	case "AWS::Lambda::Version":
+		// arn:${Partition}:lambda:${Region}:${Account}:function:${FunctionName}:${Version}
+		_, props := s11n.GetMapValue(input.resource, "Properties")
+		if props == nil {
+			config.Debugf("unexpected, AWS::Lambda::Version props are nil: %v",
+				node.ToJson(input.resource))
+			return ""
+		}
+		_, functionName := s11n.GetMapValue(props, "FunctionName")
+		if functionName == nil {
+			config.Debugf("unexpected, AWS::Lambda::Version FunctionName is nil: %v",
+				node.ToJson(input.resource))
+			return ""
+		}
+		fn := functionName.Value
+		if fn == "" {
+			fn = physicalId
+			if fn == "" {
+				fn = "function-name"
+			}
+		}
+		return fmt.Sprintf("arn:%v:lambda:%v:%v:function:%v:%v",
+			input.env.partition, input.env.region, input.env.account,
+			fn, "1")
+	case "AWS::IAM::AccessKey":
+		return ""
+	case "AWS::IAM::Group":
+		// arn:${Partition}:iam::${Account}:group/${GroupNameWithPath}
+		return globalFormat("iam", "group", input, physicalId)
+	case "AWS::IAM::GroupPolicy":
+		// arn:${Partition}:iam::${Account}:policy/${PolicyNameWithPath}
+		return globalFormat("iam", "policy", input, physicalId)
+	case "AWS::IAM::InstanceProfile":
+		// arn:${Partition}:iam::${Account}:instance-profile/${InstanceProfileNameWithPath}
+		return globalFormat("iam", "instance-profile", input, physicalId)
+	case "AWS::IAM::ManagedPolicy":
+		// arn:${Partition}:iam::${Account}:policy/${PolicyNameWithPath}
+		return globalFormat("iam", "policy", input, physicalId)
+	case "AWS::IAM::OIDCProvider":
+		// arn:${Partition}:iam::${Account}:oidc-provider/${OidcProviderName}
+		return globalFormat("iam", "oidc-provider", input, physicalId)
+	case "AWS::IAM::Policy":
+		// arn:${Partition}:iam::${Account}:policy/${PolicyNameWithPath}
+		return globalFormat("iam", "policy", input, physicalId)
+	case "AWS::IAM::Role":
+		// arn:${Partition}:iam::${Account}:role/${RoleNameWithPath}
+		return globalFormat("iam", "role", input, physicalId)
+	case "AWS::IAM::RolePolicy":
+		return ""
+	case "AWS::IAM::SAMLProvider":
+		// arn:${Partition}:iam::${Account}:saml-provider/${SamlProviderName}
+		return globalFormat("iam", "saml-provider", input, physicalId)
+	case "AWS::IAM::ServerCertificate":
+		// arn:${Partition}:iam::${Account}:server-certificate/${CertificateNameWithPath}
+		return globalFormat("iam", "server-provider", input, physicalId)
+	case "AWS::IAM::ServiceLinkedRole":
+		return ""
+	case "AWS::IAM::User":
+		// arn:${Partition}:iam::${Account}:user/${UserNameWithPath}
+		return globalFormat("iam", "user", input, physicalId)
+	case "AWS::IAM::UserPolicy":
+		// arn:${Partition}:iam::${Account}:policy/${PolicyNameWithPath}
+		return globalFormat("iam", "policy", input, physicalId)
+	case "AWS::IAM::UserToGroupAddition":
+		return ""
+	case "AWS::IAM::VirtualMFADevice":
+		return ""
+	default:
+		return ""
+	}
+}
+
+// standardFormat returns an arn that fits the standard arn format.
+// for example
+// arn:${Partition}:ec2:${Region}:${Account}:capacity-reservation/${CapacityReservationId}
+func standardFormat(service string, subType string, input PredictionInput, physicalId string) string {
+	return fmt.Sprintf("arn:%v:%v:%v:%v:%v/%v",
+		input.env.partition, service, input.env.region, input.env.account, subType, physicalId)
+}
+
+// colonFormat returns an arn that fits the (other) standard arn format.
+// for example
+// arn:${Partition}:ec2:${Region}:${Account}:capacity-reservation:${CapacityReservationId}
+func colonFormat(service string, subType string, input PredictionInput, physicalId string) string {
+	return fmt.Sprintf("arn:%v:%v:%v:%v:%v:%v",
+		input.env.partition, service, input.env.region, input.env.account, subType, physicalId)
+}
+
+// GlobalFormat returns an arn that fits IAM and other non-regional services
+// for example
+// arn:${Partition}:iam::${Account}:group/${GroupNameWithPath}
+func globalFormat(service string, subType string, input PredictionInput, physicalId string) string {
+	return fmt.Sprintf("arn:%v:%v::%v:%v:%v",
+		input.env.partition, service, input.env.account, subType, physicalId)
+}
diff --git a/internal/cmd/forecast/forecast.go b/internal/cmd/forecast/forecast.go
index 144385bd..ffede7be 100644
--- a/internal/cmd/forecast/forecast.go
+++ b/internal/cmd/forecast/forecast.go
@@ -7,10 +7,13 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/aws-cloudformation/rain/cft"
 	"github.com/aws-cloudformation/rain/cft/parse"
+	"github.com/aws-cloudformation/rain/internal/aws"
 	"github.com/aws-cloudformation/rain/internal/aws/cfn"
+	"github.com/aws-cloudformation/rain/internal/aws/iam"
 	"github.com/aws-cloudformation/rain/internal/cmd/deploy"
 	"github.com/aws-cloudformation/rain/internal/config"
 	"github.com/aws-cloudformation/rain/internal/console/spinner"
@@ -44,6 +47,12 @@ var tags []string
 // The optional path to a file that contains params (--config)
 var configFilePath string
 
+type Env struct {
+	partition string
+	region    string
+	account   string
+}
+
 // Input to forecast prediction functions
 type PredictionInput struct {
 	source      cft.Template
@@ -54,6 +63,8 @@ type PredictionInput struct {
 	stack       types.Stack
 	typeName    string
 	dc          *dc.DeployConfig
+	env         Env
+	roleArn     string
 }
 
 // The current line number in the template
@@ -137,11 +148,10 @@ func forecastForType(input PredictionInput) Forecast {
 		forecast.Add(true, "Does not exist")
 	}
 
-	// Below are some ideas for things we might be able to check in a generic way
-
 	// Check permissions
-	// (see S3 example, we would need to figure out the arn for each service)
-	// TODO - Not sure if this is practical in a generic way
+	if !SkipIAM {
+		checkPermissions(input, &forecast)
+	}
 
 	// Check service quotas
 	// TODO - Can we do this in a generic way?
@@ -220,6 +230,20 @@ func predict(source cft.Template, stackName string, stack types.Stack, stackExis
 		input.stack = stack
 		input.typeName = typeName
 		input.dc = dc
+		cfg := aws.Config()
+		callerArn, err := iam.GetCallerArn(cfg) // arn:aws:iam::755952356119:role/Admin
+		if err != nil {
+			panic("unable to get caller arn")
+		}
+		arnTokens := strings.Split(callerArn, ":")
+		if len(arnTokens) != 6 {
+			panic(fmt.Sprintf("unexpected number of tokens in caller arn: %v", callerArn))
+		}
+		input.env = Env{partition: arnTokens[1], region: cfg.Region, account: arnTokens[4]}
+		input.roleArn = RoleArn
+		if input.roleArn == "" {
+			input.roleArn = callerArn
+		}
 
 		forecast.Append(forecastForType(input))
 	}
@@ -235,7 +259,7 @@ func predict(source cft.Template, stackName string, stack types.Stack, stackExis
 		}
 		return false
 	} else {
-		fmt.Println("Clear skies! 🌤  All", forecast.GetNumChecked(), "checks passed.")
+		fmt.Println("Clear skies! 🌞 All", forecast.GetNumChecked(), "checks passed.")
 		return true
 	}
 
@@ -274,7 +298,7 @@ Resource-specific checks:
 - S3 bucket policy has an invalid principal
 - (Many more to come...)
 `,
-	Args:                  cobra.ExactArgs(2),
+	Args:                  cobra.RangeArgs(1, 2),
 	DisableFlagsInUseLine: true,
 	Run: func(cmd *cobra.Command, args []string) {
 		fn := args[0]
diff --git a/internal/cmd/forecast/permissions.go b/internal/cmd/forecast/permissions.go
index 9e15e03c..b4a74109 100644
--- a/internal/cmd/forecast/permissions.go
+++ b/internal/cmd/forecast/permissions.go
@@ -1,31 +1,121 @@
 package forecast
 
 import (
+	"fmt"
+	"strings"
+
 	"github.com/aws-cloudformation/rain/internal/aws/cfn"
 	"github.com/aws-cloudformation/rain/internal/aws/iam"
+	"github.com/aws-cloudformation/rain/internal/config"
 	"github.com/aws-cloudformation/rain/internal/console/spinner"
+	"golang.org/x/exp/slices"
 )
 
 // Returns true if the user has the required permissions on the resource
 // verb is create, delete, or update
-func checkPermissions(input PredictionInput, resourceArn string, verb string) (bool, []string) {
+func checkTypePermissions(input PredictionInput, resourceArn string, verb string) (bool, []string) {
 
 	spin(input.typeName, input.logicalId, "permitted?")
 
 	// Go get the list of permissions from the registry
-	actions, err := cfn.GetTypePermissions("AWS::S3::Bucket", verb)
+	actions, err := cfn.GetTypePermissions(input.typeName, verb)
 	if err != nil {
 		return false, []string{err.Error()}
 	}
 
+	// Not all of these will work with the Simulator, since the registry
+	// schema also includes permissions for related services.
+	// For example, the create permissions on a lambda function include s3:GetObject,
+	// and we don't know what the arn would be.
+	// This means we are not checking everything that could go wrong.
+	// TODO - Is there a way we can figure out the arns for related services?
+	// This would likely not be practical in a generic way,
+	// but it's something we should eventually add to custom handling for each service.
+	svcLower := strings.ToLower(strings.Split(input.typeName, "::")[1])
+	actionsToRemove := make([]string, 0)
+	for _, action := range actions {
+		// Remove all actions that don't belong to this service
+		if !strings.HasPrefix(action, svcLower) {
+			actionsToRemove = append(actionsToRemove, action)
+		}
+	}
+
+	// Exceptions
+	// Sometimes the registry documents actions that the resource type
+	// might need in some situations, but these checks will fail under
+	// other circumstances, and it's not easy to know when they are relevant.
+	if svcLower == "lambda" {
+		actionsToRemove = append(actionsToRemove, "lambda:GetCodeSigningConfig")
+	}
+	if input.typeName == "AWS::IAM::Policy" {
+		actionsToRemove = append(actionsToRemove, "iam:PutUserPolicy")
+		actionsToRemove = append(actionsToRemove, "iam:PutRolePolicy")
+		actionsToRemove = append(actionsToRemove, "iam:PutGroupPolicy")
+		actionsToRemove = append(actionsToRemove, "iam:DeleteRolePolicy")
+		actionsToRemove = append(actionsToRemove, "iam:DeleteUserPolicy")
+		actionsToRemove = append(actionsToRemove, "iam:DeleteGroupPolicy")
+		actionsToRemove = append(actionsToRemove, "iam:GetRolePolicy")
+		actionsToRemove = append(actionsToRemove, "iam:GetUserPolicy")
+		actionsToRemove = append(actionsToRemove, "iam:GetGroupPolicy")
+	}
+
+	// Make a new slice with the actions we care about
+	actionsToCheck := make([]string, 0)
+	for _, action := range actions {
+		if !slices.Contains(actionsToRemove, action) {
+			actionsToCheck = append(actionsToCheck, action)
+		}
+	}
+
 	// Update the spinner with the action being checked
 	spinnerCallback := func(action string) {
 		spin(input.typeName, input.logicalId, action+" permitted?")
 	}
 
 	// Simulate the actions
-	result, messages := iam.Simulate(actions, resourceArn, RoleArn, spinnerCallback)
+	result, messages := iam.Simulate(actionsToCheck,
+		resourceArn, input.roleArn, spinnerCallback)
 
 	spinner.Pop()
 	return result, messages
 }
+
+// Check permissions to make sure the current role can create-update-delete
+func checkPermissions(input PredictionInput, forecast *Forecast) error {
+	resourceArn := predictResourceArn(input)
+	config.Debugf("checkPermissions arn: %v", resourceArn)
+	if resourceArn == "" {
+		// We don't know how to make an arn for this type
+		config.Debugf("Can't check permissions for %v %v, ARN unknown", input.typeName, input.logicalId)
+		return nil
+	}
+
+	var ok bool
+	var reason []string
+	if input.stackExists {
+		ok, reason = checkTypePermissions(input, resourceArn, "update")
+		if !ok {
+			forecast.Add(false,
+				fmt.Sprintf("Insufficient permissions to update %v\n\t%v", resourceArn, strings.Join(reason, "\n\t")))
+		} else {
+			forecast.Add(true, "Role has update permissions")
+		}
+
+		ok, reason = checkTypePermissions(input, resourceArn, "delete")
+		if !ok {
+			forecast.Add(false,
+				fmt.Sprintf("Insufficient permissions to delete %v\n\t%v", resourceArn, strings.Join(reason, "\n\t")))
+		} else {
+			forecast.Add(true, "Role has delete permissions")
+		}
+	} else {
+		ok, reason = checkTypePermissions(input, resourceArn, "create")
+		if !ok {
+			forecast.Add(false,
+				fmt.Sprintf("Insufficient permissions to create %v\n\t%v", resourceArn, strings.Join(reason, "\n\t")))
+		} else {
+			forecast.Add(true, "Role has create permissions")
+		}
+	}
+	return nil
+}
diff --git a/internal/cmd/forecast/s3_bucket.go b/internal/cmd/forecast/s3_bucket.go
index ac64fa58..649c750f 100644
--- a/internal/cmd/forecast/s3_bucket.go
+++ b/internal/cmd/forecast/s3_bucket.go
@@ -2,7 +2,6 @@ package forecast
 
 import (
 	"fmt"
-	"strings"
 
 	"github.com/aws-cloudformation/rain/internal/aws/cfn"
 	"github.com/aws-cloudformation/rain/internal/aws/s3"
@@ -10,8 +9,6 @@ import (
 	"github.com/aws-cloudformation/rain/internal/console/spinner"
 	"github.com/aws-cloudformation/rain/internal/s11n"
 	"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
-
-	"github.com/google/uuid"
 )
 
 // An empty bucket cannot be deleted, which will cause a stack DELETE to fail.
@@ -60,10 +57,6 @@ func checkBucketNotEmpty(input PredictionInput, bucket *types.StackResourceDetai
 // Check everything that could go wrong with an AWS::S3::Bucket resource
 func checkS3Bucket(input PredictionInput) Forecast {
 
-	// A uuid will be used for policy silumation if the bucket does not already exist
-	bucketName := fmt.Sprintf("rain-%v", uuid.New())
-	bucketArn := ""
-
 	forecast := makeForecast(input.typeName, input.logicalId)
 
 	if input.stackExists {
@@ -88,40 +81,5 @@ func checkS3Bucket(input PredictionInput) Forecast {
 		config.Debugf("Stack does not exist, not checking if bucket is empty")
 	}
 
-	bucketArn = fmt.Sprintf("arn:aws:s3:::%v", bucketName)
-
-	// TODO - Can we make the permissions check generic so we can
-	// run it on all types? What if we can't predict what the arn will be?
-	// We could have a map of resource names to functions that provide the arn..
-	if !SkipIAM {
-		var ok bool
-		var reason []string
-		if input.stackExists {
-			ok, reason = checkPermissions(input, bucketArn, "update")
-			if !ok {
-				forecast.Add(false,
-					fmt.Sprintf("Insufficient permissions to update %v\n\t%v", bucketArn, strings.Join(reason, "\n\t")))
-			} else {
-				forecast.Add(true, "Role has update permissions")
-			}
-
-			ok, reason = checkPermissions(input, bucketArn, "delete")
-			if !ok {
-				forecast.Add(false,
-					fmt.Sprintf("Insufficient permissions to delete %v\n\t%v", bucketArn, strings.Join(reason, "\n\t")))
-			} else {
-				forecast.Add(true, "Role has delete permissions")
-			}
-		} else {
-			ok, reason = checkPermissions(input, bucketArn, "create")
-			if !ok {
-				forecast.Add(false,
-					fmt.Sprintf("Insufficient permissions to create %v\n\t%v", bucketArn, strings.Join(reason, "\n\t")))
-			} else {
-				forecast.Add(true, "Role has create permissions")
-			}
-		}
-	}
-
 	return forecast
 }


Test patch:
diff --git a/internal/cmd/forecast/permissions_test.go b/internal/cmd/forecast/permissions_test.go
new file mode 100644
index 00000000..089f4797
--- /dev/null
+++ b/internal/cmd/forecast/permissions_test.go
@@ -0,0 +1,37 @@
+package forecast
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/aws-cloudformation/rain/cft"
+	"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
+	"gopkg.in/yaml.v3"
+)
+
+func TestPredictResourceArn(t *testing.T) {
+	input := PredictionInput{
+		source:      cft.Template{},
+		stackName:   "mystack",
+		resource:    &yaml.Node{},
+		logicalId:   "myresource",
+		stackExists: false,
+		stack:       types.Stack{},
+		typeName:    "AWS::S3::Bucket",
+		dc:          nil,
+		env:         Env{partition: "aws", region: "us-east-1", account: "123456789012"},
+		roleArn:     "arn:aws:iam::123456789012:role/Admin",
+	}
+
+	s3Arn := predictResourceArn(input)
+	if !strings.HasPrefix(s3Arn, "arn:aws:s3:::") {
+		t.Errorf("Expected arn:aws:s3:::*, got %s", s3Arn)
+	}
+
+	input.typeName = "AWS::EC2::Instance"
+
+	ec2Arn := predictResourceArn(input)
+	if !strings.HasPrefix(ec2Arn, "arn:aws:ec2:us-east-1:123456789012:instance/") {
+		t.Errorf("Expected arn:aws:ec2:us-east-1:123456789012:instance/*, got %s", ec2Arn)
+	}
+}
diff --git a/test/templates/forecast-ec2.yaml b/test/templates/forecast-ec2.yaml
new file mode 100644
index 00000000..e5d33a34
--- /dev/null
+++ b/test/templates/forecast-ec2.yaml
@@ -0,0 +1,20 @@
+Resources:
+
+  Instance1:
+    Type: AWS::EC2::Instance
+    Properties:
+      InstanceType: t3.micro
+      SecurityGroups: [!Ref 'Instance1SecurityGroup']
+      KeyName: abc
+      ImageId: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
+
+  Instance1SecurityGroup:
+    Type: AWS::EC2::SecurityGroup
+    Properties:
+      GroupDescription: Enable SSH access via port 22
+      SecurityGroupIngress:
+      - IpProtocol: tcp
+        FromPort: 22
+        ToPort: 22
+        CidrIp: 54.240.196.187/1
+
diff --git a/test/templates/forecast-fail.yml b/test/templates/forecast-fail.yml
index dd96ef4d..202d4a05 100644
--- a/test/templates/forecast-fail.yml
+++ b/test/templates/forecast-fail.yml
@@ -37,6 +37,24 @@ Resources:
               - !Sub "arn:${AWS::Partition}:s3:::${RetainBucket}"
               - !Sub "arn:${AWS::Partition}:s3:::${RetainBucket}/*" 
 
+  Instance1:
+    Type: AWS::EC2::Instance
+    Properties:
+      InstanceType: t3.micro 
+      SecurityGroups: [!Ref 'Instance1SecurityGroup']
+      KeyName: abc
+      ImageId: "does.not.exist"
+
+  Instance1SecurityGroup:
+    Type: AWS::EC2::SecurityGroup
+    Properties:
+      GroupDescription: Enable SSH access via port 22
+      SecurityGroupIngress:
+      - IpProtocol: tcp
+        FromPort: 22
+        ToPort: 22
+        CidrIp: 54.240.196.187/1
+
 
 Outputs:
 
@@ -44,4 +62,4 @@ Outputs:
     Value: !Ref RetainBucket   
     
   NoRetainBucketName:
-    Value: !Ref NoRetainBucket    
\ No newline at end of file
+    Value: !Ref NoRetainBucket    
diff --git a/test/templates/forecast-lambda-pkg.yaml b/test/templates/forecast-lambda-pkg.yaml
new file mode 100644
index 00000000..43ecdd38
--- /dev/null
+++ b/test/templates/forecast-lambda-pkg.yaml
@@ -0,0 +1,62 @@
+Resources:
+  MyFunction:
+    Type: AWS::Lambda::Function
+    Properties:
+      Code:
+        ZipFile: |
+          def handler(evt, ctx)
+            print("Hello")
+      Runtime: python3.9
+      Role: !GetAtt MyFunctionRole.Arn
+      Handler: index.handler
+
+  MyFunctionAlias:
+    Type: AWS::Lambda::Alias
+    Properties:
+      FunctionName: !Ref MyFunction
+      Name: aaa
+      FunctionVersion: $LATEST
+
+  MyFunctionPolicy:
+    Type: AWS::IAM::Policy
+    Properties:
+      PolicyName: lambdaexec
+      PolicyDocument:
+        Version: "2012-10-17"
+        Statement:
+          - Action:
+              - logs:CreateLogStream
+              - logs:PutLogEvents
+            Effect: Allow
+            Resource: '*'
+      Roles:
+        - !Ref MyFunctionRole
+
+  MyFunctionRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action:
+              - sts:AssumeRole
+            Effect: Allow
+            Principal:
+              Service:
+                - lambda.amazonaws.com
+        Version: "2012-10-17"
+      ManagedPolicyArns:
+        - arn:aws:iam::aws:policy/AWSLambdaExecute
+      Path: /
+
+  MyFunctionVersion:
+    Type: AWS::Lambda::Version
+    Properties:
+      FunctionName: !Ref MyFunction
+
+  MyLayer:
+    Type: AWS::Lambda::LayerVersion
+    Properties:
+      Content:
+        S3Bucket: rain-artifacts-755952356119-us-east-1
+        S3Key: cec8e526ef89eb818a06d9800ac079be326db9dfcab87c1ec34c59e70fedc1b3
+
diff --git a/test/templates/forecast-lambda.yaml b/test/templates/forecast-lambda.yaml
new file mode 100644
index 00000000..f26009ce
--- /dev/null
+++ b/test/templates/forecast-lambda.yaml
@@ -0,0 +1,64 @@
+Resources:
+
+  MyFunction:
+    Type: AWS::Lambda::Function
+    Properties:
+      Code:
+        ZipFile: |
+          def handler(evt, ctx)
+            print("Hello")
+      Runtime: python3.9
+      Role: !GetAtt MyFunctionRole.Arn
+      Handler: index.handler
+
+  MyFunctionAlias:
+    Type: AWS::Lambda::Alias
+    Properties:
+      FunctionName: !Ref MyFunction
+      Name: aaa
+      FunctionVersion: $LATEST
+
+  MyFunctionPolicy:
+    Type: AWS::IAM::Policy
+    Properties:
+      PolicyName: lambdaexec
+      PolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Action:
+              - logs:CreateLogStream
+              - logs:PutLogEvents
+            Effect: Allow
+            Resource: "*"
+      Roles: 
+        - !Ref MyFunctionRole
+
+  MyFunctionRole:
+    Type: AWS::IAM::Role
+    Properties:
+      AssumeRolePolicyDocument:
+        Statement:
+          - Action:
+              - sts:AssumeRole
+            Effect: Allow
+            Principal:
+              Service:
+                - lambda.amazonaws.com
+        Version: "2012-10-17"
+      ManagedPolicyArns:
+        - arn:aws:iam::aws:policy/AWSLambdaExecute
+      Path: /
+
+  MyFunctionVersion:
+    Type: AWS::Lambda::Version
+    Properties:
+      FunctionName: !Ref MyFunction
+    
+  MyLayer:
+    Type: AWS::Lambda::LayerVersion
+    Properties:
+      Content: !Rain::S3 
+        Path: lambda-src 
+        Zip: true
+        BucketProperty: S3Bucket
+        KeyProperty: S3Key
diff --git a/test/templates/forecast-succeed.yml b/test/templates/forecast-succeed.yml
index 64d67aa9..17b7d4c6 100644
--- a/test/templates/forecast-succeed.yml
+++ b/test/templates/forecast-succeed.yml
@@ -26,9 +26,27 @@ Resources:
               - !Sub "arn:${AWS::Partition}:s3:::${RetainBucket}"
               - !Sub "arn:${AWS::Partition}:s3:::${RetainBucket}/*" 
 
+  Instance1:
+    Type: AWS::EC2::Instance
+    Properties:
+      InstanceType: t3.micro
+      SecurityGroups: [!Ref 'Instance1SecurityGroup']
+      KeyName: abc
+      ImageId: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
+
+  Instance1SecurityGroup:
+    Type: AWS::EC2::SecurityGroup
+    Properties:
+      GroupDescription: Enable SSH access via port 22
+      SecurityGroupIngress:
+      - IpProtocol: tcp
+        FromPort: 22
+        ToPort: 22
+        CidrIp: 54.240.196.187/1
+
 
 Outputs:
 
   RetainBucketName:
     Value: !Ref RetainBucket   
-    
\ No newline at end of file
+    
diff --git a/test/templates/lambda-src/handler.py b/test/templates/lambda-src/handler.py
new file mode 100644
index 00000000..53105ff4
--- /dev/null
+++ b/test/templates/lambda-src/handler.py
@@ -0,0 +1,3 @@
+def handler(evt, ctx):
+    print("hello")
+